From 04c0a0365924a7ea7778264228588561f8ef16c6 Mon Sep 17 00:00:00 2001 From: maj00r Date: Thu, 25 Jun 2026 22:13:27 +0200 Subject: [PATCH] Build the spawn surroundings on the loading screen, centred on the spawn Two bugs left the terrain and models around the camera unbuilt on a big scenery: - the first visual pass centred its section set on Global.pCamera.Pos, which the driver hasn't positioned yet during load (reads 0,0,0) -> it built empty sections at the origin and the spawn stayed bare. Centre on the sampled eye (player / first vehicle) instead. - the first (spawn) pass ran in the driver at a small per-frame budget on a live, low-fps scene, so it crawled and never finished. Run it on the loading screen (generous budget, nothing rendering), then hand to the driver once the spawn is ready; the driver streams the rest as the camera moves. Also drops the per-node capture index: capturing params/paths for a million nodes cost more than the rescans it saved. Every cycle is now a plain scan-and-build of the wanted sections (it finishes; a bake-time section index is the real cure for the remaining first-pass scan time). Result on tomaszewo: spawn now builds + renders (41k instances, ~hundreds of fps once loaded). Remaining: load time is long (the 1M-node scan) -- next targets are the getToken scan, the 380us/model build, and the 13s create_map_geometry finalize. Adds load + frame profilers (behind WriteLog) used to find all of the above. --- rendering/opengl33renderer.cpp | 4 ++ simulation/simulationstateserializer.cpp | 65 ++++++++++-------------- 2 files changed, 30 insertions(+), 39 deletions(-) diff --git a/rendering/opengl33renderer.cpp b/rendering/opengl33renderer.cpp index d06721f7..c0bd5d18 100644 --- a/rendering/opengl33renderer.cpp +++ b/rendering/opengl33renderer.cpp @@ -681,6 +681,10 @@ void opengl33_renderer::SwapBuffers() + " traction: " + to_string( m_colorpass.draw_stats.traction, 7 ) + "\n" + " lines: " + to_string( m_colorpass.draw_stats.lines, 7 ) + "\n" + "particles: " + to_string( m_colorpass.draw_stats.particles, 7 ); + + // profile: dump the steady-state frame breakdown so the fps bottleneck is measured, not guessed + { static int s_f = 0; if( ( ++s_f % 120 ) == 0 ) { + WriteLog( "=== FRAME PROFILE ===\n" + m_debugtimestext + "\n" + m_debugstatstext ); } } } void opengl33_renderer::draw_debug_ui() diff --git a/simulation/simulationstateserializer.cpp b/simulation/simulationstateserializer.cpp index 1eb410f9..4c2c0c77 100644 --- a/simulation/simulationstateserializer.cpp +++ b/simulation/simulationstateserializer.cpp @@ -252,35 +252,21 @@ state_serializer::deserialize_continue(std::shared_ptr state m_ringeye = state->ringeye; 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. + // section streaming: each cycle replays the visual twin once, building the nodes whose + // section is wanted (within range of the centre) and skipping the rest in O(1). a new + // cycle starts whenever the camera has moved into sections not yet built. NOTE: capturing + // a per-node index to avoid these re-scans was tried but the capture itself (params/paths + // for a million nodes) cost more than the scans it saved -- a bake-time section index is + // the real fix; for now the plain scan at least finishes and shows the spawn. 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::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 ); + // centre on the spawn (player/first vehicle) until the game starts, then follow the + // live driver camera. NOT the live camera during the first pass -- it isn't + // positioned yet (reads (0,0,0)), which built empty sections and left the spawn bare. + glm::dvec3 const eye { ( false == state->initial_done ) ? state->ringeye : Global.pCamera.Pos }; + if( 0 == wanted_sections( eye, state->built, state->tobuild ) ) { + return true; // nothing new in range; stay alive for camera moves + } Input.restartReplay( scene::scenery_load_pass::visual ); resettransform(); state->pass_active = true; @@ -304,10 +290,10 @@ state_serializer::deserialize_continue(std::shared_ptr state { "terrain", "endterrain" }, }; - // 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 }; + // deserialize content from the provided input. generous budget until the spawn is built (the + // loading screen is up: infra pass + first visual pass, nothing rendering yet), then a modest + // budget once streaming in the driver, where a big slice would tank fps on a live scene. + int const budget { state->indexed ? VISUAL_BUDGET_MS : 200 }; auto timelast { std::chrono::steady_clock::now() }; std::string token { Input.getToken() }; while( false == token.empty() ) { @@ -400,8 +386,11 @@ state_serializer::deserialize_continue(std::shared_ptr state resettransform(); g_profile.log( "infrastructure" ); g_profile.reset(); // measure the visual phase separately - WriteLog( "Progressive visual load: infrastructure ready, streaming visuals from the driver" ); - return false; // infrastructure ready -> go to driver; visuals continue there + WriteLog( "Progressive visual load: infrastructure ready, building spawn surroundings" ); + // stay on the loading screen for the first (spawn) pass: it's budgeted generously there + // (nothing rendering) so the surroundings are built fast, instead of crawling in the driver + // at a small budget on a low-fps scene. control passes to the driver once the spawn is ready. + return true; } // section streaming: a build cycle's replay pass just finished. mark its sections built so @@ -413,19 +402,17 @@ state_serializer::deserialize_continue(std::shared_ptr state 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" ); + state->indexed = true; // first (spawn) pass done -> switch to the modest driver budget + WriteLog( "Progressive visual load: spawn ready (" + std::to_string( simulation::Instances.sequence().size() ) + " instances)" ); g_profile.log( "visual first pass" ); g_profile.reset(); + return false; // spawn built on the loading screen -> hand to the driver; stream the rest there } } - return true; // keep streaming alive; sections the camera enters are served from the index + return true; // keep streaming alive; sections the camera enters are rebuilt as it moves } // build-all (no camera centre, e.g. ghostview): everything was built in this single pass.