16
0
mirror of https://github.com/MaSzyna-EU07/maszyna.git synced 2026-07-22 05:49:19 +02:00

Replace per-node text capture with camera-distance ring multi-pass

The previous nearest-first build captured every deferred visual node as text into a
sorted vector, which does not scale: one tomaszewo flora file alone holds 440k model
nodes (the scenery has 1M+), so the capture ran the process to ~7 GB and its
enumeration never finished.

Stream the visual nodes in camera-distance rings instead, with no per-node capture
(O(1) memory). The visual pass replays the twin once per ring (nearest first); a node
is built only when its squared distance to the camera -- sampled once when the visual
phase starts, so the partition is stable across passes -- falls in the current ring,
otherwise the rest of its body is skipped in O(1) by jumping over the v6 marker span.
Each node is therefore built exactly once, in roughly nearest-first order, through the
normal node path (instancing buckets unchanged). Explicit triangles/lines shapes have
no single position to ring-test by, so they build in the nearest ring pass only.

Reader gains skip_to_node_end() (remembers the served node's end and jumps the cursor
there); cParser::skipReplayNode() delegates it down the active include child.

Verified: td.scn builds 4 rings, no duplicates, no unexpected tokens, complete ~1s
after the infrastructure pass. tomaszewo stays memory-bounded (no OOM, no duplicates)
where the capture approach previously hung.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
maj00r
2026-06-23 17:41:46 +02:00
parent 1160bfecac
commit d7a3e1310a
6 changed files with 127 additions and 239 deletions

View File

@@ -14,24 +14,6 @@ http://mozilla.org/MPL/2.0/.
namespace simulation {
// a deferred visual node captured during the visual enumeration pass, to be (re)built
// later in camera-distance order. holds the node's fully resolved tokens as text -- numbers
// come through cParser losslessly (std::to_chars shortest round-trip), so rebuilding from
// this text reproduces the exact same node -- plus a snapshot of the transform/group context
// it was read under, so the out-of-order rebuild places and groups it identically.
struct visual_record {
std::string text; // "node <range> <range> <name> <type> ... end<type>"
glm::dvec3 offset { 0.0 }; // top of the origin stack at capture (identity if none)
glm::vec3 scale { 1.f }; // top of the scale stack at capture (identity if none)
glm::vec3 rotation { 0.f }; // active rotation at capture
bool has_offset { false };
bool has_scale { false };
scene::group_handle group {}; // group the node was read under
glm::dvec3 worldpos { 0.0 }; // transformed position (for models; sort key source)
bool isshape { false }; // terrain shape/lines -> load before models (ground first)
double sortkey { 0.0 }; // squared distance to camera; lower = built sooner
};
struct deserializer_state {
std::string scenariofile;
cParser input;
@@ -45,13 +27,12 @@ struct deserializer_state {
bool visualphase { false };
// set once the whole load (both passes / single text pass) has fully finished
bool done { false };
// visual phase sub-state: while enumerate is true the visual pass captures deferred
// nodes into records instead of building them; once the replay is exhausted they are
// sorted by camera distance and built in that order (enumdone).
bool enumerate { false };
bool enumdone { false };
std::vector<visual_record> records;
std::size_t buildcursor { 0 };
// camera-ring visual streaming: the visual phase replays the twin once per distance
// ring (nearest first), building only the nodes whose distance to ringeye falls in the
// current ring and O(1)-skipping the rest. ringeye is sampled once when the visual phase
// starts so the ring partition is stable across passes.
int ringindex { 0 };
glm::dvec3 ringeye { 0.0 };
deserializer_state(std::string const &File, cParser::buffertype const Type, const std::string &Path, bool const Loadtraction)
: scenariofile(File), input(File, Type, Path, Loadtraction) { }
@@ -93,10 +74,6 @@ private:
void deserialize_endgroup( cParser &Input, scene::scratch_data &Scratchpad );
void deserialize_light( cParser &Input, scene::scratch_data &Scratchpad );
void deserialize_node( cParser &Input, scene::scratch_data &Scratchpad );
// visual streaming (camera-ordered deferred load): capture one visual node (already
// past its "node" token) into a record; build budgeted records in sorted order.
void enumerate_visual_node( deserializer_state &State );
bool build_visual_records( std::shared_ptr<deserializer_state> State );
void deserialize_origin( cParser &Input, scene::scratch_data &Scratchpad );
void deserialize_endorigin( cParser &Input, scene::scratch_data &Scratchpad );
void deserialize_scale( cParser &Input, scene::scratch_data &Scratchpad );
@@ -122,6 +99,15 @@ private:
// transforms provided location by specifed rotation and offset
glm::dvec3 transform( glm::dvec3 Location, scene::scratch_data const &Scratchpad );
void export_nodes_to_stream( std::ostream &, bool Dirty ) const;
// members
// camera-ring visual streaming state, mirrored from deserializer_state each
// deserialize_continue() call so deserialize_model()/deserialize_node() can ring-test a
// node by distance. inactive (builds everything) outside the ring/visual phase.
bool m_ringactive { false };
int m_ringindex { 0 };
glm::dvec3 m_ringeye { 0.0 };
double m_ringmin2 { 0.0 };
double m_ringmax2 { 0.0 };
};
} // simulation