mirror of
https://github.com/MaSzyna-EU07/maszyna.git
synced 2026-07-18 01:59:19 +02:00
Section-index visual streaming (no per-cycle twin re-scan)
Replaces the camera-distance ring passes with section-following streaming and a persistent section index, so a million-node scenery no longer re-scans the whole twin every time the camera moves into new ground. - v9 node marker also stores range_max; a model visible from beyond the stream radius (or unlimited) is built once up front, the rest stream by section so distant landmarks/traction/buildings don't pop out while flora stays local. - Reader gains node_offset()/seek_node(); cParser exposes the deepest twin's file/path/offset/params and seekReplayNode/setReplayParams to rebuild one node. - First visual pass indexes every deferred node (models via the dispatch fast path, origin-placed flora/shapes in deserialize_node) under its region section while building the spawn area; later cycles rebuild only the newly-wanted sections by seeking straight to their nodes -- O(visible), not O(whole twin). - Build-all fallback retained for ghostview with no camera centre. Known: absolute terrain triangles (no origin) still build in the first pass; only origin-placed content section-streams. Untested in-game (format bump needs a rebake).
This commit is contained in:
@@ -37,25 +37,43 @@ http://mozilla.org/MPL/2.0/.
|
||||
namespace simulation {
|
||||
|
||||
namespace {
|
||||
// camera-distance rings for nearest-first visual streaming: the squared outer radius of
|
||||
// each ring. the visual pass is replayed once per ring (nearest first), building only the
|
||||
// nodes whose squared distance to the camera falls in [inner, outer); the last ring is
|
||||
// unbounded so every remaining node is built exactly once.
|
||||
constexpr double RING_OUTER2[] = {
|
||||
500.0 * 500.0,
|
||||
1500.0 * 1500.0,
|
||||
4000.0 * 4000.0,
|
||||
std::numeric_limits<double>::infinity() };
|
||||
constexpr int RING_COUNT { static_cast<int>( sizeof( RING_OUTER2 ) / sizeof( RING_OUTER2[ 0 ] ) ) };
|
||||
inline int ring_lastindex() { return RING_COUNT - 1; }
|
||||
inline double ring_min2( int const K ) { return ( K <= 0 ? 0.0 : RING_OUTER2[ K - 1 ] ); }
|
||||
inline double ring_max2( int const K ) { return RING_OUTER2[ ( K < RING_COUNT ? K : RING_COUNT - 1 ) ]; }
|
||||
// camera-following visual streaming: visual nodes are built only for region sections within
|
||||
// this radius (m) of the camera, and more are built as the camera moves into new sections.
|
||||
// should comfortably cover the model render range so nothing visibly pops in at the edge.
|
||||
constexpr double STREAM_RADIUS { 2000.0 };
|
||||
// per-frame time budget (ms) the driver spends streaming visual nodes. larger = the
|
||||
// surroundings fill in faster but the frame it runs on is longer; on a heavy scene (low fps)
|
||||
// a too-small budget is a tiny duty cycle, so streaming a million flora instances drags.
|
||||
constexpr int VISUAL_BUDGET_MS { 24 };
|
||||
|
||||
// fills Tobuild with the region-section indices within STREAM_RADIUS of Eye that are not yet
|
||||
// in Built. mirrors basic_region::section() indexing (clamped to the grid). returns count.
|
||||
std::size_t wanted_sections( glm::dvec3 const &Eye, std::unordered_set<int> const &Built, std::unordered_set<int> &Tobuild ) {
|
||||
Tobuild.clear();
|
||||
int const N { scene::EU07_REGIONSIDESECTIONCOUNT };
|
||||
int const ccol { static_cast<int>( std::floor( Eye.x / scene::EU07_SECTIONSIZE + N / 2 ) ) };
|
||||
int const crow { static_cast<int>( std::floor( Eye.z / scene::EU07_SECTIONSIZE + N / 2 ) ) };
|
||||
int const span { static_cast<int>( std::ceil( STREAM_RADIUS / scene::EU07_SECTIONSIZE ) ) };
|
||||
for( int r = crow - span; r <= crow + span; ++r ) {
|
||||
for( int c = ccol - span; c <= ccol + span; ++c ) {
|
||||
int const idx { std::clamp( r, 0, N - 1 ) * N + std::clamp( c, 0, N - 1 ) };
|
||||
if( 0 == Built.count( idx ) ) { Tobuild.insert( idx ); }
|
||||
}
|
||||
}
|
||||
return Tobuild.size();
|
||||
}
|
||||
} // anonymous namespace
|
||||
|
||||
// region-section index enclosing a world position (row-major, clamped) -- matches
|
||||
// basic_region::section() so a node buckets into the same section it inserts into.
|
||||
int
|
||||
state_serializer::section_index( glm::dvec3 const &World ) {
|
||||
int const N { scene::EU07_REGIONSIDESECTIONCOUNT };
|
||||
int const col { static_cast<int>( std::floor( World.x / scene::EU07_SECTIONSIZE + N / 2 ) ) };
|
||||
int const row { static_cast<int>( std::floor( World.z / scene::EU07_SECTIONSIZE + N / 2 ) ) };
|
||||
return std::clamp( row, 0, N - 1 ) * N + std::clamp( col, 0, N - 1 );
|
||||
}
|
||||
|
||||
std::shared_ptr<deserializer_state>
|
||||
state_serializer::deserialize_begin( std::string const &Scenariofile ) {
|
||||
|
||||
@@ -139,16 +157,30 @@ state_serializer::deserialize_continue(std::shared_ptr<deserializer_state> state
|
||||
cParser &Input = state->input;
|
||||
scene::scratch_data &Scratchpad = state->scratchpad;
|
||||
|
||||
// mirror the camera-ring state so deserialize_model()/deserialize_node() can ring-test
|
||||
// each node by distance: in the visual phase the twin is replayed once per ring and only
|
||||
// the nodes in the current ring are built, the rest skipped in O(1)
|
||||
// reset the transform stack before each replay pass. the directives (origin/rotate/scale)
|
||||
// are replayed in order, so resetting here reproduces the single-pass placement exactly;
|
||||
// without it an unbalanced origin left on the stack would be applied again and shift nodes.
|
||||
auto const resettransform = [ &Scratchpad ]() {
|
||||
// a well-formed pass ends with a balanced (empty) transform stack; a leftover means an
|
||||
// origin/scale was pushed but never popped -- e.g. a node whose binary marker span
|
||||
// over-ran its terminator and skipped the following endorigin. warn rather than let it
|
||||
// silently accumulate into the next pass.
|
||||
if( false == Scratchpad.location.offset.empty() ) {
|
||||
WriteLog( "Bad scenery: " + std::to_string( Scratchpad.location.offset.size() ) + " unbalanced origin(s) left on the stack at end of a load pass" );
|
||||
}
|
||||
Scratchpad.location.offset = {};
|
||||
Scratchpad.location.scale = {};
|
||||
Scratchpad.location.rotation = glm::vec3{}; };
|
||||
|
||||
// mirror the visual-streaming state so deserialize_model()/deserialize_node() can decide
|
||||
// whether a node belongs to the section set being built this cycle (or, in ringall, build
|
||||
// everything). inactive (builds everything) outside the visual phase.
|
||||
m_ringactive = state->visualphase;
|
||||
if( true == m_ringactive ) {
|
||||
if( false == state->ringeye_valid ) {
|
||||
// the visual streaming builds nodes nearest-this-point first, so it must be the
|
||||
// player's spawn. the camera isn't positioned during load (especially in ghostview),
|
||||
// so prefer the player vehicle's position; fall back to the camera, then the
|
||||
// scenery's first camera directive. wait a few frames if nothing is available yet.
|
||||
// the camera centre decides spawn-area-first streaming; the camera isn't positioned
|
||||
// during load (especially ghostview), so prefer the player vehicle, then the camera,
|
||||
// then the scenery's first camera directive. wait a few frames if nothing is ready.
|
||||
auto const iszero = []( glm::dvec3 const &V ) { return ( V.x == 0.0 ) && ( V.y == 0.0 ) && ( V.z == 0.0 ); };
|
||||
glm::dvec3 eye = Global.pCamera.Pos;
|
||||
char const *src = "camera";
|
||||
@@ -156,6 +188,12 @@ state_serializer::deserialize_continue(std::shared_ptr<deserializer_state> state
|
||||
auto *player = simulation::Vehicles.find( Global.local_start_vehicle );
|
||||
if( player != nullptr ) { eye = player->GetPosition(); src = "player vehicle"; }
|
||||
}
|
||||
if( ( true == iszero( eye ) ) && ( false == simulation::Vehicles.sequence().empty() ) ) {
|
||||
// no designated player (e.g. ghostview), but the scenery has consists -- centre on
|
||||
// the first one; it sits on the network, near where the action is.
|
||||
eye = simulation::Vehicles.sequence().front()->GetPosition();
|
||||
src = "first vehicle";
|
||||
}
|
||||
if( true == iszero( eye ) ) { eye = Global.FreeCameraInit[ 0 ]; src = "camera directive"; }
|
||||
if( ( true == iszero( eye ) ) && ( state->ringeye_waits < 120 ) ) {
|
||||
++state->ringeye_waits;
|
||||
@@ -163,20 +201,55 @@ state_serializer::deserialize_continue(std::shared_ptr<deserializer_state> state
|
||||
}
|
||||
state->ringeye = eye;
|
||||
state->ringeye_valid = true;
|
||||
// no spawn/camera to centre on (e.g. ghostview): nearest-first is meaningless, so
|
||||
// build every visual node in a single budgeted pass instead of partitioning into
|
||||
// distance rings (which would dump everything in the far ring and stream it last).
|
||||
// no spawn/camera to centre on (e.g. ghostview at the origin): camera-following is
|
||||
// meaningless, so build every visual node in one pass. otherwise stream by section.
|
||||
state->ringall = iszero( eye );
|
||||
state->sectionmode = ( false == state->ringall );
|
||||
WriteLog( std::string( "Progressive visual load: " )
|
||||
+ ( state->ringall ?
|
||||
"no camera centre -- building all visual nodes in one pass" :
|
||||
"ring centre at " + std::to_string( eye.x ) + " " + std::to_string( eye.y ) + " " + std::to_string( eye.z ) + " (from " + src + ")" ) );
|
||||
"streaming sections within " + std::to_string( static_cast<int>( STREAM_RADIUS ) ) + "m of the camera (from " + src + ")" ) );
|
||||
}
|
||||
m_ringall = state->ringall;
|
||||
m_ringindex = state->ringindex;
|
||||
m_sectionmode = state->sectionmode;
|
||||
m_shapes_built = state->shapes_built;
|
||||
m_ringeye = state->ringeye;
|
||||
m_ringmin2 = ring_min2( state->ringindex );
|
||||
m_ringmax2 = ring_max2( state->ringindex );
|
||||
m_tobuild = &state->tobuild;
|
||||
|
||||
// section streaming. the first pass replays the whole twin once, indexing every deferred
|
||||
// node under its section while building the spawn area. afterwards (state->indexed) the
|
||||
// sections the camera moves into are rebuilt by seeking straight to their nodes -- no more
|
||||
// whole-twin re-scans, which is what was tanking fps / dragging on a million-node scenery.
|
||||
if( true == state->sectionmode ) {
|
||||
if( true == state->indexed ) {
|
||||
if( true == state->tobuild.empty() ) {
|
||||
if( 0 == wanted_sections( Global.pCamera.Pos, state->built, state->tobuild ) ) {
|
||||
return true; // surroundings already built; stay alive for camera moves
|
||||
}
|
||||
}
|
||||
m_rebuilding = true;
|
||||
m_state = state.get();
|
||||
auto streamstart { std::chrono::steady_clock::now() };
|
||||
while( false == state->tobuild.empty() ) {
|
||||
int const sec = *state->tobuild.begin();
|
||||
state->tobuild.erase( state->tobuild.begin() );
|
||||
rebuild_section( *state, sec );
|
||||
state->built.insert( sec );
|
||||
if( std::chrono::duration_cast<std::chrono::milliseconds>( std::chrono::steady_clock::now() - streamstart ).count() >= VISUAL_BUDGET_MS ) { break; }
|
||||
}
|
||||
m_rebuilding = false;
|
||||
return true;
|
||||
}
|
||||
// first pass: index every deferred node while building the spawn area
|
||||
m_indexing = true;
|
||||
m_state = state.get();
|
||||
if( false == state->pass_active ) {
|
||||
wanted_sections( Global.pCamera.Pos, state->built, state->tobuild );
|
||||
Input.restartReplay( scene::scenery_load_pass::visual );
|
||||
resettransform();
|
||||
state->pass_active = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// stateful directives that build objects/lists; on the visual (second) pass they are
|
||||
@@ -195,7 +268,10 @@ state_serializer::deserialize_continue(std::shared_ptr<deserializer_state> state
|
||||
{ "terrain", "endterrain" },
|
||||
};
|
||||
|
||||
// deserialize content from the provided input
|
||||
// deserialize content from the provided input. modest budget while streaming visuals in the
|
||||
// driver (rendering is live, so a big slice would tank fps), generous budget while the loading
|
||||
// screen is up (infrastructure pass, nothing rendering yet).
|
||||
int const budget { state->visualphase ? VISUAL_BUDGET_MS : 200 };
|
||||
auto timelast { std::chrono::steady_clock::now() };
|
||||
std::string token { Input.getToken<std::string>() };
|
||||
while( false == token.empty() ) {
|
||||
@@ -208,21 +284,28 @@ state_serializer::deserialize_continue(std::shared_ptr<deserializer_state> state
|
||||
token = Input.getToken<std::string>();
|
||||
continue;
|
||||
}
|
||||
// fast camera-ring skip: a visual model node carries its local position in its v7
|
||||
// marker, so we can distance-test it and drop it in O(1) -- without deserialize_node
|
||||
// decoding any of its tokens. this is what keeps the per-ring replay cheap when a
|
||||
// scenery has a million flora instances. (shapes/older twins have no marker
|
||||
// position; they fall through to deserialize_node, which ring-tests them itself.)
|
||||
if( ( false == m_ringall ) && ( token == "node" ) ) {
|
||||
double x, y, z;
|
||||
if( true == Input.currentNodePosition( x, y, z ) ) {
|
||||
// fast section skip: a visual model node carries its local position in its v7
|
||||
// marker, so we can section-test it and drop it in O(1) -- without deserialize_node
|
||||
// decoding any of its tokens. this is what keeps the per-cycle replay cheap when a
|
||||
// scenery has a million flora instances. (shapes/older twins have no marker position;
|
||||
// they fall through to deserialize_node, which section-tests them itself.)
|
||||
if( ( true == m_sectionmode ) && ( token == "node" ) ) {
|
||||
double x, y, z, range;
|
||||
if( true == Input.currentNodePosition( x, y, z, range ) ) {
|
||||
auto const world { transform( glm::dvec3{ x, y, z }, Scratchpad ) };
|
||||
auto const d { world - m_ringeye };
|
||||
auto const d2 { d.x * d.x + d.y * d.y + d.z * d.z };
|
||||
if( ( ( d2 < m_ringmin2 ) || ( d2 >= m_ringmax2 ) )
|
||||
// a model visible from beyond the stream radius (large/unlimited range_max) is
|
||||
// built once, up front, regardless of section -- otherwise it would vanish at
|
||||
// distance. the rest are indexed and built when their section comes into range.
|
||||
bool const eager { ( range < 0.0 ) || ( range > STREAM_RADIUS ) };
|
||||
if( ( true == m_indexing ) && ( false == eager ) ) { capture_node( Input, Scratchpad, world ); }
|
||||
bool const wanted {
|
||||
eager ?
|
||||
( false == m_shapes_built ) :
|
||||
( 0 != m_tobuild->count( section_index( world ) ) ) };
|
||||
if( ( false == wanted )
|
||||
&& ( true == Input.skipReplayNode() ) ) {
|
||||
auto timenow = std::chrono::steady_clock::now();
|
||||
if( std::chrono::duration_cast<std::chrono::milliseconds>( timenow - timelast ).count() >= VISUAL_BUDGET_MS ) {
|
||||
if( std::chrono::duration_cast<std::chrono::milliseconds>( timenow - timelast ).count() >= budget ) {
|
||||
Application.set_progress( Input.getProgress(), Input.getFullProgress() );
|
||||
return true;
|
||||
}
|
||||
@@ -242,9 +325,6 @@ state_serializer::deserialize_continue(std::shared_ptr<deserializer_state> state
|
||||
}
|
||||
|
||||
auto timenow = std::chrono::steady_clock::now();
|
||||
// per-frame budget while streaming visuals in the driver (kept modest to limit
|
||||
// stutter), generous budget while the loading screen is up (infrastructure pass)
|
||||
auto const budget = ( state->visualphase ? VISUAL_BUDGET_MS : 200 );
|
||||
if( std::chrono::duration_cast<std::chrono::milliseconds>( timenow - timelast ).count() >= budget ) {
|
||||
Application.set_progress( Input.getProgress(), Input.getFullProgress() );
|
||||
return true;
|
||||
@@ -258,67 +338,116 @@ state_serializer::deserialize_continue(std::shared_ptr<deserializer_state> state
|
||||
deserialize_firstinit( Input, Scratchpad );
|
||||
}
|
||||
|
||||
// helper: reset the transform stack before each replay pass. the directives
|
||||
// (origin/rotate/scale) are replayed in order, so resetting here reproduces the
|
||||
// single-pass placement exactly; without it an unbalanced origin left on the stack would
|
||||
// be applied again and shift every deferred visual node ("terrain dumped beside the tracks").
|
||||
auto const resettransform = [ &Scratchpad ]() {
|
||||
// a well-formed pass ends with a balanced (empty) transform stack; a leftover means
|
||||
// an origin/scale was pushed but never popped -- e.g. a node whose binary marker span
|
||||
// over-ran its terminator and skipped the following endorigin. warn rather than let it
|
||||
// silently accumulate into the next pass.
|
||||
if( false == Scratchpad.location.offset.empty() ) {
|
||||
WriteLog( "Bad scenery: " + std::to_string( Scratchpad.location.offset.size() ) + " unbalanced origin(s) left on the stack at end of a load pass" );
|
||||
}
|
||||
Scratchpad.location.offset = {};
|
||||
Scratchpad.location.scale = {};
|
||||
Scratchpad.location.rotation = glm::vec3{}; };
|
||||
// helper: make the scenario playable / persist the twin. the map, instance-bound events and
|
||||
// twin flush are done once, after the first cycle (or the single build-all pass). the active
|
||||
// group stack is left open on purpose in section mode -- later cycles keep inserting into it
|
||||
// (update_map reads the persistent group map, not the stack, so it works either way).
|
||||
auto const finalize = [ & ]( bool const Closegroups ) {
|
||||
if( true == Closegroups ) { scene::Groups.close(); }
|
||||
scene::Groups.update_map();
|
||||
Region->create_map_geometry();
|
||||
simulation::Events.InitInstanceEvents();
|
||||
Input.flushBinaryTwin();
|
||||
scene::scenerybinary_wait_all(); };
|
||||
|
||||
// first (infrastructure) pass finished: the scenario is now playable (tracks, events,
|
||||
// signals, the player train are all loaded). hand control back so the loader can switch
|
||||
// to the driver; the visual nodes load progressively from the driver, replayed once per
|
||||
// camera-distance ring (nearest first). the ring centre is sampled later, on the first
|
||||
// driver pass (the camera isn't positioned here yet). only possible when replaying a binary
|
||||
// twin -- a text/compile load did everything in one pass (restartReplay returns false).
|
||||
// signals, the player train are all loaded). hand control back so the loader can switch to
|
||||
// the driver; the visual nodes stream in from the driver. the camera centre / mode are
|
||||
// resolved later, on the first driver pass (the camera isn't positioned here yet). only
|
||||
// possible when replaying a binary twin -- a text/compile load did everything in one pass.
|
||||
if( ( false == state->visualphase )
|
||||
&& ( true == Input.restartReplay( scene::scenery_load_pass::visual ) ) ) {
|
||||
state->visualphase = true;
|
||||
state->ringindex = 0;
|
||||
resettransform();
|
||||
WriteLog( "Progressive visual load: streaming deferred nodes nearest-camera first (" + std::to_string( RING_COUNT ) + " rings)" );
|
||||
WriteLog( "Progressive visual load: infrastructure ready, streaming visuals from the driver" );
|
||||
return false; // infrastructure ready -> go to driver; visuals continue there
|
||||
}
|
||||
|
||||
// a ring pass finished: advance to the next (farther) ring and replay again, until the
|
||||
// outermost ring has been built. skipped in build-all mode, which builds everything in one pass.
|
||||
if( ( true == state->visualphase )
|
||||
&& ( false == state->ringall )
|
||||
&& ( state->ringindex < ring_lastindex() )
|
||||
&& ( true == Input.restartReplay( scene::scenery_load_pass::visual ) ) ) {
|
||||
++state->ringindex;
|
||||
resettransform();
|
||||
return true; // more rings to build
|
||||
// section streaming: a build cycle's replay pass just finished. mark its sections built so
|
||||
// they aren't rebuilt, finalize once after the first cycle, and stay alive so the next call
|
||||
// can pick up sections the camera has since moved into. the load never reports "done".
|
||||
if( ( true == state->visualphase ) && ( true == state->sectionmode ) ) {
|
||||
if( true == state->pass_active ) {
|
||||
state->built.insert( std::begin( state->tobuild ), std::end( state->tobuild ) );
|
||||
state->tobuild.clear();
|
||||
state->pass_active = false;
|
||||
state->shapes_built = true; // explicit shapes + eager models are built in the first pass
|
||||
m_indexing = false;
|
||||
if( false == state->initial_done ) {
|
||||
finalize( /*Closegroups*/ false ); // keep groups open for later cycles
|
||||
state->initial_done = true;
|
||||
state->indexed = true; // the first pass has indexed every deferred node by section
|
||||
std::size_t refs = 0; for( auto const &s : state->index ) { refs += s.second.size(); }
|
||||
WriteLog( "Progressive visual load: spawn ready (" + std::to_string( simulation::Instances.sequence().size() )
|
||||
+ " instances), " + std::to_string( refs ) + " nodes indexed across " + std::to_string( state->index.size() ) + " sections" );
|
||||
}
|
||||
}
|
||||
return true; // keep streaming alive; sections the camera enters are served from the index
|
||||
}
|
||||
|
||||
scene::Groups.close();
|
||||
// build-all (no camera centre, e.g. ghostview): everything was built in this single pass.
|
||||
finalize( /*Closegroups*/ true );
|
||||
state->done = true;
|
||||
return false;
|
||||
}
|
||||
|
||||
scene::Groups.update_map();
|
||||
Region->create_map_geometry();
|
||||
int
|
||||
state_serializer::twin_id( deserializer_state &State, std::string const &File, std::string const &Path ) {
|
||||
std::string key = Path + "|" + File;
|
||||
auto const it = State.twinids.find( key );
|
||||
if( it != State.twinids.end() ) { return it->second; }
|
||||
int const id = static_cast<int>( State.twins.size() );
|
||||
State.twins.emplace_back( File, Path );
|
||||
State.twinids.emplace( std::move( key ), id );
|
||||
return id;
|
||||
}
|
||||
|
||||
// all nodes (including visual model instances) are now loaded, so initialise the
|
||||
// events that bind to model instances; in a single-pass (text/compile) load nothing
|
||||
// was deferred, but InitEvents() still skipped them, so do it here too
|
||||
simulation::Events.InitInstanceEvents();
|
||||
void
|
||||
state_serializer::capture_node( cParser &Input, scene::scratch_data const &Scratchpad, glm::dvec3 const &World ) {
|
||||
// record where the node lives and the context it needs, so rebuild_section() can place it
|
||||
// identically later without re-scanning the twin. only small-range nodes are indexed; large/
|
||||
// unlimited-range ("eager") ones are built once up front and never streamed again.
|
||||
if( m_state == nullptr ) { return; }
|
||||
visual_ref ref;
|
||||
ref.twin = twin_id( *m_state, Input.currentReplayFile(), Input.currentReplayPath() );
|
||||
ref.offset = Input.currentReplayOffset();
|
||||
ref.has_offset = ( false == Scratchpad.location.offset.empty() );
|
||||
if( true == ref.has_offset ) { ref.t_offset = Scratchpad.location.offset.top(); }
|
||||
ref.has_scale = ( false == Scratchpad.location.scale.empty() );
|
||||
if( true == ref.has_scale ) { ref.t_scale = Scratchpad.location.scale.top(); }
|
||||
ref.t_rotation = Scratchpad.location.rotation;
|
||||
ref.params = Input.currentReplayParams();
|
||||
m_state->index[ section_index( World ) ].emplace_back( std::move( ref ) );
|
||||
}
|
||||
|
||||
// loading finished: flush the top-level scenario's binary twin now rather than
|
||||
// waiting for the parser to be destroyed (the loader keeps the state around)
|
||||
Input.flushBinaryTwin();
|
||||
// wait out any background twin writes (includes) so they are complete and logged
|
||||
// before we report the scenario as loaded
|
||||
scene::scenerybinary_wait_all();
|
||||
|
||||
state->done = true;
|
||||
return false;
|
||||
void
|
||||
state_serializer::rebuild_section( deserializer_state &State, int Section ) {
|
||||
auto const it = State.index.find( Section );
|
||||
if( it == State.index.end() ) { return; }
|
||||
scene::scratch_data &Scratchpad = State.scratchpad;
|
||||
for( auto &ref : it->second ) {
|
||||
// reuse one parser per twin across the whole stream (re-opening an .inc is not free)
|
||||
auto pit = State.rebuild_parsers.find( ref.twin );
|
||||
if( pit == State.rebuild_parsers.end() ) {
|
||||
auto const &tw = State.twins[ ref.twin ];
|
||||
pit = State.rebuild_parsers.emplace(
|
||||
ref.twin,
|
||||
std::make_unique<cParser>( tw.first, cParser::buffer_FILE, tw.second, Global.bLoadTraction ) ).first;
|
||||
}
|
||||
cParser &cp = *pit->second;
|
||||
cp.seekReplayNode( ref.offset );
|
||||
cp.setReplayParams( ref.params );
|
||||
// restore the transform context captured for this node
|
||||
Scratchpad.location.offset = {};
|
||||
if( true == ref.has_offset ) { Scratchpad.location.offset.push( ref.t_offset ); }
|
||||
Scratchpad.location.scale = {};
|
||||
if( true == ref.has_scale ) { Scratchpad.location.scale.push( ref.t_scale ); }
|
||||
Scratchpad.location.rotation = ref.t_rotation;
|
||||
auto const tok = cp.getToken<std::string>();
|
||||
if( tok == "node" ) { deserialize_node( cp, Scratchpad ); }
|
||||
}
|
||||
// the section is built; release its index entries
|
||||
std::vector<visual_ref>().swap( it->second );
|
||||
}
|
||||
|
||||
void
|
||||
@@ -703,12 +832,23 @@ state_serializer::deserialize_node( cParser &Input, scene::scratch_data &Scratch
|
||||
|| ( nodedata.type == "triangle_strip" )
|
||||
|| ( nodedata.type == "triangle_fan" ) ) {
|
||||
|
||||
// explicit shapes have no single position to ring-test by, so build them only in the
|
||||
// nearest ring pass (ring 0) and skip them in O(1) on the farther passes (build-all
|
||||
// mode is a single pass, so they always build there)
|
||||
if( ( true == m_ringactive ) && ( false == m_ringall ) && ( m_ringindex > 0 ) ) {
|
||||
if( false == Input.skipReplayNode() ) { skip_until( Input, "endtri" ); }
|
||||
return;
|
||||
// origin-placed shapes (e.g. flora includes) carry their world position in the active
|
||||
// origin, so they section-stream like models: indexed on the first pass, rebuilt per
|
||||
// section after. absolute shapes (terrain, no origin) have no single position -> built
|
||||
// once in the first pass. (m_rebuilding: chosen from the index -> build unconditionally.)
|
||||
if( ( true == m_sectionmode ) && ( false == m_rebuilding ) && ( nullptr != m_tobuild ) ) {
|
||||
if( false == Scratchpad.location.offset.empty() ) {
|
||||
glm::dvec3 const world { Scratchpad.location.offset.top() };
|
||||
if( true == m_indexing ) { capture_node( Input, Scratchpad, world ); }
|
||||
if( 0 == m_tobuild->count( section_index( world ) ) ) {
|
||||
if( false == Input.skipReplayNode() ) { skip_until( Input, "endtri" ); }
|
||||
return;
|
||||
}
|
||||
}
|
||||
else if( true == m_shapes_built ) {
|
||||
if( false == Input.skipReplayNode() ) { skip_until( Input, "endtri" ); }
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
auto const skip {
|
||||
@@ -734,10 +874,20 @@ state_serializer::deserialize_node( cParser &Input, scene::scratch_data &Scratch
|
||||
|| ( nodedata.type == "line_strip" )
|
||||
|| ( nodedata.type == "line_loop" ) ) {
|
||||
|
||||
// see the triangles branch: explicit shapes build in ring 0 only (or always, build-all)
|
||||
if( ( true == m_ringactive ) && ( false == m_ringall ) && ( m_ringindex > 0 ) ) {
|
||||
if( false == Input.skipReplayNode() ) { skip_until( Input, "endline" ); }
|
||||
return;
|
||||
// see the triangles branch: origin-placed lines section-stream, absolute ones build once.
|
||||
if( ( true == m_sectionmode ) && ( false == m_rebuilding ) && ( nullptr != m_tobuild ) ) {
|
||||
if( false == Scratchpad.location.offset.empty() ) {
|
||||
glm::dvec3 const world { Scratchpad.location.offset.top() };
|
||||
if( true == m_indexing ) { capture_node( Input, Scratchpad, world ); }
|
||||
if( 0 == m_tobuild->count( section_index( world ) ) ) {
|
||||
if( false == Input.skipReplayNode() ) { skip_until( Input, "endline" ); }
|
||||
return;
|
||||
}
|
||||
}
|
||||
else if( true == m_shapes_built ) {
|
||||
if( false == Input.skipReplayNode() ) { skip_until( Input, "endline" ); }
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
simulation::Region->insert(
|
||||
@@ -1085,21 +1235,28 @@ state_serializer::deserialize_model( cParser &Input, scene::scratch_data &Scratc
|
||||
>> location.z
|
||||
>> rotation.y;
|
||||
|
||||
// camera-ring visual streaming: build this model only if it falls in the ring currently
|
||||
// being streamed; otherwise skip the rest of its body in O(1) and let a later (farther)
|
||||
// ring pass pick it up. covers terrain models (range_min<0) too -- they also have X Y Z.
|
||||
// most out-of-ring models are already dropped O(1) at the dispatch loop via their v7
|
||||
// marker; this is the fallback for nodes that reached here (in-ring, or no marker pos).
|
||||
// use the marker position when present so this ring decision matches the dispatch one
|
||||
// exactly -- otherwise a node near a ring boundary could be dropped by both adjacent rings.
|
||||
if( ( true == m_ringactive ) && ( false == m_ringall ) ) {
|
||||
glm::dvec3 ringlocal { location };
|
||||
double mx, my, mz;
|
||||
if( true == Input.currentNodePosition( mx, my, mz ) ) { ringlocal = glm::dvec3{ mx, my, mz }; }
|
||||
auto const world { transform( ringlocal, Scratchpad ) };
|
||||
auto const d { world - m_ringeye };
|
||||
auto const d2 { d.x * d.x + d.y * d.y + d.z * d.z };
|
||||
if( ( d2 < m_ringmin2 ) || ( d2 >= m_ringmax2 ) ) {
|
||||
// camera-following visual streaming: build this model only if its region section is in the
|
||||
// set being built this cycle; otherwise skip the rest of its body in O(1) and let a later
|
||||
// cycle (once the camera is near) pick it up. covers terrain models (range_min<0) too -- they
|
||||
// also have X Y Z. most out-of-range models are already dropped O(1) at the dispatch loop via
|
||||
// their v7 marker; this is the fallback for nodes that reached here (in-range, or no marker).
|
||||
// use the marker position when present so this decision matches the dispatch one exactly.
|
||||
// m_rebuilding: this node was chosen from the section index, so build it unconditionally.
|
||||
if( ( true == m_sectionmode ) && ( false == m_rebuilding ) && ( nullptr != m_tobuild ) ) {
|
||||
// models visible from beyond the stream radius build once (first cycle); the rest build
|
||||
// when their section is in range. mirrors the dispatch-loop fast path exactly.
|
||||
bool const eager { ( Nodedata.range_max < 0.0 ) || ( Nodedata.range_max > STREAM_RADIUS ) };
|
||||
bool wanted;
|
||||
if( true == eager ) {
|
||||
wanted = ( false == m_shapes_built );
|
||||
}
|
||||
else {
|
||||
glm::dvec3 modellocal { location };
|
||||
double mx, my, mz, mr;
|
||||
if( true == Input.currentNodePosition( mx, my, mz, mr ) ) { modellocal = glm::dvec3{ mx, my, mz }; }
|
||||
wanted = ( 0 != m_tobuild->count( section_index( transform( modellocal, Scratchpad ) ) ) );
|
||||
}
|
||||
if( false == wanted ) {
|
||||
if( false == Input.skipReplayNode() ) { skip_until( Input, "endmodel" ); }
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user