17
0
mirror of https://github.com/MaSzyna-EU07/maszyna.git synced 2026-11-08 23:07:38 +01:00

Walk the shadow passes and the culled model groups on worker threads in the Vulkan renderer

This commit is contained in:
2026-10-27 20:56:11 +01:00
parent c44b243698
commit 1447dcf075
55 changed files with 4114 additions and 427 deletions

View File

@@ -9,6 +9,7 @@ http://mozilla.org/MPL/2.0/.
#include "stdafx.h"
#include "rendering/common/instancecull.h"
#include "rendering/common/walksync.h"
#include "model/AnimModel.h"
#include "model/Model3d.h"
@@ -96,9 +97,14 @@ void instance_cull::add(scene::basic_cell::instance_bucket_key const &Key, scene
{
// lod bounds of the model, the same set Render_Instanced() splits the distance into bands with, found once for the model
auto const lodfirst{static_cast<std::uint32_t>(m_lodlist.size())};
auto const &lodbounds{Key.pModel->lod_bounds()};
m_lodlist.insert(m_lodlist.end(), lodbounds.begin(), lodbounds.end());
auto const lodcount{static_cast<std::uint32_t>(lodbounds.size())};
std::uint32_t lodcount{0};
{
// found by the first pass which asks, of the passes walked at the same time (walk_sync)
gfx::walk_sync::model_hold hold{Key.pModel};
auto const &lodbounds{Key.pModel->lod_bounds()};
m_lodlist.insert(m_lodlist.end(), lodbounds.begin(), lodbounds.end());
lodcount = static_cast<std::uint32_t>(lodbounds.size());
}
group_data const group{static_cast<std::uint32_t>(m_grouplist.size()), m_entries, lodcount + 1, distancemin, distancemax};
m_grouplist.push_back({lodfirst, lodcount, m_entries, 0});
@@ -142,7 +148,12 @@ void instance_cull::add(scene::basic_cell::instance_bucket_key const &Key, scene
}
}
place(Bucket);
{
// the places in the record buffer the passes share are given by one walk at a time (walk_sync)
auto const &shared{*m_records};
gfx::walk_sync::change change{(Bucket.gpugeneration != shared.recordgeneration) || Bucket.gpustale};
place(Bucket);
}
gl::instance_cull_job job{};
job.origin = packed_vec3{Bucket.recordorigin - m_setup.camera};
@@ -222,6 +233,24 @@ void instance_cull::place(scene::basic_cell::instance_bucket const &Bucket)
allocate(Bucket);
}
void instance_cull::refresh_places()
{
auto const &shared{*m_records};
auto const moved{std::any_of(m_buckets.begin(), m_buckets.end(), [&](scene::basic_cell::instance_bucket const *Bucket) { return (Bucket->gpugeneration != shared.recordgeneration) || Bucket->gpustale; })};
if (moved)
{
for (std::size_t job = 0; job < m_buckets.size(); ++job)
{
place(*m_buckets[job]);
}
}
// another pass which started the buffer over placed the buckets it shares with this one anew, current again
for (std::size_t job = 0; job < m_buckets.size(); ++job)
{
m_joblist[job].first = m_buckets[job]->gpufirst;
}
}
bool instance_cull::dispatch_begin()
{
m_occlusion = false;

View File

@@ -205,6 +205,10 @@ class instance_cull
void frustum_planes(glm::vec4 (&Planes)[6]) const;
// the end of the dispatch of the first phase
void dispatch_end();
// the buckets of the pass placed again where the record buffer was started over by another pass since they were placed, and the
// places of the jobs taken again where another pass placed their buckets anew (the passes walked by several threads at once,
// walk_sync). before the dispatch, with the change held by the caller while the walks are parallel
void refresh_places();
// with a culling made for another pass: shares the records with it
void share_records(instance_cull const &Shared)
{

View File

@@ -188,6 +188,12 @@ class local_shadows
// the timing of the drawing of the maps of the headlights in this frame
void headlights_timer_begin();
void headlights_timer_end();
// the draws of the maps of lamps and of headlights the walkers of the vulkan renderer made in the frame (M2), counted once they're done
void add_drawcalls(std::uint32_t const Lamps, std::uint32_t const Headlights)
{
m_lampsdrawcalls += Lamps;
m_headlightsdrawcalls += Headlights;
}
// the slot of the headlights with their map ready in this frame, -1 if there is none
int headlight_map(TDynamicObject const *Owner, int const Index) const;
// the headlight_shadow_params of the map of the slot, see gl::scene_ubs

View File

@@ -13,6 +13,7 @@ http://mozilla.org/MPL/2.0/.
#include "stdafx.h"
#include "rendering/common/scenewalk.h"
#include "rendering/common/walksync.h"
#include "utilities/color.h"
#include "utilities/Globals.h"
#include "vehicle/Train.h"
@@ -99,6 +100,7 @@ std::size_t scene_walk::pick_index(glm::ivec3 const &Color)
}
bool scene_walk::Render_interior( bool const Alpha ) {
gfx::walk_sync::vehicle_hold hold;
// TODO: early frustum based cull, camera might be pointing elsewhere
std::vector< std::pair<float, TDynamicObject *> > dynamics;
auto *dynamic { simulation::Train->Dynamic() };
@@ -135,6 +137,7 @@ bool scene_walk::Render_interior( bool const Alpha ) {
bool scene_walk::Render_lowpoly( TDynamicObject *Dynamic, float const Squaredistance, bool const Setup, bool const Alpha ) {
if( Dynamic->mdLowPolyInt == nullptr ) { return false; }
gfx::walk_sync::vehicle_hold hold;
// low poly interior
if( Setup ) {
@@ -209,6 +212,7 @@ bool scene_walk::Render_lowpoly( TDynamicObject *Dynamic, float const Squaredist
bool scene_walk::Render_coupler_adapter( TDynamicObject *Dynamic, float const Squaredistance, int const End, bool const Alpha ) {
if( Dynamic->m_coupleradapters[ End ] == nullptr ) { return false; }
gfx::walk_sync::vehicle_hold hold;
auto const position { glm::dvec3 {
0.f,
@@ -352,7 +356,12 @@ void scene_walk::Render(section_sequence::iterator First, section_sequence::iter
{
auto *section = *First;
section->create_geometry();
gfx::walk_sync::yield();
{
// made by the first walk which needs it, one at a time (walk_sync)
gfx::walk_sync::change change{section->geometry_pending()};
section->create_geometry();
}
// render shapes held by the section
switch (m_renderpass.draw_mode)
@@ -453,8 +462,12 @@ void scene_walk::Render(cell_sequence::iterator First, cell_sequence::iterator L
{
auto *cell = First->second;
// przeliczenia animacji torów w sektorze
cell->RaAnimate(m_framestamp);
gfx::walk_sync::yield();
{
// przeliczenia animacji torów w sektorze
gfx::walk_sync::change change{cell->animation_pending(m_framestamp)};
cell->RaAnimate(m_framestamp);
}
bool const tracks_only{editor_far_tracks_only(far_paths, cell, camera, scenery_limit)};
switch (m_renderpass.draw_mode)
@@ -561,6 +574,7 @@ void scene_walk::Render(cell_sequence::iterator First, cell_sequence::iterator L
{
auto const *cell = first->second;
gfx::walk_sync::yield();
switch (m_renderpass.draw_mode)
{
@@ -698,6 +712,7 @@ void scene_walk::Render(cell_sequence::iterator First, cell_sequence::iterator L
auto const compare { walk_instances_begin() };
for( auto &bucket : m_frame_instance_buckets ) {
if( bucket.second.empty() ) { continue; }
gfx::walk_sync::yield();
Render_Instanced( bucket.first.pModel, bucket.second, ( compare ? &m_instance_bandcounts : nullptr ) );
if( compare ) {
walk_instances_compare( bucket.first, m_instance_bandcounts );
@@ -853,6 +868,8 @@ void scene_walk::Render(TAnimModel *Instance)
}
}
// the animations and the lights of the instance are set in the model the instances share, held till it's drawn (walk_sync)
gfx::walk_sync::model_hold hold{Instance->pModel};
Instance->RaAnimate(m_framestamp); // jednorazowe przeliczenie animacji
Instance->RaPrepare();
if (Instance->pModel)
@@ -913,9 +930,15 @@ void scene_walk::Queue_Instances( scene::basic_cell const *Cell )
for( auto const &bucket : Cell->m_instancebuckets_opaque ) {
auto const &instancebucket { bucket.second };
if( ( instancebucket.recordsversion != m_instancerecordsversion )
|| ( instancebucket.records.size() != instancebucket.instances.size() ) ) {
Update_Instance_Records( bucket.first.pModel, instancebucket, Cell->m_area.center );
auto const stale = [ & ]() {
return ( instancebucket.recordsversion != m_instancerecordsversion )
|| ( instancebucket.records.size() != instancebucket.instances.size() ); };
if( stale() ) {
// the records the passes share are made by one walk at a time (walk_sync)
gfx::walk_sync::change change;
if( stale() ) {
Update_Instance_Records( bucket.first.pModel, instancebucket, Cell->m_area.center );
}
}
if( buckettests && Bucket_Culled( instancebucket ) ) {
walk_bucket_culled( instancebucket );
@@ -1086,6 +1109,8 @@ void scene_walk::Render_Instanced( TModel3d *Model, std::vector<queued_instance>
if( Bandcounts != nullptr ) { Bandcounts->clear(); }
if( Model == nullptr ) { return; }
if( Instances.empty() ) { return; }
// the first instance drawn prepares the model the instances share, held till the bands are drawn (walk_sync)
gfx::walk_sync::model_hold hold { Model };
// LOD: a submodel is drawn only while fSquareDist lies inside its [fSquareMinDist, fSquareMaxDist)
// range, so the distinct range bounds split distance into bands with constant LOD. The bucket
@@ -1394,6 +1419,7 @@ void scene_walk::Render_Sleepers( TTrack *Track )
// draw uses an in-band fSquareDist, so each sleeper renders at the LOD its own
// distance selects while instancing stays fully in effect.
auto *Model = Track->m_sleeper_model;
gfx::walk_sync::model_hold hold { Model };
std::size_t const total = instance_modelviews.size();
auto const baseinstance { walk_instances( instance_modelviews.data(), total ) };
std::size_t band_start = 0;
@@ -1457,9 +1483,15 @@ void scene_walk::Render_Timbers( scene::basic_cell::path_sequence::const_iterato
auto const camera = ( shadows ? m_renderpass.viewport_camera.position() : m_renderpass.pass_camera.position() );
auto const range = static_cast<double>( shadows ? std::min( Global.SleeperDistance, 50.f ) : Global.SleeperDistance );
if( m_renderpass.draw_mode == rendermode::color && m_timbersweepframe != m_framestamp ) {
// once a frame the beams of the tracks well past where they're laid and worked out ahead are let go
// once a frame the beams of the tracks well past where they're laid and worked out ahead are let go. with the passes walked
// by several threads, after they're done (Release_Deferred())
m_timbersweepframe = m_framestamp;
track_sleepers::release_far( camera, range + 600.0 );
if( gfx::walk_sync::parallel() ) {
m_deferredrelease = std::make_pair( camera, range + 600.0 );
}
else {
track_sleepers::release_far( camera, range + 600.0 );
}
}
glm::mat4 const originmodelview = OpenGLMatrices.data( GL_MODELVIEW );
for( auto &bucket : m_timberbuckets ) {
@@ -1504,12 +1536,21 @@ void scene_walk::Render_Timbers( scene::basic_cell::path_sequence::const_iterato
if( glm::length( track->location() - camera ) > range + track->radius() ) {
// worked out ahead, in the background, before it comes in range
if( false == shadows && track->m_sleepers3d.dirty && glm::length( track->location() - camera ) < range + track->radius() + 300.0 ) {
track_sleepers::prefetch( *track );
if( gfx::walk_sync::parallel() ) {
m_deferredprefetch.push_back( track );
}
else {
track_sleepers::prefetch( *track );
}
}
continue;
}
if( track->m_sleepers3d.dirty ) {
track_sleepers::lay( *track );
// laid by one walk at a time (walk_sync)
gfx::walk_sync::change change;
if( track->m_sleepers3d.dirty ) {
track_sleepers::lay( *track );
}
}
auto const &laid = track->m_sleepers3d;
if( laid.timbers == nullptr ) { continue; }
@@ -1545,6 +1586,8 @@ void scene_walk::Render_Timbers( scene::basic_cell::path_sequence::const_iterato
total += instances.size();
auto const &beammodel = *bucket.model;
auto *model = beammodel.model;
// the parts of the model shown and hidden for the bucket, held till it's drawn (walk_sync)
gfx::walk_sync::model_hold hold { model };
if( bucket.part >= 0 ) {
// only the part of the model is visible, its siblings are skipped as the model is walked
for( int part = 0; part < track_sleepers::part_count; ++part ) {
@@ -1587,11 +1630,11 @@ bool scene_walk::Render(TDynamicObject *Dynamic)
if (!Global.render_cab && Global.pCamera.m_owner == Dynamic)
return false;
Dynamic->renderme = (
set_renderme( Dynamic,
( Global.pCamera.m_owner == Dynamic && !FreeFlyModeFlag )
|| ( m_renderpass.pass_camera.visible( Dynamic ) ) );
if (false == Dynamic->renderme) { return false; }
if (false == renderme( Dynamic )) { return false; }
// setup
TSubModel::iInstance = reinterpret_cast<std::uintptr_t>(Dynamic); //żeby nie robić cudzych animacji
@@ -1642,14 +1685,16 @@ bool scene_walk::Render(TDynamicObject *Dynamic)
}
// second stage visibility cull, reject vehicles too far away to be noticeable
auto const squareradius{ Dynamic->radius() * Dynamic->radius() };
Dynamic->renderme = ( squareradius * Global.ZoomFactor / squaredistance > 0.003 * 0.003 );
if( false == Dynamic->renderme ) {
set_renderme( Dynamic, ( squareradius * Global.ZoomFactor / squaredistance > 0.003 * 0.003 ) );
if( false == renderme( Dynamic ) ) {
return false;
}
// debug data
++m_renderpass.draw_stats.dynamics;
// the submodels of the models the vehicle shares, set up for it and held till it's drawn (walk_sync)
gfx::walk_sync::vehicle_hold hold;
Dynamic->ABuLittleUpdate(squaredistance); // ustawianie zmiennych submodeli dla wspólnego modelu
glm::mat4 future_stack = model_ubs.future;
@@ -1761,6 +1806,8 @@ bool scene_walk::Render(TDynamicObject *Dynamic)
// rendering kabiny gdy jest oddzielnym modelem i ma byc wyswietlana
bool scene_walk::Render_cab(TDynamicObject const *Dynamic, float const Lightlevel, bool const Alpha)
{
// the cab models of the vehicles are held with the vehicles (walk_sync)
gfx::walk_sync::vehicle_hold hold;
if (Dynamic == nullptr)
{
@@ -2524,6 +2571,7 @@ void scene_walk::Render_Alpha(cell_sequence::reverse_iterator First, cell_sequen
{
auto const *cell = first->second;
gfx::walk_sync::yield();
// translucent parts of instanced models
for (auto *instance : cell->m_instancetranslucent)
@@ -2612,6 +2660,7 @@ void scene_walk::Render_Alpha(TAnimModel *Instance)
return;
}
gfx::walk_sync::model_hold hold{Instance->pModel};
Instance->RaPrepare();
if (Instance->pModel)
{
@@ -2733,12 +2782,66 @@ void scene_walk::Render_Alpha(scene::lines_node const &Lines)
++m_renderpass.draw_stats.drawcalls;
}
// TDynamicObject::renderme, which only the walks read: kept by the walk itself while the passes are walked by several threads at once
// (walk_sync), where it falls back on the value of the vehicle for a vehicle it didn't set (as when the passes come one after another)
void scene_walk::set_renderme(TDynamicObject *Dynamic, bool const Value)
{
if (gfx::walk_sync::parallel())
{
m_renderme[Dynamic] = Value;
return;
}
Dynamic->renderme = Value;
}
bool scene_walk::renderme(TDynamicObject const *Dynamic) const
{
if (false == m_renderme.empty())
{
auto const lookup{m_renderme.find(Dynamic)};
if (lookup != m_renderme.end())
{
return lookup->second;
}
}
return Dynamic->renderme;
}
void scene_walk::Settle_Renderme(bool const Keep)
{
if (Keep)
{
for (auto const &[dynamic, value] : m_renderme)
{
const_cast<TDynamicObject *>(dynamic)->renderme = value;
}
}
m_renderme.clear();
}
void scene_walk::Release_Deferred()
{
if (m_deferredrelease)
{
track_sleepers::release_far(m_deferredrelease->first, m_deferredrelease->second);
m_deferredrelease.reset();
}
for (auto *track : m_deferredprefetch)
{
if (track->m_sleepers3d.dirty)
{
track_sleepers::prefetch(*track);
}
}
m_deferredprefetch.clear();
}
bool scene_walk::Render_Alpha(TDynamicObject *Dynamic)
{
if (!Global.render_cab && Global.pCamera.m_owner == Dynamic)
return false;
if (false == Dynamic->renderme)
if (false == renderme(Dynamic))
{
return false;
}
@@ -2758,6 +2861,7 @@ bool scene_walk::Render_Alpha(TDynamicObject *Dynamic)
break;
}
}
gfx::walk_sync::vehicle_hold hold;
Dynamic->ABuLittleUpdate(squaredistance); // ustawianie zmiennych submodeli dla wspólnego modelu
glm::mat4 future_stack = model_ubs.future;

View File

@@ -29,6 +29,8 @@ http://mozilla.org/MPL/2.0/.
#include "rendering/common/instancecull.h"
#include <array>
#include <functional>
#include <optional>
#include <tuple>
#include <unordered_map>
#include <utility>
@@ -185,6 +187,15 @@ class scene_walk
void Render_Alpha(TSubModel *Submodel);
void Render_Alpha_text(TSubModel *Submodel); // characters of a text display
// TDynamicObject::renderme as the walk sets and reads it: the walk keeps its own while the passes are walked by several threads at
// once (walk_sync), which the main thread puts in the vehicles once they're done (Keep) or drops
void set_renderme(TDynamicObject *Dynamic, bool const Value);
bool renderme(TDynamicObject const *Dynamic) const;
void Settle_Renderme(bool const Keep);
// what the walks put off while the passes are walked by several threads at once, done by the main thread once they're done: the
// beams of the far tracks let go, the sleepers of the tracks coming near worked out ahead (Render_Timbers())
void Release_Deferred();
// the colour of the picking of the item of the index, and the index of a colour read back
static glm::vec3 pick_color(std::size_t const Index);
static std::size_t pick_index(glm::ivec3 const &Color);
@@ -252,6 +263,21 @@ class scene_walk
virtual void walk_instances_end() {}
// the geometry of the backend and whether it draws blended now, for the draws recorded for the gpu culling
virtual gfx::geometrybank_manager &walk_geometry() = 0;
// the walks of the groups of a culling split among several threads (the stage M3 of claude/vulkan14-renderer-plan.md): whether the
// backend does it for this many groups (by default not), and the tasks run, each by a helper given the state of this walk (its
// pass, its model block and blending, its recording of the draws), which Work walks a part of the groups with; returns once all of
// them are done
virtual bool walk_groups_parallel(std::size_t const Groups)
{
return false;
}
virtual void walk_groups_run(std::size_t const Tasks, std::function<void(scene_walk &Helper, std::size_t const Task)> const &Work) {}
// where the chunk is in its bank, for a draw recorded for the gpu culling (by default geometrybank_manager::indirect_source(), which
// marks the bank as used)
virtual bool walk_indirect_source(gfx::geometry_handle const &Geometry, gfx::indirect_source &Source)
{
return walk_geometry().indirect_source(Geometry, Source);
}
virtual bool walk_blending_enabled() const = 0;
// the gpu culling of the instances (Global.gfx_gpucull, stage N6c): what the walk queues in the culling of the pass and the
@@ -278,6 +304,9 @@ class scene_walk
// the walk of the submodel trees of the groups culled by Cull, once per lod band, which records their draws
// (m_indirectdraws, m_indirectcommands). Cascade: the pass is one of the cascades of the sun shadows, which share the walks
void Walk_Indirect_Draws(gfx::instance_cull const &Cull, bool const Cascade);
// the walks of the groups split among the helpers of the backend (walk_groups_run()): what they share made first, the groups walked
// by them, their draws put in the order of the groups as Walk_Indirect_Draws() would record them
void Walk_Groups_Parallel(gfx::instance_cull const &Cull, bool const Cascade, bool const Walkcache, bool const Shadows);
// a draw of the walk of Walk_Indirect_Draws(), with the state the walk left for it (model_ubs with the modelview set)
void Record_Indirect_Draw(gfx::geometry_handle const &Geometry);
// multi draws of the recorded draws (Global.gfx_gpucull_multidraw): puts the draws in an order in which the ones with the
@@ -463,6 +492,32 @@ class scene_walk
walk_counts m_walkcounts;
walk_counts m_lastwalkcounts;
unsigned int m_walkcountsframe{0};
// the working space of Walk_Groups_Parallel(): for each group what its walks share, and for each task of the helpers the draws it
// recorded with the bands they're of
struct group_plan
{
walk_record *kept{nullptr};
cascade_record *walks{nullptr};
bool skip{true};
};
struct group_span
{
std::uint32_t group;
std::uint32_t band;
std::uint32_t first;
std::uint32_t count;
};
struct group_task
{
std::vector<indirect_draw> draws;
std::vector<gl::instance_draw_command> commands;
std::vector<group_span> spans;
walk_counts counts;
int submodels{0};
int drawcalls{0};
};
std::vector<group_plan> m_groupplans;
std::vector<group_task> m_grouptasks;
// appends the draws of a walk kept from an earlier frame to the recorded draws, with the commands of the entry under way
void Replay_Walk(walk_band const &Walk, bool const Shadows);
// working space of Order_Indirect_Draws(), kept between frames
@@ -528,6 +583,10 @@ class scene_walk
// the quad of the glare of lamps and its texture
gfx::geometry_handle m_billboardgeometry{0, 0};
texture_handle m_glaretexture{-1};
// the walk's own TDynamicObject::renderme, and what it put off (set_renderme(), Release_Deferred())
std::unordered_map<TDynamicObject const *, bool> m_renderme;
std::optional<std::pair<glm::dvec3, double>> m_deferredrelease;
std::vector<TTrack *> m_deferredprefetch;
private:
static float editor_ortho_reach();

View File

@@ -13,12 +13,15 @@ http://mozilla.org/MPL/2.0/.
#include "stdafx.h"
#include "rendering/common/scenewalk.h"
#include "rendering/common/walksync.h"
#include "model/AnimModel.h"
#include "model/Model3d.h"
#include "utilities/Globals.h"
#include "utilities/utilities.h"
#include <cassert>
namespace gfx
{
@@ -434,13 +437,22 @@ void scene_walk::Walk_Indirect_Draws(gfx::instance_cull const &Cull, bool const
}
auto const shadows{m_renderpass.draw_mode == rendermode::shadows};
m_indirectrecording = true;
for (auto const &group : Cull.draw_groups())
// the groups walked by several threads (the backend decides), or one after another below
auto const parallel{walk_groups_parallel(Cull.draw_groups().size())};
if (parallel)
{
Walk_Groups_Parallel(Cull, cascade, walkcache, shadows);
}
static std::vector<gfx::instance_cull::draw_group> const walkedalready;
for (auto const &group : (parallel ? walkedalready : Cull.draw_groups()))
{
auto *model{group.key->pModel};
if ((model == nullptr) || (model->Root == nullptr) || ((group.instance == nullptr) && (false == group.sleepers)))
{
continue;
}
// the model the instances share is prepared and walked for the group, held till the walks are made (walk_sync)
gfx::walk_sync::model_hold hold{model};
// a model drawn on its own is prepared with its lights and animations each time; the others are prepared only for a walk.
// the version is taken before, so a walk prepared by something which changes the model isn't taken up again
walk_record *kept{nullptr};
@@ -585,12 +597,246 @@ void scene_walk::Walk_Indirect_Draws(gfx::instance_cull const &Cull, bool const
m_walkcounts.walktime += std::chrono::duration<double, std::milli>(std::chrono::steady_clock::now() - walkstart).count();
}
void scene_walk::Walk_Groups_Parallel(gfx::instance_cull const &Cull, bool const Cascade, bool const Walkcache, bool const Shadows)
{
auto const &groups{Cull.draw_groups()};
auto const &lodbounds{Cull.lod_bounds()};
auto constexpr nowalk{std::numeric_limits<std::uint32_t>::max()};
// what the walks of the groups share, made before they're split: the records of the walks of the earlier frames and those of the
// cascades of this one
m_groupplans.assign(groups.size(), group_plan{});
for (std::size_t index = 0; index < groups.size(); ++index)
{
auto const &group{groups[index]};
auto *model{group.key->pModel};
auto &plan{m_groupplans[index]};
plan.skip = (model == nullptr) || (model->Root == nullptr) || ((group.instance == nullptr) && (false == group.sleepers));
if (plan.skip)
{
continue;
}
if (Walkcache && (group.key->instance == nullptr))
{
plan.kept = &m_walkrecords[*group.key];
plan.kept->frame = m_framestamp;
auto &bands{plan.kept->bands[Shadows ? 1 : 0]};
if (bands.size() != group.lodcount + 1)
{
bands.clear();
bands.resize(group.lodcount + 1);
}
}
if (Cascade)
{
plan.walks = &m_cascaderecords[*group.key];
if ((plan.walks->frame != m_framestamp) || (plan.walks->bands.size() != group.lodcount + 1))
{
plan.walks->frame = m_framestamp;
plan.walks->bands.assign(group.lodcount + 1, cascade_band{nowalk, 0});
}
}
}
// the bands of a group: those within the distances its instances are at
auto const band_range = [&](gfx::instance_cull::draw_group const &Group, std::uint32_t const Band, float &Bandmin) {
Bandmin = (Band == 0 ? 0.f : lodbounds[Group.lodfirst + Band - 1]);
auto const bandmax{Band == Group.lodcount ? std::numeric_limits<float>::max() : lodbounds[Group.lodfirst + Band]};
return (Bandmin <= Group.distancemax) && (bandmax > Group.distancemin);
};
// the groups split among the tasks one run after another, each walked by a helper as Walk_Indirect_Draws() walks it (the walks the
// cascades made before are copied below)
auto const tasks{std::max<std::size_t>(1, std::min<std::size_t>(groups.size() / 8, 4))};
if (m_grouptasks.size() < tasks)
{
m_grouptasks.resize(tasks);
}
walk_groups_run(tasks, [&](scene_walk &Helper, std::size_t const Task) {
auto &result{m_grouptasks[Task]};
result.spans.clear();
result.counts = {};
Helper.m_indirectdraws.clear();
Helper.m_indirectcommands.clear();
Helper.m_renderpass.draw_stats = {};
for (auto index = groups.size() * Task / tasks; index < groups.size() * (Task + 1) / tasks; ++index)
{
auto const &plan{m_groupplans[index]};
if (plan.skip)
{
continue;
}
auto const &group{groups[index]};
auto *model{group.key->pModel};
// the model the instances share is prepared and walked for the group, held till the walks are made (walk_sync)
gfx::walk_sync::model_hold hold{model};
auto *kept{plan.kept};
auto const modelversion{model->state_version()};
material_handle const *skins{nullptr};
auto alpha{0x30300030};
auto prepared{false};
auto const prepare = [&]() {
prepared = true;
if (group.sleepers)
{
if (group.key->skins[1] != null_handle)
{
skins = group.key->skins.data();
}
TSubModel::iVariantSeed = 0;
}
else
{
if (group.key->instance != nullptr)
{
group.instance->RaAnimate(m_framestamp);
}
group.instance->RaPrepare();
auto const *material{group.instance->Material()};
if (material != nullptr)
{
skins = material->replacable_skins;
alpha = material->textures_alpha;
}
}
alpha ^= 0x0F0F000F;
};
if ((plan.walks == nullptr) && (kept == nullptr))
{
prepare();
}
for (std::uint32_t band = 0; band <= group.lodcount; ++band)
{
float bandmin;
if (false == band_range(group, band, bandmin))
{
continue;
}
if ((plan.walks != nullptr) && (plan.walks->bands[band].first != nowalk))
{
// walked by another cascade
continue;
}
Helper.m_indirectentry = group.entryfirst + band;
auto const walkfirst{Helper.m_indirectdraws.size()};
auto *keptwalk{kept != nullptr ? &kept->bands[Shadows ? 1 : 0][band] : nullptr};
if ((keptwalk != nullptr) && (keptwalk->valid) && (keptwalk->bandmin == bandmin) && (keptwalk->modelversion == modelversion) &&
(keptwalk->blending == Helper.walk_blending_enabled()))
{
Helper.Replay_Walk(*keptwalk, Shadows);
++result.counts.reused;
}
else
{
if (false == prepared)
{
prepare();
}
auto const blending{Helper.walk_blending_enabled()};
Helper.m_indirectchanging = false;
::glPushMatrix();
::glLoadIdentity();
model->Root->fSquareDist = bandmin;
model->Root->ReplacableSet(skins, alpha);
model->Root->pRoot = model;
Helper.Render(model->Root);
::glPopMatrix();
++result.counts.walked;
if (Helper.m_indirectchanging)
{
++result.counts.changing;
}
if (keptwalk != nullptr)
{
keptwalk->valid = (false == Helper.m_indirectchanging);
if (keptwalk->valid)
{
keptwalk->draws.assign(Helper.m_indirectdraws.begin() + walkfirst, Helper.m_indirectdraws.end());
keptwalk->bandmin = bandmin;
keptwalk->modelversion = modelversion;
keptwalk->blending = blending;
}
else
{
keptwalk->draws.clear();
}
}
}
result.spans.push_back(group_span{static_cast<std::uint32_t>(index), band, static_cast<std::uint32_t>(walkfirst), static_cast<std::uint32_t>(Helper.m_indirectdraws.size() - walkfirst)});
}
}
result.draws.swap(Helper.m_indirectdraws);
result.commands.swap(Helper.m_indirectcommands);
result.submodels = Helper.m_renderpass.draw_stats.submodels;
result.drawcalls = Helper.m_renderpass.draw_stats.drawcalls;
});
// the draws in the order of the groups and their bands, with the walks of the cascades copied, as one walk would record them
std::size_t task{0};
std::size_t cursor{0};
for (std::size_t index = 0; index < groups.size(); ++index)
{
// the task the group was in, and where its walks are among those of the task
while (index >= groups.size() * (task + 1) / tasks)
{
++task;
cursor = 0;
}
auto const &plan{m_groupplans[index]};
if (plan.skip)
{
continue;
}
auto const &group{groups[index]};
for (std::uint32_t band = 0; band <= group.lodcount; ++band)
{
float bandmin;
if (false == band_range(group, band, bandmin))
{
continue;
}
m_indirectentry = group.entryfirst + band;
if ((plan.walks != nullptr) && (plan.walks->bands[band].first != nowalk))
{
// walked by another cascade: its draws, for the instances of this entry
auto const &walk{plan.walks->bands[band]};
m_indirectdraws.insert(m_indirectdraws.end(), m_cascadedraws.begin() + walk.first, m_cascadedraws.begin() + walk.first + walk.count);
for (std::uint32_t draw = 0; draw < walk.count; ++draw)
{
auto &command{m_indirectcommands.emplace_back(m_cascadecommands[walk.first + draw])};
command.entry = m_indirectentry;
}
m_renderpass.draw_stats.submodels += static_cast<int>(walk.count);
m_renderpass.draw_stats.drawcalls += static_cast<int>(walk.count);
continue;
}
auto &result{m_grouptasks[task]};
auto const &span{result.spans[cursor++]};
assert((span.group == index) && (span.band == band));
auto const walkfirst{m_indirectdraws.size()};
m_indirectdraws.insert(m_indirectdraws.end(), result.draws.begin() + span.first, result.draws.begin() + span.first + span.count);
m_indirectcommands.insert(m_indirectcommands.end(), result.commands.begin() + span.first, result.commands.begin() + span.first + span.count);
if (plan.walks != nullptr)
{
plan.walks->bands[band] = cascade_band{static_cast<std::uint32_t>(m_cascadedraws.size()), static_cast<std::uint32_t>(m_indirectdraws.size() - walkfirst)};
m_cascadedraws.insert(m_cascadedraws.end(), m_indirectdraws.begin() + walkfirst, m_indirectdraws.end());
m_cascadecommands.insert(m_cascadecommands.end(), m_indirectcommands.begin() + walkfirst, m_indirectcommands.end());
}
}
}
for (std::size_t index = 0; index < tasks; ++index)
{
auto const &result{m_grouptasks[index]};
m_walkcounts.walked += result.counts.walked;
m_walkcounts.changing += result.counts.changing;
m_walkcounts.reused += result.counts.reused;
m_renderpass.draw_stats.submodels += result.submodels;
m_renderpass.draw_stats.drawcalls += result.drawcalls;
}
}
void scene_walk::Record_Indirect_Draw(gfx::geometry_handle const &Geometry)
{
// the walk of Walk_Indirect_Draws(): the draw is made later, from the gpu culling, with this state
auto &geometry{walk_geometry()};
gfx::indirect_source source;
if (geometry.indirect_source(Geometry, source))
if (walk_indirect_source(Geometry, source))
{
m_indirectcommands.push_back({source.count, 0, source.first, static_cast<std::uint32_t>(source.basevertex), 0, m_indirectentry, (source.indexed ? 1u : 0u), 0});
m_indirectdraws.push_back({model_ubs, m_indirectmaterial, m_indirectsubmodel, Geometry, source.type, walk_blending_enabled(), geometry.draw_group(Geometry)});
@@ -605,7 +851,7 @@ void scene_walk::Replay_Walk(walk_band const &Walk, bool const Shadows)
// the place of the chunk in its bank is asked for again: the bank may have been let go since and made anew, which the
// question (as the walk) takes care of, uploading the chunk
gfx::indirect_source source;
if (false == geometry.indirect_source(walkdraw.geometry, source))
if (false == walk_indirect_source(walkdraw.geometry, source))
{
continue;
}

View File

@@ -0,0 +1,304 @@
/*
This Source Code Form is subject to the
terms of the Mozilla Public License, v.
2.0. If a copy of the MPL was not
distributed with this file, You can
obtain one at
http://mozilla.org/MPL/2.0/.
*/
#include "stdafx.h"
#include "rendering/common/walksync.h"
#include "utilities/Logs.h"
#include <array>
#include <atomic>
#include <condition_variable>
#include <cstdint>
#include <mutex>
#include <shared_mutex>
#include <string>
namespace gfx
{
namespace walk_sync
{
namespace
{
// the walk lock: shared by the walks, exclusive for a change. a change waiting goes before the walks asking for the lock again
// (yield()), which the locks of the standard library don't promise
class walk_lock
{
public:
void lock_shared()
{
std::unique_lock<std::mutex> lock(m_mutex);
m_wake.wait(lock, [this]() { return (false == m_writer) && (m_waiting.load(std::memory_order_relaxed) == 0); });
++m_readers;
}
void unlock_shared()
{
std::lock_guard<std::mutex> lock(m_mutex);
if (--m_readers == 0)
{
m_wake.notify_all();
}
}
void lock()
{
std::unique_lock<std::mutex> lock(m_mutex);
m_waiting.fetch_add(1, std::memory_order_relaxed);
m_wake.wait(lock, [this]() { return (false == m_writer) && (m_readers == 0); });
m_waiting.fetch_sub(1, std::memory_order_relaxed);
m_writer = true;
}
void unlock()
{
std::lock_guard<std::mutex> lock(m_mutex);
m_writer = false;
m_wake.notify_all();
}
bool waiting() const
{
return m_waiting.load(std::memory_order_relaxed) > 0;
}
private:
std::mutex m_mutex;
std::condition_variable m_wake;
int m_readers{0};
bool m_writer{false};
std::atomic<int> m_waiting{0};
};
constexpr std::size_t stripe_count{64};
struct sync_state
{
std::atomic<bool> parallel{false};
walk_lock walk;
// the vehicles exclusive, the scenery models shared with a stripe each
std::shared_mutex vehicles;
std::array<std::mutex, stripe_count> models;
std::atomic<bool> reported{false};
};
sync_state &state()
{
static sync_state instance;
return instance;
}
// what the thread holds
thread_local bool t_shared{false};
thread_local bool t_borrowed{false}; // the hold of another thread covers this one (borrowed_scope)
thread_local int t_exclusive{0};
thread_local int t_pattern{0}; // shared states of models held
thread_local bool t_vehicles{false};
thread_local std::uint64_t t_stripes{0}; // the stripes held, a bit each
std::size_t stripe_of(void const *Pointer)
{
auto const value{static_cast<std::uint64_t>(reinterpret_cast<std::uintptr_t>(Pointer))};
return static_cast<std::size_t>(((value >> 4) * 0x9E3779B97F4A7C15ull) >> 58) % stripe_count;
}
void report(char const *What)
{
if (false == state().reported.exchange(true))
{
ErrorLog(std::string("parallel walks of the scene: ") + What + " (reported once)");
}
}
} // namespace
bool parallel()
{
return state().parallel.load(std::memory_order_acquire);
}
void open()
{
auto &sync{state()};
if (sync.parallel.load(std::memory_order_relaxed))
{
return;
}
sync.parallel.store(true, std::memory_order_release);
sync.walk.lock_shared();
t_shared = true;
}
void close()
{
auto &sync{state()};
if (t_shared)
{
sync.walk.unlock_shared();
t_shared = false;
}
sync.parallel.store(false, std::memory_order_release);
}
void leave()
{
if (t_shared)
{
state().walk.unlock_shared();
t_shared = false;
}
}
walk_scope::walk_scope()
{
if (parallel() && (false == t_shared) && (false == t_borrowed))
{
state().walk.lock_shared();
t_shared = true;
m_held = true;
}
}
walk_scope::~walk_scope()
{
if (m_held)
{
state().walk.unlock_shared();
t_shared = false;
}
}
change::change(bool const Wanted)
{
if ((false == Wanted) || (false == parallel()) || (t_exclusive > 0))
{
return;
}
if ((t_pattern > 0) || t_borrowed)
{
// the walks waiting for the model held would wait for the change, which waits for them; the walk a helper works for waits for
// it: not taken
report(t_borrowed ? "a change made by a helper of a walk" : "a change made while the shared state of a model was held");
return;
}
auto &sync{state()};
if (t_shared)
{
sync.walk.unlock_shared();
t_shared = false;
m_shared = true;
}
sync.walk.lock();
++t_exclusive;
m_exclusive = true;
}
change::~change()
{
if (false == m_exclusive)
{
return;
}
auto &sync{state()};
--t_exclusive;
sync.walk.unlock();
if (m_shared)
{
sync.walk.lock_shared();
t_shared = true;
}
}
borrowed_scope::borrowed_scope()
{
t_borrowed = true;
}
borrowed_scope::~borrowed_scope()
{
t_borrowed = false;
}
void yield()
{
if ((false == t_shared) || (t_exclusive > 0) || (t_pattern > 0) || t_borrowed)
{
return;
}
auto &sync{state()};
if (sync.walk.waiting())
{
sync.walk.unlock_shared();
sync.walk.lock_shared();
}
}
model_hold::model_hold(TModel3d const *Model)
{
if ((Model == nullptr) || t_vehicles || (false == parallel()))
{
return;
}
m_stripe = stripe_of(Model);
auto const bit{std::uint64_t{1} << m_stripe};
if ((t_stripes & bit) != 0)
{
// held already by this thread (a model of the same stripe drawn inside the one held)
return;
}
auto &sync{state()};
sync.vehicles.lock_shared();
sync.models[m_stripe].lock();
t_stripes |= bit;
++t_pattern;
m_held = true;
}
model_hold::~model_hold()
{
if (false == m_held)
{
return;
}
auto &sync{state()};
--t_pattern;
t_stripes &= ~(std::uint64_t{1} << m_stripe);
sync.models[m_stripe].unlock();
sync.vehicles.unlock_shared();
}
vehicle_hold::vehicle_hold()
{
if (t_vehicles || (false == parallel()))
{
return;
}
if (t_pattern > 0)
{
// a scenery model held, which the vehicles would wait for
report("a vehicle drawn while the shared state of a model was held");
return;
}
state().vehicles.lock();
t_vehicles = true;
++t_pattern;
m_held = true;
}
vehicle_hold::~vehicle_hold()
{
if (false == m_held)
{
return;
}
--t_pattern;
t_vehicles = false;
state().vehicles.unlock();
}
} // namespace walk_sync
} // namespace gfx

113
rendering/common/walksync.h Normal file
View File

@@ -0,0 +1,113 @@
/*
This Source Code Form is subject to the
terms of the Mozilla Public License, v.
2.0. If a copy of the MPL was not
distributed with this file, You can
obtain one at
http://mozilla.org/MPL/2.0/.
*/
// the walks of the scene made by several threads at once (the stage M2 of claude/vulkan14-renderer-plan.md). only the vulkan renderer
// opens the window in which they are (gfx.vulkan.parallelpasses); outside of it everything here does nothing, and the opengl renderer
// never opens it. while it's open:
// - every thread walking the scene holds the walk lock shared (a pass of a walker from the start to the end, the main thread from the
// opening to the closing). what the walks make the first time they need it, and what's shared by them (the geometry of the
// sections, the animated tracks, the records of the instances in the cells and in the gpu culling, the sleepers laid, the
// resources a lazily loaded model brings: geometry banks, materials, textures) is made with the lock held exclusive (change): the
// thread lets go of its shared hold meanwhile and takes it again after. the walks let go of it at the points where they hold
// nothing which such a change could move (yield()), so a change waits only a little for them
// - the shared state of a model (the submodels the instances and the vehicles set up just before they're drawn: animations,
// lights, the variants of the seasons) is held by the thread drawing it: a scenery model by a lock of its own (a stripe of
// them), the vehicles all at once, as their models may be those of the scenery as well. nothing is changed while it's held
// - the static fields of TSubModel and the matrix stack of OpenGLMatrices are per thread
#pragma once
#include <cstddef>
class TModel3d;
namespace gfx
{
namespace walk_sync
{
// the window is open
bool parallel();
// the main thread opens the window (it holds the walk lock shared from now on), lets go of its hold before it waits for the walkers
// (leave()) and closes the window once they're done
void open();
void leave();
void close();
// the shared hold of a thread walking a pass (a walker); none outside of the window
class walk_scope
{
public:
walk_scope();
~walk_scope();
walk_scope(walk_scope const &) = delete;
walk_scope &operator=(walk_scope const &) = delete;
private:
bool m_held{false};
};
// a change of what the walks share: made with the walk lock held exclusive, while the window is open. Wanted: false when nothing
// is to be changed after all (the change isn't waited for)
class change
{
public:
explicit change(bool Wanted = true);
~change();
change(change const &) = delete;
change &operator=(change const &) = delete;
private:
bool m_exclusive{false};
bool m_shared{false}; // the thread held the lock shared before
};
// lets a change waiting for the walk lock go first; the caller holds nothing it could move
void yield();
// a thread working for a walk which holds the walk lock and waits for it (the walks of the groups of a culling split among helpers,
// the stage M3): it's covered by the hold of that walk, takes no hold of its own and makes no changes
class borrowed_scope
{
public:
borrowed_scope();
~borrowed_scope();
borrowed_scope(borrowed_scope const &) = delete;
borrowed_scope &operator=(borrowed_scope const &) = delete;
};
// the shared state of a scenery model held by the thread, while the window is open
class model_hold
{
public:
explicit model_hold(TModel3d const *Model);
~model_hold();
model_hold(model_hold const &) = delete;
model_hold &operator=(model_hold const &) = delete;
private:
std::size_t m_stripe{0};
bool m_held{false};
};
// the shared state of the vehicles held by the thread (all of them, with the scenery models)
class vehicle_hold
{
public:
vehicle_hold();
~vehicle_hold();
vehicle_hold(vehicle_hold const &) = delete;
vehicle_hold &operator=(vehicle_hold const &) = delete;
private:
bool m_held{false};
};
} // namespace walk_sync
} // namespace gfx