From 64dedb9d537c74a28192c3694e093695cc940ec1 Mon Sep 17 00:00:00 2001 From: Hirek193 Date: Sun, 18 Oct 2026 21:00:54 +0200 Subject: [PATCH] Record the opaque scene once and replay it for the depth prepass and the colour pass --- docs/opengl46-renderer.md | 19 +- gl/streambuffer.cpp | 20 +- gl/streambuffer.h | 6 +- rendering/opengl46/opengl46renderer.cpp | 528 ++++++++++++++++++++++-- rendering/opengl46/opengl46renderer.h | 84 +++- utilities/Globals.cpp | 6 + utilities/Globals.h | 4 + 7 files changed, 616 insertions(+), 51 deletions(-) diff --git a/docs/opengl46-renderer.md b/docs/opengl46-renderer.md index b579f7ac..d70e4222 100644 --- a/docs/opengl46-renderer.md +++ b/docs/opengl46-renderer.md @@ -279,6 +279,7 @@ view; the shadow maps of lamps and headlights, the cab and other views go as bef | `gfx.gpucull.reflections yes/no` | `yes` | with `gfx.gpucull yes` or `compare`: the models of the faces of the environment map too (its own range of models, as before) | | `gfx.gpucull.sleepers yes/no` | `yes` | with `gfx.gpucull yes` or `compare`: the sleepers of the tracks (`sleepermodel`) of the main view too, each one tested against the view and given its level of detail by its distance, as before (the tracks within `sleeperdistance` are chosen as before); they also take the occlusion test, the depth prepass and the two phases of the models | | `gfx.depthprepass.scene yes/no` | `no` | the depth of the whole opaque scene of the main view (terrain, tracks, buildings, models, vehicles, the cab) is drawn before its colour, see below | +| `gfx.depthprepass.replay yes/no` | `yes` | the scene is gone through once for its depth and its colour: what the colour pass draws is recorded and both are drawn from the record, see below. `no`: twice, as before | With `gfx.gpucull.occlusion.instances yes` the test goes in two phases: the models seen in the previous frame are drawn first (their depth, or their colour without the depth prepass), the depth they leave is added to what the others are tested @@ -316,6 +317,19 @@ walk and drawn from the same result in the second. The picture is the same eithe drawn last, after the precipitation) the cab isn't in the depth pass. Can be changed while the simulator runs, in the **Instance culling** debug window. +With `gfx.depthprepass.replay yes` (the default) the scene isn't gone through twice: the walk of the colour pass is made +once, in the depth pass, and every opaque draw it would make is recorded with what it needs (the data of the model part, the +material and its texture variant, the light of tunnels and of the cab). The depth is drawn from the record in runs of the +same kind (with or without the alpha test, the texture of the test, the model or section the geometry is kept in), each run +with one multi draw, and the colour pass then draws the record in the order of the walk, with the programs of the materials. +The depth is drawn before the depth pyramid of the occlusion test of the models (`gfx.gpucull.occlusion`) and before the +cursor of the editor reads the ground, so they see the same as before. When the depth pass is there only for the contact +shadows (`gfx.depthprepass.scene no`), the parts lying whole farther than the reach of the contact shadows +(`gfx.contactshadows.range` and a little more) are left out of it and only drawn in colour, as they would be without the depth +pass; the copies of models drawn from the GPU always have their depth drawn. With fewer parts in the depth the occlusion test +of the models has less to hide them behind past that reach. In the editor the whole depth is drawn. The picture is the same +either way; `no` walks the scene twice, as before, for comparison. + ### Model data of the draws Before every draw the renderer hands the graphics card the data of the drawn part of the model (its position in the view, its @@ -394,7 +408,10 @@ editor goes through the first `dynamiclights` headlights, the reflections throug `instance cull shadows 1:` … and `instance cull reflections, last face:`. With `gfx.gpucull.sleepers` the row `gpu cull:` is followed by how many sleepers of how many tracks were queued (they are counted among the models). - With `gfx.depthprepass.scene yes` Shift+F12 shows the `scene depth:` row (draws of the depth pass and its processor time) - and the timing `scene depth:` (its GPU time). + and the timing `scene depth:` (its GPU time). With `gfx.depthprepass.replay yes` the row tells how many of the recorded + parts (of models, of the scenery; neighbouring tracks count one by one) had their depth drawn, in how many calls, how many + were left beyond the reach of the contact shadows, and the processor time of the depth pass, which holds the walk of the + colour pass now (the `color:` time is the one to compare). - With `gfx.contactshadows` on Shift+F12 shows the `contact shadows:` row (the level, the length and reach of the rays, or `off in this view`), the `scene depth:` row and timing of the depth pass they turn on, and the timing `contact shadows depth:` (copying the depth for them). The rays themselves are part of the shading of the colour pass: their cost shows as the diff --git a/gl/streambuffer.cpp b/gl/streambuffer.cpp index 039ce6c7..38c6dbe2 100644 --- a/gl/streambuffer.cpp +++ b/gl/streambuffer.cpp @@ -42,6 +42,11 @@ gl::frame_stream::~frame_stream() { release(slot); } + for (auto &retired : m_retired) + { + if (!retired.empty()) + glDeleteBuffers(static_cast(retired.size()), retired.data()); + } } void gl::frame_stream::create(slot &Slot, GLsizeiptr Size) @@ -125,6 +130,13 @@ void gl::frame_stream::next_frame() } m_slot = (m_slot + 1) % FRAMES; wait(m_slots[m_slot]); + // the buffers this slot outgrew in its last frame, which the gpu is done with now + auto &retired{m_retired[m_slot]}; + if (!retired.empty()) + { + glDeleteBuffers(static_cast(retired.size()), retired.data()); + retired.clear(); + } m_fill = m_start; } @@ -139,8 +151,12 @@ GLintptr gl::frame_stream::push(void const *Data, GLsizeiptr Size) if (size > current->size) { // a bigger buffer for the rest of the frame, starting after its header again. the fence of the slot (if any) was - // waited for when the frame took the slot over, the old buffer is only read by the draws of this frame - release(*current); + // waited for when the frame took the slot over, the old buffer is only read by this frame, which may still bind it + // for what it wrote there (a recorded draw drawn later), so it's kept, mapped, until the slot is taken over again + m_retired[m_slot].push_back(current->buffer); + current->buffer = 0; + current->data = nullptr; + current->size = 0; create(*current, size); ++m_grows; } diff --git a/gl/streambuffer.h b/gl/streambuffer.h index 4fc9d26d..e8363750 100644 --- a/gl/streambuffer.h +++ b/gl/streambuffer.h @@ -31,8 +31,8 @@ namespace gl // ends the frame: what was written so far goes with its fence, the next frame writes to the next buffer void next_frame(); // appends Size bytes to the buffer of the frame and returns their offset. may replace the buffer of the frame with a - // bigger one (generation() changes, buffer() tells which to bind); the draws issued before keep the old one until the - // gpu is done with them + // bigger one (generation() changes, buffer() tells which to bind); the old one stays until the frame is done with, so + // what was written to it can still be bound and drawn from later in the frame GLintptr push(void const *Data, GLsizeiptr Size); GLuint buffer() const @@ -83,6 +83,8 @@ namespace gl void wait(slot &Slot); std::array m_slots; + // buffers of the slots replaced by bigger ones in their frame, deleted when the slot is taken over again + std::array, FRAMES> m_retired; std::size_t m_slot = 0; GLsizeiptr m_alignment; bool m_mapped; // buffers made from now on are mapped diff --git a/rendering/opengl46/opengl46renderer.cpp b/rendering/opengl46/opengl46renderer.cpp index b26c52dc..ee0a3734 100644 --- a/rendering/opengl46/opengl46renderer.cpp +++ b/rendering/opengl46/opengl46renderer.cpp @@ -214,6 +214,9 @@ bool opengl46_renderer::Init(GLFWwindow *Window) m_alpha_shadow_multidraw_shader = std::make_unique(std::vector>{shadowvertex, alphashadowfragment}); m_drawblockstream = std::make_unique( static_cast(4096 * sizeof(gl::model_ubs)), static_cast(sizeof(gl::model_ubs)), Global.gfx_bufferstreaming); + // and the commands of the multi draws of the recorded depth pass of the scene (Render_Scene_Prepass()) + m_scenecommandstream = std::make_unique( + static_cast(4096 * sizeof(gl::instance_draw_command)), static_cast(sizeof(gl::instance_draw_command)), Global.gfx_bufferstreaming); } catch (gl::shader_exception const &e) { @@ -224,6 +227,7 @@ bool opengl46_renderer::Init(GLFWwindow *Window) m_shadow_multidraw_shader.reset(); m_alpha_shadow_multidraw_shader.reset(); m_drawblockstream.reset(); + m_scenecommandstream.reset(); } try @@ -712,6 +716,8 @@ bool opengl46_renderer::Render() model_ubo->next_frame(); if (m_drawblockstream) m_drawblockstream->next_frame(); + if (m_scenecommandstream) + m_scenecommandstream->next_frame(); m_renderpass.draw_mode = rendermode::none; // force setup anew m_renderpass.draw_stats = debug_stats(); @@ -851,7 +857,12 @@ void opengl46_renderer::SwapBuffers() + " =" + to_string( m_colorpass.draw_stats.instances + shadowstats.instances, 7 ) + " (" + std::to_string( m_colorpass.draw_stats.instanced_drawcalls + shadowstats.instanced_drawcalls ) + " batches)\n" + ( ( Global.gfx_depthprepass_scene || m_contactshadowslast ) ? - " scene depth: " + std::to_string( m_scenedepthstats.drawcalls ) + " draws, cpu " + to_string( m_scenedepthcputime, 2 ) + " ms\n" : + ( m_scenereplayed ? + " scene depth: " + std::to_string( m_sceneprepassdraws ) + " of " + std::to_string( m_scenerecordedparts ) + " recorded parts in " + + std::to_string( m_sceneprepasscalls ) + " calls" + + ( m_scenebeyondreach > 0 ? ", " + std::to_string( m_scenebeyondreach ) + " beyond the contact shadows" : std::string{} ) + + ", cpu " + to_string( m_scenedepthcputime, 2 ) + " ms with the walk of the colour pass\n" : + " scene depth: " + std::to_string( m_scenedepthstats.drawcalls ) + " draws, cpu " + to_string( m_scenedepthcputime, 2 ) + " ms\n" ) : std::string{} ) + ( Global.gfx_contactshadows > 0 ? " contact shadows: " + std::string{ Global.gfx_contactshadows == 3 ? "high" : Global.gfx_contactshadows == 2 ? "medium" : "low" } @@ -1014,6 +1025,7 @@ void opengl46_renderer::draw_debug_ui() ImGui::Checkbox("draw the depth of the whole scene before its colour (gfx.depthprepass.scene)", &Global.gfx_depthprepass_scene); if (Global.gfx_contactshadows > 0) ImGui::TextUnformatted("(drawn anyway for the contact shadows, gfx.contactshadows)"); + ImGui::Checkbox("walk the scene once for its depth and colour, drawing both from a record (gfx.depthprepass.replay)", &Global.gfx_depthprepass_replay); auto info{m_instancecull.info()}; if (Global.gfx_gpucull_shadows) { @@ -1279,12 +1291,13 @@ void opengl46_renderer::Render_pass(viewport_config &vp, rendermode const Mode) // the depth of the whole opaque scene before its colour, which the contact shadows need m_scenedepthdrawn = false; m_indirectcolourpending = false; + m_scenerecorded = false; auto const contactshadows{Contact_Shadows_Wanted(vp)}; if ((Global.gfx_depthprepass_scene || contactshadows) && vp.main && (false == Global.bWireFrame)) { glDebug("render scene depth"); gpupart("scene depth"); - Render_Scene_Depth(future); + Render_Scene_Depth(future, contactshadows && (false == Global.gfx_depthprepass_scene)); if (contactshadows) { gpupart("contact shadows depth"); @@ -1298,27 +1311,37 @@ void opengl46_renderer::Render_pass(viewport_config &vp, rendermode const Mode) gpupart("colour, opaque"); setup_drawing(false); - // without rain/snow we can render the cab early to limit the overdraw - // precipitation happens when overcast is in 1-2 range - if (!FreeFlyModeFlag && Global.Overcast <= 1.0f && Global.render_cab) + if (m_scenerecorded) { - glDebug("render opaque cab"); - model_ubs.future = glm::mat4(); - auto const *vehicle{ simulation::Train->Dynamic() }; - // the lamp lighting the shadows of the sun, unless it is a light of its own - if( ( vehicle->InteriorLightLevel > 0.f ) && ( false == m_cablamplit ) ) { - setup_shadow_color( glm::min( colors::white, m_shadowcolor + glm::vec4( vehicle->InteriorLight * vehicle->InteriorLightLevel, 1.f ) ) ); - } - Render_cab( vehicle, vehicle->InteriorLightLevel, false ); + // the depth pass walked the scene for this pass, which draws what it recorded (the cab drawn early included) + glDebug("render recorded opaque scene"); + Render_Scene_Colour(); + m_scenerecorded = false; } + else + { + // without rain/snow we can render the cab early to limit the overdraw + // precipitation happens when overcast is in 1-2 range + if (!FreeFlyModeFlag && Global.Overcast <= 1.0f && Global.render_cab) + { + glDebug("render opaque cab"); + model_ubs.future = glm::mat4(); + auto const *vehicle{ simulation::Train->Dynamic() }; + // the lamp lighting the shadows of the sun, unless it is a light of its own + if( ( vehicle->InteriorLightLevel > 0.f ) && ( false == m_cablamplit ) ) { + setup_shadow_color( glm::min( colors::white, m_shadowcolor + glm::vec4( vehicle->InteriorLight * vehicle->InteriorLightLevel, 1.f ) ) ); + } + Render_cab( vehicle, vehicle->InteriorLightLevel, false ); + } - setup_shadow_color( m_shadowcolor ); + setup_shadow_color( m_shadowcolor ); - glDebug("render opaque region"); + glDebug("render opaque region"); - model_ubs.future = future; + model_ubs.future = future; - Render(simulation::Region); + Render(simulation::Region); + } m_scenedepthdrawn = false; m_indirectcolourpending = false; @@ -2327,6 +2350,13 @@ void opengl46_renderer::setup_sunlight_intensity( float const Factor ) { // the header of the light buffer and the sun in its first slot. the headlights are written once per pass by Update_Lights() void opengl46_renderer::upload_light_header() { + if( m_scenerecording ) { + // the recorded walk of Render_Scene_Depth() draws nothing lit: the draws which follow go with this header, uploaded + // when the colour pass draws them + auto &header { m_sceneheaders.emplace_back() }; + std::memcpy( header.data(), &light_data, header.size() ); + return; + } light_ssbo->update( &light_data, 0, offsetof( gl::light_data, lights ) + sizeof( gl::light_element ) ); } @@ -2568,6 +2598,7 @@ bool opengl46_renderer::Replace(gfx::vertex_array &Vertices, gfx::userdata_array { gfx::calculate_tangents(Vertices, gfx::index_array(), Type); + m_chunkspheres.erase(static_cast(Geometry)); return m_geometry.replace(Vertices, Userdata, Geometry, Offset); } @@ -2576,6 +2607,7 @@ bool opengl46_renderer::Append(gfx::vertex_array &Vertices, gfx::userdata_array { gfx::calculate_tangents(Vertices, gfx::index_array(), Type); + m_chunkspheres.erase(static_cast(Geometry)); return m_geometry.append(Vertices, Userdata, Geometry); } @@ -2736,6 +2768,15 @@ void opengl46_renderer::Bind_Material( material_handle const Material, TSubModel m_indirectsubmodel = sm; return; } + if (m_scenerecording) + { + // the recorded walk of Render_Scene_Depth(): the draws which follow are made with this material, bound later + m_scenematerial = Material; + m_scenesubmodel = sm; + m_sceneseed = TSubModel::iVariantSeed; + m_scenevariant = (material.texture_variants.empty() ? 0 : TSubModel::VariantId(sm)); + return; + } // materials which declare texture variants provide a different texture set per model instance, // picked from the seed of the rendered instance and the variant group of the submodel @@ -3010,6 +3051,11 @@ void opengl46_renderer::Render(scene::basic_region *Region) // when editor mode is active calculate world position of the cursor // at this stage the z-buffer is filled with only ground geometry // (with the depth pass of the whole scene it's done there, at the same stage) + if (m_scenerecording) + { + // the depth of what the walk recorded so far + Render_Scene_Prepass(); + } Update_Mouse_Position(); } // draw opaque cells front-to-back: with the depth test enabled this @@ -3405,25 +3451,7 @@ void opengl46_renderer::Render(cell_sequence::iterator First, cell_sequence::ite // (For pickscenery/pickcontrols modes the map stays empty, so this is a no-op.) if( m_indirectcolourpending ) { // the instances culled and recorded by the depth pass of the whole scene, the buckets weren't queued for the cpu - m_indirectcolourpending = false; - indirect_counts colourcounts; - if( false == m_indirectdraws.empty() ) { - auto const cpustart { std::chrono::steady_clock::now() }; - m_indirectcull = &m_instancecull; - m_instancecull.bind_draw(); - colourcounts = Render_Instances_Indirect_Colour( m_indirectcolourset ); - ::glBindBuffer( GL_DRAW_INDIRECT_BUFFER, 0 ); - Bind_Instance_Stream(); - m_instancecull.draw_result( - colourcounts.draws, - m_instancecull.prepass_draws(), - colourcounts.calls, - m_instancecull.prepass_calls(), - m_instancecull.cpu_time() + std::chrono::duration( std::chrono::steady_clock::now() - cpustart ).count() ); - } - m_renderpass.draw_stats.instanced_drawcalls += static_cast( colourcounts.calls ); - m_renderpass.draw_stats.instances += static_cast( m_instancecull.last_count() ); - m_renderpass.draw_stats.models += static_cast( m_instancecull.last_count() ); + Render_Instances_Indirect_Pending(); } if( m_instancecullpass ) { if( m_passcull->drawing() ) { @@ -3492,6 +3520,11 @@ void opengl46_renderer::draw(const gfx::geometry_handle &handle) } return; } + if( m_scenerecording ) { + // the recorded walk of Render_Scene_Depth(): the depth pass and the colour pass draw it later, with this state + Record_Scene_Draw( &handle, 1, false ); + return; + } if( m_scenedepthpass && ( false == Scene_Depth_Draws( handle ) ) ) { return; } @@ -3510,6 +3543,12 @@ void opengl46_renderer::draw(const gfx::geometry_handle &handle) void opengl46_renderer::draw(std::vector::iterator it, std::vector::iterator end) { model_ubs.set_modelview(OpenGLMatrices.data(GL_MODELVIEW)); + if( m_scenerecording ) { + if( it != end ) { + Record_Scene_Draw( &( *it ), static_cast( std::distance( it, end ) ), true ); + } + return; + } if( m_scenedepthpass && ( ( it == end ) || ( false == Scene_Depth_Draws( *it ) ) ) ) { return; } @@ -4045,6 +4084,11 @@ void opengl46_renderer::Render_Instances_Indirect() auto const cpustart { std::chrono::steady_clock::now() }; auto *viewport { m_current_viewport }; + if( m_scenerecording ) { + // the depth of what the recorded walk of the depth pass of the scene went through so far, for the occlusion test + // and for the depth of the instances to be tested against + Render_Scene_Prepass(); + } // occlusion test against the depth drawn so far in the pass: cab, terrain, shapes, tracks, vehicles, other models opengl46_instance_cull::occlusion_setup occlusion; auto const useocclusion { @@ -4114,6 +4158,12 @@ void opengl46_renderer::Render_Instances_Indirect() m_indirectcolourpending = true; m_indirectcolourset = colourset; m_indirectcoloursleepers = m_sleeperspass; + if( m_scenerecording ) { + // where the colour pass, which draws the record, draws them, in the light the walk had here + auto &marker { m_scenedraws.emplace_back() }; + marker.instances = true; + marker.header = static_cast( m_sceneheaders.size() - 1 ); + } m_renderpass.draw_stats.drawcalls += static_cast( prepasscounts.calls ); colourcounts.draws = static_cast( m_indirectdraws.size() ); } @@ -4647,7 +4697,7 @@ gl::program *opengl46_renderer::Multidraw_Program( gl::program const *Program ) // them (Bind_Material(), draw()), with the depth a little farther than the surface like the prepass of the instances, so the // colour pass, which tests and writes its depth as usual, passes where it's the same surface. the gpu culling of the // instances runs here, against the same depth as without this pass, and the colour pass draws what it recorded -void opengl46_renderer::Render_Scene_Depth( glm::mat4 const &Future ) +void opengl46_renderer::Render_Scene_Depth( glm::mat4 const &Future, bool const Contactonly ) { auto const cpustart { std::chrono::steady_clock::now() }; m_scenedepthtimer.begin(); @@ -4661,14 +4711,63 @@ void opengl46_renderer::Render_Scene_Depth( glm::mat4 const &Future ) ::glEnable( GL_POLYGON_OFFSET_FILL ); ::glPolygonOffset( -1.f, -1.f ); - // the conditions of the cab drawn early by the colour pass - if( ( false == FreeFlyModeFlag ) && ( Global.Overcast <= 1.0f ) && ( Global.render_cab ) ) { - model_ubs.future = glm::mat4(); - auto const *vehicle { simulation::Train->Dynamic() }; - Render_cab( vehicle, vehicle->InteriorLightLevel, false ); + m_scenereplayed = Global.gfx_depthprepass_replay; + if( m_scenereplayed ) { + // the walk of the colour pass, recorded: the same steps as in Render_pass(), the shadow tone of the cab lamp included + m_scenerecording = true; + m_scenedraws.clear(); + m_scenechunks.clear(); + m_scenegeometry.clear(); + m_sceneheaders.clear(); + std::memcpy( m_sceneheaders.emplace_back().data(), &light_data, std::tuple_size_v ); + m_sceneflushed = 0; + m_scenematerial = null_handle; + m_scenesubmodel = nullptr; + m_sceneseed = TSubModel::iVariantSeed; + m_scenevariant = 0; + m_sceneprepassdraws = 0; + m_sceneprepasscalls = 0; + m_scenebeyondreach = 0; + // the contact shadows alone need the depth only within their reach: a ray of a pixel there runs up to their length + // towards the light, and a surface counts as in its way up to 0.3 m and 4 pixels behind it (see contact_shadow()). + // past that the colour pass draws with no depth before it, as without the depth pass. the cursor of the editor reads + // the depth of the ground anywhere + m_scenedepthreach = ( Contactonly && ( false == EditorModeFlag ) ); + if( m_scenedepthreach ) { + auto const reach { Global.gfx_contactshadows_range + Global.gfx_contactshadows_length }; + auto const scale { scene_ubs.projection[ 1 ][ 1 ] }; + auto const height { ( m_current_viewport != nullptr ? m_current_viewport->height : Global.gfx_framebuffer_height ) }; + auto const pixel { ( ( scale > 0.f ) && ( height > 0 ) ) ? 2.f * reach / ( scale * static_cast( height ) ) : reach }; + m_scenedepthlimit = reach + 0.3f + 4.f * pixel + 1.f; + } + + if( ( false == FreeFlyModeFlag ) && ( Global.Overcast <= 1.0f ) && ( Global.render_cab ) ) { + model_ubs.future = glm::mat4(); + auto const *vehicle { simulation::Train->Dynamic() }; + if( ( vehicle->InteriorLightLevel > 0.f ) && ( false == m_cablamplit ) ) { + setup_shadow_color( glm::min( colors::white, m_shadowcolor + glm::vec4( vehicle->InteriorLight * vehicle->InteriorLightLevel, 1.f ) ) ); + } + Render_cab( vehicle, vehicle->InteriorLightLevel, false ); + } + setup_shadow_color( m_shadowcolor ); + model_ubs.future = Future; + Render( simulation::Region ); + // the depth of what's left + Render_Scene_Prepass(); + m_scenerecording = false; + m_scenerecorded = true; + m_scenerecordedparts = static_cast( m_scenechunks.size() ); + } + else { + // the conditions of the cab drawn early by the colour pass + if( ( false == FreeFlyModeFlag ) && ( Global.Overcast <= 1.0f ) && ( Global.render_cab ) ) { + model_ubs.future = glm::mat4(); + auto const *vehicle { simulation::Train->Dynamic() }; + Render_cab( vehicle, vehicle->InteriorLightLevel, false ); + } + model_ubs.future = Future; + Render( simulation::Region ); } - model_ubs.future = Future; - Render( simulation::Region ); ::glDisable( GL_POLYGON_OFFSET_FILL ); ::glColorMask( GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE ); @@ -4678,10 +4777,351 @@ void opengl46_renderer::Render_Scene_Depth( glm::mat4 const &Future ) m_scenedepthstats = m_renderpass.draw_stats; m_renderpass.draw_stats = colourstats; + if( m_scenereplayed ) { + // the walk was the one of the colour pass, which counts it + m_renderpass.draw_stats += m_scenedepthstats; + } m_scenedepthtimer.end(); m_scenedepthcputime = std::chrono::duration( std::chrono::steady_clock::now() - cpustart ).count(); } +void opengl46_renderer::Record_Scene_Draw( gfx::geometry_handle const *First, std::size_t const Count, bool const Batch ) +{ + auto const index { static_cast( m_scenedraws.size() ) }; + auto &draw { m_scenedraws.emplace_back() }; + draw.ubs = model_ubs; + draw.material = m_scenematerial; + draw.submodel = m_scenesubmodel; + draw.seed = m_sceneseed; + draw.variant = m_scenevariant; + draw.chunkfirst = static_cast( m_scenechunks.size() ); + draw.chunkcount = static_cast( Count ); + // a batch is drawn without instances, as Draw_Geometry() draws it + draw.instancecount = ( Batch ? 0 : static_cast( m_current_instance_count ) ); + draw.instancebase = ( draw.instancecount > 0 ? m_current_instance_base : 0 ); + draw.instancebuffer = ( draw.instancecount > 0 ? m_instancestream->buffer() : 0 ); + draw.header = static_cast( m_sceneheaders.size() - 1 ); + draw.blending = m_blendingenabled; + draw.batch = Batch; + // what the depth pass leaves out, as Bind_Material() and draw() do without the record (V41): the blended draws, those of + // negative opacity, of materials the depth programs can't stand for, and the ones with the alpha test without a texture + draw.prepassclass = 0; + draw.prepasstexture = null_handle; + if( ( false == m_blendingenabled ) && ( model_ubs.opacity >= 0.f ) ) { + draw.prepassclass = Prepass_Class( m_scenematerial ); + if( draw.prepassclass == 2 ) { + // the diffuse texture of the colour pass, the same choice as Bind_Material() makes + auto const &material { m_materials.material( m_scenematerial ) }; + auto const &textures { material.texture_variants.empty() ? material.textures : material.GetTextures( m_scenevariant ) }; + draw.prepasstexture = textures[ 0 ]; + if( draw.prepasstexture == null_handle ) { + draw.prepassclass = 0; + } + } + } + // the sphere of a chunk of a draw which isn't instanced, in the view: centre and the radius scaled with the model + glm::mat3 const rotationscale { draw.ubs.modelview }; + auto const scale { std::sqrt( std::max( { glm::length2( rotationscale[ 0 ] ), glm::length2( rotationscale[ 1 ] ), glm::length2( rotationscale[ 2 ] ) } ) ) }; + for( std::size_t chunk = 0; chunk < Count; ++chunk ) { + auto const &geometry { First[ chunk ] }; + auto depth { draw.prepassclass != 0 }; + if( depth ) { + // the program of the prepass doesn't size points, and lines aren't surfaces + auto const type { m_geometry.type( geometry ) }; + depth = ( ( type == GL_TRIANGLES ) || ( type == GL_TRIANGLE_STRIP ) || ( type == GL_TRIANGLE_FAN ) ); + } + if( depth && m_scenedepthreach && ( draw.instancecount == 0 ) ) { + auto const &sphere { Chunk_Sphere( geometry ) }; + if( sphere.w >= 0.f ) { + auto const centre { glm::vec3{ draw.ubs.modelview * glm::vec4{ glm::vec3{ sphere }, 1.f } } }; + if( glm::length( centre ) - sphere.w * scale > m_scenedepthlimit ) { + depth = false; + ++m_scenebeyondreach; + } + } + } + m_scenechunks.push_back( { geometry, index, depth } ); + m_scenegeometry.push_back( geometry ); + } +} + +void opengl46_renderer::Render_Scene_Prepass() +{ + auto const end { m_scenechunks.size() }; + if( m_sceneflushed >= end ) { return; } + + // the chunks the depth is drawn for, in runs of the same program, texture of the alpha test, bank, primitive and instance + // buffer. the depth doesn't depend on the order of the draws; within a bank the chunks keep the order of the walk + m_sceneprepassitems.clear(); + for( auto chunkindex { m_sceneflushed }; chunkindex < end; ++chunkindex ) { + auto const &chunk { m_scenechunks[ chunkindex ] }; + if( false == chunk.depth ) { continue; } + auto const &draw { m_scenedraws[ chunk.draw ] }; + gfx::indirect_source source; + if( false == m_geometry.indirect_source( chunk.geometry, source ) ) { continue; } + auto &item { m_sceneprepassitems.emplace_back() }; + item.prepassclass = draw.prepassclass; + item.texture = ( draw.prepassclass == 2 ? draw.prepasstexture : null_handle ); + item.bank = chunk.geometry.bank; + item.type = source.type; + item.indexed = ( source.indexed ? 1u : 0u ); + item.instancebuffer = draw.instancebuffer; + item.chunk = static_cast( chunkindex ); + // count, instances, first, base vertex (the base instance of an array draw), base instance + auto const instances { std::max( draw.instancecount, 1 ) }; + item.command = { + source.count, + instances, + source.first, + ( source.indexed ? static_cast( source.basevertex ) : draw.instancebase ), + ( source.indexed ? draw.instancebase : 0u ), + 0, item.indexed, 0 }; + } + m_sceneflushed = end; + if( m_sceneprepassitems.empty() ) { return; } + std::sort( + std::begin( m_sceneprepassitems ), std::end( m_sceneprepassitems ), + []( scene_prepass_item const &Left, scene_prepass_item const &Right ) { return Left.tie() < Right.tie(); } ); + + auto const multidraw { ( m_drawblockstream != nullptr ) && ( m_scenecommandstream != nullptr ) }; + GLuint const frameinstances { m_instancestream->buffer() }; + GLuint boundinstances { frameinstances }; + auto const bind_instances = [ & ]( GLuint const Buffer ) { + // a draw which isn't instanced reads the identity at the start of any of the buffers + if( ( Buffer == 0 ) || ( Buffer == boundinstances ) ) { return; } + ::glBindBuffersBase( GL_SHADER_STORAGE_BUFFER, gl::INSTANCE_SSBO_BINDING, 1, &Buffer ); + boundinstances = Buffer; + }; + auto const count { m_sceneprepassitems.size() }; + auto const samerun = []( scene_prepass_item const &Left, scene_prepass_item const &Right ) { + return ( Left.prepassclass == Right.prepassclass ) + && ( Left.texture == Right.texture ) + && ( Left.bank == Right.bank ) + && ( Left.type == Right.type ) + && ( Left.indexed == Right.indexed ) + && ( Left.instancebuffer == Right.instancebuffer ); }; + + if( multidraw ) { + // the model blocks and the commands of all of them go to the buffers of the frame at once, a run is one call + m_sceneprepassblocks.clear(); + m_sceneprepasscommands.clear(); + for( auto const &item : m_sceneprepassitems ) { + m_sceneprepassblocks.push_back( m_scenedraws[ m_scenechunks[ item.chunk ].draw ].ubs ); + m_sceneprepasscommands.push_back( item.command ); + } + auto const blockoffset { m_drawblockstream->push( m_sceneprepassblocks.data(), static_cast( count * sizeof( gl::model_ubs ) ) ) }; + GLuint const blockbuffer { m_drawblockstream->buffer() }; + auto const commandoffset { m_scenecommandstream->push( m_sceneprepasscommands.data(), static_cast( count * sizeof( gl::instance_draw_command ) ) ) }; + ::glBindBuffer( GL_DRAW_INDIRECT_BUFFER, m_scenecommandstream->buffer() ); + auto boundclass { 0 }; + for( std::size_t first = 0; first < count; ) { + auto const &item { m_sceneprepassitems[ first ] }; + auto last { first + 1 }; + while( ( last < count ) && samerun( item, m_sceneprepassitems[ last ] ) ) { + ++last; + } + auto const drawcount { last - first }; + auto const run { first }; + first = last; + if( ( item.prepassclass == 2 ) && ( false == m_textures.mark_as_used( item.texture ).bind( 0 ) ) ) { + // a texture which isn't there (yet) would leave the one bound before to the alpha test + continue; + } + if( item.prepassclass != boundclass ) { + ( item.prepassclass == 2 ? m_prepass_alpha_multidraw_shader : m_prepass_multidraw_shader )->bind(); + boundclass = item.prepassclass; + } + bind_instances( item.instancebuffer ); + GLintptr const offset { blockoffset + static_cast( run * sizeof( gl::model_ubs ) ) }; + GLsizeiptr const size { static_cast( drawcount * sizeof( gl::model_ubs ) ) }; + ::glBindBuffersRange( GL_SHADER_STORAGE_BUFFER, gl::MODEL_DRAW_SSBO_BINDING, 1, &blockbuffer, &offset, &size ); + m_geometry.draw_indirect_multi( + m_scenechunks[ item.chunk ].geometry, + commandoffset + static_cast( run * sizeof( gl::instance_draw_command ) ), + static_cast( drawcount ), + sizeof( gl::instance_draw_command ) ); + m_sceneprepassdraws += static_cast( drawcount ); + ++m_sceneprepasscalls; + } + ::glBindBuffer( GL_DRAW_INDIRECT_BUFFER, 0 ); + } + else { + // one by one, with the model block of each draw, as without the record + auto const walkubs { model_ubs }; + auto boundclass { 0 }; + auto drawn { false }; + for( std::size_t first = 0; first < count; ) { + auto const &item { m_sceneprepassitems[ first ] }; + auto last { first + 1 }; + while( ( last < count ) && samerun( item, m_sceneprepassitems[ last ] ) ) { + ++last; + } + auto const run { first }; + first = last; + if( ( item.prepassclass == 2 ) && ( false == m_textures.mark_as_used( item.texture ).bind( 0 ) ) ) { + continue; + } + if( item.prepassclass != boundclass ) { + ( item.prepassclass == 2 ? m_prepass_alpha_shader : m_prepass_shader )->bind(); + boundclass = item.prepassclass; + } + bind_instances( item.instancebuffer ); + for( auto index { run }; index < last; ++index ) { + auto const &chunk { m_scenechunks[ m_sceneprepassitems[ index ].chunk ] }; + auto const &draw { m_scenedraws[ chunk.draw ] }; + model_ubs = draw.ubs; + model_ubo->update( model_ubs ); + if( draw.instancecount > 0 ) { + m_geometry.draw_instanced( chunk.geometry, draw.instancecount, draw.instancebase ); + } + else { + m_geometry.draw( chunk.geometry ); + } + ++m_sceneprepassdraws; + ++m_sceneprepasscalls; + drawn = true; + } + } + // the walk goes on with its own state + model_ubs = walkubs; + if( drawn ) { + model_ubo->update( model_ubs ); + } + } + if( boundinstances != frameinstances ) { + Bind_Instance_Stream(); + } +} + +void opengl46_renderer::Render_Scene_Colour() +{ + // what the walk left behind, which the passes after this one go on with + auto const walkubs { model_ubs }; + auto const walkseed { TSubModel::iVariantSeed }; + + // the light buffer has the header the walk began with (it uploaded none while recording) + std::uint32_t header { 0 }; + auto blending { false }; + auto bound { false }; + material_handle boundmaterial { null_handle }; + std::uint32_t boundvariant { 0 }; + GLuint const frameinstances { m_instancestream->buffer() }; + GLuint boundinstances { frameinstances }; + for( auto const &draw : m_scenedraws ) { + if( draw.instances ) { + // the instances culled in the walk, where the colour pass would draw them + if( blending ) { + setup_drawing( false ); + blending = false; + } + if( draw.header != header ) { + light_ssbo->update( m_sceneheaders[ draw.header ].data(), 0, std::tuple_size_v ); + header = draw.header; + } + if( m_indirectcolourpending ) { + Render_Instances_Indirect_Pending(); + } + // they bind their own programs, textures and the instance buffer of the frame + bound = false; + boundinstances = m_instancestream->buffer(); + continue; + } + if( draw.header != header ) { + light_ssbo->update( m_sceneheaders[ draw.header ].data(), 0, std::tuple_size_v ); + header = draw.header; + } + if( draw.blending != blending ) { + // the transparency hack of submodels faded out + setup_drawing( draw.blending ); + blending = draw.blending; + } + if( ( false == bound ) || ( draw.material != boundmaterial ) || ( draw.variant != boundvariant ) ) { + // the textures of the variant follow from the seed of the vehicle and the submodel + TSubModel::iVariantSeed = draw.seed; + Bind_Material( draw.material, draw.submodel ); + bound = true; + boundmaterial = draw.material; + boundvariant = draw.variant; + } + model_ubs = draw.ubs; + model_ubo->update( model_ubs ); + auto const *chunks { m_scenegeometry.data() + draw.chunkfirst }; + if( draw.instancecount > 0 ) { + if( draw.instancebuffer != boundinstances ) { + ::glBindBuffersBase( GL_SHADER_STORAGE_BUFFER, gl::INSTANCE_SSBO_BINDING, 1, &draw.instancebuffer ); + boundinstances = draw.instancebuffer; + } + m_geometry.draw_instanced( chunks[ 0 ], draw.instancecount, draw.instancebase ); + } + else if( draw.batch ) { + m_geometry.draw_batch( chunks, chunks + draw.chunkcount ); + } + else { + m_geometry.draw( chunks[ 0 ] ); + } + } + if( blending ) { + setup_drawing( false ); + } + if( header + 1 != m_sceneheaders.size() ) { + // the header the walk ended with + light_ssbo->update( m_sceneheaders.back().data(), 0, std::tuple_size_v ); + } + if( boundinstances != m_instancestream->buffer() ) { + Bind_Instance_Stream(); + } + model_ubs = walkubs; + TSubModel::iVariantSeed = walkseed; +} + +void opengl46_renderer::Render_Instances_Indirect_Pending() +{ + m_indirectcolourpending = false; + indirect_counts colourcounts; + if( false == m_indirectdraws.empty() ) { + auto const cpustart { std::chrono::steady_clock::now() }; + m_indirectcull = &m_instancecull; + m_instancecull.bind_draw(); + colourcounts = Render_Instances_Indirect_Colour( m_indirectcolourset ); + ::glBindBuffer( GL_DRAW_INDIRECT_BUFFER, 0 ); + Bind_Instance_Stream(); + m_instancecull.draw_result( + colourcounts.draws, + m_instancecull.prepass_draws(), + colourcounts.calls, + m_instancecull.prepass_calls(), + m_instancecull.cpu_time() + std::chrono::duration( std::chrono::steady_clock::now() - cpustart ).count() ); + } + m_renderpass.draw_stats.instanced_drawcalls += static_cast( colourcounts.calls ); + m_renderpass.draw_stats.instances += static_cast( m_instancecull.last_count() ); + m_renderpass.draw_stats.models += static_cast( m_instancecull.last_count() ); +} + +glm::vec4 const &opengl46_renderer::Chunk_Sphere( gfx::geometry_handle const &Geometry ) +{ + auto const key { static_cast( Geometry ) }; + if( auto const lookup { m_chunkspheres.find( key ) }; lookup != m_chunkspheres.end() ) { + return lookup->second; + } + glm::vec4 sphere { 0.f, 0.f, 0.f, -1.f }; + auto const &vertices { m_geometry.vertices( Geometry ) }; + if( false == vertices.empty() ) { + glm::vec3 low { std::numeric_limits::max() }; + glm::vec3 high { std::numeric_limits::lowest() }; + for( auto const &vertex : vertices ) { + low = glm::min( low, vertex.position ); + high = glm::max( high, vertex.position ); + } + glm::vec3 const centre { ( low + high ) * 0.5f }; + auto radius { 0.f }; + for( auto const &vertex : vertices ) { + radius = std::max( radius, glm::length2( vertex.position - centre ) ); + } + sphere = glm::vec4{ centre, std::sqrt( radius ) }; + } + return m_chunkspheres.emplace( key, sphere ).first->second; +} + bool opengl46_renderer::Scene_Depth_Draws( gfx::geometry_handle const &Geometry ) const { if( m_scenedepthskip ) { return false; } diff --git a/rendering/opengl46/opengl46renderer.h b/rendering/opengl46/opengl46renderer.h index 7333a1ef..5f98759e 100644 --- a/rendering/opengl46/opengl46renderer.h +++ b/rendering/opengl46/opengl46renderer.h @@ -404,8 +404,23 @@ class opengl46_renderer : public gfx_renderer { int Prepass_Class( material_handle const Material ); // depth of the whole opaque scene of the colour pass, before its colour (Global.gfx_depthprepass_scene): the colour pass // walks the scene a second time, Bind_Material() binds the programs of the depth prepass and draw() leaves out what they - // can't draw. Future: the motion matrix the colour pass draws the region with - void Render_Scene_Depth( glm::mat4 const &Future ); + // can't draw. with Global.gfx_depthprepass_replay the walk of the colour pass is made here, once: its opaque draws are + // recorded (Record_Scene_Draw()), their depth is drawn from the record (Render_Scene_Prepass()) and the colour pass draws + // them from it (Render_Scene_Colour()). Future: the motion matrix the colour pass draws the region with. Contactonly: the + // depth is drawn only for the contact shadows, which need it within their reach + void Render_Scene_Depth( glm::mat4 const &Future, bool const Contactonly ); + // notes a draw of the walk recorded by Render_Scene_Depth(): the chunks drawn, with the state the walk left for them + void Record_Scene_Draw( gfx::geometry_handle const *First, std::size_t const Count, bool const Batch ); + // draws the depth of the recorded draws not drawn yet, in runs of the same program, bank and texture of the alpha test, + // with multi draws when there are any. the depth pyramid of the occlusion test and the cursor of the editor take the depth + // drawn so far, and the end of the walk the rest + void Render_Scene_Prepass(); + // the colour of the recorded draws, in the order of the walk, with the programs of their materials + void Render_Scene_Colour(); + // the colour of the instances culled and recorded by the depth pass of the whole scene (m_indirectcolourpending) + void Render_Instances_Indirect_Pending(); + // the bounding sphere of a geometry chunk in the space of its vertices, w the radius (negative if there's none) + glm::vec4 const &Chunk_Sphere( gfx::geometry_handle const &Geometry ); // whether the colour pass of the viewport gets contact shadows, which need the depth pass of the scene bool Contact_Shadows_Wanted( viewport_config const &Viewport ) const; // resolves the depth drawn by Render_Scene_Depth() for the contact shadows and sets them up for the colour pass @@ -683,6 +698,71 @@ class opengl46_renderer : public gfx_renderer { debug_stats m_scenedepthstats; // of its last run double m_scenedepthcputime { 0.0 }; // ms gl::gpu_timer m_scenedepthtimer; + // the recorded walk of the colour pass (Global.gfx_depthprepass_replay): a draw with the state it was made with + struct scene_draw { + gl::model_ubs ubs; + material_handle material { null_handle }; // bound last before it (Bind_Material()) + TSubModel const *submodel { nullptr }; + std::uint32_t seed { 0 }; // TSubModel::iVariantSeed then + std::uint32_t variant { 0 }; // TSubModel::VariantId() then: which textures of the material were bound + std::uint32_t chunkfirst { 0 }; // in m_scenechunks + std::uint32_t chunkcount { 0 }; + std::uint32_t instancecount { 0 }; // instanced draw (Render_Instanced(), Render_Sleepers()), 0 if not + std::uint32_t instancebase { 0 }; + GLuint instancebuffer { 0 }; // the instance buffer of the frame the matrices of the instances went to + std::uint32_t header { 0 }; // the light header (m_sceneheaders) it is lit with + texture_handle prepasstexture { null_handle }; // of the alpha test of the depth pass + int prepassclass { 0 }; // Prepass_Class(), 0 as well for what the depth pass leaves out (blending, opacity < 0) + bool blending { false }; + bool batch { false }; // drawn with draw_batch() (neighbouring tracks) + bool instances { false }; // no draw: the colour of the instances culled in the walk goes here + }; + struct scene_chunk { + gfx::geometry_handle geometry; + std::uint32_t draw; + bool depth; // drawn in the depth pass + }; + using light_header = std::array; + bool m_scenerecording { false }; // inside the walk of Render_Scene_Depth(): draw(), Bind_Material(), upload_light_header() record + bool m_scenerecorded { false }; // the colour pass under way draws the record + bool m_scenereplayed { false }; // the last depth pass of the scene was drawn from a record + bool m_scenedepthreach { false }; // the depth pass leaves out the draws lying whole farther than m_scenedepthlimit + float m_scenedepthlimit { 0.f }; + material_handle m_scenematerial { null_handle }; // what Bind_Material() bound last in the walk + TSubModel const *m_scenesubmodel { nullptr }; + std::uint32_t m_sceneseed { 0 }; + std::uint32_t m_scenevariant { 0 }; + std::vector m_scenedraws; + std::vector m_scenechunks; + std::vector m_scenegeometry; // the chunks of m_scenechunks, for draw_batch() + std::vector m_sceneheaders; // the first one is in the light buffer when the walk begins + std::size_t m_sceneflushed { 0 }; // chunks Render_Scene_Prepass() went through + // working space of Render_Scene_Prepass(): a chunk drawn and what its run has in common + struct scene_prepass_item { + int prepassclass; + texture_handle texture; + std::uint32_t bank; + unsigned int type; + std::uint32_t indexed; + GLuint instancebuffer; // 0 for a draw which isn't instanced, which reads the identity every buffer starts with + std::uint32_t chunk; + gl::instance_draw_command command; + auto tie() const { + return std::tie( prepassclass, texture, bank, type, indexed, instancebuffer, chunk ); } + }; + std::vector m_sceneprepassitems; + std::vector m_sceneprepassblocks; + std::vector m_sceneprepasscommands; + // the commands of the multi draws of the depth pass, one buffer for each of the last frames (nullptr: no multi draws) + std::unique_ptr m_scenecommandstream; + // of the last recorded walk: parts drawn (chunks of geometry; neighbouring tracks are drawn together), those of them drawn in + // the depth pass and its gl calls, and those left out of it as farther than the reach of the contact shadows + std::uint32_t m_scenerecordedparts { 0 }; + std::uint32_t m_sceneprepassdraws { 0 }; + std::uint32_t m_sceneprepasscalls { 0 }; + std::uint32_t m_scenebeyondreach { 0 }; + // Chunk_Sphere() of the chunks asked about, dropped when their vertices change (Replace(), Append()) + std::unordered_map m_chunkspheres; // contact shadows: set up for the colour pass under way, and whether the last main view had them bool m_contactshadows { false }; bool m_contactshadowslast { false }; diff --git a/utilities/Globals.cpp b/utilities/Globals.cpp index 93cf05c4..a61232ce 100644 --- a/utilities/Globals.cpp +++ b/utilities/Globals.cpp @@ -1474,6 +1474,11 @@ global_settings::ConfigParse_gfx( cParser &Parser, std::string_view const Token Parser.getTokens(1); Parser >> gfx_depthprepass_scene; } + else if (Token == "gfx.depthprepass.replay") + { + Parser.getTokens(1); + Parser >> gfx_depthprepass_replay; + } else if (Token == "gfx.bufferstreaming") { Parser.getTokens(1); @@ -1969,6 +1974,7 @@ global_settings::export_as_text( std::ostream &Output ) const { export_as_text( Output, "gfx.gpucull.reflections", gfx_gpucull_reflections ); export_as_text( Output, "gfx.gpucull.sleepers", gfx_gpucull_sleepers ); export_as_text( Output, "gfx.depthprepass.scene", gfx_depthprepass_scene ); + export_as_text( Output, "gfx.depthprepass.replay", gfx_depthprepass_replay ); export_as_text( Output, "gfx.bufferstreaming", gfx_bufferstreaming ); export_as_text( Output, "gfx.postfx.motionblur.enabled", gfx_postfx_motionblur_enabled ); export_as_text( Output, "gfx.postfx.motionblur.shutter", gfx_postfx_motionblur_shutter ); diff --git a/utilities/Globals.h b/utilities/Globals.h index 9b9f950d..0cd15fa8 100644 --- a/utilities/Globals.h +++ b/utilities/Globals.h @@ -303,6 +303,10 @@ struct global_settings { bool gfx_gpucull_sleepers = true; // (opengl 4.6) the depth of the whole opaque scene of the main view is drawn before its colour, the instances included bool gfx_depthprepass_scene = false; + // (opengl 4.6) the depth pass of the scene (gfx_depthprepass_scene, or the one of the contact shadows) walks the scene for + // the colour pass as well: its opaque draws are recorded once, their depth drawn from the record (with the contact shadows + // alone only within their reach) and their colour too. false: the scene is walked twice, as before + bool gfx_depthprepass_replay = true; // (opengl 4.6) the model block of every draw and the matrices of instances are written to persistently mapped buffers, // one for each of the last frames, instead of with glBufferSubData. read at the start bool gfx_bufferstreaming = true;