The camera-ring visual load replays the twin once per distance ring, and each pass
had to read every node's header + X Y Z tokens (~8 tokens through the cParser pipeline)
just to decide whether the node is in the current ring -- expensive when a scenery has
a million flora instances.
Bump the twin format to v7: a visual model node's marker now carries its local position
(3 f32), captured at bake time from the node's first three numbers. The reader hands the
position out (scenery_binary_reader::node_position), and the deserializer ring-tests a
model and skips it in O(1) over the marker span, straight from the dispatch loop, without
deserialize_node decoding any of its tokens. deserialize_model uses the same marker
position for its own ring test so the two decisions agree exactly (a node near a ring
boundary can't be dropped by both adjacent rings). Shapes/older twins have no marker
position and fall back to the token-based test. The per-frame visual budget is factored
into VISUAL_BUDGET_MS.
Verified: td.scn rebakes to v7, no duplicates, no unexpected tokens, completes ~1s.
tomaszewo (1M+ flora) stays memory-bounded with no duplicates; full streaming is still
paced by walking every node per ring (the remaining cost is the replay pipeline itself,
which a spatial section index would address).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>
The progressive load previously streamed the deferred visual nodes (3d model
instances + terrain shapes/lines) in file order, so distant scenery could load
before the player's surroundings. This builds them nearest-camera first instead.
The visual pass now runs in two steps. Enumeration replays the twin and captures
each visual node verbatim (its resolved tokens as text -- numbers round-trip
losslessly through cParser) together with the transform/group context it was read
under and, for models, its transformed world position. Once the replay is
exhausted the records are sorted by squared distance to the camera (terrain shapes
first so the ground appears before the props on it), then built a budgeted slice
per frame through the normal node path with the captured transform and group
restored -- so placement, grouping and the per-cell instance buckets come out
identical to an in-order load.
Two supporting fixes make out-of-order/late insertion correct:
- a cell/section whose geometry was already baked (the renderer finalised it
before a deferred node arrived) now appends the new shape/lines straight into
its live geometry bank instead of merging into vertex-freed geometry, which
would silently drop it; create_geometry() remembers the bank for every cell.
- events that bind to visual model instances (lights/animation/texture/visible)
are deferred from InitEvents() to a new InitInstanceEvents() run after the
visual nodes are built, so their target models exist when they initialise.
Verified on td.scn: playable ~2s, 540 deferred nodes enumerated and built
nearest-first ~0.5s later, no duplicate instances.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Defensive fix: when starting the deferred visual pass, clear the origin/scale
stacks and rotation before the directives are replayed. A scenery that leaves an
unbalanced origin on the stack during the infrastructure pass would otherwise have
it applied a second time, shifting every deferred visual node. Balanced sceneries
(the common case) are unaffected -- the directives rebuild the same state.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Splits the progressive load across modes: the loader runs only the first
(infrastructure) pass and then hands off to the driver, so play begins as soon
as tracks/traction/events/signals/the player train are ready. The deferred
visual nodes (3d models, terrain shapes/lines) then stream in from the driver,
a small budget per frame, so the world fills in without a long up-front wait.
- state_serializer: the infrastructure pass now returns to the caller (instead of
chaining the visual pass), restarting the twin for the visual pass; the visual
pass uses a small ~8 ms/frame budget and marks the load done at the end. A
text/compile load (no twin) stays single-pass and finishes as before.
- state_manager: retains the load state across the loader->driver hand-off and
exposes loading_visuals()/continue_loading_visuals().
- driver_mode::update(): advances the deferred visual load each frame.
Verified on td.scn (playable in ~2 s, visuals complete a few seconds later, no
duplicate objects) and on the large tomaszewo scenery (flora/terrain/models
deferred while infrastructure loads). Inserts run on the main thread before the
render step, so progressive loading needs no locking.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
When replaying a binary twin, load in two passes over the same data: the first
pass loads infrastructure (tracks/traction/events/memcells/sounds + directives),
the second pass loads the visual nodes (3d models, terrain shapes/lines) that the
reader skipped via the v6 node-class markers.
- cParser: setReplayPass() selects the served node class (propagated to include
children); restartReplay() rewinds the twin for the second pass.
- state_serializer: first pass uses the infrastructure pass; on completion it
restarts the twin for the visual pass. Stateful directives (trainset, event,
camera, light, sky, time, ...) are skipped on the visual pass so their side
effects do not duplicate; transform/group directives re-run so deferred visual
nodes get correct placement. A text/compile load (no twin) stays single-pass.
Verified: td.scn replays through both passes with no duplicate vehicles/events
and reaches the normal load endpoint. This is the sequential foundation; moving
the visual pass into the driver (so play starts after the infrastructure pass) is
the next step.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Text scenery component files (.scn/.inc/.scm/.ctr) are compiled into
per-file binary twins (.scnb/.incb/.scmb/.ctrb), handled transparently
at the cParser layer: a fresh twin (mtime-checked, version-matched) is
replayed instead of re-tokenizing text; otherwise the text is parsed and
a twin is compiled alongside it for next time.
Format details:
- per-file string interning: keywords/paths stored once, referenced by
varint index (so node/endmodel/... are not repeated as text)
- numeric tokens stored as 8-byte IEEE doubles, not ASCII
- includes kept as references with parameters; random sets stored verbatim
and re-evaluated on every load (choice not frozen at compile time)
- twin writing offloaded to a bounded thread pool so baking overlaps
scene construction instead of blocking the load
The legacy terrain-only .sbt path is removed; terrain now loads as
ordinary scenery content.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>