From fd8bfdbcf3b6ba7072f28c4f68f88770f6353944 Mon Sep 17 00:00:00 2001 From: maj00r Date: Mon, 22 Jun 2026 23:10:06 +0200 Subject: [PATCH] Add two-pass progressive scenery load (sequential; infra then visuals) 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 --- simulation/simulationstateserializer.cpp | 39 ++++++++++++++++++++++++ simulation/simulationstateserializer.h | 3 ++ utilities/parser.cpp | 26 ++++++++++++++++ utilities/parser.h | 9 ++++++ 4 files changed, 77 insertions(+) diff --git a/simulation/simulationstateserializer.cpp b/simulation/simulationstateserializer.cpp index 166cf1a6..78864177 100644 --- a/simulation/simulationstateserializer.cpp +++ b/simulation/simulationstateserializer.cpp @@ -49,6 +49,10 @@ state_serializer::deserialize_begin( std::string const &Scenariofile ) { std::make_shared( Scenariofile, cParser::buffer_FILE, Global.asCurrentSceneryPath, Global.bLoadTraction ); state->scenariofile = Scenariofile; state->scratchpad.name = Scenariofile; + // first pass loads infrastructure (tracks/traction/events/memcells/sounds + directives); + // visual nodes are skipped by the reader and loaded in a second pass. for a text/compile + // load (no twin) this is a no-op and everything loads in a single pass. + state->input.setReplayPass( scene::scenery_load_pass::infrastructure ); scene::Groups.create(); if( false == state->input.ok() ) @@ -104,11 +108,37 @@ state_serializer::deserialize_continue(std::shared_ptr state cParser &Input = state->input; scene::scratch_data &Scratchpad = state->scratchpad; + // stateful directives that build objects/lists; on the visual (second) pass they are + // skipped wholesale so their side effects (trainsets, events, cameras, ...) don't + // duplicate. transform/group directives (origin/rotate/scale/group) and idempotent + // setters are re-run, so deferred visual nodes get the correct placement. + static std::unordered_map const visualskip { + { "trainset", "endtrainset" }, + { "event", "endevent" }, + { "camera", "endcamera" }, + { "light", "endlight" }, + { "description", "enddescription" }, + { "test", "endtest" }, + { "sky", "endsky" }, + { "time", "endtime" }, + { "terrain", "endterrain" }, + }; + // deserialize content from the provided input auto timelast { std::chrono::steady_clock::now() }; std::string token { Input.getToken() }; while( false == token.empty() ) { + if( state->visualphase ) { + auto const skip = visualskip.find( token ); + if( skip != visualskip.end() ) { + // consume the stateful directive without running its handler + skip_until( Input, skip->second ); + token = Input.getToken(); + continue; + } + } + auto lookup = state->functionmap.find( token ); if( lookup != state->functionmap.end() ) { lookup->second(); @@ -131,6 +161,15 @@ state_serializer::deserialize_continue(std::shared_ptr state deserialize_firstinit( Input, Scratchpad ); } + // first (infrastructure) pass finished: run a second pass over the same twin to load + // the visual nodes that were skipped. only possible when replaying a binary twin; a + // text/compile load did everything in one pass (restartReplay returns false). + if( ( false == state->visualphase ) + && ( true == Input.restartReplay( scene::scenery_load_pass::visual ) ) ) { + state->visualphase = true; + return true; // continue with the visual pass + } + scene::Groups.close(); scene::Groups.update_map(); diff --git a/simulation/simulationstateserializer.h b/simulation/simulationstateserializer.h index dea1e20e..346aee54 100644 --- a/simulation/simulationstateserializer.h +++ b/simulation/simulationstateserializer.h @@ -22,6 +22,9 @@ struct deserializer_state { std::unordered_map< std::string, deserializefunctionbind> functionmap; + // progressive (two-pass) load over a binary twin: first pass loads infrastructure, + // second pass loads visual nodes. false while in the first (infrastructure) pass. + bool visualphase { false }; deserializer_state(std::string const &File, cParser::buffertype const Type, const std::string &Path, bool const Loadtraction) : scenariofile(File), input(File, Type, Path, Loadtraction) { } diff --git a/utilities/parser.cpp b/utilities/parser.cpp index 741de367..d9443158 100644 --- a/utilities/parser.cpp +++ b/utilities/parser.cpp @@ -641,6 +641,9 @@ void cParser::startIncludeDirect(std::string includefile, std::vectorallowRandomIncludes = allowRandomIncludes; mIncludeParser->autoclear(m_autoclear); + // the child inherits the current load pass so the whole include tree is filtered + // consistently (e.g. visuals-only on the second pass) + mIncludeParser->setReplayPass(m_replaypass); // a binary-twin replay child reports mSize 0 but is still valid if (mIncludeParser->mSize <= 0 && (false == mIncludeParser->m_replay)) { @@ -648,6 +651,29 @@ void cParser::startIncludeDirect(std::string includefile, std::vectorset_pass(Pass); + } +} + +bool cParser::restartReplay(scene::scenery_load_pass Pass) +{ + if ((false == m_replay) || (false == static_cast(m_reader))) + { + return false; + } + m_reader->open(m_twinbuf); + m_reader->set_pass(Pass); + m_replaypass = Pass; + m_replayexhausted = false; + mIncludeParser = nullptr; + return true; +} + bool cParser::handleIncludeIfPresent(std::string& token, bool ToLower, const char* Break) { // token-mode include: token == "include". NOTE: we only process the directive here // and report it; readToken loops to fetch the next token. fetching it here (the old diff --git a/utilities/parser.h b/utilities/parser.h index b6be4c70..4488adb3 100644 --- a/utilities/parser.h +++ b/utilities/parser.h @@ -22,6 +22,7 @@ http://mozilla.org/MPL/2.0/. // before cParser is defined. namespace scene { enum class scenery_file_kind : std::uint8_t; + enum class scenery_load_pass : std::uint8_t; class scenery_binary_writer; class scenery_binary_reader; } @@ -46,6 +47,13 @@ class cParser //: public std::stringstream // touching scene state, and return the (relative) filenames it includes so a parallel // baker can compile each of those twins on its own thread. requires BakeOnly ctor. std::vector bakeFile(); + // select which class of nodes the replay serves (propagated to include children). + // only meaningful in replay (twin) mode; ignored for text parsing. + void setReplayPass( scene::scenery_load_pass Pass ); + // rewind the replay to the start of the twin with a (possibly new) pass, dropping any + // open include child. used to run a second pass (visual) over an already-loaded twin. + // returns false if this parser isn't replaying a twin (no second pass possible). + bool restartReplay( scene::scenery_load_pass Pass ); // methods: template cParser & @@ -180,6 +188,7 @@ class cParser //: public std::stringstream // replay: this file is served from its binary twin instead of text bool m_replay { false }; bool m_replayexhausted { false }; // all twin entries consumed (for inline eof()) + scene::scenery_load_pass m_replaypass {}; // value-init == scenery_load_pass::all std::string m_twinbuf; // whole twin file held in memory; the reader views into it std::unique_ptr m_reader; // streams entries from m_twinbuf // compile: this file is being captured into a binary twin alongside the text read