From 1447dcf075469e7915ec1895adbf87c2eebe77a1 Mon Sep 17 00:00:00 2001 From: Hirek193 Date: Tue, 27 Oct 2026 20:56:11 +0100 Subject: [PATCH] Walk the shadow passes and the culled model groups on worker threads in the Vulkan renderer --- CMakeLists.txt | 3 + model/Model3d.cpp | 14 +- model/Model3d.h | 16 +- model/Texture.cpp | 6 + model/Texture.h | 5 + rendering/common/instancecull.cpp | 37 +- rendering/common/instancecull.h | 4 + rendering/common/localshadows.h | 6 + rendering/common/scenewalk.cpp | 134 +++- rendering/common/scenewalk.h | 59 ++ rendering/common/scenewalkindirect.cpp | 252 ++++++- rendering/common/walksync.cpp | 304 +++++++++ rendering/common/walksync.h | 113 ++++ rendering/geometrybank.cpp | 9 + rendering/geometrybank.h | 2 + rendering/openglmatrixstack.cpp | 2 +- rendering/openglmatrixstack.h | 3 +- rendering/vulkan14/vulkan14ao.cpp | 4 +- rendering/vulkan14/vulkan14cabprobe.cpp | 2 +- rendering/vulkan14/vulkan14capabilities.cpp | 13 +- rendering/vulkan14/vulkan14capabilities.h | 2 + rendering/vulkan14/vulkan14commands.cpp | 288 +++++++- rendering/vulkan14/vulkan14commands.h | 78 ++- rendering/vulkan14/vulkan14compute.cpp | 4 +- rendering/vulkan14/vulkan14compute.h | 3 +- rendering/vulkan14/vulkan14context.h | 118 +++- rendering/vulkan14/vulkan14cull.cpp | 13 +- rendering/vulkan14/vulkan14cull.h | 2 + rendering/vulkan14/vulkan14debug.cpp | 2 +- rendering/vulkan14/vulkan14device.h | 5 + rendering/vulkan14/vulkan14frame.cpp | 256 +++++--- rendering/vulkan14/vulkan14geometry.cpp | 13 +- rendering/vulkan14/vulkan14geometry.h | 11 +- rendering/vulkan14/vulkan14indirect.cpp | 82 ++- rendering/vulkan14/vulkan14lamps.cpp | 112 +++- rendering/vulkan14/vulkan14memory.cpp | 124 +++- rendering/vulkan14/vulkan14memory.h | 47 +- rendering/vulkan14/vulkan14passes.cpp | 111 +++- rendering/vulkan14/vulkan14passes.h | 16 + rendering/vulkan14/vulkan14renderer.cpp | 201 +++++- rendering/vulkan14/vulkan14renderer.h | 102 ++- rendering/vulkan14/vulkan14scene.cpp | 689 +++++++++++++++++--- rendering/vulkan14/vulkan14shaders.cpp | 90 ++- rendering/vulkan14/vulkan14shaders.h | 18 +- rendering/vulkan14/vulkan14sky.cpp | 19 +- rendering/vulkan14/vulkan14tasks.cpp | 114 ++++ rendering/vulkan14/vulkan14tasks.h | 62 ++ rendering/vulkan14/vulkan14texture.cpp | 399 ++++++++++-- rendering/vulkan14/vulkan14texture.h | 99 ++- rendering/vulkan14/vulkan14walkers.cpp | 402 ++++++++++++ rendering/vulkan14/vulkan14weather.cpp | 20 +- scene/scene.h | 8 + utilities/Globals.cpp | 24 + utilities/Globals.h | 11 + vehicle/Train.cpp | 8 +- 55 files changed, 4114 insertions(+), 427 deletions(-) create mode 100644 rendering/common/walksync.cpp create mode 100644 rendering/common/walksync.h create mode 100644 rendering/vulkan14/vulkan14tasks.cpp create mode 100644 rendering/vulkan14/vulkan14tasks.h create mode 100644 rendering/vulkan14/vulkan14walkers.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 6d697ae4..6128ad61 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -325,6 +325,7 @@ set(SOURCES "rendering/common/shadersource.cpp" "rendering/common/scenewalk.cpp" "rendering/common/scenewalkindirect.cpp" +"rendering/common/walksync.cpp" "rendering/common/instancecull.cpp" "rendering/common/shadowfit.cpp" "rendering/common/localshadows.cpp" @@ -487,6 +488,8 @@ if (WITH_VULKAN) "rendering/vulkan14/vulkan14commands.cpp" "rendering/vulkan14/vulkan14raytracing.cpp" "rendering/vulkan14/vulkan14dlss.cpp" + "rendering/vulkan14/vulkan14tasks.cpp" + "rendering/vulkan14/vulkan14walkers.cpp" "ref/volk/volk.c") set(DEFINITIONS ${DEFINITIONS} "WITH_VULKAN") diff --git a/model/Model3d.cpp b/model/Model3d.cpp index f1c23d70..2456d556 100644 --- a/model/Model3d.cpp +++ b/model/Model3d.cpp @@ -31,13 +31,13 @@ Copyright (C) 2001-2004 Marcin Wozniak, Maciej Czapkiewicz and others using namespace Mtable; -float TSubModel::fSquareDist = 0.f; -std::uintptr_t TSubModel::iInstance; // numer renderowanego egzemplarza obiektu -std::uint32_t TSubModel::iVariantSeed = 0; // ziarno losowania wariantów tekstur renderowanego egzemplarza -texture_handle const *TSubModel::ReplacableSkinId = nullptr; -int TSubModel::iAlpha = 0x30300030; // maska do testowania flag tekstur wymiennych -TModel3d *TSubModel::pRoot; // Ra: tymczasowo wskaźnik na model widoczny z submodelu -std::string *TSubModel::pasText; +thread_local float TSubModel::fSquareDist = 0.f; +thread_local std::uintptr_t TSubModel::iInstance; // numer renderowanego egzemplarza obiektu +thread_local std::uint32_t TSubModel::iVariantSeed = 0; // ziarno losowania wariantów tekstur renderowanego egzemplarza +thread_local texture_handle const *TSubModel::ReplacableSkinId = nullptr; +thread_local int TSubModel::iAlpha = 0x30300030; // maska do testowania flag tekstur wymiennych +thread_local TModel3d *TSubModel::pRoot; // Ra: tymczasowo wskaźnik na model widoczny z submodelu +thread_local std::string *TSubModel::pasText; // przykłady dla TSubModel::iAlpha: // 0x30300030 - wszystkie bez kanału alfa // 0x31310031 - tekstura -1 używana w danym cyklu, pozostałe nie diff --git a/model/Model3d.h b/model/Model3d.h index 4ca7e345..b646b2c5 100644 --- a/model/Model3d.h +++ b/model/Model3d.h @@ -172,15 +172,17 @@ public: bool is_emitter() const; public: - static size_t iInstance; // identyfikator egzemplarza, który aktualnie renderuje model + // the state of the walk drawing a model: per thread, as the vulkan renderer walks the passes of the scene on several of them at + // once (gfx.vulkan.parallelpasses) + static thread_local size_t iInstance; // identyfikator egzemplarza, który aktualnie renderuje model // seed of the currently rendered model instance, combined with m_variantgroup it picks // texture variants for materials which declare them. 0 for instances without own seed - static std::uint32_t iVariantSeed; - static material_handle const *ReplacableSkinId; - static int iAlpha; // maska bitowa dla danego przebiegu - static float fSquareDist; - static TModel3d *pRoot; - static std::string *pasText; // tekst dla wyświetlacza (!!!! do przemyślenia) + static thread_local std::uint32_t iVariantSeed; + static thread_local material_handle const *ReplacableSkinId; + static thread_local int iAlpha; // maska bitowa dla danego przebiegu + static thread_local float fSquareDist; + static thread_local TModel3d *pRoot; + static thread_local std::string *pasText; // tekst dla wyświetlacza (!!!! do przemyślenia) TSubModel() = default; ~TSubModel(); std::pair Load(cParser &Parser, /*TModel3d *Model, int Pos,*/ bool dynamic); diff --git a/model/Texture.cpp b/model/Texture.cpp index ce063104..f91e84f6 100644 --- a/model/Texture.cpp +++ b/model/Texture.cpp @@ -1590,6 +1590,8 @@ texture_bank::memory() const { memory_data result; for( auto const &texture : m_textures ) { if( texture.first == nullptr ) { continue; } + // made by another thread at the moment + if( texture.first->busy() ) { continue; } if( false == texture.first->data.empty() ) { ++result.cpu.count; result.cpu.bytes += memory_of( texture.first->data ); @@ -1668,6 +1670,10 @@ texture_bank::info() const { for( auto const& texture : m_textures ) { + if( texture.first->busy() ) { + // made by another thread at the moment, counted once it's done + continue; + } totaltexturesize += texture.first->size; #ifdef EU07_DEFERRED_TEXTURE_UPLOAD diff --git a/model/Texture.h b/model/Texture.h index 5fb5d70a..a17e8e15 100644 --- a/model/Texture.h +++ b/model/Texture.h @@ -82,6 +82,11 @@ struct texture_source : public ITexture { // the object of the renderer goes, with the bank of the textures virtual void destroy() = 0; + // another thread of the renderer works on the texture (makes its object from the data, which it reads too): what it holds isn't for + // the other threads to read until it's done + virtual bool + busy() const { + return false; } // members bool has_alpha{ false }; // indicates the texture has alpha channel diff --git a/rendering/common/instancecull.cpp b/rendering/common/instancecull.cpp index 5625b03c..279d8316 100644 --- a/rendering/common/instancecull.cpp +++ b/rendering/common/instancecull.cpp @@ -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(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(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(lodbounds.size()); + } group_data const group{static_cast(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; diff --git a/rendering/common/instancecull.h b/rendering/common/instancecull.h index d63147b7..2533abea 100644 --- a/rendering/common/instancecull.h +++ b/rendering/common/instancecull.h @@ -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) { diff --git a/rendering/common/localshadows.h b/rendering/common/localshadows.h index 9d3c4c52..5cb00265 100644 --- a/rendering/common/localshadows.h +++ b/rendering/common/localshadows.h @@ -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 diff --git a/rendering/common/scenewalk.cpp b/rendering/common/scenewalk.cpp index 5acf150f..c2caa3c5 100644 --- a/rendering/common/scenewalk.cpp +++ b/rendering/common/scenewalk.cpp @@ -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 > 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 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( 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(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(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; diff --git a/rendering/common/scenewalk.h b/rendering/common/scenewalk.h index 35dfd15a..5c2ca6ad 100644 --- a/rendering/common/scenewalk.h +++ b/rendering/common/scenewalk.h @@ -29,6 +29,8 @@ http://mozilla.org/MPL/2.0/. #include "rendering/common/instancecull.h" #include +#include +#include #include #include #include @@ -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 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 draws; + std::vector commands; + std::vector spans; + walk_counts counts; + int submodels{0}; + int drawcalls{0}; + }; + std::vector m_groupplans; + std::vector 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 m_renderme; + std::optional> m_deferredrelease; + std::vector m_deferredprefetch; private: static float editor_ortho_reach(); diff --git a/rendering/common/scenewalkindirect.cpp b/rendering/common/scenewalkindirect.cpp index 95493305..c526fabc 100644 --- a/rendering/common/scenewalkindirect.cpp +++ b/rendering/common/scenewalkindirect.cpp @@ -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 + 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 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(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::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::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(1, std::min(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(index), band, static_cast(walkfirst), static_cast(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(walk.count); + m_renderpass.draw_stats.drawcalls += static_cast(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(m_cascadedraws.size()), static_cast(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(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; } diff --git a/rendering/common/walksync.cpp b/rendering/common/walksync.cpp new file mode 100644 index 00000000..c854a9fe --- /dev/null +++ b/rendering/common/walksync.cpp @@ -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 +#include +#include +#include +#include +#include +#include + +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 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 lock(m_mutex); + if (--m_readers == 0) + { + m_wake.notify_all(); + } + } + void lock() + { + std::unique_lock 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 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 m_waiting{0}; +}; + +constexpr std::size_t stripe_count{64}; + +struct sync_state +{ + std::atomic parallel{false}; + walk_lock walk; + // the vehicles exclusive, the scenery models shared with a stripe each + std::shared_mutex vehicles; + std::array models; + std::atomic 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(reinterpret_cast(Pointer))}; + return static_cast(((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 diff --git a/rendering/common/walksync.h b/rendering/common/walksync.h new file mode 100644 index 00000000..c99f6650 --- /dev/null +++ b/rendering/common/walksync.h @@ -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 + +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 diff --git a/rendering/geometrybank.cpp b/rendering/geometrybank.cpp index 7075f64d..35c40e81 100644 --- a/rendering/geometrybank.cpp +++ b/rendering/geometrybank.cpp @@ -571,6 +571,15 @@ geometrybank_manager::indirect_source( gfx::geometry_handle const &Geometry, gfx return bankrecord.first->indirect_source( Geometry, Source ); } +// marks the bank of the chunk as used, as the draws do +void +geometrybank_manager::mark_used( gfx::geometry_handle const &Geometry ) { + + if( false == valid( Geometry ) ) { return; } + + bank( Geometry ).second = m_garbagecollector.timestamp(); +} + // draws geometry stored in specified chunk with the command at Offset in the bound draw indirect buffer void geometrybank_manager::draw_indirect( gfx::geometry_handle const &Geometry, std::intptr_t const Offset, unsigned int const Streams ) { diff --git a/rendering/geometrybank.h b/rendering/geometrybank.h index c9f70e6a..04cc6ec8 100644 --- a/rendering/geometrybank.h +++ b/rendering/geometrybank.h @@ -277,6 +277,8 @@ public: // buffers of the bank of the specified chunk. the same for chunks of banks which share their buffers, the bank otherwise. // valid once the chunk was made ready for drawing (indirect_source()) auto draw_group( gfx::geometry_handle const &Geometry ) const -> std::uint32_t; + // marks the bank of the chunk as used, as the draws do (the vulkan renderer marks the banks its walkers drew from, M2) + void mark_used( gfx::geometry_handle const &Geometry ); template void draw( Iterator_ First, Iterator_ Last, unsigned int const Streams = basic_streams ) { while( First != Last ) { diff --git a/rendering/openglmatrixstack.cpp b/rendering/openglmatrixstack.cpp index 2b4ce4e0..989105f1 100644 --- a/rendering/openglmatrixstack.cpp +++ b/rendering/openglmatrixstack.cpp @@ -9,4 +9,4 @@ http://mozilla.org/MPL/2.0/. #include "stdafx.h" -opengl_matrices OpenGLMatrices; +thread_local opengl_matrices OpenGLMatrices; diff --git a/rendering/openglmatrixstack.h b/rendering/openglmatrixstack.h index 7d3597ce..321d4d37 100644 --- a/rendering/openglmatrixstack.h +++ b/rendering/openglmatrixstack.h @@ -194,7 +194,8 @@ private: openglstack_array m_stacks; }; -extern opengl_matrices OpenGLMatrices; +// per thread: the vulkan renderer walks the passes of the scene on several of them at once (gfx.vulkan.parallelpasses) +extern thread_local opengl_matrices OpenGLMatrices; // NOTE: standard opengl calls re-definitions diff --git a/rendering/vulkan14/vulkan14ao.cpp b/rendering/vulkan14/vulkan14ao.cpp index 0f374bd9..4cd57555 100644 --- a/rendering/vulkan14/vulkan14ao.cpp +++ b/rendering/vulkan14/vulkan14ao.cpp @@ -141,7 +141,7 @@ bool vulkan14_renderer::compute_ao_lighting(std::size_t const Index) auto &ctx{*m_context}; auto &scene{ctx.scene}; auto &targets{scene.targets[Index]}; - auto const commands{ctx.commands}; + auto const commands{m_commands}; if ((false == bool(targets.ssao)) || (false == bool(targets.ssao_blur)) || (false == bool(targets.scene_depth))) { return false; @@ -187,7 +187,7 @@ void vulkan14_renderer::render_ao(std::size_t const Index, int const Mode) auto &ctx{*m_context}; auto &scene{ctx.scene}; auto &targets{scene.targets[Index]}; - auto const commands{ctx.commands}; + auto const commands{m_commands}; if ((false == bool(targets.ssao)) || (false == bool(targets.ssao_blur))) { return; diff --git a/rendering/vulkan14/vulkan14cabprobe.cpp b/rendering/vulkan14/vulkan14cabprobe.cpp index 7ad1992d..098339e3 100644 --- a/rendering/vulkan14/vulkan14cabprobe.cpp +++ b/rendering/vulkan14/vulkan14cabprobe.cpp @@ -318,7 +318,7 @@ void vulkan14_renderer::render_cab_probe() auto &ctx{*m_context}; auto &scene{ctx.scene}; auto &probe{scene.cabprobe}; - auto const commands{ctx.commands}; + auto const commands{m_commands}; auto *vehicle{cab_probe_vehicle()}; glm::vec3 center{0.f}; glm::vec3 extent{0.f}; diff --git a/rendering/vulkan14/vulkan14capabilities.cpp b/rendering/vulkan14/vulkan14capabilities.cpp index 94dc7898..274b6514 100644 --- a/rendering/vulkan14/vulkan14capabilities.cpp +++ b/rendering/vulkan14/vulkan14capabilities.cpp @@ -227,6 +227,17 @@ void query_capabilities(VkPhysicalDevice Physical, feature_chain &Queried, capab properties2.pNext = &properties14; vkGetPhysicalDeviceProperties2(Physical, &properties2); Caps.max_push_descriptors = properties14.maxPushDescriptors; + // the layouts the host can fill images in (hostImageCopy): the textures are filled straight to the one they're sampled in + std::vector copylayouts(properties14.copyDstLayoutCount); + if (false == copylayouts.empty()) + { + properties14.pCopyDstLayouts = copylayouts.data(); + properties14.copySrcLayoutCount = 0; + properties14.pCopySrcLayouts = nullptr; + vkGetPhysicalDeviceProperties2(Physical, &properties2); + copylayouts.resize(properties14.copyDstLayoutCount); + Caps.host_copy_sampled = (std::find(copylayouts.begin(), copylayouts.end(), VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL) != copylayouts.end()); + } } // features @@ -319,10 +330,10 @@ std::string capability_report(capabilities const &Caps) line("driver " + Caps.driver_name + " " + Caps.driver_info); line("device memory " + memory_size_string(Caps.device_local_memory)); line(std::string("draw indirect count ") + flag(Caps.draw_indirect_count)); + line(std::string("host image copy ") + flag(Caps.host_image_copy) + (Caps.host_image_copy ? (Caps.host_copy_sampled ? " (to the sampled layout)" : " (not to the sampled layout)") : "")); line(std::string("extended dynamic state3 ") + flag(Caps.ext_extended_dynamic_state3)); line(std::string("memory budget ") + flag(Caps.ext_memory_budget)); line(std::string("swapchain maintenance1 ") + flag(Caps.ext_swapchain_maintenance1)); - line(std::string("host image copy ") + flag(Caps.host_image_copy)); line(std::string("sampler filter minmax ") + flag(Caps.sampler_filter_minmax)); line(std::string("shader output layer ") + flag(Caps.shader_output_layer)); line(std::string("fill mode non solid ") + flag(Caps.fill_mode_non_solid)); diff --git a/rendering/vulkan14/vulkan14capabilities.h b/rendering/vulkan14/vulkan14capabilities.h index dc347828..b3999634 100644 --- a/rendering/vulkan14/vulkan14capabilities.h +++ b/rendering/vulkan14/vulkan14capabilities.h @@ -89,6 +89,8 @@ struct capabilities // optional features, each with a way without it bool draw_indirect_count{false}; bool host_image_copy{false}; + // the host fills images straight to VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL (one of the copy layouts of 1.4) + bool host_copy_sampled{false}; bool fill_mode_non_solid{false}; bool pipeline_statistics_query{false}; bool sampler_filter_minmax{false}; diff --git a/rendering/vulkan14/vulkan14commands.cpp b/rendering/vulkan14/vulkan14commands.cpp index 7357bbe4..59a4d2ba 100644 --- a/rendering/vulkan14/vulkan14commands.cpp +++ b/rendering/vulkan14/vulkan14commands.cpp @@ -208,6 +208,55 @@ struct label_call std::size_t name; }; +// the stage and the access of one side of a barrier, without what a queue of the compute family doesn't have (the async compute of M6) +void compute_queue_scope(VkPipelineStageFlags2 &Stage, VkAccessFlags2 &Access) +{ + auto stage{Stage}; + if ((stage & VK_PIPELINE_STAGE_2_ALL_TRANSFER_BIT) != 0) + { + stage = (stage & ~VK_PIPELINE_STAGE_2_ALL_TRANSFER_BIT) | VK_PIPELINE_STAGE_2_COPY_BIT | VK_PIPELINE_STAGE_2_CLEAR_BIT; + } + stage &= (VK_PIPELINE_STAGE_2_TOP_OF_PIPE_BIT | VK_PIPELINE_STAGE_2_BOTTOM_OF_PIPE_BIT | VK_PIPELINE_STAGE_2_DRAW_INDIRECT_BIT | VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT | VK_PIPELINE_STAGE_2_COPY_BIT | + VK_PIPELINE_STAGE_2_CLEAR_BIT | VK_PIPELINE_STAGE_2_HOST_BIT | VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT); + VkAccessFlags2 allowed{VK_ACCESS_2_MEMORY_READ_BIT | VK_ACCESS_2_MEMORY_WRITE_BIT}; + if ((stage & VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT) != 0) + { + allowed |= VK_ACCESS_2_INDIRECT_COMMAND_READ_BIT | VK_ACCESS_2_UNIFORM_READ_BIT | VK_ACCESS_2_SHADER_READ_BIT | VK_ACCESS_2_SHADER_WRITE_BIT | VK_ACCESS_2_SHADER_SAMPLED_READ_BIT | VK_ACCESS_2_SHADER_STORAGE_READ_BIT | + VK_ACCESS_2_SHADER_STORAGE_WRITE_BIT | VK_ACCESS_2_TRANSFER_READ_BIT | VK_ACCESS_2_TRANSFER_WRITE_BIT | VK_ACCESS_2_HOST_READ_BIT | VK_ACCESS_2_HOST_WRITE_BIT; + } + if ((stage & VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT) != 0) + { + allowed |= VK_ACCESS_2_UNIFORM_READ_BIT | VK_ACCESS_2_SHADER_READ_BIT | VK_ACCESS_2_SHADER_WRITE_BIT | VK_ACCESS_2_SHADER_SAMPLED_READ_BIT | VK_ACCESS_2_SHADER_STORAGE_READ_BIT | VK_ACCESS_2_SHADER_STORAGE_WRITE_BIT; + } + if ((stage & VK_PIPELINE_STAGE_2_DRAW_INDIRECT_BIT) != 0) + { + allowed |= VK_ACCESS_2_INDIRECT_COMMAND_READ_BIT; + } + if ((stage & VK_PIPELINE_STAGE_2_COPY_BIT) != 0) + { + allowed |= VK_ACCESS_2_TRANSFER_READ_BIT | VK_ACCESS_2_TRANSFER_WRITE_BIT; + } + if ((stage & VK_PIPELINE_STAGE_2_CLEAR_BIT) != 0) + { + allowed |= VK_ACCESS_2_TRANSFER_WRITE_BIT; + } + if ((stage & VK_PIPELINE_STAGE_2_HOST_BIT) != 0) + { + allowed |= VK_ACCESS_2_HOST_READ_BIT | VK_ACCESS_2_HOST_WRITE_BIT; + } + Stage = (stage != 0 ? stage : VK_PIPELINE_STAGE_2_NONE); + Access = (stage != 0 ? (Access & allowed) : VK_ACCESS_2_NONE); +} + +template void compute_queue_barriers(Barrier_ *Barriers, std::uint32_t const Count) +{ + for (std::uint32_t i = 0; i < Count; ++i) + { + compute_queue_scope(Barriers[i].srcStageMask, Barriers[i].srcAccessMask); + compute_queue_scope(Barriers[i].dstStageMask, Barriers[i].dstAccessMask); + } +} + bool compute_stages(VkShaderStageFlags const Stages) { return (Stages & VK_SHADER_STAGE_COMPUTE_BIT) != 0; @@ -231,6 +280,115 @@ void command_stream::clear() m_work = 0; m_calls = 0; m_finished = false; + m_pieces.clear(); + m_piecefirst = 0; + m_order.clear(); + m_split = no_call; + m_splitchunk = 0; +} + +void command_stream::split() +{ + assert((m_rendering == no_call) && (m_queries == 0)); + if (m_chunks.empty()) + { + clear(); + } + if (m_split != no_call) + { + return; + } + cut(); + m_split = m_chunks.size() - 1; +} + +void command_stream::splice(command_stream const &Other) +{ + assert((m_rendering == no_call) && (m_queries == 0)); + assert(Other.m_pieces.empty()); + if (m_chunks.empty()) + { + clear(); + } + // the chunk under way ends here, with the part of the stream it closes and the state the part leaves + auto ¤t{m_chunks.back()}; + current.end = m_data.size(); + current.suspended = no_call; + piece own; + own.stream = this; + own.first = m_piecefirst; + own.end = m_chunks.size(); + own.state.assign(m_state.begin(), m_state.end()); + own.pushes = m_pushes; + m_pieces.push_back(std::move(own)); + piece spliced; + spliced.stream = &Other; + m_pieces.push_back(std::move(spliced)); + // the calls after it begin a part of their own, with no state of its own yet: what they inherit comes from the spliced stream + chunk next; + next.first = m_data.size(); + m_chunks.push_back(std::move(next)); + m_piecefirst = m_chunks.size() - 1; + m_state.fill(no_call); + m_pushes.clear(); + m_work = 0; +} + +bool command_stream::empty() const +{ + if (false == (m_chunks.empty() || m_data.empty())) + { + return false; + } + for (auto const &piece : m_pieces) + { + if ((piece.stream != this) && (false == piece.stream->empty())) + { + return false; + } + } + return true; +} + +std::size_t command_stream::calls() const +{ + auto calls{m_calls}; + for (auto const &piece : m_pieces) + { + if (piece.stream != this) + { + calls += piece.stream->m_calls; + } + } + return calls; +} + +void command_stream::leave_state(std::size_t const Piece, merged_state &State) const +{ + auto const &piece{m_pieces[Piece]}; + auto const *stream{piece.stream}; + // the part of the stream itself left what was kept when it was closed, a spliced stream what it set last + auto const &slots{(stream == this) ? piece.state : std::vector{stream->m_state.begin(), stream->m_state.end()}}; + auto const &pushes{(stream == this) ? piece.pushes : stream->m_pushes}; + State.slots.resize(slot_count); + for (std::size_t slot = 0; (slot < slots.size()) && (slot < State.slots.size()); ++slot) + { + if (slots[slot] != no_call) + { + State.slots[slot] = state_ref{stream, slots[slot], Piece}; + } + } + for (auto const push : pushes) + { + auto const &info{stream->payload(push).info}; + State.pushes.erase(std::remove_if(State.pushes.begin(), State.pushes.end(), + [&](state_ref const &Other) { + auto const &other{Other.stream->payload(Other.call).info}; + return (other.offset == info.offset) && (other.size == info.size) && (other.stageFlags == info.stageFlags); + }), + State.pushes.end()); + State.pushes.push_back(state_ref{stream, push, Piece}); + } } std::size_t command_stream::add(std::uint32_t const Type, std::size_t const Size) @@ -306,12 +464,92 @@ void command_stream::finish() m_chunks.back().end = m_data.size(); m_chunks.back().suspended = no_call; // a chunk left without calls - if ((m_chunks.size() > 1) && (m_chunks.back().first == m_chunks.back().end)) + if ((m_chunks.size() > 1) && (m_chunks.back().first == m_chunks.back().end) && (m_chunks.size() - 1 > m_piecefirst)) { m_chunks.pop_back(); m_chunks.back().suspended = no_call; } m_finished = true; + if (m_pieces.empty()) + { + m_splitchunk = std::min(m_split, m_chunks.size()); + return; + } + // the last part of the stream itself + piece last; + last.stream = this; + last.first = m_piecefirst; + last.end = m_chunks.size(); + m_pieces.push_back(std::move(last)); + // the spliced streams with all their chunks, and the state each part begins with: what the parts before it left + merged_state state; + state.slots.resize(slot_count); + m_order.clear(); + for (std::size_t index = 0; index < m_pieces.size(); ++index) + { + auto &piece{m_pieces[index]}; + if (piece.stream != this) + { + assert(piece.stream->m_finished); + piece.first = 0; + piece.end = piece.stream->m_chunks.size(); + } + piece.inherited.clear(); + std::vector refs; + for (auto const &slot : state.slots) + { + if (slot.stream != nullptr) + { + refs.push_back(slot); + } + } + refs.insert(refs.end(), state.pushes.begin(), state.pushes.end()); + // in the order they were made: by the part, then by the place in its stream + std::sort(refs.begin(), refs.end(), [](state_ref const &Left, state_ref const &Right) { return (Left.piece != Right.piece) ? (Left.piece < Right.piece) : (Left.call < Right.call); }); + refs.erase(std::unique(refs.begin(), refs.end(), [](state_ref const &Left, state_ref const &Right) { return (Left.stream == Right.stream) && (Left.call == Right.call); }), refs.end()); + for (auto const &ref : refs) + { + piece.inherited.emplace_back(ref.stream, ref.call); + } + for (auto chunk = piece.first; chunk < piece.end; ++chunk) + { + auto const &source{piece.stream->m_chunks[chunk]}; + if ((source.first == source.end) && (source.resumed == no_call)) + { + // nothing in it: no command buffer + continue; + } + m_order.emplace_back(index, chunk); + } + leave_state(index, state); + } + if (m_order.empty()) + { + // a frame with nothing in it still has a command buffer + m_order.emplace_back(0, m_pieces.front().first); + } + // the split is in a part of the stream itself: the chunks after it begin the second submit, with the parts after that part + m_splitchunk = m_order.size(); + if (m_split != no_call) + { + for (std::size_t index = 0; index < m_pieces.size(); ++index) + { + auto const &piece{m_pieces[index]}; + if ((piece.stream != this) || (m_split < piece.first) || (m_split >= piece.end)) + { + continue; + } + for (std::size_t position = 0; position < m_order.size(); ++position) + { + if ((m_order[position].first > index) || ((m_order[position].first == index) && (m_order[position].second >= m_split))) + { + m_splitchunk = position; + break; + } + } + break; + } + } } // the calls @@ -358,6 +596,12 @@ void command_stream::pipeline_barrier2(VkDependencyInfo const *Dependency) data.memory = memory; data.buffers = buffers; data.images = images; + if (m_compute) + { + compute_queue_barriers(memory != no_data ? reinterpret_cast(m_data.data() + memory) : nullptr, data.memory_count); + compute_queue_barriers(buffers != no_data ? reinterpret_cast(m_data.data() + buffers) : nullptr, data.buffer_count); + compute_queue_barriers(images != no_data ? reinterpret_cast(m_data.data() + images) : nullptr, data.image_count); + } close(call); } @@ -1041,6 +1285,27 @@ void command_stream::execute(std::size_t const Call, VkCommandBuffer const Buffe } void command_stream::replay(std::size_t const Chunk, VkCommandBuffer const Buffer) const +{ + if (false == m_pieces.empty()) + { + if (Chunk >= m_order.size()) + { + return; + } + auto const &[index, chunk]{m_order[Chunk]}; + auto const &piece{m_pieces[index]}; + // the state the parts before it left, then the chunk of its stream as it was recorded there + for (auto const &[stream, call] : piece.inherited) + { + stream->execute(call, Buffer); + } + piece.stream->replay_chunk(chunk, Buffer); + return; + } + replay_chunk(Chunk, Buffer); +} + +void command_stream::replay_chunk(std::size_t const Chunk, VkCommandBuffer const Buffer) const { if (Chunk >= m_chunks.size()) { @@ -1070,9 +1335,28 @@ void command_stream::replay(std::size_t const Chunk, VkCommandBuffer const Buffe } void command_stream::replay(VkCommandBuffer const Buffer) const +{ + if (false == m_pieces.empty()) + { + // the parts in their order, each a range of the calls of its stream + for (auto const &piece : m_pieces) + { + if (piece.first >= piece.end) + { + continue; + } + auto const &chunks{piece.stream->m_chunks}; + piece.stream->replay_calls(chunks[piece.first].first, chunks[piece.end - 1].end, Buffer); + } + return; + } + replay_calls(0, m_data.size(), Buffer); +} + +void command_stream::replay_calls(std::size_t const First, std::size_t const End, VkCommandBuffer const Buffer) const { // the chunks one after another, the cuts made whole again: each call as it was made - for (auto call = std::size_t{0}; call < m_data.size();) + for (auto call = First; call < End;) { auto const &header{*reinterpret_cast(m_data.data() + call)}; execute(call, Buffer); diff --git a/rendering/vulkan14/vulkan14commands.h b/rendering/vulkan14/vulkan14commands.h index c4f6fd06..c37fdb9d 100644 --- a/rendering/vulkan14/vulkan14commands.h +++ b/rendering/vulkan14/vulkan14commands.h @@ -86,30 +86,74 @@ class command_stream using external_function = void (*)(VkCommandBuffer Buffer, void *Data); void external(external_function Function, void *Data); + // the calls of Other go to the gpu here, where this stream is now (the stage M2: the passes recorded by other threads at the same + // time): the calls made after this one follow them, with the state they leave (the pipelines, the sets, the constants, the buffers, + // the dynamic state), as if they were made in this stream. outside of a rendering and of a query; Other is a stream without + // streams of its own spliced into it, finished before this one is, and kept until this one is replayed + void splice(command_stream const &Other); + + // the stream goes to a queue of the compute family (the async compute of M6): the stages and the accesses of its barriers which such + // a queue doesn't have are left out. the work of the graphics queue they wait for is done before it through the semaphores + void set_compute_queue(bool Compute) + { + m_compute = Compute; + } + // the frame goes to the queue in two submits from here on, the calls after this one in the second, which waits for the work of the + // compute queue (M6). outside of a rendering and of a query; the first one made counts + void split(); + // the first chunk of the second submit (chunks() when nothing was split), valid after finish() + std::size_t split_chunk() const + { + return m_splitchunk; + } + // the stream is made anew void clear(); - bool empty() const - { - return m_chunks.empty() || (m_data.empty()); - } + bool empty() const; // the stream is ended: no calls after it void finish(); - // the chunks, valid after finish() + // the chunks, valid after finish(); those of the streams spliced into it, in their places std::size_t chunks() const { - return m_chunks.size(); - } - // the calls recorded since the start - std::size_t calls() const - { - return m_calls; + return m_pieces.empty() ? m_chunks.size() : m_order.size(); } + // the calls recorded since the start, with those of the streams spliced into it + std::size_t calls() const; // the chunk recorded into Buffer (begun already, ended by the caller) void replay(std::size_t Chunk, VkCommandBuffer Buffer) const; // all of it into one command buffer void replay(VkCommandBuffer Buffer) const; private: + // a call of a stream, the stream itself or one spliced into it + using call_ref = std::pair; + // a part of the stream as the gpu gets it: chunks of the stream itself or of a stream spliced into it, the state the calls before + // it left (the calls they made last of each kind, in the order they were made) and the state the part leaves with them + struct piece + { + command_stream const *stream{nullptr}; + std::size_t first{0}; // chunks of the stream + std::size_t end{0}; + std::vector inherited; + // the calls of the part setting the state last, kept when the part is of the stream itself (splice()) + std::vector state; + std::vector pushes; + }; + // the calls setting the state, of all the parts so far: a slot each (state_slot), and the pushes of the constants of each range + // and stages; the part of each, for their order + struct state_ref + { + command_stream const *stream{nullptr}; + std::size_t call{0}; + std::size_t piece{0}; + }; + struct merged_state + { + std::vector slots; + std::vector pushes; + }; + // the state the calls of the part leave behind, over the state the parts before it left + void leave_state(std::size_t Piece, merged_state &State) const; // the calls of the state a chunk sets again; the dynamic states have a slot each enum state_slot : std::uint32_t { @@ -176,6 +220,9 @@ class command_stream void cut(); // Flags: added to those of a begin of a rendering void execute(std::size_t Call, VkCommandBuffer Buffer, VkRenderingFlags Flags = 0) const; + // a chunk of this stream alone, and the calls between two offsets + void replay_chunk(std::size_t Chunk, VkCommandBuffer Buffer) const; + void replay_calls(std::size_t First, std::size_t End, VkCommandBuffer Buffer) const; static constexpr std::size_t header_size{8}; std::vector m_data; @@ -188,6 +235,15 @@ class command_stream std::uint32_t m_work{0}; // of the chunk under way std::size_t m_calls{0}; bool m_finished{false}; + // the parts of the stream with the streams spliced into it, empty without them; the first chunk of the part of the stream itself + // under way; the chunks in the order of the parts (the part, the chunk of its stream) + std::vector m_pieces; + std::size_t m_piecefirst{0}; + std::vector> m_order; + bool m_compute{false}; + // the chunk of the stream itself the split begins, and the first chunk of the second submit + std::size_t m_split{no_call}; + std::size_t m_splitchunk{0}; }; } // namespace vk14 diff --git a/rendering/vulkan14/vulkan14compute.cpp b/rendering/vulkan14/vulkan14compute.cpp index e1c7ba64..c5dd6e55 100644 --- a/rendering/vulkan14/vulkan14compute.cpp +++ b/rendering/vulkan14/vulkan14compute.cpp @@ -317,14 +317,14 @@ void update_buffer(command_stream *Commands, buffer const &Target, void const *D // readback -bool readback_ring::create(allocator const &Allocator, VkDeviceSize const Size, std::uint32_t const Frames, std::string const &Name) +bool readback_ring::create(allocator const &Allocator, VkDeviceSize const Size, std::uint32_t const Frames, std::string const &Name, bool const Shared) { m_size = Size; m_slots.clear(); m_slots.resize(Frames); for (std::uint32_t i = 0; i < Frames; ++i) { - if (false == m_slots[i].data.create(Allocator, Size, VK_BUFFER_USAGE_TRANSFER_DST_BIT, memory_kind::readback, Name + " " + std::to_string(i))) + if (false == m_slots[i].data.create(Allocator, Size, VK_BUFFER_USAGE_TRANSFER_DST_BIT, memory_kind::readback, Name + " " + std::to_string(i), Shared)) { return false; } diff --git a/rendering/vulkan14/vulkan14compute.h b/rendering/vulkan14/vulkan14compute.h index acc9f2ae..318d063c 100644 --- a/rendering/vulkan14/vulkan14compute.h +++ b/rendering/vulkan14/vulkan14compute.h @@ -122,7 +122,8 @@ void update_buffer(command_stream *Commands, buffer const &Target, void const *D class readback_ring { public: - bool create(allocator const &Allocator, VkDeviceSize Size, std::uint32_t Frames, std::string const &Name); + // Shared: copied to by the queues of the families the allocator shares between (the async compute of M6) + bool create(allocator const &Allocator, VkDeviceSize Size, std::uint32_t Frames, std::string const &Name, bool Shared = false); void destroy(); // the copy of Size bytes (at most the size of the ring) of Source from Offset, in the frame of the slot, done by the gpu once // it reaches Value of the timeline diff --git a/rendering/vulkan14/vulkan14context.h b/rendering/vulkan14/vulkan14context.h index b07cb50d..97703641 100644 --- a/rendering/vulkan14/vulkan14context.h +++ b/rendering/vulkan14/vulkan14context.h @@ -37,6 +37,7 @@ http://mozilla.org/MPL/2.0/. #include "rendering/vulkan14/vulkan14commands.h" #include "rendering/vulkan14/vulkan14raytracing.h" #include "rendering/vulkan14/vulkan14dlss.h" +#include "rendering/vulkan14/vulkan14tasks.h" #include "utilities/workerpool.h" #include "model/material.h" #include "gl/ubo.h" @@ -49,6 +50,7 @@ http://mozilla.org/MPL/2.0/. #include #include #include +#include #include namespace vk14 @@ -92,7 +94,36 @@ struct scene_program : public gfx::shader_program std::shared_ptr multidraw; bool multidraw_tried{false}; vertex_kind vertices{vertex_kind::scene}; + // the pipelines made, by the key of bind_scene_pipeline(), and the ones the background threads are making (M5). guarded by the + // mutex, as are the variants above made when they're first needed: the walks of the passes and the background threads ask for + // them at once + std::mutex mutex; std::unordered_map pipelines; + std::unordered_set pending; +}; + +// a pipeline of the scene as the list of the pipelines made in a session keeps it, to be made ahead at the start of the next one +// (gfx.vulkan.asyncresources, the stage M5): what bind_scene_pipeline() makes it of, by the names of the shaders +struct scene_pipeline_record +{ + // the shaders, by the names and the variants shader_library::get() takes + std::string vertex; + std::string vertex_defines; + std::string fragment; + std::string fragment_defines; + vertex_kind vertices{vertex_kind::scene}; + VkPrimitiveTopology topology{VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST}; // the class + std::uint32_t blending{0}; + std::array colour{VK_FORMAT_UNDEFINED, VK_FORMAT_UNDEFINED}; + std::uint32_t colours{0}; + VkFormat depth{VK_FORMAT_UNDEFINED}; + std::uint32_t samples{1}; + std::uint32_t view_mask{0}; + std::uint32_t settings{0}; + std::string name; + // one line of the list, and the record of it; false: not a line of the list + std::string line() const; + bool parse(std::string const &Line); }; // what a pass of the scene draws to; the pipelines are made for these @@ -188,13 +219,18 @@ struct generation_targets // the ring of a frame some data of the draws goes through, the descriptor of which reads a window of it: the window starts again // further on when it's full, and the pass set is bound again with the new offset +// the threads of the walkers (the stage M2): as many as the passes they're given in a frame use, within gfx.threads +std::size_t walk_threads(); + +// the parts of the rings of the draws and of the instances a renderer (the main one, a walker) takes for its slices (vk14::ring_cache) +constexpr VkDeviceSize draw_ring_part{64 * 1024}; +constexpr VkDeviceSize instance_ring_part{128 * 1024}; + struct window_ring { frame_ring ring; VkDeviceSize window{0}; // the size of the window, bytes VkDeviceSize stride{0}; - VkDeviceSize base{0}; // where the current window starts - bool open{false}; }; // the light clusters (gl::cluster_grid), of the colour pass of each viewport in turn @@ -487,6 +523,11 @@ struct vulkan14_renderer::context std::vector workers; // the command buffers of the chunks of the frame, in their order std::vector chunks; + // the passes of the frame for the compute queue (the async compute of M6, the sky and the weather), recorded into a command + // buffer of the compute family + vk14::command_stream compute_stream; + VkCommandPool compute_pool{VK_NULL_HANDLE}; + VkCommandBuffer compute_buffer{VK_NULL_HANDLE}; // the value of the timeline semaphore the gpu reaches when it's done with the frame; 0: never submitted std::uint64_t done{0}; // the number of the frame, for the report of a lost device @@ -521,12 +562,36 @@ struct vulkan14_renderer::context double submit_time{0.0}; // seconds // the textures of the files; made after the objects above and gone before them std::unique_ptr textures; + // the background threads (the stage M5): the pipelines and the textures made when they're first needed, the pipelines of the last + // session made ahead. stopped before anything they use goes + vk14::background_tasks background; + // the threads of the walkers (the stage M2): the passes of the frame walked at the same time as the main thread walks its own + vk14::background_tasks walks; + // the threads of the helpers of the walks (the stage M3): the groups of a culling walked by several of them, the walk waiting + vk14::background_tasks models; + // the groups they walked since the frame began + std::atomic helped{0}; + // the pass sets of the scene and those of the cullings made anew (the main thread) and those of the cullings written (any walker) + std::mutex passsets_mutex; + // the pipelines of the scene made in the session, written to the list at the end (cache/vulkan14/pipelines.txt), and how many of the + // list of the last session were made ahead and how many weren't made in time for the draw which needed them + std::mutex warm_mutex; + std::vector warm_records; + std::unordered_set warm_lines; + std::atomic warm_made{0}; + std::atomic pipelines_late{0}; // [0]: the main window, then the windows of the extra viewports std::vector> windows; std::vector frames; // one semaphore counts the frames done by the gpu; the binary ones are only between the swapchains and the queue VkSemaphore timeline{VK_NULL_HANDLE}; std::uint64_t timelinevalue{0}; + // the async compute (gfx.vulkan.asynccompute, M6), on a device with a compute family of its own: the passes of the sky and the + // weather go to its queue, which signals the frames done by it on a timeline of its own. its work of a frame waits for the + // graphics queue to be done with the frame before (what it writes is read there), the part of the frame after the first pass + // which reads what it makes waits for it + bool async_compute{false}; + VkSemaphore compute_timeline{VK_NULL_HANDLE}; // frames recorded since the start std::uint64_t framecount{0}; // the windows with an image acquired in the frame being made @@ -547,6 +612,9 @@ struct vulkan14_renderer::context { frame *target{nullptr}; bool uploaded{false}; + // the value of the timeline of the frame, and whether part of it goes to the compute queue + std::uint64_t value{0}; + bool async{false}; std::vector waits; std::vector signals; // the windows presented once the frame is submitted (the render thread) @@ -575,8 +643,9 @@ struct vulkan14_renderer::context } bool create_frames(std::uint32_t const Count, std::string &Reason); - // the streams of Frame recorded into its command buffers; false: no command buffer for them - bool record(frame &Frame, bool Uploads); + // the streams of Frame recorded into its command buffers (Compute: the one of the compute queue too); false: no command buffer for + // them + bool record(frame &Frame, bool Uploads, bool Compute); // the job done; false: Failure says where it failed bool execute(render_job &Job, char const *&Failure); @@ -604,6 +673,18 @@ struct vulkan14_renderer::context void wait_submitted(); // the parts of the renderer made once the device is there bool create_core(std::uint32_t const Frames, std::string &Reason); + // the pipelines of the scene (vulkan14scene.cpp, vulkan14_renderer::bind_scene_pipeline()): the fragment shader of a program for + // targets with or without the velocity (Compile: made when it isn't there yet, otherwise null then); the pipeline made now, found + // made already, or made by the background threads (Key: of the program's map); the ones of the list of the last session made + // ahead, the list of this one written + std::shared_ptr scene_fragment(vk14::scene_program &Program, bool Single, bool Compile); + VkPipeline make_scene_pipeline(vk14::scene_program &Program, vk14::target_formats const &Targets, vk14::blend_mode Blend, VkPrimitiveTopology Primitiveclass, std::uint32_t Settings, bool Single); + bool find_scene_pipeline(vk14::scene_program &Program, vk14::target_formats const &Targets, vk14::blend_mode Blend, VkPrimitiveTopology Primitiveclass, std::uint32_t Settings, bool Single, + VkPipeline &Pipeline); + void queue_scene_pipeline(vk14::scene_program &Program, vk14::target_formats const &Targets, vk14::blend_mode Blend, VkPrimitiveTopology Primitiveclass, std::uint32_t Settings, bool Single, + std::uint64_t Key); + void warm_scene_pipelines(); + void save_scene_pipelines(); // the value of the timeline the gpu reached, 0 when it can't be read std::uint64_t completed() const; // waits until the gpu reached the value of the timeline; false: the device is lost @@ -647,6 +728,32 @@ struct vulkan14_renderer::draw_state bool cullface{true}; bool glare{false}; bool depthmask_off{false}; // the editor overlay: no depth written whatever the blending + // the pipelines and the textures a draw needs first may be made by the background threads, the draw left out till then (the walks + // of the scene, gfx.vulkan.asyncresources); the draws outside of them (the cab, the sky, the particles) wait for what they need + bool async{false}; + // the pipeline of the draw before, which the next one takes without asking the program when it's the same + vk14::scene_program const *lastprogram{nullptr}; + std::uint64_t lastkey{0}; + VkPipeline lastpipeline{VK_NULL_HANDLE}; + // the windows of the rings of the draws and of the instances the draws read (where the current one starts), and the parts of the + // rings the renderer places its slices in (vk14::ring_cache; each walker has its own, M2) + struct ring_window + { + VkDeviceSize base{0}; + bool open{false}; + }; + ring_window draws_window; + ring_window instances_window; + vk14::ring_cache draws_cache; + vk14::ring_cache instances_cache; + // the map of a lamp or of headlights the pass draws to (begin_local_shadow_rendering()) + vk14::local_shadow_maps::draw_target local_target; + // the pass sets of the scene a walker binds: those of the frame when it was given its pass (the main renderer binds them as they are) + std::array pass_sets{}; + // what a walker's draws used, marked as used by the main renderer once the walks are done (the marks aren't made by several + // threads): the banks of the geometry and the textures + std::vector used_banks; + std::vector used_textures; // the occlusion queries of the glare of the lamps in the frame: the submodels, by the index of their query std::vector queries; std::uint32_t query{~0u}; // the query under way @@ -658,6 +765,9 @@ struct vulkan14_renderer::draw_state bool depthskip{true}; // the probe of the cab (the stage A4): the walk draws with the programs of the probe, what they don't draw is left out alike bool probepass{false}; + // the draws recorded by the walk of the groups of a culling are made (draw_indirect_colour()): they take the model blocks recorded + // with them, the state of their submodels isn't read (the passes walked by other threads set it up for theirs, walk_sync) + bool replaying{false}; // the pass sets of a culling, the instance buffer its output, for the indirect draws of its instances; null: the pass sets VkDescriptorSet const *cull_sets{nullptr}; // the rendering under way, begun again where it was after it was ended for the compute passes of the culling diff --git a/rendering/vulkan14/vulkan14cull.cpp b/rendering/vulkan14/vulkan14cull.cpp index e0727ea1..3f2a91e0 100644 --- a/rendering/vulkan14/vulkan14cull.cpp +++ b/rendering/vulkan14/vulkan14cull.cpp @@ -9,6 +9,7 @@ http://mozilla.org/MPL/2.0/. #include "stdafx.h" #include "rendering/vulkan14/vulkan14cull.h" +#include "rendering/common/walksync.h" #include "rendering/vulkan14/vulkan14commands.h" #include "rendering/frustum.h" @@ -108,6 +109,7 @@ void instance_cull::destroy(VkDevice Device) m_shared->commands.destroy(Device); } m_shared.reset(); + m_recordbuffer.reset(); m_counters.reset(); m_output.reset(); m_commands.reset(); @@ -234,7 +236,7 @@ std::vector instance_cull::cull_descriptors(cull_occlusion const *Oc { auto const &environment{m_shared->environment}; auto const slice = [](ring_slice const &Slice) { return buffer_descriptor(Slice.buffer, Slice.offset, Slice.size); }; - return {buffer_descriptor(m_shared->records->handle(), 0, VK_WHOLE_SIZE), + return {buffer_descriptor(m_recordbuffer->handle(), 0, VK_WHOLE_SIZE), slice(m_jobs), slice(m_groups), slice(m_lods), @@ -252,7 +254,14 @@ void instance_cull::dispatch(command_stream *Commands, glm::mat4 const &View, cu } auto &shared{*m_shared}; auto const &environment{shared.environment}; - flush_records(); + { + // the places of the records of the pass, the records the passes sent and the buffer they're in, together: another pass may start + // the buffer over in between (walk_sync, the passes walked by several threads at once) + gfx::walk_sync::change change{gfx::walk_sync::parallel()}; + refresh_places(); + flush_records(); + m_recordbuffer = shared.records; + } m_countersize = (gl::INSTANCE_COUNTER_HEADER + 3 * m_entries) * sizeof(std::uint32_t); if ((false == reserve(m_counters, std::max(m_countersize, READBACK_SIZE), VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT | VK_BUFFER_USAGE_TRANSFER_SRC_BIT, "instance counters")) || diff --git a/rendering/vulkan14/vulkan14cull.h b/rendering/vulkan14/vulkan14cull.h index a1d7024f..d72b98a6 100644 --- a/rendering/vulkan14/vulkan14cull.h +++ b/rendering/vulkan14/vulkan14cull.h @@ -136,6 +136,8 @@ class instance_cull : public gfx::instance_cull void read_counters(command_stream *Commands); std::shared_ptr m_shared; + // the record buffer of the dispatch, which its second phase reads too (another pass may make the shared one anew in between) + std::shared_ptr m_recordbuffer; std::shared_ptr m_counters; std::shared_ptr m_output; std::shared_ptr m_commands; diff --git a/rendering/vulkan14/vulkan14debug.cpp b/rendering/vulkan14/vulkan14debug.cpp index ef28945a..c609def2 100644 --- a/rendering/vulkan14/vulkan14debug.cpp +++ b/rendering/vulkan14/vulkan14debug.cpp @@ -414,7 +414,7 @@ void vulkan14_renderer::render_cluster_heatmap(std::size_t const Index) auto &scene{ctx.scene}; auto &targets{scene.targets[Index]}; auto &draw{*m_draw}; - auto const commands{ctx.commands}; + auto const commands{m_commands}; if ((scene.clusters.params.valid == 0) || (false == bool(targets.output)) || (false == bool(targets.depth))) { return; diff --git a/rendering/vulkan14/vulkan14device.h b/rendering/vulkan14/vulkan14device.h index 41bb6560..4f9d1008 100644 --- a/rendering/vulkan14/vulkan14device.h +++ b/rendering/vulkan14/vulkan14device.h @@ -72,6 +72,11 @@ class device { return m_graphics_queue; } + // the queue of the compute family; the graphics queue where the family is the same + VkQueue compute_queue() const + { + return m_compute_queue; + } // VK_EXT_memory_budget is on (the allocator keeps to the budget of the heaps) bool memory_budget_enabled() const { diff --git a/rendering/vulkan14/vulkan14frame.cpp b/rendering/vulkan14/vulkan14frame.cpp index 87e0aed2..22129e48 100644 --- a/rendering/vulkan14/vulkan14frame.cpp +++ b/rendering/vulkan14/vulkan14frame.cpp @@ -526,6 +526,8 @@ bool vulkan14_renderer::Render() auto const device{ctx.device.handle()}; auto const slot{static_cast(ctx.framecount % ctx.frames.size())}; auto &frame{ctx.frames[slot]}; + // the walkers of a frame which didn't get to its end + join_walks(); Timer::subsystem.gfx_total.start(); @@ -566,6 +568,10 @@ bool vulkan14_renderer::Render() // what the frames done used last goes ctx.deletions.collect(std::max(ctx.completed(), frame.done)); VK14_VERIFY(vkResetCommandPool(device, frame.pool, 0)); + if (frame.compute_pool != VK_NULL_HANDLE) + { + VK14_VERIFY(vkResetCommandPool(device, frame.compute_pool, 0)); + } for (auto &worker : frame.workers) { VK14_VERIFY(vkResetCommandPool(device, worker.pool, 0)); @@ -574,8 +580,11 @@ bool vulkan14_renderer::Render() ctx.ring.begin_frame(slot); scene.draws.ring.begin_frame(slot); scene.instances.ring.begin_frame(slot); - scene.draws.open = false; - scene.instances.open = false; + // the parts of the rings the main renderer places its draws in, and their windows, anew + m_draw->draws_cache.reset(&scene.draws.ring, vk14::draw_ring_part); + m_draw->instances_cache.reset(&scene.instances.ring, vk14::instance_ring_part); + m_draw->draws_window = {}; + m_draw->instances_window = {}; // the draws which aren't instanced read the identity at the start of the window from the first draw of the frame on open_instance_window(); // the occlusion queries of the lamps the slot made the last time @@ -599,37 +608,46 @@ bool vulkan14_renderer::Render() // the calls of the frame are kept, recorded into the command buffers once the frame is done frame.upload_stream.clear(); frame.commands.clear(); + frame.compute_stream.clear(); auto const value{ctx.timelinevalue + 1}; ctx.uploads.begin_frame(&frame.upload_stream, value); ctx.slot = slot; ctx.commands = &frame.commands; + m_commands = ctx.commands; if (scene.cull.ready) { // the cullings share what they need of the frame scene.cull.main.begin_frame(slot, value, ctx.completed()); } - scene.lamps.begin_frame(ctx.commands, value); + scene.lamps.begin_frame(m_commands, value); scene.cabprobe.begin_frame(value); scene.skyocclusion.begin_frame(value); if (scene.terrain) { // textures of new heightmap terrain layers go to the arrays the terrain shader reads them from - scene.terrain->begin_frame(ctx.commands, value); + scene.terrain->begin_frame(m_commands, value); scene.terrain->update(); } // the copies of the light buffer were in the part of the ring of another frame m_lightheaders.clear(); - // the content other threads gave the textures (python) + // the content other threads gave the textures (python), and the textures the background threads made: in the table before the + // walks of the frame, their copies with the uploads of the frame { std::vector pending; + std::vector made; { std::lock_guard lock(ctx.pending.mutex); pending.swap(ctx.pending.textures); + made.swap(ctx.pending.made); } for (auto *texture : pending) { texture->apply_pending(); } + for (auto *texture : made) + { + texture->publish(); + } } // the frame generation of dlss: the images of the main window for the frames it makes, where the scene is drawn with dlss. the @@ -675,7 +693,11 @@ bool vulkan14_renderer::Render() } frame.number = ctx.framecount; - ctx.passes.begin_frame(ctx.commands, slot, ctx.framecount); + ctx.passes.begin_frame(m_commands, slot, ctx.framecount); + // the passes of the sky and the weather go to the compute queue once the images of the scene are in their layouts (cleared by the + // graphics queue below the first time) and the noises of the clouds are made (their mipmaps are blitted, which the compute queue + // can't do) + m_asyncframe = ctx.async_compute && scene.images_ready && scene.weather.noise_made && simulation::is_ready && (false == Global.gfx_skiprendering); // the stand-ins put in place the first time: the empty maps cleared if (false == scene.images_ready) @@ -699,11 +721,11 @@ bool vulkan14_renderer::Render() VkImageSubresourceRange const range{image->aspect(), 0, image->desc().mips, 0, image->desc().layers}; if (image->aspect() == VK_IMAGE_ASPECT_DEPTH_BIT) { - ctx.commands->clear_depth_stencil_image(image->handle(), VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, &farthest, 1, &range); + m_commands->clear_depth_stencil_image(image->handle(), VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, &farthest, 1, &range); } else { - ctx.commands->clear_color_image(image->handle(), VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, &black, 1, &range); + m_commands->clear_color_image(image->handle(), VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, &black, 1, &range); } uses.push_back(vk14::use_of(*image, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, sampled_stage, sampled_access)); } @@ -716,11 +738,11 @@ bool vulkan14_renderer::Render() { uses.push_back(vk14::use_of(*scene.env_map, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, sampled_stage, sampled_access)); } - vk14::barriers(ctx.commands, uses); + vk14::barriers(m_commands, uses); // no light is assigned to any cluster until the culling pass runs - ctx.commands->fill_buffer(scene.clusters.ranges.handle(), 0, VK_WHOLE_SIZE, 0); + m_commands->fill_buffer(scene.clusters.ranges.handle(), 0, VK_WHOLE_SIZE, 0); // nor does the cab light itself until its probe is drawn - ctx.commands->fill_buffer(scene.cabprobe_bounce.handle(), 0, VK_WHOLE_SIZE, 0); + m_commands->fill_buffer(scene.cabprobe_bounce.handle(), 0, VK_WHOLE_SIZE, 0); VkMemoryBarrier2 barrier{}; barrier.sType = VK_STRUCTURE_TYPE_MEMORY_BARRIER_2; barrier.srcStageMask = VK_PIPELINE_STAGE_2_CLEAR_BIT; @@ -731,7 +753,7 @@ bool vulkan14_renderer::Render() dependency.sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO; dependency.memoryBarrierCount = 1; dependency.pMemoryBarriers = &barrier; - ctx.commands->pipeline_barrier2(&dependency); + m_commands->pipeline_barrier2(&dependency); // the light of the sky and the exposure from their first state clear_sky_images(); ctx.passes.end_pass(); @@ -739,7 +761,7 @@ bool vulkan14_renderer::Render() // the set of the ray tracing of the frame has a structure to read from its first frame on if (scene.raytracing) { - scene.raytracing->begin_frame(ctx.commands, slot, value); + scene.raytracing->begin_frame(m_commands, slot, value); } // the block of the weather and the scene for the compute passes before the first pass of the scene m_draw->scene_offset = 0; @@ -760,12 +782,22 @@ bool vulkan14_renderer::Render() // directional part of the ambient light. the volumetric clouds of the previous frame tell the sky whether to look overcast // and dim the sun itself m_skylight.set_clouds(m_weatherstate.clouds_drawn(), m_weatherstate.cloud_shadows()); + if (m_asyncframe) + { + m_commands = &frame.compute_stream; + ctx.passes.begin_async(m_commands); + } update_sky(); // fog, clouds and stars of the weather, in the light of the sky worked out above if (false == Global.gfx_skiprendering) { update_weather(); } + if (m_asyncframe) + { + ctx.passes.end_async(); + m_commands = ctx.commands; + } // lamps of models and vehicles around the camera and the cab lamp, for the views of the frame to take into their light buffers if ((false == Global.gfx_skiprendering) && (simulation::Region != nullptr)) { @@ -800,7 +832,7 @@ bool vulkan14_renderer::Render() m_nodestateversion = scene::node_state_version; } // what the maps of the occlusion of the sky were given, copied to them - scene.skyocclusion.flush(ctx.commands); + scene.skyocclusion.flush(m_commands); // the sleeper records of tracks out of sight for a while, and the records of the models drawn on their own Expire_Indirect_Records(); // scenery lamps which get shadows in this frame. their maps hold the scenery as of the version of the instance records @@ -851,7 +883,9 @@ bool vulkan14_renderer::Render() } } - // the windows: the image of their viewport, the user interface over the main one + // the windows: the image of their viewport, the user interface over the main one. what's left of the frame waits for the compute + // queue, where the scene wasn't drawn + split_frame(); bool uidrawn{false}; for (auto *window : ctx.acquired) { @@ -882,19 +916,19 @@ bool vulkan14_renderer::Render() rendering.layerCount = 1; rendering.colorAttachmentCount = 1; rendering.pColorAttachments = &attachment; - ctx.commands->begin_rendering(&rendering); + m_commands->begin_rendering(&rendering); if (drawscene && (viewport < scene.targets.size()) && scene.targets[viewport].output) { present_viewport(viewport); } if (ismain) { - ctx.imgui.record(ctx.commands, window->extent(), window->format(), window->srgb()); + ctx.imgui.record(m_commands, window->extent(), window->format(), window->srgb()); uidrawn = true; } - ctx.commands->end_rendering(); + m_commands->end_rendering(); ctx.passes.end_pass(); - vk14::barriers(ctx.commands, {window->use(VK_IMAGE_LAYOUT_PRESENT_SRC_KHR, VK_PIPELINE_STAGE_2_NONE, VK_ACCESS_2_NONE)}); + vk14::barriers(m_commands, {window->use(VK_IMAGE_LAYOUT_PRESENT_SRC_KHR, VK_PIPELINE_STAGE_2_NONE, VK_ACCESS_2_NONE)}); } if (false == uidrawn) { @@ -910,7 +944,7 @@ bool vulkan14_renderer::Render() scene.screenshot = std::make_shared(); if (scene.screenshot->create(ctx.memory, static_cast(extent.width) * extent.height * 4, VK_BUFFER_USAGE_TRANSFER_DST_BIT, vk14::memory_kind::readback, "screenshot")) { - vk14::barriers(ctx.commands, {vk14::use_of(output, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_PIPELINE_STAGE_2_COPY_BIT, VK_ACCESS_2_TRANSFER_READ_BIT)}); + vk14::barriers(m_commands, {vk14::use_of(output, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_PIPELINE_STAGE_2_COPY_BIT, VK_ACCESS_2_TRANSFER_READ_BIT)}); VkBufferImageCopy2 region{}; region.sType = VK_STRUCTURE_TYPE_BUFFER_IMAGE_COPY_2; region.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}; @@ -922,19 +956,24 @@ bool vulkan14_renderer::Render() copy.dstBuffer = scene.screenshot->handle(); copy.regionCount = 1; copy.pRegions = ®ion; - ctx.commands->copy_image_to_buffer2(©); - host_read_barrier(ctx.commands); + m_commands->copy_image_to_buffer2(©); + host_read_barrier(m_commands); scene.screenshot_pending = true; scene.screenshot_value = value; scene.screenshot_extent = extent; } } + // nothing is left to the walkers when the frame is handed over + join_walks(); ctx.passes.end_frame(); vulkan14_renderer::context::render_job job; job.target = &frame; job.uploaded = ctx.uploads.end_frame(); + job.value = value; + job.async = m_asyncframe; // the stream of the frame is the render thread's now: nothing records into it any more ctx.commands = nullptr; + m_commands = nullptr; m_draw->commands = nullptr; scene.lamps.end_frame(); if (scene.terrain) @@ -1043,12 +1082,15 @@ void vulkan14_renderer::render_scene() ctx.passes.begin_pass("reflection history", {vk14::use_of(image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_PIPELINE_STAGE_2_CLEAR_BIT, VK_ACCESS_2_TRANSFER_WRITE_BIT)}); VkClearColorValue const black{{0.f, 0.f, 0.f, 0.f}}; VkImageSubresourceRange const range{VK_IMAGE_ASPECT_COLOR_BIT, 0, image.desc().mips, 0, 1}; - ctx.commands->clear_color_image(image.handle(), VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, &black, 1, &range); - vk14::barriers(ctx.commands, {vk14::use_of(image, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, sampled_stage, sampled_access)}); + m_commands->clear_color_image(image.handle(), VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, &black, 1, &range); + vk14::barriers(m_commands, {vk14::use_of(image, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, sampled_stage, sampled_access)}); ctx.passes.end_pass(); } render_viewport(i); } + // the passes given to the walkers are done before the frame goes on (they were walked at the same time as the viewports) + join_walks(); + m_helpedgroups = ctx.helped.exchange(0); m_current_viewport = (m_viewports.empty() ? nullptr : &m_viewports.front()); } @@ -1057,7 +1099,7 @@ void vulkan14_renderer::render_scene() void vulkan14_renderer::start_draws(vk14::target_formats const &Targets, int const Width, int const Height, int const Set, bool const Shadows) { auto &draw{*m_draw}; - draw.commands = m_context->commands; + draw.commands = m_commands; draw.targets = &Targets; draw.extent = {static_cast(Width), static_cast(Height)}; draw.set = static_cast(Set); @@ -1378,14 +1420,29 @@ void vulkan14_renderer::setup_pass(viewport_config &Viewport, renderpass_config } // the colour pass of a viewport (opengl46_renderer::Render_pass(), rendermode::color) +// the frame goes to the queue in two submits from here on, the second waiting for the passes of the sky and the weather on the compute +// queue (the async compute); the first split counts +void vulkan14_renderer::split_frame() +{ + if (m_asyncframe && (m_main == nullptr) && (m_commands != nullptr)) + { + m_commands->split(); + } +} + void vulkan14_renderer::render_viewport(std::size_t const Index) { auto &ctx{*m_context}; auto &scene{ctx.scene}; auto &vp{m_viewports[Index]}; auto &targets{scene.targets[Index]}; - auto const commands{ctx.commands}; + auto const commands{m_commands}; m_current_viewport = &vp; + if (false == vp.main) + { + // the views other than the main one have no sun shadows to draw before what reads the sky and the weather + split_frame(); + } setup_pass(vp, m_renderpass, rendermode::color); // the temporal anti-aliasing or dlss: the projection of the frame moved by a part of a pixel, the velocities pointed to the last @@ -1490,6 +1547,9 @@ void vulkan14_renderer::render_viewport(std::size_t const Index) m_renderpass = m_colorpass; Timer::subsystem.gfx_shadows.stop(); } + // the sun shadows went on at the same time as the passes of the sky and the weather on the compute queue; the passes from here on + // read what they made (the reflections draw the sky) + split_frame(); if (Global.gfx_envmap_enabled && vp.main && scene.env_map) { m_renderpass.draw_stats = {}; @@ -1654,7 +1714,7 @@ void vulkan14_renderer::render_viewport(std::size_t const Index) } setup_shadow_color(m_shadowcolor); model_ubs.future = future; - Render(simulation::Region); + walk_region(simulation::Region, false); m_indirectcolourpending = false; // the grass of the terrain, opaque, over the ground drawn @@ -1667,7 +1727,7 @@ void vulkan14_renderer::render_viewport(std::size_t const Index) // ...translucent parts ctx.passes.begin_pass("colour, translucent and cab"); walk_blending(true); - Render_Alpha(simulation::Region); + walk_region(simulation::Region, true); render_editor_overlay(); // the depth drawn so far, which the particles of the gpu fade in front of m_particlesoft = (Global.gfx_particles_soft > 0.f) && ((Global.Smoke && Global.gfx_smoke_gpu && (false == simulation::Particles.sequence().empty())) || (Global.gfx_precipitation_gpu && (Global.Overcast > 1.f))) && @@ -1740,7 +1800,7 @@ void vulkan14_renderer::render_scene_depth(std::size_t const Index, glm::mat4 co auto &scene{ctx.scene}; auto &targets{scene.targets[Index]}; auto &draw{*m_draw}; - auto const commands{ctx.commands}; + auto const commands{m_commands}; auto const msaa{targets.samples != VK_SAMPLE_COUNT_1_BIT}; // the depth copied apart from the one the colour pass goes on with, for the contact shadows, the screen space reflections and the // ambient occlusion in the light of the surfaces @@ -1813,7 +1873,7 @@ void vulkan14_renderer::render_scene_depth(std::size_t const Index, glm::mat4 co Render_cab(vehicle, vehicle->InteriorLightLevel, false); } model_ubs.future = Future; - Render(simulation::Region); + walk_region(simulation::Region, false); m_renderpass.draw_stats = colourstats; commands->set_depth_bias_enable(VK_FALSE); draw.depthpass = false; @@ -1966,7 +2026,7 @@ void vulkan14_renderer::render_ray_scene(viewport_config &Viewport) auto const buildstart{std::chrono::steady_clock::now()}; ctx.passes.begin_pass("ray tracing scene"); - scene.raytracing->build(ctx.commands, ctx.slot, ctx.uploads.current_value(), m_framestamp, m_rtdraws, ctx.images); + scene.raytracing->build(m_commands, ctx.slot, ctx.uploads.current_value(), m_framestamp, m_rtdraws, ctx.images); ctx.passes.end_pass(); auto const buildend{std::chrono::steady_clock::now()}; m_rtwalkms += (std::chrono::duration(buildstart - walkstart).count() - m_rtwalkms) * 0.05; @@ -1986,12 +2046,11 @@ void vulkan14_renderer::gather_ray_draw(gfx::geometry_handle const &Geometry) { return; } - gfx::indirect_source indirect; - if (false == m_geometry.indirect_source(Geometry, indirect)) + vk14::chunk_source chunk; + if (false == chunk_of(Geometry, chunk)) { return; } - auto const &chunk{static_cast(m_banks[Geometry.bank - 1])->last()}; if (((chunk.topology != VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST) && (chunk.topology != VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP) && (chunk.topology != VK_PRIMITIVE_TOPOLOGY_TRIANGLE_FAN)) || (chunk.vertex_address == 0)) { return; @@ -2060,7 +2119,7 @@ void vulkan14_renderer::store_ssr_history(std::size_t const Index, vk14::image & { return; } - auto const commands{ctx.commands}; + auto const commands{m_commands}; auto &image{*targets.ssr_history}; auto const mips{image.desc().mips}; auto const width{static_cast(image.desc().extent.width)}; @@ -2165,7 +2224,7 @@ void vulkan14_renderer::begin_colour_rendering(std::size_t const Index, bool con rendering.colorAttachmentCount = targets.formats.colours; rendering.pColorAttachments = colours.data(); rendering.pDepthAttachment = &depth; - ctx.commands->begin_rendering(&rendering); + m_commands->begin_rendering(&rendering); remember_rendering(rendering, colour_target_uses(targets)); } @@ -2181,7 +2240,7 @@ void vulkan14_renderer::walk_editor_cursor() return; } auto &targets{scene.targets.front()}; - auto const commands{ctx.commands}; + auto const commands{m_commands}; glm::vec2 mousepos = Global.cursor_pos; // cursor coordinates are flipped compared to opengl, whose rows the images keep mousepos.y = Global.window_size.y - mousepos.y; @@ -2238,7 +2297,7 @@ void vulkan14_renderer::render_shadows(viewport_config &Viewport) { auto &ctx{*m_context}; auto &scene{ctx.scene}; - auto const commands{ctx.commands}; + auto const commands{m_commands}; setup_pass(Viewport, m_renderpass, rendermode::shadows); auto csmstageboundaries{gfx::shadow_fit::fixed_splits()}; // the cascades fitted to the depth of the view (gfx.shadow.sdsm) go between the distances of its pixels, each map covering the @@ -2247,6 +2306,12 @@ void vulkan14_renderer::render_shadows(viewport_config &Viewport) auto const fitted{shadow_fit(Viewport, csmstageboundaries, fitareas)}; scene_ubs.shadow_fit = glm::vec4{fitted ? 1.f : 0.f, 0.f, 0.f, 0.f}; m_shadowfitstate.splits = csmstageboundaries; + // the cascades walked by the walkers (gfx.vulkan.parallelpasses), each with a culling of its own made beforehand + auto const walkers{walks_wanted()}; + if (walkers) + { + ensure_cull(); + } static char const *const parts[] = {"sun shadows 1", "sun shadows 2", "sun shadows 3"}; for (std::size_t idx = 0; idx < m_shadowpass.size(); ++idx) { @@ -2261,39 +2326,56 @@ void vulkan14_renderer::render_shadows(viewport_config &Viewport) scene_ubs.projection = OpenGLMatrices.data(GL_PROJECTION); auto const csmstagezfar{(csmstageboundaries[idx + 1]) + (m_shadowpass.size() - idx)}; scene_ubs.cascade_end[idx] = csmstagezfar * csmstagezfar; - - VkClearValue clear{}; - clear.depthStencil = {0.f, 0}; - auto depth{attachment_info(scene.shadow_layers[idx], VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_OPTIMAL, true, clear)}; - VkRenderingInfo rendering{}; - rendering.sType = VK_STRUCTURE_TYPE_RENDERING_INFO; - rendering.renderArea = {{0, 0}, {scene.shadow_size, scene.shadow_size}}; - rendering.layerCount = 1; - rendering.pDepthAttachment = &depth; - commands->begin_rendering(&rendering); - remember_rendering(rendering, {vk14::use_of(*scene.shadow_map, VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_OPTIMAL, depth_stage, depth_access)}); - start_draws(scene.shadow_formats, static_cast(scene.shadow_size), static_cast(scene.shadow_size), vk14::scene_state::set_plain, true); - m_shadowcascade = static_cast(idx); - walk_blending(false); - upload_scene_block(); - if (m_shadowcolor != colors::white) + if (walkers) { - Render(simulation::Region); - // render cab only in the closest csm stage - if ((idx == 0) && !FreeFlyModeFlag && Global.render_cab && simulation::Train) - { - Render_cab(simulation::Train->Dynamic(), 0.0f, false); - Render_cab(simulation::Train->Dynamic(), 0.0f, true); - } + fork_walk([idx](vulkan14_renderer &Walker) { Walker.walk_cascade(idx); }); + } + else + { + walk_cascade(idx); } - m_shadowcascade = -1; - commands->end_rendering(); - end_draws(); ctx.passes.end_pass(); } vk14::barriers(commands, {vk14::use_of(*scene.shadow_map, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, sampled_stage, sampled_access)}); } +// the walk of a cascade of the sun shadows set up by render_shadows(): by the main renderer, or by a walker given the state of the frame +void vulkan14_renderer::walk_cascade(std::size_t const Index) +{ + auto &ctx{*m_context}; + auto &scene{ctx.scene}; + auto const commands{m_commands}; + // the matrices of the camera of the cascade (the walker's own, made from the pass it was given) + setup_matrices(); + VkClearValue clear{}; + clear.depthStencil = {0.f, 0}; + auto depth{attachment_info(scene.shadow_layers[Index], VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_OPTIMAL, true, clear)}; + VkRenderingInfo rendering{}; + rendering.sType = VK_STRUCTURE_TYPE_RENDERING_INFO; + rendering.renderArea = {{0, 0}, {scene.shadow_size, scene.shadow_size}}; + rendering.layerCount = 1; + rendering.pDepthAttachment = &depth; + commands->begin_rendering(&rendering); + remember_rendering(rendering, {vk14::use_of(*scene.shadow_map, VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_OPTIMAL, depth_stage, depth_access)}); + start_draws(scene.shadow_formats, static_cast(scene.shadow_size), static_cast(scene.shadow_size), vk14::scene_state::set_plain, true); + m_shadowcascade = static_cast(Index); + walk_blending(false); + upload_scene_block(); + if (m_shadowcolor != colors::white) + { + walk_region(simulation::Region, false); + // render cab only in the closest csm stage + if ((Index == 0) && !FreeFlyModeFlag && Global.render_cab && simulation::Train) + { + Render_cab(simulation::Train->Dynamic(), 0.0f, false); + Render_cab(simulation::Train->Dynamic(), 0.0f, true); + } + } + m_shadowcascade = -1; + commands->end_rendering(); + end_draws(); +} + // the environment map (the sky and the clouds around the camera), a face of it per frame (opengl46_renderer::Render_reflections()) bool vulkan14_renderer::render_reflections(viewport_config &Viewport) { @@ -2322,14 +2404,14 @@ bool vulkan14_renderer::render_reflections(viewport_config &Viewport) for (; m_environmentupdateface <= lastface; ++m_environmentupdateface) { // the depth of the faces is the same image: the clear of a face waits for the draws of the one before - vk14::barriers(ctx.commands, {vk14::use_of(*scene.env_depth, VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_OPTIMAL, depth_stage, depth_access)}); + vk14::barriers(m_commands, {vk14::use_of(*scene.env_depth, VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_OPTIMAL, depth_stage, depth_access)}); m_environmentcubetextureface = m_environmentupdateface; render_reflection_face(Viewport); } if (m_environmentupdateface >= 6) { // the mipmaps, each level from the one before it - auto const commands{ctx.commands}; + auto const commands{m_commands}; auto &image{*scene.env_map}; auto const mips{image.desc().mips}; auto const size{static_cast(image.desc().extent.width)}; @@ -2390,7 +2472,7 @@ bool vulkan14_renderer::render_reflections(viewport_config &Viewport) } else { - vk14::barriers(ctx.commands, {vk14::use_of(*scene.env_map, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, sampled_stage, sampled_access)}); + vk14::barriers(m_commands, {vk14::use_of(*scene.env_map, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, sampled_stage, sampled_access)}); } return true; } @@ -2399,7 +2481,7 @@ void vulkan14_renderer::render_reflection_face(viewport_config &Viewport) { auto &ctx{*m_context}; auto &scene{ctx.scene}; - auto const commands{ctx.commands}; + auto const commands{m_commands}; auto const size{scene.env_map->desc().extent.width}; setup_pass(Viewport, m_renderpass, rendermode::reflections); @@ -2825,7 +2907,7 @@ bool vulkan14_renderer::setup_particles_depth(std::size_t const Index) return false; } } - auto const commands{ctx.commands}; + auto const commands{m_commands}; suspend_rendering(); vk14::barriers(commands, {vk14::use_of(*targets.depth, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_PIPELINE_STAGE_2_COPY_BIT, VK_ACCESS_2_TRANSFER_READ_BIT), vk14::use_of(*targets.scene_depth, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_PIPELINE_STAGE_2_COPY_BIT, VK_ACCESS_2_TRANSFER_WRITE_BIT)}); @@ -3108,7 +3190,7 @@ void vulkan14_renderer::post_pass(char const *Fragment, vk14::image &Target, std { auto &ctx{*m_context}; auto &scene{ctx.scene}; - auto const commands{ctx.commands}; + auto const commands{m_commands}; VkExtent2D const extent{Target.desc().extent.width, Target.desc().extent.height}; std::vector uses{vk14::use_of(Target, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, colour_stage, colour_access)}; for (auto *input : Inputs) @@ -3204,7 +3286,7 @@ void vulkan14_renderer::render_dlss(std::size_t const Index) inputs.jitter = m_taa.jitter(); inputs.reset = m_dlssreset || (false == m_taa.history_valid()); inputs.frame_time = static_cast(Timer::GetDeltaRenderTime() * 1000.0); - ctx.dlss.evaluate(*ctx.commands, inputs, ctx.deletions, ctx.uploads.current_value()); + ctx.dlss.evaluate(*m_commands, inputs, ctx.deletions, ctx.uploads.current_value()); m_dlssreset = false; ctx.passes.end_pass(); } @@ -3381,14 +3463,14 @@ void vulkan14_renderer::render_generation(std::size_t const Index) rendering.layerCount = 1; rendering.colorAttachmentCount = 1; rendering.pColorAttachments = &attachment; - ctx.commands->begin_rendering(&rendering); + m_commands->begin_rendering(&rendering); if (drawn) { present_viewport(Index); } // the user interface in each of them (kept for the next one but the last) - ctx.imgui.record(ctx.commands, extent, window.format(), window.srgb(), false == last); - ctx.commands->end_rendering(); + ctx.imgui.record(m_commands, extent, window.format(), window.srgb(), false == last); + m_commands->end_rendering(); ctx.passes.end_pass(); } std::vector presents{window.use(VK_IMAGE_LAYOUT_PRESENT_SRC_KHR, VK_PIPELINE_STAGE_2_NONE, VK_ACCESS_2_NONE)}; @@ -3396,7 +3478,7 @@ void vulkan14_renderer::render_generation(std::size_t const Index) { presents.push_back(window.generated_use(i, VK_IMAGE_LAYOUT_PRESENT_SRC_KHR, VK_PIPELINE_STAGE_2_NONE, VK_ACCESS_2_NONE)); } - vk14::barriers(ctx.commands, presents); + vk14::barriers(m_commands, presents); return; } @@ -3410,13 +3492,13 @@ void vulkan14_renderer::render_generation(std::size_t const Index) rendering.layerCount = 1; rendering.colorAttachmentCount = 1; rendering.pColorAttachments = &attachment; - ctx.commands->begin_rendering(&rendering); + m_commands->begin_rendering(&rendering); present_viewport(Index, &Target); if (Ui) { - ctx.imgui.record(ctx.commands, extent, Target.desc().format, false); + ctx.imgui.record(m_commands, extent, Target.desc().format, false); } - ctx.commands->end_rendering(); + m_commands->end_rendering(); ctx.passes.end_pass(); }; drawto(*generation.hudless, false, "frame generation hudless"); @@ -3455,7 +3537,7 @@ void vulkan14_renderer::render_generation(std::size_t const Index) // its history is of no use after a frame without it (minimized, not drawn, without the images) inputs.reset = (false == m_taa.history_valid()) || (m_generationframe + 1 != ctx.framecount); m_generationframe = ctx.framecount; - ctx.dlss.generate(*ctx.commands, inputs, ctx.deletions, ctx.uploads.current_value()); + ctx.dlss.generate(*m_commands, inputs, ctx.deletions, ctx.uploads.current_value()); ctx.passes.end_pass(); // the frames to the images of the window: the ones generated first, in the order they are shown, then the one drawn @@ -3487,7 +3569,7 @@ void vulkan14_renderer::render_generation(std::size_t const Index) blit.regionCount = 1; blit.pRegions = ®ion; blit.filter = VK_FILTER_NEAREST; - ctx.commands->blit_image2(&blit); + m_commands->blit_image2(&blit); return; } VkImageCopy2 region{}; @@ -3503,7 +3585,7 @@ void vulkan14_renderer::render_generation(std::size_t const Index) copy.dstImageLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; copy.regionCount = 1; copy.pRegions = ®ion; - ctx.commands->copy_image2(©); + m_commands->copy_image2(©); }; for (std::uint32_t i = 0; i < window.generated_count(); ++i) { @@ -3516,7 +3598,7 @@ void vulkan14_renderer::render_generation(std::size_t const Index) { presents.push_back(window.generated_use(i, VK_IMAGE_LAYOUT_PRESENT_SRC_KHR, VK_PIPELINE_STAGE_2_NONE, VK_ACCESS_2_NONE)); } - vk14::barriers(ctx.commands, presents); + vk14::barriers(m_commands, presents); } // the post-processing of a viewport: from the colour of the scene to its output, which the window shows (the part of @@ -3526,7 +3608,7 @@ void vulkan14_renderer::render_post(std::size_t const Index, bool const Motionbl auto &ctx{*m_context}; auto &scene{ctx.scene}; auto &targets{scene.targets[Index]}; - auto const commands{ctx.commands}; + auto const commands{m_commands}; // the occlusion goes into the image before the blur, which smears both alike. the targets are there only when it was on as they // were made. in the light of the surfaces it's in the image already @@ -3624,7 +3706,7 @@ void vulkan14_renderer::present_viewport(std::size_t const Index, vk14::image co auto &scene{ctx.scene}; auto &targets{scene.targets[Index]}; auto const &window{*ctx.windows[m_viewports[Index].window]}; - auto const commands{ctx.commands}; + auto const commands{m_commands}; auto const format{Target != nullptr ? Target->desc().format : window.format()}; auto const srgb{Target != nullptr ? false : window.srgb()}; VkExtent2D const extent{Target != nullptr ? VkExtent2D{Target->desc().extent.width, Target->desc().extent.height} : window.extent()}; @@ -3858,7 +3940,7 @@ void vulkan14_renderer::update_lights(light_array &Lights, bool const Headlights auto &ctx{*m_context}; auto &scene{ctx.scene}; auto &clusters{scene.clusters}; - auto const commands{ctx.commands}; + auto const commands{m_commands}; auto const lightsize{offsetof(gl::light_data, lights) + sizeof(gl::light_element) * m_lightcount}; auto const lights{ctx.ring.allocate(sizeof(gl::light_data), ctx.device.caps().limits.minStorageBufferOffsetAlignment)}; if (false == bool(lights)) @@ -3945,7 +4027,7 @@ void vulkan14_renderer::update_clusters(glm::ivec2 const &Targetsize) auto &ctx{*m_context}; auto &scene{ctx.scene}; auto &clusters{scene.clusters}; - auto const commands{ctx.commands}; + auto const commands{m_commands}; auto const projection{OpenGLMatrices.data(GL_PROJECTION)}; auto const znear{m_renderpass.znear}; auto const zfar{m_renderpass.zfar}; @@ -4010,7 +4092,7 @@ void vulkan14_renderer::render_picking(rendermode const Mode) { auto &ctx{*m_context}; auto &scene{ctx.scene}; - auto const commands{ctx.commands}; + auto const commands{m_commands}; auto &readback{Mode == rendermode::pickcontrols ? scene.pick_controls : scene.pick_scenery}; if (readback.pending || m_viewports.empty()) { diff --git a/rendering/vulkan14/vulkan14geometry.cpp b/rendering/vulkan14/vulkan14geometry.cpp index 8bd1527e..bff2da75 100644 --- a/rendering/vulkan14/vulkan14geometry.cpp +++ b/rendering/vulkan14/vulkan14geometry.cpp @@ -237,6 +237,7 @@ bool vulkan14_geometrybank::source(gfx::geometry_handle const &Geometry, chunk_s { return false; } + std::lock_guard lock(m_mutex); if (m_layoutdirty) { build(); @@ -281,15 +282,15 @@ bool vulkan14_geometrybank::source(gfx::geometry_handle const &Geometry, chunk_s bool vulkan14_geometrybank::indirect_source_(gfx::geometry_handle const &Geometry, gfx::indirect_source &Source) { - m_last = chunk_source{}; - if (false == source(Geometry, m_last)) + chunk_source chunk; + if (false == source(Geometry, chunk)) { return false; } - Source.count = m_last.count; - Source.first = m_last.first; - Source.basevertex = m_last.basevertex; - Source.indexed = (m_last.indices != VK_NULL_HANDLE); + Source.count = chunk.count; + Source.first = chunk.first; + Source.basevertex = chunk.basevertex; + Source.indexed = (chunk.indices != VK_NULL_HANDLE); Source.type = m_chunks[Geometry.chunk - 1].type; return true; } diff --git a/rendering/vulkan14/vulkan14geometry.h b/rendering/vulkan14/vulkan14geometry.h index 804641bc..e508f351 100644 --- a/rendering/vulkan14/vulkan14geometry.h +++ b/rendering/vulkan14/vulkan14geometry.h @@ -20,6 +20,7 @@ http://mozilla.org/MPL/2.0/. #include "rendering/vulkan14/vulkan14memory.h" #include +#include namespace vk14 { @@ -55,12 +56,9 @@ class vulkan14_geometrybank : public gfx::geometry_bank ~vulkan14_geometrybank() override; // the chunk with its data on the gpu: uploaded now when it changed since the last time. false: nothing to draw. - // gfx::geometrybank_manager::indirect_source() calls it too (which marks the bank as used), and keeps its answer in last() + // gfx::geometrybank_manager::indirect_source() calls it too (which marks the bank as used). any thread walking the passes (M2); + // what changes the chunks is made while none of them draws (gfx::walk_sync) bool source(gfx::geometry_handle const &Geometry, chunk_source &Source); - chunk_source const &last() const - { - return m_last; - } // the bytes on the gpu, for the debug panel VkDeviceSize gpu_bytes() const { @@ -101,7 +99,8 @@ class vulkan14_geometrybank : public gfx::geometry_bank bool m_layoutdirty{true}; // the buffers have to be made anew bool m_chunksdirty{false}; // some chunks have to be copied VkDeviceSize m_bytes{0}; - chunk_source m_last; + // the buffers made or updated by one of the threads drawing from the bank at a time + std::mutex m_mutex; }; } // namespace vk14 diff --git a/rendering/vulkan14/vulkan14indirect.cpp b/rendering/vulkan14/vulkan14indirect.cpp index 7d5df18b..e6c38c2b 100644 --- a/rendering/vulkan14/vulkan14indirect.cpp +++ b/rendering/vulkan14/vulkan14indirect.cpp @@ -15,6 +15,7 @@ http://mozilla.org/MPL/2.0/. #include "stdafx.h" #include "rendering/vulkan14/vulkan14context.h" #include "rendering/vulkan14/vulkan14commands.h" +#include "rendering/common/walksync.h" #include "model/AnimModel.h" #include "model/Model3d.h" #include "utilities/Globals.h" @@ -103,22 +104,29 @@ vk14::instance_cull &vulkan14_renderer::pass_cull() // the gpu culling runs along the first color pass of the main viewport of a frame, and the passes of the cascades of the sun // shadows and of the faces of the environment map drawn for it, each with a culling of its own (opengl46_renderer::walk_region_begin()) +// the cullings made the first time they're wanted, by the main renderer (before it gives the passes to the walkers) +void vulkan14_renderer::ensure_cull() +{ + auto &cull{m_context->scene.cull}; + static bool tried{false}; + if ((m_main == nullptr) && (Global.gfx_gpucull != 0) && (false == cull.ready) && (false == tried)) + { + tried = true; + if (false == init_cull()) + { + ErrorLog("vulkan: the gpu culling of the instances can't be used, the cpu culls them (gfx.gpucull)"); + destroy_cull(); + } + } +} + void vulkan14_renderer::walk_region_begin() { vk14::instance_cull *passcull{nullptr}; auto &cull{m_context->scene.cull}; if ((Global.gfx_gpucull != 0) && (m_current_viewport != nullptr) && (m_current_viewport->main) && m_draw->rendering) { - static bool tried{false}; - if ((false == cull.ready) && (false == tried)) - { - tried = true; - if (false == init_cull()) - { - ErrorLog("vulkan: the gpu culling of the instances can't be used, the cpu culls them (gfx.gpucull)"); - destroy_cull(); - } - } + ensure_cull(); if (cull.ready) { if ((m_renderpass.draw_mode == rendermode::color) && (m_instancecullframe != m_framestamp)) @@ -169,7 +177,7 @@ bool vulkan14_renderer::walk_instances_begin() { // the view of the pass is in place here, the one Render_Instanced() takes suspend_rendering(); - pass_cull().dispatch(m_context->commands, OpenGLMatrices.data(GL_MODELVIEW)); + pass_cull().dispatch(m_commands, OpenGLMatrices.data(GL_MODELVIEW)); resume_rendering(); // what the cpu drew of the sleepers and the models drawn on their own Compare_Pass_Cull(); @@ -219,7 +227,7 @@ void vulkan14_renderer::suspend_rendering() { return; } - m_context->commands->end_rendering(); + m_commands->end_rendering(); draw.rendering = false; } @@ -231,9 +239,17 @@ void vulkan14_renderer::resume_rendering() { return; } - auto const commands{m_context->commands}; - // what the rendering stored before is loaded again - vk14::barriers(commands, resume.uses); + auto const commands{m_commands}; + // what the rendering stored before is loaded again. a walker leaves the states of the images the main thread tracks as they were: + // the rendering left them as it found them + if (m_main != nullptr) + { + vk14::barriers_again(commands, resume.uses); + } + else + { + vk14::barriers(commands, resume.uses); + } commands->begin_rendering(&resume.info); // the dynamic state starts anew with the rendering, the state of the walk goes on auto const set{draw.set}; @@ -263,6 +279,8 @@ void vulkan14_renderer::resume_rendering() vk14::scene_program *vulkan14_renderer::multidraw_program(vk14::scene_program &Program) { + // made by one of the threads walking the passes (M2) + std::lock_guard lock(Program.mutex); if (Program.multidraw_tried) { return Program.multidraw.get(); @@ -309,12 +327,11 @@ bool vulkan14_renderer::draw_indirect(vk14::instance_cull &Cull, command_set con return false; } // the buffers of the bank of the chunks, with their data on the gpu (the chunks of a multi draw are of one bank) - gfx::indirect_source source; - if (false == m_geometry.indirect_source(geometry, source)) + vk14::chunk_source chunk; + if (false == chunk_of(geometry, chunk)) { return false; } - auto const &chunk{static_cast(m_banks[geometry.bank - 1])->last()}; if (false == bind_scene_pipeline(Program, *draw.targets, Blend, static_cast(chunk.topology))) { return false; @@ -334,21 +351,26 @@ bool vulkan14_renderer::draw_indirect(vk14::instance_cull &Cull, command_set con else { auto &draws{scene.draws}; + auto &window{draw.draws_window}; auto const size{static_cast(Count) * draws.stride}; - auto const slice{size <= draws.window ? draws.ring.allocate(size, draws.stride) : vk14::ring_slice{}}; - if (slice) + auto slice{(window.open && (size <= draws.window)) ? draw.draws_cache.allocate_before(size, draws.stride, window.base + draws.window) : vk14::ring_slice{}}; + if ((false == bool(slice)) && (size <= draws.window)) { - if ((false == draws.open) || (slice.offset + size > draws.base + draws.window)) + slice = draw.draws_cache.allocate(size, draws.stride); + if (slice) { - draws.base = slice.offset; - draws.open = true; + window.base = slice.offset; + window.open = true; draw.set_dirty = true; } + } + if (slice) + { for (std::uint32_t i = 0; i < Count; ++i) { std::memcpy(slice.data + i * draws.stride, &m_indirectdraws[First + i].ubs, sizeof(gl::model_ubs)); } - block = static_cast((slice.offset - draws.base) / draws.stride); + block = static_cast((slice.offset - window.base) / draws.stride); } } // the next draw of the walk pushes a block of its own @@ -416,6 +438,7 @@ vulkan14_renderer::indirect_counts vulkan14_renderer::draw_indirect_colour(vk14: indirect_counts counts; auto const count{m_indirectdraws.size()}; auto const longest{static_cast(scene.draws.window / scene.draws.stride)}; + draw.replaying = true; for (std::size_t index = 0; index < count;) { auto const blending{m_indirectdraws[index].blending}; @@ -450,6 +473,7 @@ vulkan14_renderer::indirect_counts vulkan14_renderer::draw_indirect_colour(vk14: walk_blending(false); } } + draw.replaying = false; if (count > 0) { // the model block the draws made one by one leave behind @@ -463,7 +487,7 @@ vulkan14_renderer::indirect_counts vulkan14_renderer::draw_indirect_depth(vk14:: { auto &draw{*m_draw}; auto &scene{m_context->scene}; - auto const commands{m_context->commands}; + auto const commands{m_commands}; // as in draw_indirect_colour(): the seed the draws were recorded with TSubModel::iVariantSeed = 0; indirect_counts counts; @@ -601,7 +625,7 @@ void vulkan14_renderer::record_indirect_draws(vk14::instance_cull &Cull, bool co { Order_Indirect_Draws(); } - Cull.fill_commands(m_context->commands, m_indirectcommands); + Cull.fill_commands(m_commands, m_indirectcommands); m_walkcounts.ordertime += std::chrono::duration(std::chrono::steady_clock::now() - orderstart).count(); } @@ -614,7 +638,7 @@ void vulkan14_renderer::render_instances_indirect() auto &scene{ctx.scene}; auto &cull{scene.cull.main}; auto &draw{*m_draw}; - auto const commands{ctx.commands}; + auto const commands{m_commands}; auto const viewport{static_cast(m_current_viewport - m_viewports.data())}; auto *targets{viewport < scene.targets.size() ? &scene.targets[viewport] : nullptr}; auto const projection{OpenGLMatrices.data(GL_PROJECTION)}; @@ -718,7 +742,7 @@ void vulkan14_renderer::render_instances_indirect_pass() auto &cull{pass_cull()}; auto &cascades{m_context->scene.cull.cascades}; suspend_rendering(); - cull.dispatch(m_context->commands, OpenGLMatrices.data(GL_MODELVIEW)); + cull.dispatch(m_commands, OpenGLMatrices.data(GL_MODELVIEW)); record_indirect_draws(cull, (&cull >= cascades.data()) && (&cull < cascades.data() + cascades.size())); resume_rendering(); @@ -807,7 +831,7 @@ void vulkan14_renderer::measure_shadow_fit(std::size_t const Viewport) return; } auto &targets{scene.targets[Viewport]}; - auto const commands{ctx.commands}; + auto const commands{m_commands}; vk14::barriers(commands, {vk14::use_of(*targets.depth, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, compute_stage, VK_ACCESS_2_SHADER_SAMPLED_READ_BIT)}); gfx::shadow_fit::view const view{m_colorpass.pass_camera.position(), m_colorpass.pass_camera.projection(), glm::mat3{m_colorpass.pass_camera.modelview()}, gfx::shadow_fit::light_rotation(m_sunlight.direction), gfx::shadow_fit::fixed_splits().back()}; diff --git a/rendering/vulkan14/vulkan14lamps.cpp b/rendering/vulkan14/vulkan14lamps.cpp index 0a36aeac..34b7a562 100644 --- a/rendering/vulkan14/vulkan14lamps.cpp +++ b/rendering/vulkan14/vulkan14lamps.cpp @@ -28,19 +28,31 @@ VkPipelineStageFlags2 const sampled_stage{VK_PIPELINE_STAGE_2_FRAGMENT_SHADER_BI } // namespace -// the rendering of the map the maps set up last (begin(), begin_lamp(), begin_headlight()), and the pass of the walk into it +// the rendering of the map the maps set up last (begin(), begin_lamp(), begin_headlight()), and the pass of the walk into it: the map +// made ready for it (the states of the images are tracked by the main renderer), then begun (by the main renderer, or by a walker given +// the target, begin_local_shadow_target()) void vulkan14_renderer::begin_local_shadow_rendering() +{ + m_draw->local_target = m_context->scene.lamps.target(); + auto const &target{m_draw->local_target}; + if ((target.map == nullptr) || (target.view == VK_NULL_HANDLE)) + { + return; + } + vk14::barriers(m_commands, {vk14::use_of(*target.map, VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_OPTIMAL, depth_stage, depth_access)}); +} + +void vulkan14_renderer::begin_local_shadow_target() { auto &ctx{*m_context}; auto &scene{ctx.scene}; - auto const &target{scene.lamps.target()}; - auto const commands{ctx.commands}; + auto const &target{m_draw->local_target}; + auto const commands{m_commands}; if ((target.map == nullptr) || (target.view == VK_NULL_HANDLE)) { return; } auto const use{vk14::use_of(*target.map, VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_OPTIMAL, depth_stage, depth_access)}; - vk14::barriers(commands, {use}); VkRenderingAttachmentInfo depth{}; depth.sType = VK_STRUCTURE_TYPE_RENDERING_ATTACHMENT_INFO; depth.imageView = target.view; @@ -70,7 +82,7 @@ void vulkan14_renderer::end_local_shadow_rendering() { if (m_draw->rendering) { - m_context->commands->end_rendering(); + m_commands->end_rendering(); } end_draws(); } @@ -89,7 +101,7 @@ void vulkan14_renderer::ready_lamp_maps() } if (false == uses.empty()) { - vk14::barriers(m_context->commands, uses); + vk14::barriers(m_commands, uses); } } @@ -112,6 +124,7 @@ void vulkan14_renderer::render_local_shadows() OpenGLMatrices.load_matrix(glm::mat4{glm::translate(glm::dmat4{1.0}, -glm::dvec3{lamps.lamp()}) * glm::inverse(vehicle->mMatrix)}); lamps.begin(); begin_local_shadow_rendering(); + begin_local_shadow_target(); m_cubeshadows = true; Render_cab(vehicle, 0.0f, false); m_cubeshadows = false; @@ -198,13 +211,46 @@ void vulkan14_renderer::render_lamp_shadow(std::size_t const Slot, bool const Ve camera.update_frustum(); OpenGLMatrices.mode(GL_MODELVIEW); OpenGLMatrices.load_matrix(glm::mat4{1.f}); - lamps.begin_lamp(Slot, Vehicles, m_instancerecordsversion, m_framestamp); begin_local_shadow_rendering(); + // the vehicles by the lamp; or the scenery, with its instances culled on the gpu, each scenery part drawn in a frame with a culling + // of its own + std::vector vehicles; + vk14::instance_cull *cull{nullptr}; + if (Vehicles) + { + vehicles = lamps.slot_vehicles(); + } + else + { + cull = light_shadow_cull(m_context->scene.cull.lamps, m_lampscenerydrawn++, Global.gfx_gpucull_shadows_lamps); + } + auto const *owner{static_cast(slot.owner)}; + if (walks_wanted()) + { + // walked by a walker (gfx.vulkan.parallelpasses), its draws counted once it's done + fork_walk([target = m_draw->local_target, Vehicles, vehicles, Cab, owner, cull](vulkan14_renderer &Walker) { + Walker.m_draw->local_target = target; + Walker.walk_lamp(Vehicles, vehicles, Cab, owner, cull); + }, walk_kind::lamp); + lamps.end_lamp(0); + } + else + { + walk_lamp(Vehicles, vehicles, Cab, owner, cull); + lamps.end_lamp(static_cast(m_renderpass.draw_stats.drawcalls)); + } + m_renderpass = m_colorpass; +} + +// the walk of the map of a lamp set up by render_lamp_shadow() +void vulkan14_renderer::walk_lamp(bool const Vehicles, std::vector const &Dynamics, TDynamicObject *Cab, TAnimModel const *Owner, vk14::instance_cull *Cull) +{ + begin_local_shadow_target(); m_cubeshadows = true; if (Vehicles) { - for (auto *vehicle : lamps.slot_vehicles()) + for (auto *vehicle : Dynamics) { Render(vehicle); } @@ -216,18 +262,15 @@ void vulkan14_renderer::render_lamp_shadow(std::size_t const Slot, bool const Ve else { m_cubescenery = true; - m_cubeskipinstance = static_cast(slot.owner); - // the instances culled on the gpu, each scenery part drawn in a frame with a culling of its own - m_lightshadowcull = light_shadow_cull(m_context->scene.cull.lamps, m_lampscenerydrawn++, Global.gfx_gpucull_shadows_lamps); - Render(simulation::Region); + m_cubeskipinstance = Owner; + m_lightshadowcull = Cull; + walk_region(simulation::Region, false); m_lightshadowcull = nullptr; m_cubeskipinstance = nullptr; m_cubescenery = false; } m_cubeshadows = false; end_local_shadow_rendering(); - lamps.end_lamp(static_cast(m_renderpass.draw_stats.drawcalls)); - m_renderpass = m_colorpass; } void vulkan14_renderer::render_headlight_shadows() @@ -311,23 +354,46 @@ void vulkan14_renderer::render_headlight_shadow(std::size_t const Slot, light_ar setup_matrices(); auto const colorprojection{scene_ubs.projection}; scene_ubs.projection = projection; - lamps.begin_headlight(Slot, projector, rotation, projection, Light.direction, m_framestamp); begin_local_shadow_rendering(); - m_shadowskipvehicle = Light.owner; - // the instances culled on the gpu, each slot with a culling of its own - m_lightshadowcull = light_shadow_cull(m_context->scene.cull.headlights, Slot, Global.gfx_gpucull_shadows_headlights); - Render(simulation::Region); + // the instances culled on the gpu, each slot with a culling of its own; the occupied cab, within the reach + auto *cull{light_shadow_cull(m_context->scene.cull.headlights, Slot, Global.gfx_gpucull_shadows_headlights)}; + auto *cab{((Cab != nullptr) && (Cab != Light.owner) && (glm::length(Cab->GetPosition() - projector) <= reach + Cab->radius())) ? Cab : nullptr}; + auto const *owner{Light.owner}; + if (walks_wanted()) + { + // walked by a walker (gfx.vulkan.parallelpasses), its draws counted once it's done + fork_walk([target = m_draw->local_target, owner, cab, cull](vulkan14_renderer &Walker) { + Walker.m_draw->local_target = target; + Walker.walk_headlight(owner, cab, cull); + }, walk_kind::headlight); + lamps.end_headlight(0); + } + else + { + walk_headlight(owner, cab, cull); + lamps.end_headlight(static_cast(m_renderpass.draw_stats.drawcalls)); + } + scene_ubs.projection = colorprojection; + m_renderpass = m_colorpass; +} + +// the walk of the map of a set of headlights set up by render_headlight_shadow() +void vulkan14_renderer::walk_headlight(TDynamicObject const *Owner, TDynamicObject *Cab, vk14::instance_cull *Cull) +{ + // the matrices of the projector (the walker's own, made from the pass it was given) + setup_matrices(); + begin_local_shadow_target(); + m_shadowskipvehicle = Owner; + m_lightshadowcull = Cull; + walk_region(simulation::Region, false); m_lightshadowcull = nullptr; - if ((Cab != nullptr) && (Cab != Light.owner) && (glm::length(Cab->GetPosition() - projector) <= reach + Cab->radius())) + if (Cab != nullptr) { Render_cab(Cab, 0.0f, false); } m_shadowskipvehicle = nullptr; end_local_shadow_rendering(); - lamps.end_headlight(static_cast(m_renderpass.draw_stats.drawcalls)); - scene_ubs.projection = colorprojection; - m_renderpass = m_colorpass; } vk14::instance_cull *vulkan14_renderer::light_shadow_cull(std::vector> &Culls, std::size_t const Index, bool const Enabled) diff --git a/rendering/vulkan14/vulkan14memory.cpp b/rendering/vulkan14/vulkan14memory.cpp index 682137d8..9c14e8bf 100644 --- a/rendering/vulkan14/vulkan14memory.cpp +++ b/rendering/vulkan14/vulkan14memory.cpp @@ -77,6 +77,24 @@ void allocator::destroy() m_device = VK_NULL_HANDLE; } +void allocator::share_between(std::uint32_t const First, std::uint32_t const Second) +{ + m_shared = {First, Second}; +} + +VkSharingMode allocator::sharing(bool const Shared, std::uint32_t &Count, std::uint32_t const *&Families) const +{ + Count = 0; + Families = nullptr; + if ((false == Shared) || (m_shared[0] == VK_QUEUE_FAMILY_IGNORED) || (m_shared[1] == VK_QUEUE_FAMILY_IGNORED) || (m_shared[0] == m_shared[1])) + { + return VK_SHARING_MODE_EXCLUSIVE; + } + Count = 2; + Families = m_shared.data(); + return VK_SHARING_MODE_CONCURRENT; +} + void allocator::budget(VkDeviceSize &Usage, VkDeviceSize &Budget) const { Usage = 0; @@ -124,7 +142,7 @@ buffer::~buffer() destroy(); } -bool buffer::create(allocator const &Allocator, VkDeviceSize Size, VkBufferUsageFlags Usage, memory_kind Kind, std::string const &Name) +bool buffer::create(allocator const &Allocator, VkDeviceSize Size, VkBufferUsageFlags Usage, memory_kind Kind, std::string const &Name, bool const Shared) { destroy(); @@ -132,7 +150,7 @@ bool buffer::create(allocator const &Allocator, VkDeviceSize Size, VkBufferUsage info.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO; info.size = std::max(Size, 4); info.usage = Usage; - info.sharingMode = VK_SHARING_MODE_EXCLUSIVE; + info.sharingMode = Allocator.sharing(Shared, info.queueFamilyIndexCount, info.pQueueFamilyIndices); VmaAllocationCreateInfo allocation{}; allocation.usage = VMA_MEMORY_USAGE_AUTO; @@ -249,7 +267,7 @@ bool image::create(allocator const &Allocator, image_desc const &Desc, std::stri info.samples = Desc.samples; info.tiling = VK_IMAGE_TILING_OPTIMAL; info.usage = Desc.usage; - info.sharingMode = VK_SHARING_MODE_EXCLUSIVE; + info.sharingMode = Allocator.sharing(Desc.shared, info.queueFamilyIndexCount, info.pQueueFamilyIndices); info.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; VmaAllocationCreateInfo allocation{}; @@ -353,38 +371,50 @@ void transition(command_stream *Commands, image &Image, VkImageLayout Layout, Vk void deletion_queue::push(std::uint64_t Value, std::function &&Deleter) { + std::lock_guard lock(m_mutex); m_entries.emplace_back(Value, std::move(Deleter)); } void deletion_queue::collect(std::uint64_t Completed) { - // the values only grow, so the queue is in their order - while ((false == m_entries.empty()) && (m_entries.front().first <= Completed)) + // the values only grow, so the queue is in their order (an object let go by another thread while the frame is made takes the + // value of the frame too). the deleters run outside of the lock + std::vector> deleters; + { + std::lock_guard lock(m_mutex); + while ((false == m_entries.empty()) && (m_entries.front().first <= Completed)) + { + deleters.emplace_back(std::move(m_entries.front().second)); + m_entries.pop_front(); + } + } + for (auto &deleter : deleters) { - auto deleter{std::move(m_entries.front().second)}; - m_entries.pop_front(); deleter(); } } void deletion_queue::flush() { - while (false == m_entries.empty()) + std::deque>> entries; { - auto deleter{std::move(m_entries.front().second)}; - m_entries.pop_front(); - deleter(); + std::lock_guard lock(m_mutex); + entries.swap(m_entries); + } + for (auto &entry : entries) + { + entry.second(); } } // frame ring -bool frame_ring::create(allocator const &Allocator, VkDeviceSize SizePerFrame, std::uint32_t Frames, VkBufferUsageFlags Usage, std::string const &Name, VkDeviceSize Tail) +bool frame_ring::create(allocator const &Allocator, VkDeviceSize SizePerFrame, std::uint32_t Frames, VkBufferUsageFlags Usage, std::string const &Name, VkDeviceSize Tail, bool const Shared) { m_sizeperframe = SizePerFrame; m_base = 0; m_used = 0; - return m_buffer.create(Allocator, SizePerFrame * Frames + Tail, Usage, memory_kind::upload, Name) && (m_buffer.mapped() != nullptr); + return m_buffer.create(Allocator, SizePerFrame * Frames + Tail, Usage, memory_kind::upload, Name, Shared) && (m_buffer.mapped() != nullptr); } void frame_ring::destroy() @@ -396,12 +426,14 @@ void frame_ring::destroy() void frame_ring::begin_frame(std::uint32_t Frame) { + std::lock_guard lock(m_mutex); m_base = m_sizeperframe * Frame; m_used = 0; } ring_slice frame_ring::allocate(VkDeviceSize Size, VkDeviceSize Alignment) { + std::lock_guard lock(m_mutex); auto const alignment{std::max(Alignment, 4)}; auto const offset{(m_used + alignment - 1) / alignment * alignment}; if ((m_buffer.mapped() == nullptr) || (offset + Size > m_sizeperframe)) @@ -419,7 +451,73 @@ ring_slice frame_ring::allocate(VkDeviceSize Size, VkDeviceSize Alignment) void frame_ring::flush() const { + std::lock_guard lock(m_mutex); m_buffer.flush(m_base, m_used); } +void ring_cache::reset(frame_ring *const Ring, VkDeviceSize const Part) +{ + m_ring = Ring; + m_partsize = Part; + m_part = ring_slice{}; + m_used = 0; +} + +VkDeviceSize ring_cache::next(VkDeviceSize const Alignment) const +{ + auto const alignment{std::max(Alignment, 4)}; + return (m_part.offset + m_used + alignment - 1) / alignment * alignment; +} + +ring_slice ring_cache::allocate_before(VkDeviceSize const Size, VkDeviceSize const Alignment, VkDeviceSize const Limit) +{ + if (false == bool(m_part)) + { + return {}; + } + auto const offset{next(Alignment)}; + if ((offset + Size > m_part.offset + m_part.size) || (offset + Size > Limit)) + { + return {}; + } + m_used = offset + Size - m_part.offset; + ring_slice slice; + slice.buffer = m_part.buffer; + slice.offset = offset; + slice.size = Size; + slice.data = m_part.data + (offset - m_part.offset); + return slice; +} + +ring_slice ring_cache::allocate(VkDeviceSize const Size, VkDeviceSize const Alignment) +{ + if (auto const slice{allocate_before(Size, Alignment, std::numeric_limits::max())}) + { + return slice; + } + if (m_ring == nullptr) + { + return {}; + } + // the parts begin aligned for any of the slices + auto const partalignment{std::max(Alignment, 256)}; + if (Size > m_partsize / 2) + { + // a large one gets a part of its own, the part held is kept for the small ones + return m_ring->allocate(Size, partalignment); + } + m_part = m_ring->allocate(m_partsize, partalignment); + m_used = 0; + if (false == bool(m_part)) + { + // the ring is nearly full: the rest of it in a part of the size of the slice + m_part = m_ring->allocate(Size, partalignment); + if (false == bool(m_part)) + { + return {}; + } + } + return allocate_before(Size, Alignment, std::numeric_limits::max()); +} + } // namespace vk14 diff --git a/rendering/vulkan14/vulkan14memory.h b/rendering/vulkan14/vulkan14memory.h index 28cfcc2e..10a328e3 100644 --- a/rendering/vulkan14/vulkan14memory.h +++ b/rendering/vulkan14/vulkan14memory.h @@ -16,6 +16,7 @@ http://mozilla.org/MPL/2.0/. #include "rendering/vulkan14/vulkan14common.h" #include +#include #include #include @@ -47,10 +48,16 @@ class allocator } // device memory used and the budget of the heaps which are local to the device, bytes void budget(VkDeviceSize &Usage, VkDeviceSize &Budget) const; + // the buffers and images made shared from now on are used by the queues of both families without transfers of their ownership + // (VK_SHARING_MODE_CONCURRENT, the async compute of M6); nothing changes when the families are the same + void share_between(std::uint32_t First, std::uint32_t Second); + // the sharing of a buffer or an image made shared: the mode, and the families of the concurrent one (two of them) + VkSharingMode sharing(bool Shared, std::uint32_t &Count, std::uint32_t const *&Families) const; private: VmaAllocator m_allocator{VK_NULL_HANDLE}; VkDevice m_device{VK_NULL_HANDLE}; + std::array m_shared{VK_QUEUE_FAMILY_IGNORED, VK_QUEUE_FAMILY_IGNORED}; std::uint32_t m_heapcount{0}; std::vector m_deviceheaps; }; @@ -73,7 +80,8 @@ class buffer buffer &operator=(buffer &&Other) noexcept; ~buffer(); - bool create(allocator const &Allocator, VkDeviceSize Size, VkBufferUsageFlags Usage, memory_kind Kind, std::string const &Name = {}); + // Shared: used by the queues of the families the allocator shares between (allocator::share_between()) + bool create(allocator const &Allocator, VkDeviceSize Size, VkBufferUsageFlags Usage, memory_kind Kind, std::string const &Name = {}, bool Shared = false); void destroy(); VkBuffer handle() const @@ -130,6 +138,8 @@ struct image_desc VkImageCreateFlags flags{0}; // the channels the default view reads, e.g. a single channel texture seen as rrr1 VkComponentMapping components{VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY}; + // used by the queues of the families the allocator shares between (allocator::share_between()) + bool shared{false}; }; class image @@ -183,6 +193,8 @@ class image void transition(command_stream *Commands, image &Image, VkImageLayout Layout, VkPipelineStageFlags2 Stage, VkAccessFlags2 Access); // objects destroyed once the gpu is done with the frame which used them last +// pushed to from any thread (the walks of the passes and the background threads make and let go of objects too), collected by the +// thread of the renderer class deletion_queue { public: @@ -194,10 +206,12 @@ class deletion_queue void flush(); std::size_t size() const { + std::lock_guard lock(m_mutex); return m_entries.size(); } private: + mutable std::mutex m_mutex; std::deque>> m_entries; }; @@ -220,21 +234,23 @@ class frame_ring { public: // Tail: bytes past the part of the last frame, so a descriptor of a fixed range can be given an offset anywhere in a part - bool create(allocator const &Allocator, VkDeviceSize SizePerFrame, std::uint32_t Frames, VkBufferUsageFlags Usage, std::string const &Name, VkDeviceSize Tail = 0); + bool create(allocator const &Allocator, VkDeviceSize SizePerFrame, std::uint32_t Frames, VkBufferUsageFlags Usage, std::string const &Name, VkDeviceSize Tail = 0, bool Shared = false); void destroy(); // the part of the frame Frame is used from its start again void begin_frame(std::uint32_t Frame); - // Size bytes aligned to Alignment; empty when the part of the frame is full + // Size bytes aligned to Alignment; empty when the part of the frame is full. any thread (the walks of the passes, M2) ring_slice allocate(VkDeviceSize Size, VkDeviceSize Alignment); // makes the writes of the frame seen by the gpu, where the memory isn't coherent void flush() const; VkDeviceSize used() const { + std::lock_guard lock(m_mutex); return m_used; } // the offset in the buffer the next allocation aligned to Alignment would get VkDeviceSize next_offset(VkDeviceSize Alignment) const { + std::lock_guard lock(m_mutex); auto const alignment{std::max(Alignment, 4)}; return m_base + (m_used + alignment - 1) / alignment * alignment; } @@ -248,10 +264,35 @@ class frame_ring } private: + mutable std::mutex m_mutex; buffer m_buffer; VkDeviceSize m_sizeperframe{0}; VkDeviceSize m_base{0}; VkDeviceSize m_used{0}; }; +// a part of a frame ring taken by a thread for the many small slices it places there (the blocks of the draws, the matrices of the +// instances), so the threads walking the passes (M2) don't take the lock of the ring for each of them. the slices follow one another +// in the part; one which doesn't fit in what's left of it takes a new part (or a part of its own size when it's larger than a part) +class ring_cache +{ + public: + // Ring: where the parts come from (null: none), Part: their size, bytes. the part held is let go + void reset(frame_ring *Ring, VkDeviceSize Part); + // Size bytes aligned to Alignment (at most 256); empty when the ring is full + ring_slice allocate(VkDeviceSize Size, VkDeviceSize Alignment); + // the same, only when the slice fits in the part held and ends before Limit (an offset in the buffer); empty, with nothing taken, + // otherwise + ring_slice allocate_before(VkDeviceSize Size, VkDeviceSize Alignment, VkDeviceSize Limit); + + private: + // where the next slice aligned to Alignment would begin in the part held, as an offset in the buffer + VkDeviceSize next(VkDeviceSize Alignment) const; + + frame_ring *m_ring{nullptr}; + VkDeviceSize m_partsize{0}; + ring_slice m_part; + VkDeviceSize m_used{0}; // of the part held +}; + } // namespace vk14 diff --git a/rendering/vulkan14/vulkan14passes.cpp b/rendering/vulkan14/vulkan14passes.cpp index 1c0f884c..85fe2972 100644 --- a/rendering/vulkan14/vulkan14passes.cpp +++ b/rendering/vulkan14/vulkan14passes.cpp @@ -79,6 +79,21 @@ void barriers(command_stream *Commands, std::vector const &Uses) Commands->pipeline_barrier2(&dependency); } +void barriers_again(command_stream *Commands, std::vector const &Uses) +{ + // what barriers() makes of a use following the same use + std::vector uses; + std::vector states(Uses.size()); + for (std::size_t i = 0; i < Uses.size(); ++i) + { + auto use{Uses[i]}; + states[i] = image_state{use.layout, use.stage, use.access}; + use.state = &states[i]; + uses.push_back(use); + } + barriers(Commands, uses); +} + pass_list::~pass_list() { destroy(); @@ -95,6 +110,8 @@ bool pass_list::create(device const &Device, std::uint32_t Frames, std::string & vkGetPhysicalDeviceQueueFamilyProperties(Device.physical(), &count, families.data()); auto const bits{(Device.families().graphics < count) ? families[Device.families().graphics].timestampValidBits : 64u}; m_mask = (bits >= 64 ? ~0ull : ((1ull << bits) - 1)); + auto const asyncbits{(Device.families().compute < count) ? families[Device.families().compute].timestampValidBits : 64u}; + m_asyncmask = (asyncbits >= 64 ? ~0ull : ((1ull << asyncbits) - 1)); m_frames.resize(Frames); for (std::uint32_t i = 0; i < Frames; ++i) @@ -112,6 +129,15 @@ bool pass_list::create(device const &Device, std::uint32_t Frames, std::string & // the queries are reset from the host before their first use (hostQueryReset) vkResetQueryPool(m_device, m_frames[i].pool, 0, info.queryCount); name_object(m_device, VK_OBJECT_TYPE_QUERY_POOL, handle_value(m_frames[i].pool), "pass times " + std::to_string(i)); + info.queryCount = max_async * 2; + if (!VK14_VERIFY(vkCreateQueryPool(m_device, &info, nullptr, &m_frames[i].async_pool))) + { + m_frames[i].async_pool = VK_NULL_HANDLE; + Reason = "no query pool for the times of the passes"; + return false; + } + vkResetQueryPool(m_device, m_frames[i].async_pool, 0, info.queryCount); + name_object(m_device, VK_OBJECT_TYPE_QUERY_POOL, handle_value(m_frames[i].async_pool), "async pass times " + std::to_string(i)); } return true; } @@ -124,6 +150,10 @@ void pass_list::destroy() { vkDestroyQueryPool(m_device, frame.pool, nullptr); } + if (frame.async_pool != VK_NULL_HANDLE) + { + vkDestroyQueryPool(m_device, frame.async_pool, nullptr); + } } m_frames.clear(); m_device = VK_NULL_HANDLE; @@ -132,6 +162,8 @@ void pass_list::destroy() void pass_list::begin_frame(command_stream *Commands, std::uint32_t Frame, std::uint64_t Number) { m_commands = Commands; + m_graphics = nullptr; + m_async = false; m_frame = Frame; m_open = -1; auto &frame{m_frames[Frame]}; @@ -140,6 +172,14 @@ void pass_list::begin_frame(command_stream *Commands, std::uint32_t Frame, std:: frame.number = Number; frame.recorded = true; Commands->reset_query_pool(frame.pool, 0, max_passes * 2); + // the gpu is done with the frame which used the slot before: the queries of the compute queue are reset from here, the queue may + // write them before the graphics queue gets to a reset of its own + if (frame.async_used > 0) + { + vkResetQueryPool(m_device, frame.async_pool, 0, max_async * 2); + } + frame.async_names.clear(); + frame.async_used = 0; } void pass_list::begin_pass(char const *Name, std::vector const &Uses) @@ -151,6 +191,21 @@ void pass_list::begin_pass(char const *Name, std::vector const &Uses) barriers(m_commands, Uses); begin_label(m_commands, Name); auto &frame{m_frames[m_frame]}; + if (m_async) + { + if ((frame.async_used < max_async) && (m_asyncmask != 0)) + { + m_commands->write_timestamp2(VK_PIPELINE_STAGE_2_TOP_OF_PIPE_BIT, frame.async_pool, frame.async_used * 2); + frame.async_names.emplace_back(Name); + m_open = static_cast(frame.async_used); + ++frame.async_used; + } + else + { + m_open = static_cast(max_passes); + } + return; + } // (no timestamps on a queue without valid bits; the device is chosen with them, this is for safety) if ((frame.used < max_passes) && (m_mask != 0)) { @@ -173,14 +228,39 @@ void pass_list::end_pass() } if (m_open < static_cast(max_passes)) { - m_commands->write_timestamp2(VK_PIPELINE_STAGE_2_BOTTOM_OF_PIPE_BIT, m_frames[m_frame].pool, static_cast(m_open) * 2 + 1); + m_commands->write_timestamp2(VK_PIPELINE_STAGE_2_BOTTOM_OF_PIPE_BIT, m_async ? m_frames[m_frame].async_pool : m_frames[m_frame].pool, static_cast(m_open) * 2 + 1); } end_label(m_commands); m_open = -1; } +void pass_list::begin_async(command_stream *Commands) +{ + end_pass(); + if (m_async) + { + return; + } + m_graphics = m_commands; + m_commands = Commands; + m_async = true; +} + +void pass_list::end_async() +{ + end_pass(); + if (false == m_async) + { + return; + } + m_commands = m_graphics; + m_graphics = nullptr; + m_async = false; +} + void pass_list::end_frame() { + end_async(); end_pass(); m_commands = nullptr; } @@ -193,14 +273,34 @@ void pass_list::collect(std::uint32_t Frame) return; } frame.recorded = false; + // a running average, so the numbers can be read + auto const blend = [](double &Average, double const Value) { Average = (Average <= 0.0 ? Value : Average * 0.9 + Value * 0.1); }; + // the passes of the compute queue, apart + if (frame.async_used > 0) + { + std::vector stamps(frame.async_used * 2); + if (vkGetQueryPoolResults(m_device, frame.async_pool, 0, frame.async_used * 2, stamps.size() * sizeof(std::uint64_t), stamps.data(), sizeof(std::uint64_t), VK_QUERY_RESULT_64_BIT) == VK_SUCCESS) + { + for (std::uint32_t i = 0; i < frame.async_used; ++i) + { + auto const ticks{((stamps[i * 2 + 1] & m_asyncmask) - (stamps[i * 2] & m_asyncmask)) & m_asyncmask}; + auto const name{frame.async_names[i] + " (async)"}; + auto lookup{std::find_if(m_asynctimes.begin(), m_asynctimes.end(), [&](auto const &Entry) { return Entry.first == name; })}; + if (lookup == m_asynctimes.end()) + { + m_asynctimes.emplace_back(name, 0.0); + lookup = std::prev(m_asynctimes.end()); + } + blend(lookup->second, static_cast(ticks) * m_period / 1000000.0); + } + } + } std::vector stamps(frame.used * 2); auto const result{vkGetQueryPoolResults(m_device, frame.pool, 0, frame.used * 2, stamps.size() * sizeof(std::uint64_t), stamps.data(), sizeof(std::uint64_t), VK_QUERY_RESULT_64_BIT)}; if (result != VK_SUCCESS) { return; } - // a running average, so the numbers can be read - auto const blend = [](double &Average, double const Value) { Average = (Average <= 0.0 ? Value : Average * 0.9 + Value * 0.1); }; double total{0.0}; for (std::uint32_t i = 0; i < frame.used; ++i) { @@ -246,6 +346,11 @@ std::string pass_list::info() const } std::snprintf(buffer, sizeof(buffer), " %-14s %7.3f ms\n", "total", m_framems); text += buffer; + for (auto const &[name, ms] : m_asynctimes) + { + std::snprintf(buffer, sizeof(buffer), " %-14s %7.3f ms\n", name.c_str(), ms); + text += buffer; + } return text; } diff --git a/rendering/vulkan14/vulkan14passes.h b/rendering/vulkan14/vulkan14passes.h index a2b1b481..168c07bf 100644 --- a/rendering/vulkan14/vulkan14passes.h +++ b/rendering/vulkan14/vulkan14passes.h @@ -40,6 +40,9 @@ image_use use_of(image &Image, VkImageLayout Layout, VkPipelineStageFlags2 Stage // the barriers which bring the images from their states to the uses, the states updated void barriers(command_stream *Commands, std::vector const &Uses); +// the barriers between two uses of the images in the states of the uses, made one after another (a rendering ended and begun again +// by a walker, M2), without the states, which the main thread tracks +void barriers_again(command_stream *Commands, std::vector const &Uses); class pass_list { @@ -57,6 +60,10 @@ class pass_list // a pass: the barriers of its images, its label and the timestamp of its start void begin_pass(char const *Name, std::vector const &Uses = {}); void end_pass(); + // the passes from here on go to Commands, for the compute queue (the async compute of M6), until end_async(); timed with queries of + // their own, as their times overlap those of the passes of the graphics queue: they're shown apart, not in the total + void begin_async(command_stream *Commands); + void end_async(); void end_frame(); // the gpu times of the frame Frame, once the gpu is done with it (after the wait for its value of the timeline) void collect(std::uint32_t Frame); @@ -76,20 +83,29 @@ class pass_list VkQueryPool pool{VK_NULL_HANDLE}; std::vector names; std::uint32_t used{0}; + // those of the compute queue, reset from the host + VkQueryPool async_pool{VK_NULL_HANDLE}; + std::vector async_names; + std::uint32_t async_used{0}; std::uint64_t number{0}; bool recorded{false}; }; static constexpr std::uint32_t max_passes{32}; + static constexpr std::uint32_t max_async{8}; VkDevice m_device{VK_NULL_HANDLE}; float m_period{1.f}; // ns per tick std::uint64_t m_mask{~0ull}; // the valid bits of the timestamps + std::uint64_t m_asyncmask{~0ull}; // those of the compute queue std::vector m_frames; command_stream *m_commands{nullptr}; + command_stream *m_graphics{nullptr}; // the stream of the graphics queue, while the passes go to the compute one + bool m_async{false}; std::uint32_t m_frame{0}; int m_open{-1}; // averaged times of the parts, ms std::vector> m_times; + std::vector> m_asynctimes; // of the compute queue double m_framems{0.0}; }; diff --git a/rendering/vulkan14/vulkan14renderer.cpp b/rendering/vulkan14/vulkan14renderer.cpp index c876318e..9cd43e9b 100644 --- a/rendering/vulkan14/vulkan14renderer.cpp +++ b/rendering/vulkan14/vulkan14renderer.cpp @@ -113,6 +113,21 @@ bool vulkan14_renderer::context::create_frames(std::uint32_t const Count, std::s } } + // the async compute: a family of its own for the passes of the sky and the weather, the frames it's done with counted apart + async_compute = Global.gfx_vulkan_asynccompute && device.families().dedicated_compute && (device.families().compute != vk14::queue_families::invalid) && (device.compute_queue() != VK_NULL_HANDLE); + if (async_compute) + { + if (!VK14_VERIFY(vkCreateSemaphore(handle, &semaphoreinfo, nullptr, &compute_timeline))) + { + compute_timeline = VK_NULL_HANDLE; + async_compute = false; + } + else + { + instance.set_object_name(handle, VK_OBJECT_TYPE_SEMAPHORE, reinterpret_cast(compute_timeline), "compute timeline"); + } + } + frames.resize(Count); for (std::uint32_t i = 0; i < Count; ++i) { @@ -136,6 +151,22 @@ bool vulkan14_renderer::context::create_frames(std::uint32_t const Count, std::s return false; } instance.set_object_name(handle, VK_OBJECT_TYPE_COMMAND_BUFFER, reinterpret_cast(frames[i].uploads), "uploads " + std::to_string(i)); + frames[i].compute_stream.set_compute_queue(true); + if (async_compute) + { + poolinfo.queueFamilyIndex = device.families().compute; + bufferinfo.commandPool = VK_NULL_HANDLE; + if (VK14_VERIFY(vkCreateCommandPool(handle, &poolinfo, nullptr, &frames[i].compute_pool))) + { + bufferinfo.commandPool = frames[i].compute_pool; + } + if ((bufferinfo.commandPool == VK_NULL_HANDLE) || !VK14_VERIFY(vkAllocateCommandBuffers(handle, &bufferinfo, &frames[i].compute_buffer))) + { + Reason = "no command buffer of the compute queue"; + return false; + } + instance.set_object_name(handle, VK_OBJECT_TYPE_COMMAND_BUFFER, reinterpret_cast(frames[i].compute_buffer), "async compute " + std::to_string(i)); + } } if (Global.gfx_vulkan_renderthread) { @@ -144,7 +175,7 @@ bool vulkan14_renderer::context::create_frames(std::uint32_t const Count, std::s return true; } -bool vulkan14_renderer::context::record(frame &Frame, bool const Uploads) +bool vulkan14_renderer::context::record(frame &Frame, bool const Uploads, bool const Compute) { auto const handle{device.handle()}; auto const start{std::chrono::steady_clock::now()}; @@ -159,6 +190,13 @@ bool vulkan14_renderer::context::record(frame &Frame, bool const Uploads) Frame.upload_stream.replay(Frame.uploads); VK14_VERIFY(vkEndCommandBuffer(Frame.uploads)); } + if (Compute) + { + Frame.compute_stream.finish(); + VK14_VERIFY(vkBeginCommandBuffer(Frame.compute_buffer, &begin)); + Frame.compute_stream.replay(Frame.compute_buffer); + VK14_VERIFY(vkEndCommandBuffer(Frame.compute_buffer)); + } // the chunks split evenly between the workers, each with a command pool of its own (a pool is used by one thread at a time); // which worker records a chunk doesn't matter: each chunk has a command buffer of its own, they go to the queue in their order auto const chunks{Frame.commands.chunks()}; @@ -233,11 +271,41 @@ bool vulkan14_renderer::context::submit(render_job &Job, char const *&Failure) { main->latency_marker(Job.present_id, VK_LATENCY_MARKER_RENDERSUBMIT_START_NV); } - if (false == record(frame, Job.uploaded)) + if (false == record(frame, Job.uploaded, Job.async)) { Failure = "recording a frame"; return false; } + if (Job.async) + { + // the passes of the compute queue, once the graphics queue is done with the frame before, which read what they write + VkSemaphoreSubmitInfo wait{}; + wait.sType = VK_STRUCTURE_TYPE_SEMAPHORE_SUBMIT_INFO; + wait.semaphore = timeline; + wait.value = Job.value - 1; + wait.stageMask = VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT; + VkSemaphoreSubmitInfo signal{}; + signal.sType = VK_STRUCTURE_TYPE_SEMAPHORE_SUBMIT_INFO; + signal.semaphore = compute_timeline; + signal.value = Job.value; + signal.stageMask = VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT; + VkCommandBufferSubmitInfo buffer{}; + buffer.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_SUBMIT_INFO; + buffer.commandBuffer = frame.compute_buffer; + VkSubmitInfo2 compute{}; + compute.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO_2; + compute.waitSemaphoreInfoCount = (Job.value > 1 ? 1 : 0); + compute.pWaitSemaphoreInfos = &wait; + compute.commandBufferInfoCount = 1; + compute.pCommandBufferInfos = &buffer; + compute.signalSemaphoreInfoCount = 1; + compute.pSignalSemaphoreInfos = &signal; + if (!VK14_VERIFY(vkQueueSubmit2(device.compute_queue(), 1, &compute, VK_NULL_HANDLE))) + { + Failure = "submitting the async compute of a frame"; + return false; + } + } // the uploads first, in the same submit: their barriers make them done before the frame uses what they copied std::vector commands; if (Job.uploaded) @@ -255,22 +323,43 @@ bool vulkan14_renderer::context::submit(render_job &Job, char const *&Failure) info.commandBuffer = chunk; commands.push_back(info); } - VkSubmitInfo2 submit{}; - submit.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO_2; VkLatencySubmissionPresentIdNV latency{}; latency.sType = VK_STRUCTURE_TYPE_LATENCY_SUBMISSION_PRESENT_ID_NV; latency.presentID = Job.present_id; + // with the async compute in two submits: the part before the split goes on at the same time as the compute queue, the rest waits + // for it. the waits for the images of the windows and the signals go with the rest, which draws them + std::array submits{}; + std::uint32_t count{0}; + auto first{commands.size()}; + std::vector waits; + if (Job.async) + { + first = (Job.uploaded ? 1 : 0) + std::min(frame.commands.split_chunk(), frame.chunks.size()); + auto &before{submits[count++]}; + before.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO_2; + before.commandBufferInfoCount = static_cast(first); + before.pCommandBufferInfos = commands.data(); + VkSemaphoreSubmitInfo compute{}; + compute.sType = VK_STRUCTURE_TYPE_SEMAPHORE_SUBMIT_INFO; + compute.semaphore = compute_timeline; + compute.value = Job.value; + compute.stageMask = VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT; + waits.push_back(compute); + } + waits.insert(waits.end(), Job.waits.begin(), Job.waits.end()); + auto &submit{submits[count++]}; + submit.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO_2; if (lowlatency) { submit.pNext = &latency; } - submit.waitSemaphoreInfoCount = static_cast(Job.waits.size()); - submit.pWaitSemaphoreInfos = Job.waits.data(); - submit.commandBufferInfoCount = static_cast(commands.size()); - submit.pCommandBufferInfos = commands.data(); + submit.waitSemaphoreInfoCount = static_cast(waits.size()); + submit.pWaitSemaphoreInfos = waits.data(); + submit.commandBufferInfoCount = static_cast(commands.size() - (Job.async ? first : 0)); + submit.pCommandBufferInfos = commands.data() + (Job.async ? first : 0); submit.signalSemaphoreInfoCount = static_cast(Job.signals.size()); submit.pSignalSemaphoreInfos = Job.signals.data(); - if (!VK14_VERIFY(vkQueueSubmit2(device.graphics_queue(), 1, &submit, VK_NULL_HANDLE))) + if (!VK14_VERIFY(vkQueueSubmit2(device.graphics_queue(), count, submits.data(), VK_NULL_HANDLE))) { Failure = "submitting a frame"; return false; @@ -505,14 +594,23 @@ bool vulkan14_renderer::context::create_core(std::uint32_t const Frames, std::st { return false; } + if (async_compute) + { + // what the passes of the compute queue read and write is shared by the two families + memory.share_between(device.families().graphics, device.families().compute); + } uploads.init(memory, deletions, device.physical()); + // the textures of the background threads filled by the host, where the device can do it to the layout they're sampled in + uploads.enable_host_copy(device.caps().host_image_copy && device.caps().host_copy_sampled); if (false == sampling.create(device, static_cast(Global.AnisotropicFiltering), Reason, lod_bias)) { return false; } auto const &limits{device.caps().limits}; (void)limits; - if (false == ring.create(memory, ring_size, Frames, VK_BUFFER_USAGE_VERTEX_BUFFER_BIT | VK_BUFFER_USAGE_INDEX_BUFFER_BIT | VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT | VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_SRC_BIT, "frame ring", ring_tail)) + // the blocks of the frame are read by the passes of the compute queue too (the async compute) + auto const ringusage{VK_BUFFER_USAGE_VERTEX_BUFFER_BIT | VK_BUFFER_USAGE_INDEX_BUFFER_BIT | VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT | VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_SRC_BIT}; + if (false == ring.create(memory, ring_size, Frames, ringusage, "frame ring", ring_tail, true)) { Reason = "no ring for the data of the frames"; return false; @@ -529,6 +627,7 @@ bool vulkan14_renderer::context::create_core(std::uint32_t const Frames, std::st resources.images = &images; resources.thread = std::this_thread::get_id(); resources.pending = &pending; + resources.background = (Global.gfx_vulkan_asyncresources ? &background : nullptr); resources.raytracing = device.raytracing_enabled(); textures = std::make_unique([this]() -> texture_source * { return new vk14::vulkan14_texture(resources); }); return imgui.create(resources, shaders, pipelines, ring, Reason); @@ -562,6 +661,8 @@ bool vulkan14_renderer::context::wait(std::uint64_t const Value) const void vulkan14_renderer::context::destroy() { auto const handle{device.handle()}; + // the background threads use what goes below + background.stop(); // the frame given to the render thread goes to the queue first stop_render_thread(); if (handle != VK_NULL_HANDLE) @@ -575,7 +676,7 @@ void vulkan14_renderer::context::destroy() // the textures give their images to the deletion queue, which goes now that the gpu is idle textures.reset(); uploads.end_frame(); - uploads = vk14::uploader{}; + uploads.reset(); images.clear(); deletions.flush(); // the features of dlss released by the deletions above, ngx goes before the device @@ -597,6 +698,10 @@ void vulkan14_renderer::context::destroy() { vkDestroyCommandPool(handle, worker.pool, nullptr); } + if (frame.compute_pool != VK_NULL_HANDLE) + { + vkDestroyCommandPool(handle, frame.compute_pool, nullptr); + } } if (timeline != VK_NULL_HANDLE) { @@ -606,11 +711,17 @@ void vulkan14_renderer::context::destroy() { vkDestroySemaphore(handle, reflex, nullptr); } + if (compute_timeline != VK_NULL_HANDLE) + { + vkDestroySemaphore(handle, compute_timeline, nullptr); + } } memory.destroy(); frames.clear(); timeline = VK_NULL_HANDLE; reflex = VK_NULL_HANDLE; + compute_timeline = VK_NULL_HANDLE; + async_compute = false; device.destroy(); instance.destroy(); } @@ -719,7 +830,8 @@ void vulkan14_renderer::start_dlss() bool vulkan14_renderer::Init(GLFWwindow *Window) { - m_context = std::make_unique(); + m_contextstore = std::make_unique(); + m_context = m_contextstore.get(); auto &ctx{*m_context}; std::string reason; @@ -772,7 +884,8 @@ bool vulkan14_renderer::Init(GLFWwindow *Window) if (false == ok) { vk14::log_error("the renderer can't start: " + reason); - m_context.reset(); + m_contextstore.reset(); + m_context = nullptr; return false; } @@ -780,7 +893,8 @@ bool vulkan14_renderer::Init(GLFWwindow *Window) { vk14::log_error("the renderer can't start: " + reason); destroy_scene(); - m_context.reset(); + m_contextstore.reset(); + m_context = nullptr; return false; } init_frame_data(); @@ -855,6 +969,15 @@ bool vulkan14_renderer::AddViewport(global_settings::extraviewport_config const void vulkan14_renderer::Shutdown() { + if (m_main != nullptr) + { + // a walker has nothing of its own to let go of + return; + } + if (m_context) + { + join_walks(); + } if (m_context && (m_context->device.handle() != VK_NULL_HANDLE)) { // the end of the renderer, not a frame: everything on the gpu has to be done, the frame of the render thread with it @@ -870,8 +993,11 @@ void vulkan14_renderer::Shutdown() } } destroy_scene(); + m_walkers.clear(); + m_walkstreams.clear(); m_draw.reset(); - m_context.reset(); + m_contextstore.reset(); + m_context = nullptr; } void vulkan14_renderer::report_device_lost(char const *Where) @@ -1049,7 +1175,7 @@ void vulkan14_renderer::update_info() ctx.memory.budget(usage, budget); m_infostats = "device memory: " + vk14::memory_size_string(usage) + " of " + vk14::memory_size_string(budget) + (ctx.device.memory_budget_enabled() ? "" : " (no budget from the driver)") + "\n" + "images: " + std::to_string(ctx.images.size()) + ", uploaded " + vk14::memory_size_string(ctx.uploads.uploaded()) + ", waiting for the gpu to be deleted: " + std::to_string(ctx.deletions.size()) + "\n" + (ctx.textures ? ctx.textures->info() + "\n" : std::string{}) + ctx.shaders.info() + - "\n" + ctx.pipelines.info() + "\n" + "frame ring: " + vk14::memory_size_string(ctx.ring.used()) + " of " + vk14::memory_size_string(ctx.ring.capacity()) + "\n"; + "\n" + ctx.pipelines.info() + "\n" + background_info() + "frame ring: " + vk14::memory_size_string(ctx.ring.used()) + " of " + vk14::memory_size_string(ctx.ring.capacity()) + "\n"; for (auto const &window : ctx.windows) { auto const extent{window->extent()}; @@ -1065,6 +1191,49 @@ void vulkan14_renderer::update_info() m_infostats += "\n" + scene_info() + cull_info(); } +std::string vulkan14_renderer::background_info() const +{ + auto const &ctx{*m_context}; + std::string info; + if (ctx.background.running()) + { + auto const pending{ctx.pending.made_count.load()}; + info += "background threads: " + std::to_string(ctx.background.pending()) + " tasks waiting; pipelines made ahead: " + std::to_string(ctx.warm_made.load()) + ", made late: " + + std::to_string(ctx.pipelines_late.load()) + "; textures made: " + std::to_string(pending) + ", draws without theirs: " + std::to_string(ctx.pending.late_count.load()) + "\n"; + } + else + { + info += "background threads: off (gfx.vulkan.asyncresources)\n"; + } + if (ctx.walks.running()) + { + info += "walkers: " + std::to_string(m_walkpasses) + " passes in the last frame, " + std::to_string(vk14::walk_threads()) + (vk14::walk_threads() == 1 ? " thread" : " threads") + + ", the main thread waited for them: " + to_string(m_walkwait, 2) + " ms\n"; + } + else + { + info += "walkers: off (gfx.vulkan.parallelpasses)\n"; + } + if (ctx.models.running()) + { + info += "helpers of the walks: " + std::to_string(m_helpedgroups) + " groups of the cullings walked by them in the last frame\n"; + } + else + { + info += "helpers of the walks: off (gfx.vulkan.parallelmodels)\n"; + } + if (ctx.async_compute) + { + info += std::string("async compute: the sky and the weather on the compute queue (family ") + std::to_string(ctx.device.families().compute) + "), " + + (m_asyncframe ? "in the last frame" : "not in the last frame") + "\n"; + } + else + { + info += (Global.gfx_vulkan_asynccompute ? "async compute: off (no compute family of its own)\n" : "async compute: off (gfx.vulkan.asynccompute)\n"); + } + return info; +} + bool vulkan14_renderer::Debug_Ui_State(std::optional State) { if (State) diff --git a/rendering/vulkan14/vulkan14renderer.h b/rendering/vulkan14/vulkan14renderer.h index 06c2fc01..ee4eb7e8 100644 --- a/rendering/vulkan14/vulkan14renderer.h +++ b/rendering/vulkan14/vulkan14renderer.h @@ -36,8 +36,10 @@ http://mozilla.org/MPL/2.0/. #include "gl/ubo.h" #include +#include class world_environment; +struct texture_source; struct VkRenderingInfo; namespace vk14 { @@ -50,12 +52,17 @@ enum class blend_mode; class instance_cull; struct image_use; struct ray_draw; +struct chunk_source; +class command_stream; } // namespace vk14 class vulkan14_renderer : public gfx_renderer, public gfx::scene_walk { public: vulkan14_renderer(); + // a walker (the stage M2): walks the passes the main renderer gives it on a thread of its own, drawing with what the main renderer + // draws with (the objects of the api, the geometry, the materials, the textures). it isn't a renderer of the application + explicit vulkan14_renderer(vulkan14_renderer &Main); ~vulkan14_renderer() override; // lifecycle @@ -184,6 +191,12 @@ class vulkan14_renderer : public gfx_renderer, public gfx::scene_walk void walk_light_query(TSubModel *Submodel, bool const Begin) override; void walk_editor_cursor() override; gfx::geometrybank_manager &walk_geometry() override; + bool walk_indirect_source(gfx::geometry_handle const &Geometry, gfx::indirect_source &Source) override; + // the walks of the groups of a culling split among the helpers (vulkan14walkers.cpp, the stage M3, gfx.vulkan.parallelmodels) + bool walk_groups_parallel(std::size_t const Groups) override; + void walk_groups_run(std::size_t const Tasks, std::function const &Work) override; + // the chunk with its data on the gpu (vk14::vulkan14_geometrybank::source()), its bank marked as used + bool chunk_of(gfx::geometry_handle const &Geometry, vk14::chunk_source &Chunk); bool walk_blending_enabled() const override; void walk_region_begin() override; bool walk_instances_begin() override; @@ -193,6 +206,7 @@ class vulkan14_renderer : public gfx_renderer, public gfx::scene_walk // rest): the cullings of the passes and the depth pyramid, made when the renderer starts bool init_cull(); void destroy_cull(); + void ensure_cull(); // the rendering of the pass under way, kept to be begun again where it was: it's ended for the compute passes of the culling // in the middle of the walk, and begun again with what it drew loaded, with the state of the draws as it was void remember_rendering(VkRenderingInfo const &Info, std::vector const &Uses); @@ -246,6 +260,7 @@ class vulkan14_renderer : public gfx_renderer, public gfx::scene_walk void render_headlight_shadow(std::size_t const Slot, light_array::light_record const &Light, TDynamicObject *Cab); vk14::instance_cull *light_shadow_cull(std::vector> &Culls, std::size_t const Index, bool const Enabled); void begin_local_shadow_rendering(); + void begin_local_shadow_target(); void end_local_shadow_rendering(); void ready_lamp_maps(); bool m_cubeshadows{false}; // the shadow pass draws to a cube map, with the programs of the cube maps @@ -267,6 +282,67 @@ class vulkan14_renderer : public gfx_renderer, public gfx::scene_walk float m_shadowslicesize{0.f}; float m_shadowdrawnsize{0.f}; + // the passes of the sky and the weather of this frame go to the compute queue (the async compute, M6, gfx.vulkan.asynccompute): the + // frame is split before the first pass which reads what they make + bool m_asyncframe{false}; + void split_frame(); + + // the passes walked by the walkers (vulkan14walkers.cpp, the stage M2, gfx.vulkan.parallelpasses): the sun cascades and the shadow + // maps of the lamps and the headlights, walked at the same time as the main thread walks the colour pass, each recorded to a stream + // of its own spliced into the commands of the frame where it would have been recorded (vk14::command_stream::splice()). the main + // thread sets up a pass as it would for itself, hands the state of the frame over to a walker and goes on (gfx::walk_sync); the + // walkers are joined once the colour pass of the main view is recorded + // whether the passes of the main view of this frame go to the walkers + bool walks_wanted() const; + // what a pass of a walker is, for what the main renderer counts of it once it's done + enum class walk_kind + { + other, + lamp, + headlight + }; + // a walker given the pass Job, with the state of the frame of this renderer, recorded to a stream spliced here + void fork_walk(std::function Job, walk_kind const Kind = walk_kind::other); + // waits for the walkers, takes what they did over + void join_walks(); + // the state of the frame a walker takes from the main renderer, before it walks a pass; and a helper from the walk it works for + void take_state(vulkan14_renderer const &Main); + void take_helper_state(vulkan14_renderer const &Walk); + // the helpers of the walks of this renderer (M3) + std::vector> m_helpers; + // for the panel: the groups the helpers walked in the last frame; the groups of the walk under way + std::size_t m_helpedgroups{0}; + std::size_t m_helpgroups{0}; + // the passes the walkers walk, as the main renderer walked them (render_shadows(), render_lamp_shadow(), render_headlight_shadow()) + void walk_cascade(std::size_t const Index); + void walk_lamp(bool const Vehicles, std::vector const &Dynamics, TDynamicObject *Cab, TAnimModel const *Owner, vk14::instance_cull *Cull); + void walk_headlight(TDynamicObject const *Owner, TDynamicObject *Cab, vk14::instance_cull *Cull); + // the walk lock taken by the main thread for the frame (walk_sync::open()) and the walkers given passes in it + bool m_walkwindow{false}; + std::size_t m_walkjobs{0}; + std::vector> m_walkers; + // the streams of the passes of the walkers, one each, kept by the frames in flight + std::vector>> m_walkstreams; + // for the panel: the passes the walkers walked in the last frame, the time the main thread waited for them, averaged + std::size_t m_walkpasses{0}; + double m_walkwait{0.0}; + // a pass the walker failed to walk (its stream is dropped, the pass left out of the frame) + bool m_walkfailed{false}; + // of a walker: what its pass is, and the state of the thread of the main renderer it takes when it begins (the matrix stack, the + // state of the walk of the submodels, both per thread) + walk_kind m_walkkind{walk_kind::other}; + opengl_matrices m_forkmatrices; + struct submodel_state + { + std::size_t instance{0}; + std::uint32_t variantseed{0}; + material_handle const *skins{nullptr}; + int alpha{0}; + float squaredistance{0.f}; + TModel3d *root{nullptr}; + std::string *text{nullptr}; + } m_forksubmodels; + // the scene (vulkan14scene.cpp) bool init_scene(std::string &Reason); // what the renderer prepares for the frames once it's there: the viewport of the main window, the common textures and geometry @@ -274,6 +350,8 @@ class vulkan14_renderer : public gfx_renderer, public gfx::scene_walk void destroy_scene(); // the scene of the frame: the passes of the viewports void render_scene(); + // a walk of the region (Render() or Render_Alpha()) whose draws may wait for their pipelines and textures (the stage M5) + void walk_region(scene::basic_region *Region, bool const Alpha); void setup_pass(viewport_config &Viewport, renderpass_config &Config, rendermode const Mode, float const Znear = 0.f, float const Zfar = 1.f, bool const Ignoredebug = false); void setup_matrices(); // the colour of a viewport, with the passes it needs first: the sun shadows, the environment map, the light clusters @@ -415,20 +493,32 @@ class vulkan14_renderer : public gfx_renderer, public gfx::scene_walk bool bind_scene_pipeline(vk14::scene_program &Program, vk14::target_formats const &Targets, vk14::blend_mode const Blend, int const Topology); void apply_draw_state(); void push_textures(); + bool texture_ready(texture_source &Texture); + texture_source &used_texture(texture_handle const Texture); std::uint32_t push_draw_block(); void open_instance_window(); void update_texture_release(); - std::unique_ptr m_context; + // the objects of the vulkan api: made by the main renderer, taken from it by a walker + std::unique_ptr m_contextstore; + context *m_context{nullptr}; + // the main renderer, for a walker + vulkan14_renderer *m_main{nullptr}; + // what this renderer records to: the commands of the frame, or the pass given to a walker + vk14::command_stream *m_commands{nullptr}; // the light of the sky and the weather of the frame, shared with the opengl renderer gfx::sky_light m_skylight; gfx::weather_state m_weatherstate; // the environment map prefiltered with the GGX lobe is bound for the pass (light_data.env_ggx) bool m_envmap_bound{false}; - // the data of the geometry, which the simulation reads back, and the banks with it on the gpu - gfx::geometrybank_manager m_geometry; - std::vector m_banks; - material_manager m_materials; + // the data of the geometry, which the simulation reads back, and the banks with it on the gpu; the materials. a walker has those + // of the main renderer + gfx::geometrybank_manager m_geometrystore; + gfx::geometrybank_manager &m_geometry{m_geometrystore}; + std::vector m_bankstore; + std::vector &m_banks{m_bankstore}; + material_manager m_materialstore; + material_manager &m_materials{m_materialstore}; material_handle m_invalid_material{null_handle}; std::vector m_viewports; viewport_config *m_current_viewport{nullptr}; @@ -519,6 +609,8 @@ class vulkan14_renderer : public gfx_renderer, public gfx::scene_walk debug_stats m_laststats; void update_info(); + // the lines of the debug panel about the threads of the renderer other than the recording ones (the stages M2, M3, M5, M6) + std::string background_info() const; // the debug windows (vulkan14debug.cpp, opengl46_renderer::draw_debug_ui()), the lines of the cullings and of the scene for them // and for the statistics, the counters of the light clusters read back, and the heatmap of the clusters over the output of a // viewport (opengl46_renderer::Render_cluster_heatmap()) diff --git a/rendering/vulkan14/vulkan14scene.cpp b/rendering/vulkan14/vulkan14scene.cpp index 0837b7b9..db6c5144 100644 --- a/rendering/vulkan14/vulkan14scene.cpp +++ b/rendering/vulkan14/vulkan14scene.cpp @@ -15,6 +15,7 @@ http://mozilla.org/MPL/2.0/. #include "stdafx.h" #include "rendering/vulkan14/vulkan14context.h" #include "rendering/vulkan14/vulkan14commands.h" +#include "rendering/common/walksync.h" #include "rendering/nullrenderer.h" #include "model/Model3d.h" #include "model/AnimModel.h" @@ -22,7 +23,10 @@ http://mozilla.org/MPL/2.0/. #include "utilities/Globals.h" #include "utilities/Logs.h" #include "utilities/color.h" +#include "utilities/utilities.h" +#include +#include #include namespace @@ -538,7 +542,7 @@ bool vulkan14_renderer::init_scene(std::string &Reason) return false; } if (false == scene.weather_horizon.create(ctx.memory, sizeof(glm::vec4) * gl::FOG_HORIZON_SAMPLES, VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT | VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT, - vk14::memory_kind::device, "fog horizon")) + vk14::memory_kind::device, "fog horizon", true)) { Reason = "no buffer of the colours of the fog"; return false; @@ -700,7 +704,7 @@ bool vulkan14_renderer::init_scene(std::string &Reason) vk14::gpu_particles::host host; host.compute = [this]() { suspend_rendering(); - return m_context->commands; + return m_commands; }; host.bind = [this](vk14::compute_kernel const &Kernel) { bind_compute_set(Kernel); }; host.draw = [this](vk14::scene_program &Program, VkBuffer const Output, VkBuffer const Command, glm::vec4 const &Data0, glm::vec4 const &Data1) { @@ -750,7 +754,7 @@ bool vulkan14_renderer::init_scene(std::string &Reason) vk14::windscreen_water::host host; host.commands = [this]() { suspend_rendering(); - return m_context->commands; + return m_commands; }; host.texture = [this](texture_handle const Texture, VkImageView &View, VkSampler &Sampler) { auto &ctx{*m_context}; @@ -825,9 +829,45 @@ bool vulkan14_renderer::init_scene(std::string &Reason) m_draw = std::make_unique(); m_draw->textures.fill(null_handle); + + // the background threads (the stage M5), with the pipelines of the last session to make ahead. two of them: the frames go on + // meanwhile, with the threads of the simulation and of the recording + if (Global.gfx_vulkan_asyncresources) + { + ctx.background.start(2); + ctx.warm_scene_pipelines(); + } + // the threads of the walkers (the stage M2), a stream of the passes they record for each frame in flight + if (Global.gfx_vulkan_parallelpasses) + { + ctx.walks.start(vk14::walk_threads()); + } + // and of the helpers of the walks (the stage M3) + if (Global.gfx_vulkan_parallelmodels) + { + ctx.models.start(vk14::walk_threads()); + } + m_walkstreams.resize(ctx.frames.size()); return true; } +// a walk of the region: what its draws need first may be made by the background threads meanwhile, the draws left out till then +void vulkan14_renderer::walk_region(scene::basic_region *Region, bool const Alpha) +{ + auto &draw{*m_draw}; + auto const async{draw.async}; + draw.async = true; + if (Alpha) + { + Render_Alpha(Region); + } + else + { + Render(Region); + } + draw.async = async; +} + void vulkan14_renderer::destroy_scene() { if (!m_context) @@ -841,6 +881,12 @@ void vulkan14_renderer::destroy_scene() { return; } + // the background threads and the threads of the walkers use the programs and the textures: done first, then the list of the + // pipelines of the session is written + ctx.walks.stop(); + ctx.models.stop(); + ctx.background.stop(); + ctx.save_scene_pipelines(); destroy_cull(); if (scene.raytracing) { @@ -938,6 +984,11 @@ void vulkan14_renderer::destroy_scene() // the pass sets, made anew when the images they read changed: the old ones go once the gpu is done with them void vulkan14_renderer::update_pass_sets() { + if (m_main != nullptr) + { + // a walker binds the pass sets of the frame as they were when it was given its pass + return; + } auto &ctx{*m_context}; auto &scene{ctx.scene}; auto const device{ctx.device.handle()}; @@ -969,6 +1020,8 @@ void vulkan14_renderer::update_pass_sets() { return; } + // the walkers write the pass sets of their cullings from the same state (use_cull_pass_sets()) + std::lock_guard lock(ctx.passsets_mutex); scene.pass_sets_stale = false; scene.headlights_view = headlights; scene.pass_images = pass_images; @@ -1084,8 +1137,15 @@ void vulkan14_renderer::use_cull_pass_sets(vk14::instance_cull *Cull) auto &ctx{*m_context}; auto &scene{ctx.scene}; update_pass_sets(); - if ((Cull->set_pool == VK_NULL_HANDLE) || (Cull->sets_generation != scene.pass_generation) || (Cull->sets_output != Cull->output_buffer())) + auto const stale = [&]() { + std::lock_guard lock(ctx.passsets_mutex); + return (Cull->set_pool == VK_NULL_HANDLE) || (Cull->sets_generation != scene.pass_generation) || (Cull->sets_output != Cull->output_buffer()); + }; + if (stale()) { + // written by a walker while the main thread doesn't change what they hold (gfx::walk_sync) + gfx::walk_sync::change change{m_main != nullptr}; + std::lock_guard lock(ctx.passsets_mutex); auto const device{ctx.device.handle()}; if (Cull->set_pool != VK_NULL_HANDLE) { @@ -1116,15 +1176,15 @@ void vulkan14_renderer::bind_pass_set() auto &scene{m_context->scene}; // the dynamic offsets in the order of the bindings // the instances of the indirect draws come from the output of the culling, in the pass sets of the culling - auto const instancebase{draw.cull_sets != nullptr ? 0u : static_cast(scene.instances.base)}; - std::array const offsets{draw.scene_offset, static_cast(scene.draws.base), instancebase, draw.light_offset, draw.cluster_offset, draw.weather_offset}; + auto const instancebase{draw.cull_sets != nullptr ? 0u : static_cast(draw.instances_window.base)}; + std::array const offsets{draw.scene_offset, static_cast(draw.draws_window.base), instancebase, draw.light_offset, draw.cluster_offset, draw.weather_offset}; VkBindDescriptorSetsInfo info{}; info.sType = VK_STRUCTURE_TYPE_BIND_DESCRIPTOR_SETS_INFO; info.stageFlags = VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT; info.layout = scene.scene_layout; info.firstSet = vk14::bindings::PASS_SET; info.descriptorSetCount = 1; - info.pDescriptorSets = (draw.cull_sets != nullptr ? &draw.cull_sets[draw.set] : &scene.pass_sets[draw.set]); + info.pDescriptorSets = (draw.cull_sets != nullptr ? &draw.cull_sets[draw.set] : (m_main != nullptr ? &draw.pass_sets[draw.set] : &scene.pass_sets[draw.set])); info.dynamicOffsetCount = static_cast(offsets.size()); info.pDynamicOffsets = offsets.data(); draw.commands->bind_descriptor_sets2(&info); @@ -1151,7 +1211,7 @@ void vulkan14_renderer::bind_compute_set(vk14::compute_kernel const &Kernel) auto &scene{m_context->scene}; auto &draw{*m_draw}; update_pass_sets(); - std::array const offsets{draw.scene_offset, static_cast(scene.draws.base), static_cast(scene.instances.base), draw.light_offset, draw.cluster_offset, draw.weather_offset}; + std::array const offsets{draw.scene_offset, static_cast(draw.draws_window.base), static_cast(draw.instances_window.base), draw.light_offset, draw.cluster_offset, draw.weather_offset}; VkBindDescriptorSetsInfo info{}; info.sType = VK_STRUCTURE_TYPE_BIND_DESCRIPTOR_SETS_INFO; info.stageFlags = VK_SHADER_STAGE_COMPUTE_BIT; @@ -1161,13 +1221,15 @@ void vulkan14_renderer::bind_compute_set(vk14::compute_kernel const &Kernel) info.pDescriptorSets = &scene.pass_sets[vk14::scene_state::set_colour]; info.dynamicOffsetCount = static_cast(offsets.size()); info.pDynamicOffsets = offsets.data(); - m_context->commands->bind_descriptor_sets2(&info); + m_commands->bind_descriptor_sets2(&info); } // geometry gfx::geometrybank_handle vulkan14_renderer::Create_Bank() { + // made while no walk of the passes draws (gfx::walk_sync) + gfx::walk_sync::change change; if (!m_context) { return m_geometry.register_bank(std::make_unique()); @@ -1185,30 +1247,40 @@ gfx::geometrybank_handle vulkan14_renderer::Create_Bank() gfx::geometry_handle vulkan14_renderer::Insert(gfx::index_array &Indices, gfx::vertex_array &Vertices, gfx::userdata_array &Userdata, gfx::geometrybank_handle const &Geometry, int const Type) { + // made while no walk of the passes draws (gfx::walk_sync) + gfx::walk_sync::change change; // NOTE: we expect indexed geometry to come with calculated tangents return m_geometry.create_chunk(Indices, Vertices, Userdata, Geometry, Type); } gfx::geometry_handle vulkan14_renderer::Insert(gfx::vertex_array &Vertices, gfx::userdata_array &Userdata, gfx::geometrybank_handle const &Geometry, int const Type) { + // made while no walk of the passes draws (gfx::walk_sync) + gfx::walk_sync::change change; gfx::calculate_tangents(Vertices, gfx::index_array(), Type); return m_geometry.create_chunk(Vertices, Userdata, Geometry, Type); } bool vulkan14_renderer::Replace(gfx::vertex_array &Vertices, gfx::userdata_array &Userdata, gfx::geometry_handle const &Geometry, int const Type, std::size_t const Offset) { + // made while no walk of the passes draws (gfx::walk_sync) + gfx::walk_sync::change change; gfx::calculate_tangents(Vertices, gfx::index_array(), Type); return m_geometry.replace(Vertices, Userdata, Geometry, Offset); } bool vulkan14_renderer::Replace_Indexed(gfx::index_array &Indices, gfx::vertex_array &Vertices, gfx::userdata_array &Userdata, gfx::geometry_handle const &Geometry) { + // made while no walk of the passes draws (gfx::walk_sync) + gfx::walk_sync::change change; // NOTE: we expect indexed geometry to come with calculated tangents return m_geometry.replace(Indices, Vertices, Userdata, Geometry); } bool vulkan14_renderer::Append(gfx::vertex_array &Vertices, gfx::userdata_array &Userdata, gfx::geometry_handle const &Geometry, int const Type) { + // made while no walk of the passes draws (gfx::walk_sync) + gfx::walk_sync::change change; gfx::calculate_tangents(Vertices, gfx::index_array(), Type); return m_geometry.append(Vertices, Userdata, Geometry); } @@ -1238,26 +1310,36 @@ int vulkan14_renderer::Primitive(gfx::geometry_handle const &Geometry) const material_handle vulkan14_renderer::Fetch_Material(std::string const &Filename, bool const Loadnow) { + // made while no walk of the passes draws (gfx::walk_sync) + gfx::walk_sync::change change; return m_materials.create(Filename, Loadnow); } material_handle vulkan14_renderer::Matte_Material(material_handle const Material) { + // made while no walk of the passes draws (gfx::walk_sync) + gfx::walk_sync::change change; return m_materials.create_matte(Material); } material_handle vulkan14_renderer::Tinted_Material(material_handle const Material, glm::vec3 const &Colour) { + // made while no walk of the passes draws (gfx::walk_sync) + gfx::walk_sync::change change; return m_materials.create_tinted(Material, Colour); } material_handle vulkan14_renderer::Blend_Material(material_handle const Material, std::array const &Others) { + // made while no walk of the passes draws (gfx::walk_sync) + gfx::walk_sync::change change; return m_materials.create_blend(Material, Others); } bool vulkan14_renderer::Blendable_Material(material_handle const Material) { + // made while no walk of the passes draws (gfx::walk_sync) + gfx::walk_sync::change change; return m_materials.blendable(Material); } @@ -1269,6 +1351,8 @@ IMaterial const *vulkan14_renderer::Material(material_handle const Material) con material_handle vulkan14_renderer::Terrain_Material(material_handle const Reuse, std::vector const &Layers, int const Samples, std::uint8_t const *Weights, glm::vec3 const &Placement, int const HoleSamples, std::uint8_t const *Holes) { + // made while no walk of the passes draws (gfx::walk_sync) + gfx::walk_sync::change change; if (m_context && m_context->scene.terrain) { auto const material{m_context->scene.terrain->make(Reuse, Layers, Samples, Weights, Placement, HoleSamples, Holes)}; @@ -1283,6 +1367,8 @@ material_handle vulkan14_renderer::Terrain_Material(material_handle const Reuse, void vulkan14_renderer::Terrain_Release(material_handle const Material) { + // made while no walk of the passes draws (gfx::walk_sync) + gfx::walk_sync::change change; if (m_context && m_context->scene.terrain) { m_context->scene.terrain->release(Material); @@ -1291,6 +1377,8 @@ void vulkan14_renderer::Terrain_Release(material_handle const Material) void vulkan14_renderer::Terrain_Grass(std::vector const &Kinds, std::vector const &Chunks) { + // made while no walk of the passes draws (gfx::walk_sync) + gfx::walk_sync::change change; if (m_context) { m_context->scene.grass.update(Kinds, Chunks); @@ -1299,6 +1387,8 @@ void vulkan14_renderer::Terrain_Grass(std::vector const &Kinds, std::shared_ptr vulkan14_renderer::Fetch_Shader(std::string const &name) { + // made while no walk of the passes draws (gfx::walk_sync) + gfx::walk_sync::change change; if (!m_context) { return nullptr; @@ -1374,22 +1464,23 @@ void vulkan14_renderer::walk_material(material_handle const Material, TSubModel } std::memcpy(&model_ubs.param[0], &material.params[0], sizeof(model_ubs.param)); + auto const *walked{m_draw->replaying ? nullptr : Submodel}; for (auto const &entry : material.params_state) { glm::vec4 src(1.0f); // submodel-based parameters - if (Submodel) + if (walked) { switch (entry.defaultparam) { case gfx::shader_param_default::ambient: - src = Submodel->f4Ambient; + src = walked->f4Ambient; break; case gfx::shader_param_default::diffuse: - src = Submodel->f4Diffuse; + src = walked->f4Diffuse; break; case gfx::shader_param_default::specular: - src = Submodel->f4Specular; + src = walked->f4Specular; break; default: break; @@ -1439,7 +1530,7 @@ void vulkan14_renderer::walk_material(material_handle const Material, TSubModel { model_ubs.opacity = (m_draw->blending ? 0.0f : 0.5f); } - model_ubs.alpha_mult = (Submodel ? Submodel->fVisible : 1.0f); + model_ubs.alpha_mult = (walked ? walked->fVisible : 1.0f); if (m_indirectrecording) { // the walk of the groups of the gpu culling only notes what would be bound, it's bound when the draw is made @@ -1477,7 +1568,7 @@ void vulkan14_renderer::walk_material(material_handle const Material, TSubModel return; } auto &texture{m_context->textures->mark_as_used(materialtextures[0])}; - if ((false == texture.create()) || (m_context->images.find(texture.get_id()) == nullptr)) + if ((false == texture_ready(texture)) || (m_context->images.find(texture.get_id()) == nullptr)) { return; } @@ -1495,7 +1586,7 @@ void vulkan14_renderer::walk_material(material_handle const Material, TSubModel return; } auto &texture{m_context->textures->mark_as_used(materialtextures[0])}; - if ((false == texture.create()) || (m_context->images.find(texture.get_id()) == nullptr)) + if ((false == texture_ready(texture)) || (m_context->images.find(texture.get_id()) == nullptr)) { return; } @@ -1596,6 +1687,45 @@ gfx::geometrybank_manager &vulkan14_renderer::walk_geometry() return m_geometry; } +bool vulkan14_renderer::walk_indirect_source(gfx::geometry_handle const &Geometry, gfx::indirect_source &Source) +{ + vk14::chunk_source chunk; + if (false == chunk_of(Geometry, chunk)) + { + return false; + } + Source.count = chunk.count; + Source.first = chunk.first; + Source.basevertex = chunk.basevertex; + Source.indexed = (chunk.indices != VK_NULL_HANDLE); + Source.type = m_geometry.type(Geometry); + return true; +} + +// the chunk with its data on the gpu, its bank marked as used: by the main renderer, or once the walks are done for the banks a +// walker drew from (the marks aren't made by several threads at once) +bool vulkan14_renderer::chunk_of(gfx::geometry_handle const &Geometry, vk14::chunk_source &Chunk) +{ + if ((Geometry == null_handle) || (Geometry.bank == 0) || (Geometry.bank > m_banks.size()) || (m_banks[Geometry.bank - 1] == nullptr)) + { + return false; + } + if (m_main != nullptr) + { + auto &used{m_draw->used_banks}; + if (used.empty() || (used.back() != Geometry.bank)) + { + used.push_back(Geometry.bank); + } + } + else + { + m_geometry.mark_used(Geometry); + } + Chunk = vk14::chunk_source{}; + return static_cast(m_banks[Geometry.bank - 1])->source(Geometry, Chunk); +} + bool vulkan14_renderer::walk_blending_enabled() const { return (m_draw != nullptr) && m_draw->blending; @@ -1715,22 +1845,24 @@ void vulkan14_renderer::setup_shadow_color(glm::vec4 const &Shadowcolor) std::uint32_t vulkan14_renderer::push_draw_block() { - auto &scene{m_context->scene}; - auto &draws{scene.draws}; - auto const slice{draws.ring.allocate(draws.stride, draws.stride)}; + auto &draws{m_context->scene.draws}; + auto &draw{*m_draw}; + auto &window{draw.draws_window}; + auto slice{window.open ? draw.draws_cache.allocate_before(draws.stride, draws.stride, window.base + draws.window) : vk14::ring_slice{}}; if (false == bool(slice)) { - return ~0u; - } - if ((false == draws.open) || (slice.offset + draws.stride > draws.base + draws.window)) - { + slice = draw.draws_cache.allocate(draws.stride, draws.stride); + if (false == bool(slice)) + { + return ~0u; + } // a new window of the blocks, the pass set is bound with its offset - draws.base = slice.offset; - draws.open = true; - m_draw->set_dirty = true; + window.base = slice.offset; + window.open = true; + draw.set_dirty = true; } std::memcpy(slice.data, &model_ubs, sizeof(gl::model_ubs)); - return static_cast((slice.offset - draws.base) / draws.stride); + return static_cast((slice.offset - window.base) / draws.stride); } std::uint32_t vulkan14_renderer::walk_instances(glm::mat4 const *Modelviews, std::size_t const Count) @@ -1742,44 +1874,69 @@ std::uint32_t vulkan14_renderer::walk_instances(glm::mat4 const *Modelviews, std m_rtinstances.insert(m_rtinstances.end(), Modelviews, Modelviews + Count); return base; } - auto &scene{m_context->scene}; - auto &instances{scene.instances}; + auto &instances{m_context->scene.instances}; + auto &draw{*m_draw}; + auto &window{draw.instances_window}; auto const capacity{static_cast(instances.window / instances.stride) - 1}; auto const count{std::min(Count, capacity)}; auto const size{static_cast(count) * instances.stride}; - if ((false == instances.open) || (instances.ring.next_offset(instances.stride) + size > instances.base + instances.window)) + auto slice{window.open ? draw.instances_cache.allocate_before(size, instances.stride, window.base + instances.window) : vk14::ring_slice{}}; + if (false == bool(slice)) { - open_instance_window(); - } - auto const slice{instances.ring.allocate(size, instances.stride)}; - if ((false == instances.open) || (false == bool(slice)) || (slice.offset + size > instances.base + instances.window)) - { - // the ring of the frame is full: the instances are drawn where the draws which aren't instanced are - return 0; + // a new window, which begins with the identity of the draws which aren't instanced, followed by the instances + auto const alignment{m_context->device.caps().limits.minStorageBufferOffsetAlignment}; + auto const block{draw.instances_cache.allocate(instances.stride + size, std::max(alignment, instances.stride))}; + if (false == bool(block)) + { + // the ring of the frame is full: the instances are drawn where the draws which aren't instanced are + window.open = false; + return 0; + } + glm::mat4 const one{1.f}; + std::memcpy(block.data, &one, sizeof(one)); + window.base = block.offset; + window.open = true; + draw.set_dirty = true; + slice = block; + slice.data += instances.stride; + slice.offset += instances.stride; + slice.size = size; } std::memcpy(slice.data, Modelviews, static_cast(size)); - return static_cast((slice.offset - instances.base) / instances.stride); + return static_cast((slice.offset - window.base) / instances.stride); } // a new window of the instance ring, which begins with the identity of the draws which aren't instanced void vulkan14_renderer::open_instance_window() { auto &instances{m_context->scene.instances}; + auto &draw{*m_draw}; + auto &window{draw.instances_window}; auto const alignment{m_context->device.caps().limits.minStorageBufferOffsetAlignment}; - auto const identity{instances.ring.allocate(instances.stride, std::max(alignment, instances.stride))}; + auto const identity{draw.instances_cache.allocate(instances.stride, std::max(alignment, instances.stride))}; if (false == bool(identity)) { - instances.open = false; + window.open = false; return; } glm::mat4 const one{1.f}; std::memcpy(identity.data, &one, sizeof(one)); - instances.base = identity.offset; - instances.open = true; - if (m_draw) + window.base = identity.offset; + window.open = true; + draw.set_dirty = true; +} + +// the texture of a draw: marked as used by the main renderer, or once the walks are done for the ones a walker drew with (the marks +// aren't made by several threads at once) +texture_source &vulkan14_renderer::used_texture(texture_handle const Texture) +{ + auto &textures{*m_context->textures}; + if (m_main != nullptr) { - m_draw->set_dirty = true; + m_draw->used_textures.push_back(Texture); + return textures.texture(Texture); } + return textures.mark_as_used(Texture); } void vulkan14_renderer::walk_draw(gfx::geometry_handle const &Geometry) @@ -1831,12 +1988,11 @@ void vulkan14_renderer::draw_chunk(gfx::geometry_handle const &Geometry, bool co return; } // marks the bank as used, and has its data on the gpu - gfx::indirect_source source; - if (false == m_geometry.indirect_source(Geometry, source)) + vk14::chunk_source chunk; + if (false == chunk_of(Geometry, chunk)) { return; } - auto const &chunk{static_cast(m_banks[Geometry.bank - 1])->last()}; if ((draw.depthpass || draw.probepass) && (draw.depthskip || (topology_class(chunk.topology) != VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST))) { @@ -1909,12 +2065,158 @@ void vulkan14_renderer::draw_chunk(gfx::geometry_handle const &Geometry, bool co ++draw.draws; } +// the pipelines of the scene (bind_scene_pipeline()): the description of one, for the shaders, the vertices of the program, the targets +// of the pass, the blending, the class of the primitive and the settings of the game the shaders take as specialization constants +namespace +{ + +vk14::pipeline_desc scene_pipeline_desc(std::shared_ptr const &Vertex, std::shared_ptr const &Fragment, vk14::vertex_kind const Vertices, + VkPipelineLayout const Layout, VkPrimitiveTopology const Primitiveclass, vk14::blend_mode const Blend, std::array const &Colour, + std::uint32_t const Colours, VkFormat const Depth, VkSampleCountFlagBits const Samples, std::uint32_t const Viewmask, std::uint32_t const Settings, + std::string const &Name) +{ + vk14::pipeline_desc desc; + desc.stages = {Vertex, Fragment}; + desc.layout = Layout; + desc.vertices = layout_of(Vertices); + { + auto const read{vertex_input_locations(Vertex->spirv)}; + auto &attributes{desc.vertices.attributes}; + attributes.erase(std::remove_if(attributes.begin(), attributes.end(), [&](VkVertexInputAttributeDescription const &Attribute) { return std::find(read.begin(), read.end(), Attribute.location) == read.end(); }), + attributes.end()); + } + desc.topology = Primitiveclass; + desc.blending = Blend; + desc.colour_formats.assign(Colour.begin(), Colour.begin() + Colours); + desc.depth_format = Depth; + desc.samples = Samples; + desc.view_mask = Viewmask; + desc.specialization = {Settings & 1u, (Settings >> 1) & 1u, (Settings >> 2) & 1u, (Settings >> 3) & 1u, (Settings >> 4) & 1u, (Settings >> 5) & 1u}; + desc.name = Name; + return desc; +} + +// the list of the pipelines of a session: one line a pipeline, the fields apart by '|', with \n, | and % in them as %0a, %7c, %25 +std::string list_field(std::string const &Value) +{ + std::string result; + for (auto const c : Value) + { + switch (c) + { + case '\n': + result += "%0a"; + break; + case '|': + result += "%7c"; + break; + case '%': + result += "%25"; + break; + default: + result += c; + break; + } + } + return result; +} + +std::string list_value(std::string const &Field) +{ + std::string result; + for (std::size_t i = 0; i < Field.size(); ++i) + { + if ((Field[i] == '%') && (i + 2 < Field.size())) + { + auto const code{Field.substr(i + 1, 2)}; + result += (code == "0a" ? '\n' : code == "7c" ? '|' : '%'); + i += 2; + continue; + } + result += Field[i]; + } + return result; +} + +std::string pipeline_list_path() +{ + auto const path{user_config_path("cache")}; + return (path.empty() ? std::string{"cache/vulkan14/pipelines.txt"} : (path / "vulkan14" / "pipelines.txt").string()); +} + +// the number of pipelines the list keeps; the oldest go +std::size_t constexpr pipeline_list_limit{4096}; + +} // namespace + +std::string vk14::scene_pipeline_record::line() const +{ + std::string result{"1"}; + for (auto const &field : {vertex, vertex_defines, fragment, fragment_defines, name}) + { + result += "|" + list_field(field); + } + for (auto const value : {static_cast(vertices), static_cast(topology), blending, static_cast(colour[0]), static_cast(colour[1]), colours, + static_cast(depth), samples, view_mask, settings}) + { + result += "|" + std::to_string(value); + } + return result; +} + +bool vk14::scene_pipeline_record::parse(std::string const &Line) +{ + std::vector fields; + std::size_t start{0}; + while (true) + { + auto const end{Line.find('|', start)}; + fields.push_back(list_value(Line.substr(start, end == std::string::npos ? std::string::npos : end - start))); + if (end == std::string::npos) + { + break; + } + start = end + 1; + } + if ((fields.size() != 16) || (fields[0] != "1") || fields[1].empty() || fields[3].empty()) + { + return false; + } + try + { + vertex = fields[1]; + vertex_defines = fields[2]; + fragment = fields[3]; + fragment_defines = fields[4]; + name = fields[5]; + std::array values; + for (std::size_t i = 0; i < values.size(); ++i) + { + values[i] = static_cast(std::stoul(fields[6 + i])); + } + vertices = static_cast(values[0]); + topology = static_cast(values[1]); + blending = values[2]; + colour = {static_cast(values[3]), static_cast(values[4])}; + colours = std::min(values[5], 2); + depth = static_cast(values[6]); + samples = values[7]; + view_mask = values[8]; + settings = values[9]; + } + catch (std::exception const &) + { + return false; + } + return true; +} + // the pipeline of the program for the targets of the pass, the blending and the class of the primitive, made the first time it's -// asked for; bound with the primitive as dynamic state. false: it can't be made +// asked for; bound with the primitive as dynamic state. false: it can't be made, or it's being made by the background threads for +// the walk of the scene, which leaves out the draw till it's there (gfx.vulkan.asyncresources, the stage M5) bool vulkan14_renderer::bind_scene_pipeline(vk14::scene_program &Program, vk14::target_formats const &Targets, vk14::blend_mode const Blend, int const Topology) { auto &ctx{*m_context}; - auto &scene{ctx.scene}; auto &draw{*m_draw}; auto const topology{static_cast(Topology)}; auto const primitiveclass{topology_class(topology)}; @@ -1923,53 +2225,45 @@ bool vulkan14_renderer::bind_scene_pipeline(vk14::scene_program &Program, vk14:: std::uint32_t const settings{(Global.gfx_shadergamma ? 1u : 0u) | (Global.gfx_shadowmap_enabled ? 2u : 0u) | (Global.gfx_envmap_enabled ? 4u : 0u) | (motion ? 8u : 0u) | 16u | (Global.gfx_extraeffects ? 32u : 0u)}; auto const key{(static_cast(Targets.id)) | (static_cast(Blend) << 16) | (static_cast(primitiveclass) << 24) | (static_cast(settings) << 32)}; VkPipeline pipeline{VK_NULL_HANDLE}; - auto const lookup{Program.pipelines.find(key)}; - if (lookup != Program.pipelines.end()) + if ((draw.lastprogram == &Program) && (draw.lastkey == key)) { - pipeline = lookup->second; + pipeline = draw.lastpipeline; } else { - auto fragment{Program.fragment}; - if ((false == motion) && (Targets.colours > 0)) + auto found{false}; + { + std::lock_guard lock(Program.mutex); + auto const lookup{Program.pipelines.find(key)}; + if (lookup != Program.pipelines.end()) + { + pipeline = lookup->second; + found = true; + } + } + if (false == found) { // a shader writing the velocity without its target is reported by the validation layer - if (!Program.fragment_single) + auto const single{(false == motion) && (Targets.colours > 0)}; + if ((draw.async || (m_main != nullptr)) && Global.gfx_vulkan_asyncresources && ctx.background.running()) { - try + // made ahead (the list of the last session), or made for another program of the same shaders: taken at once + if (false == ctx.find_scene_pipeline(Program, Targets, Blend, primitiveclass, settings, single, pipeline)) { - Program.fragment_single = ctx.shaders.get(Program.fragment_name, Program.defines + "#define NO_MOTION_OUTPUT\n"); - } - catch (gfx::shader_exception const &Exception) - { - vk14::log_error(Exception.what()); + ctx.queue_scene_pipeline(Program, Targets, Blend, primitiveclass, settings, single, key); + return false; } } - if (Program.fragment_single) + else { - fragment = Program.fragment_single; + pipeline = ctx.make_scene_pipeline(Program, Targets, Blend, primitiveclass, settings, single); } + std::lock_guard lock(Program.mutex); + Program.pipelines.emplace(key, pipeline); } - vk14::pipeline_desc desc; - desc.stages = {Program.vertex, fragment}; - desc.layout = scene.scene_layout; - desc.vertices = layout_of(Program.vertices); - { - auto const read{vertex_input_locations(Program.vertex->spirv)}; - auto &attributes{desc.vertices.attributes}; - attributes.erase(std::remove_if(attributes.begin(), attributes.end(), [&](VkVertexInputAttributeDescription const &Attribute) { return std::find(read.begin(), read.end(), Attribute.location) == read.end(); }), - attributes.end()); - } - desc.topology = primitiveclass; - desc.blending = Blend; - desc.colour_formats.assign(Targets.colour.begin(), Targets.colour.begin() + Targets.colours); - desc.depth_format = Targets.depth; - desc.samples = Targets.samples; - desc.view_mask = Targets.view_mask; - desc.specialization = {settings & 1u, (settings >> 1) & 1u, (settings >> 2) & 1u, (settings >> 3) & 1u, (settings >> 4) & 1u, (settings >> 5) & 1u}; - desc.name = Program.name; - pipeline = ctx.pipelines.get(desc); - Program.pipelines.emplace(key, pipeline); + draw.lastprogram = &Program; + draw.lastkey = key; + draw.lastpipeline = pipeline; } if (pipeline == VK_NULL_HANDLE) { @@ -1988,6 +2282,218 @@ bool vulkan14_renderer::bind_scene_pipeline(vk14::scene_program &Program, vk14:: return true; } +// the fragment shader of the program for the targets: the one without the velocity target, made when it's first needed (null when it +// isn't there and Compile is false), or the program's own +std::shared_ptr vulkan14_renderer::context::scene_fragment(vk14::scene_program &Program, bool const Single, bool const Compile) +{ + auto &ctx{*this}; + if (false == Single) + { + return Program.fragment; + } + { + std::lock_guard lock(Program.mutex); + if (Program.fragment_single) + { + return Program.fragment_single; + } + } + auto const defines{Program.defines + "#define NO_MOTION_OUTPUT\n"}; + std::shared_ptr fragment{ctx.shaders.find(Program.fragment_name, defines)}; + if ((fragment == nullptr) && Compile) + { + try + { + fragment = ctx.shaders.get(Program.fragment_name, defines); + } + catch (gfx::shader_exception const &Exception) + { + vk14::log_error(Exception.what()); + // the program's own then, as before + return Program.fragment; + } + } + if (fragment != nullptr) + { + std::lock_guard lock(Program.mutex); + if (!Program.fragment_single) + { + Program.fragment_single = fragment; + } + } + return fragment; +} + +VkPipeline vulkan14_renderer::context::make_scene_pipeline(vk14::scene_program &Program, vk14::target_formats const &Targets, vk14::blend_mode const Blend, VkPrimitiveTopology const Primitiveclass, + std::uint32_t const Settings, bool const Single) +{ + auto &ctx{*this}; + auto const fragment{scene_fragment(Program, Single, true)}; + if ((Program.vertex == nullptr) || (fragment == nullptr)) + { + return VK_NULL_HANDLE; + } + auto const desc{scene_pipeline_desc(Program.vertex, fragment, Program.vertices, ctx.scene.scene_layout, Primitiveclass, Blend, Targets.colour, Targets.colours, Targets.depth, Targets.samples, + Targets.view_mask, Settings, Program.name)}; + auto const pipeline{ctx.pipelines.get(desc)}; + if (pipeline != VK_NULL_HANDLE) + { + // for the list of the session + vk14::scene_pipeline_record record; + record.vertex = Program.vertex->name; + record.vertex_defines = Program.vertex->defines; + record.fragment = fragment->name; + record.fragment_defines = fragment->defines; + record.vertices = Program.vertices; + record.topology = Primitiveclass; + record.blending = static_cast(Blend); + record.colour = Targets.colour; + record.colours = Targets.colours; + record.depth = Targets.depth; + record.samples = static_cast(Targets.samples); + record.view_mask = Targets.view_mask; + record.settings = Settings; + record.name = Program.name; + auto line{record.line()}; + std::lock_guard lock(ctx.warm_mutex); + if (ctx.warm_lines.insert(line).second) + { + ctx.warm_records.push_back(std::move(record)); + } + } + return pipeline; +} + +bool vulkan14_renderer::context::find_scene_pipeline(vk14::scene_program &Program, vk14::target_formats const &Targets, vk14::blend_mode const Blend, VkPrimitiveTopology const Primitiveclass, + std::uint32_t const Settings, bool const Single, VkPipeline &Pipeline) +{ + auto &ctx{*this}; + auto const fragment{scene_fragment(Program, Single, false)}; + if ((Program.vertex == nullptr) || (fragment == nullptr)) + { + return false; + } + return ctx.pipelines.find(scene_pipeline_desc(Program.vertex, fragment, Program.vertices, ctx.scene.scene_layout, Primitiveclass, Blend, Targets.colour, Targets.colours, Targets.depth, + Targets.samples, Targets.view_mask, Settings, Program.name), + Pipeline); +} + +void vulkan14_renderer::context::queue_scene_pipeline(vk14::scene_program &Program, vk14::target_formats const &Targets, vk14::blend_mode const Blend, VkPrimitiveTopology const Primitiveclass, + std::uint32_t const Settings, bool const Single, std::uint64_t const Key) +{ + auto &ctx{*this}; + { + std::lock_guard lock(Program.mutex); + if (false == Program.pending.insert(Key).second) + { + // being made already + return; + } + } + ++ctx.pipelines_late; + // the program and the renderer outlive the background threads (stopped first when the scene goes) + ctx.background.add([this, program = &Program, targets = Targets, Blend, Primitiveclass, Settings, Single, Key]() { + auto const pipeline{make_scene_pipeline(*program, targets, Blend, Primitiveclass, Settings, Single)}; + std::lock_guard lock(program->mutex); + program->pipelines.emplace(Key, pipeline); + program->pending.erase(Key); + }); +} + +// the pipelines of the list of the last session made ahead by the background threads, after the ones the draws wait for +void vulkan14_renderer::context::warm_scene_pipelines() +{ + auto &ctx{*this}; + if ((false == Global.gfx_vulkan_asyncresources) || (false == ctx.background.running())) + { + return; + } + std::ifstream file(pipeline_list_path()); + if (false == file.is_open()) + { + return; + } + std::vector records; + std::string line; + while (std::getline(file, line)) + { + vk14::scene_pipeline_record record; + if (record.parse(line)) + { + records.push_back(std::move(record)); + } + } + { + // the list of this session keeps them, in their order, even when nothing asks for them now + std::lock_guard lock(ctx.warm_mutex); + for (auto const &record : records) + { + if (ctx.warm_lines.insert(record.line()).second) + { + ctx.warm_records.push_back(record); + } + } + } + vk14::log_info("pipelines of the last session made ahead: " + std::to_string(records.size())); + auto const layout{ctx.scene.scene_layout}; + for (auto const &record : records) + { + ctx.background.add( + [this, record, layout]() { + auto &ctx{*this}; + try + { + auto const vertex{ctx.shaders.get(record.vertex, record.vertex_defines)}; + auto const fragment{ctx.shaders.get(record.fragment, record.fragment_defines)}; + ctx.pipelines.get(scene_pipeline_desc(vertex, fragment, record.vertices, layout, record.topology, static_cast(record.blending), record.colour, record.colours, + record.depth, static_cast(record.samples), record.view_mask, record.settings, record.name)); + ++ctx.warm_made; + } + catch (gfx::shader_exception const &Exception) + { + // the shader went or changed meanwhile + vk14::log_info(std::string("a pipeline of the last session can't be made ahead: ") + Exception.what()); + } + }, + false); + } +} + +// the list of the pipelines of the session, for the next one +void vulkan14_renderer::context::save_scene_pipelines() +{ + auto &ctx{*this}; + std::vector records; + { + std::lock_guard lock(ctx.warm_mutex); + records = ctx.warm_records; + } + if (records.empty()) + { + return; + } + if (records.size() > pipeline_list_limit) + { + records.erase(records.begin(), records.end() - pipeline_list_limit); + } + auto const path{pipeline_list_path()}; + std::error_code error; + std::filesystem::create_directories(std::filesystem::path(path).parent_path(), error); + auto const temporary{path + ".tmp"}; + { + std::ofstream file(temporary, std::ios::trunc); + if (false == file.is_open()) + { + return; + } + for (auto const &record : records) + { + file << record.line() << '\n'; + } + } + std::filesystem::rename(temporary, path, error); +} + // the dynamic state of the draws for the state of the walk: the faces culled, the depth tested and written void vulkan14_renderer::apply_draw_state() { @@ -2027,8 +2533,8 @@ void vulkan14_renderer::push_textures() { continue; } - auto &source{ctx.textures->mark_as_used(texture)}; - if (false == source.create()) + auto &source{used_texture(texture)}; + if (false == texture_ready(source)) { continue; } @@ -2041,6 +2547,17 @@ void vulkan14_renderer::push_textures() draw.textures_dirty = false; } +// a texture a draw needs: made by the background threads for the walks of the scene (the draw goes without it till then), made now +// otherwise +bool vulkan14_renderer::texture_ready(texture_source &Texture) +{ + if ((m_draw != nullptr) && (m_draw->async || (m_main != nullptr))) + { + return static_cast(Texture).request_background(); + } + return Texture.create(); +} + // which program of the depth prepass stands for the program of the material (opengl46_renderer::Prepass_Class()): 0 none, 1 the // one without the alpha test, 2 the one with it int vulkan14_renderer::prepass_class(material_handle const Material) @@ -2084,6 +2601,8 @@ int vulkan14_renderer::prepass_class(material_handle const Material) texture_handle vulkan14_renderer::Fetch_Texture(std::string const &Filename, bool const Loadnow, GLint format_hint) { + // made while no walk of the passes draws (gfx::walk_sync) + gfx::walk_sync::change change; if (!m_context || !m_context->textures) { return null_handle; diff --git a/rendering/vulkan14/vulkan14shaders.cpp b/rendering/vulkan14/vulkan14shaders.cpp index 2a94bd0d..4999b40f 100644 --- a/rendering/vulkan14/vulkan14shaders.cpp +++ b/rendering/vulkan14/vulkan14shaders.cpp @@ -27,6 +27,7 @@ http://mozilla.org/MPL/2.0/. #include #include #include +#include namespace vk14 { @@ -103,7 +104,8 @@ void write_file(std::string const &Path, void const *Data, std::size_t const Siz { std::error_code error; std::filesystem::create_directories(std::filesystem::path(Path).parent_path(), error); - auto const temporary{Path + ".tmp"}; + // a temporary file of each thread: two of them may write the same file of the cache at once + auto const temporary{Path + ".tmp" + std::to_string(std::hash{}(std::this_thread::get_id()))}; { std::ofstream file(temporary, std::ios::binary | std::ios::trunc); if (false == file.is_open()) @@ -218,16 +220,29 @@ void shader_library::shutdown() } } +std::shared_ptr shader_library::find(std::string const &Name, std::string const &Defines) const +{ + std::lock_guard lock(m_mutex); + auto const lookup{m_shaders.find(Name + "|" + Defines)}; + return (lookup != m_shaders.end() ? lookup->second : nullptr); +} + std::shared_ptr shader_library::get(std::string const &Name, std::string const &Defines) { auto const key{Name + "|" + Defines}; - auto const lookup{m_shaders.find(key)}; - if (lookup != m_shaders.end()) { - return lookup->second; + std::lock_guard lock(m_mutex); + auto const lookup{m_shaders.find(key)}; + if (lookup != m_shaders.end()) + { + return lookup->second; + } } + // read or compiled outside of the lock: two threads asking for the same shader at once both make it, the first one kept auto code{std::make_shared()}; + code->name = Name; + code->defines = Defines; code->source = gfx::preprocess_shader(gfx::shader_api::vulkan14, Name, Defines, m_preamble, true); code->stage = stage_bit(code->source.stage); auto const label{Name + (code->source.variant.empty() ? "" : " (" + code->source.variant + ")")}; @@ -243,7 +258,6 @@ std::shared_ptr shader_library::get(std::string const &Name, if (false == code->spirv.empty()) { code->origin = "binary"; - ++m_frombinary; } } if (code->spirv.empty()) @@ -260,20 +274,24 @@ std::shared_ptr shader_library::get(std::string const &Name, if (false == code->spirv.empty()) { code->origin = "cache"; - ++m_fromcache; } else { code->spirv = compile(code->source); code->origin = "compiled"; - ++m_compiled; write_file(cachepath, code->spirv.data(), code->spirv.size() * sizeof(std::uint32_t)); } } // the text isn't needed any more code->source.text = std::string(); - m_shaders.emplace(key, code); - return code; + std::lock_guard lock(m_mutex); + auto const [entry, added]{m_shaders.emplace(key, code)}; + if (added) + { + auto const origin{std::string(code->origin)}; + ++(origin == "binary" ? m_frombinary : origin == "cache" ? m_fromcache : m_compiled); + } + return entry->second; } std::vector shader_library::compile(gfx::shader_source const &Source) const @@ -314,6 +332,7 @@ std::vector shader_library::compile(gfx::shader_source const &Sou std::string shader_library::info() const { + std::lock_guard lock(m_mutex); return "shaders: " + std::to_string(m_shaders.size()) + " (" + std::to_string(m_frombinary) + " binaries, " + std::to_string(m_fromcache) + " from the cache, " + std::to_string(m_compiled) + " compiled)"; } @@ -478,13 +497,42 @@ std::vector const dynamic_states{VK_DYNAMIC_STATE_VIEWPORT, } // namespace +bool pipeline_library::find(pipeline_desc const &Desc, VkPipeline &Pipeline) const +{ + auto const key{key_of(Desc)}; + std::lock_guard lock(m_mutex); + auto const lookup{m_pipelines.find(key)}; + if (lookup == m_pipelines.end()) + { + return false; + } + Pipeline = lookup->second; + return true; +} + +VkPipeline pipeline_library::keep(std::uint64_t const Key, VkPipeline const Pipeline, double const Milliseconds) +{ + std::lock_guard lock(m_mutex); + m_creationms += Milliseconds; + auto const [entry, added]{m_pipelines.emplace(Key, Pipeline)}; + if ((false == added) && (Pipeline != VK_NULL_HANDLE) && (entry->second != Pipeline)) + { + // made by another thread meanwhile + vkDestroyPipeline(m_device->handle(), Pipeline, nullptr); + } + return entry->second; +} + VkPipeline pipeline_library::get(pipeline_desc const &Desc) { auto const key{key_of(Desc)}; - auto const lookup{m_pipelines.find(key)}; - if (lookup != m_pipelines.end()) { - return lookup->second; + std::lock_guard lock(m_mutex); + auto const lookup{m_pipelines.find(key)}; + if (lookup != m_pipelines.end()) + { + return lookup->second; + } } auto const start{std::chrono::steady_clock::now()}; @@ -666,9 +714,7 @@ VkPipeline pipeline_library::get(pipeline_desc const &Desc) { name_object(m_device->handle(), VK_OBJECT_TYPE_PIPELINE, handle_value(pipeline), Desc.name); } - m_creationms += std::chrono::duration(std::chrono::steady_clock::now() - start).count(); - m_pipelines.emplace(key, pipeline); - return pipeline; + return keep(key, pipeline, std::chrono::duration(std::chrono::steady_clock::now() - start).count()); } VkPipeline pipeline_library::compute(std::shared_ptr const &Shader, VkPipelineLayout Layout, std::vector const &Specialization, std::string const &Name) @@ -680,10 +726,13 @@ VkPipeline pipeline_library::compute(std::shared_ptr const &S { hash_value(key, value); } - auto const lookup{m_pipelines.find(key)}; - if (lookup != m_pipelines.end()) { - return lookup->second; + std::lock_guard lock(m_mutex); + auto const lookup{m_pipelines.find(key)}; + if (lookup != m_pipelines.end()) + { + return lookup->second; + } } auto const start{std::chrono::steady_clock::now()}; @@ -722,13 +771,12 @@ VkPipeline pipeline_library::compute(std::shared_ptr const &S { name_object(m_device->handle(), VK_OBJECT_TYPE_PIPELINE, handle_value(pipeline), Name); } - m_creationms += std::chrono::duration(std::chrono::steady_clock::now() - start).count(); - m_pipelines.emplace(key, pipeline); - return pipeline; + return keep(key, pipeline, std::chrono::duration(std::chrono::steady_clock::now() - start).count()); } std::string pipeline_library::info() const { + std::lock_guard lock(m_mutex); char buffer[32] = {}; std::snprintf(buffer, sizeof(buffer), "%.1f", m_creationms); return "pipelines: " + std::to_string(m_pipelines.size()) + " (made in " + buffer + " ms)"; diff --git a/rendering/vulkan14/vulkan14shaders.h b/rendering/vulkan14/vulkan14shaders.h index 53ca32c1..5fdf4df5 100644 --- a/rendering/vulkan14/vulkan14shaders.h +++ b/rendering/vulkan14/vulkan14shaders.h @@ -15,7 +15,11 @@ http://mozilla.org/MPL/2.0/. // cache. the pipelines are made from a description of what isn't dynamic (the shaders, the layout of the vertices, the class // of the primitives, the blending, the formats of the attachments, the samples); the rest of the state is dynamic. the // VkPipelineCache is read from and written to the directory of the user, and thrown away when it comes from another device -// or driver. no VkShaderModule: the code goes to the stages of the pipelines (maintenance5) +// or driver. no VkShaderModule: the code goes to the stages of the pipelines (maintenance5). +// +// both libraries may be asked from any thread (the stage M5 of the plan: the pipelines of the scene made by the background +// threads): their maps are guarded, the shaders are compiled and the pipelines made outside of the locks (the VkPipelineCache +// is synchronized by vulkan) #pragma once @@ -23,6 +27,7 @@ http://mozilla.org/MPL/2.0/. #include "rendering/common/shadersource.h" #include +#include #include namespace vk14 @@ -38,6 +43,9 @@ struct shader_code std::vector spirv; // where it came from: "binary", "cache", "compiled" char const *origin{""}; + // what it was asked for by (shader_library::get()), for the list of the pipelines made in a session + std::string name; + std::string defines; }; class shader_library @@ -53,12 +61,15 @@ class shader_library void shutdown(); // the shader of shaders/vk14 (Defines: the variant). throws gfx::shader_exception std::shared_ptr get(std::string const &Name, std::string const &Defines = {}); + // the shader, when it was got before; nothing is loaded or compiled + std::shared_ptr find(std::string const &Name, std::string const &Defines = {}) const; // what was loaded from where, for the debug panel std::string info() const; private: std::vector compile(gfx::shader_source const &Source) const; + mutable std::mutex m_mutex; std::string m_preamble; std::string m_cachedir; std::string m_compilerversion; @@ -123,12 +134,17 @@ class pipeline_library void shutdown(); // the pipeline of the description, made at the first call (from the cache on the disk, when it has it). null when it can't be made VkPipeline get(pipeline_desc const &Desc); + // the pipeline of the description when it was made already (Pipeline: it, null when it couldn't be); false: not made yet + bool find(pipeline_desc const &Desc, VkPipeline &Pipeline) const; VkPipeline compute(std::shared_ptr const &Shader, VkPipelineLayout Layout, std::vector const &Specialization = {}, std::string const &Name = {}); std::string info() const; private: std::uint64_t key_of(pipeline_desc const &Desc) const; + // the pipeline made for the key by another thread meanwhile is kept, the one of the caller destroyed + VkPipeline keep(std::uint64_t Key, VkPipeline Pipeline, double Milliseconds); + mutable std::mutex m_mutex; device const *m_device{nullptr}; VkPipelineCache m_cache{VK_NULL_HANDLE}; std::string m_cachepath; diff --git a/rendering/vulkan14/vulkan14sky.cpp b/rendering/vulkan14/vulkan14sky.cpp index 46036125..10468b20 100644 --- a/rendering/vulkan14/vulkan14sky.cpp +++ b/rendering/vulkan14/vulkan14sky.cpp @@ -85,11 +85,11 @@ bool vulkan14_renderer::init_sky(std::string &Reason) auto const device{ctx.device.handle()}; auto const frames{static_cast(ctx.frames.size())}; - // the buffers the shading reads - if ((false == sky.sky.create(ctx.memory, sizeof(gl::sky_data), VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT, vk14::memory_kind::device, "sky ambient")) || - (false == sky.atmosphere.create(ctx.memory, sizeof(gl::atmosphere_ubs), VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT, vk14::memory_kind::device, "atmosphere")) || - (false == sky.result.create(ctx.memory, sizeof(gl::atmosphere_result), VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_SRC_BIT, vk14::memory_kind::device, "atmosphere result")) || - (false == sky.result_readback.create(ctx.memory, sizeof(gl::atmosphere_result), frames, "atmosphere result readback")) || + // the buffers the shading reads; what the passes of the sky make is shared with the compute queue (the async compute of M6) + if ((false == sky.sky.create(ctx.memory, sizeof(gl::sky_data), VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT, vk14::memory_kind::device, "sky ambient", true)) || + (false == sky.atmosphere.create(ctx.memory, sizeof(gl::atmosphere_ubs), VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT, vk14::memory_kind::device, "atmosphere", true)) || + (false == sky.result.create(ctx.memory, sizeof(gl::atmosphere_result), VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_SRC_BIT, vk14::memory_kind::device, "atmosphere result", true)) || + (false == sky.result_readback.create(ctx.memory, sizeof(gl::atmosphere_result), frames, "atmosphere result readback", true)) || (false == exposure.data.create(ctx.memory, sizeof(gl::exposure_data), VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT, vk14::memory_kind::device, "exposure")) || (false == exposure.readback.create(ctx.memory, 4 * sizeof(float), frames, "exposure readback"))) { @@ -111,6 +111,7 @@ bool vulkan14_renderer::init_sky(std::string &Reason) vk14::image_desc desc; desc.format = VK_FORMAT_R16G16B16A16_SFLOAT; desc.usage = VK_IMAGE_USAGE_STORAGE_BIT | VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT; + desc.shared = true; desc.extent = {gl::ATMOSPHERE_TRANSMITTANCE_WIDTH, gl::ATMOSPHERE_TRANSMITTANCE_HEIGHT, 1}; if (false == make_image(sky.transmittance, desc, "atmosphere transmittance")) { @@ -245,7 +246,7 @@ void vulkan14_renderer::clear_sky_images() auto &ctx{*m_context}; auto &scene{ctx.scene}; auto &sky{scene.sky}; - auto const commands{ctx.commands}; + auto const commands{m_commands}; std::vector uses; for (auto *image : {sky.transmittance.get(), sky.multiscattering.get(), sky.skyview.get(), sky.ggx.get()}) { @@ -294,7 +295,7 @@ void vulkan14_renderer::update_sky() auto &ctx{*m_context}; auto &scene{ctx.scene}; auto &sky{scene.sky}; - auto const commands{ctx.commands}; + auto const commands{m_commands}; auto const &sun{simulation::Environment.sun()}; auto const sundirection{sun.getDirection()}; @@ -353,7 +354,7 @@ void vulkan14_renderer::prefilter_environment() { return; } - auto const commands{ctx.commands}; + auto const commands{m_commands}; ctx.passes.begin_pass("envmap ggx", {vk14::use_of(*scene.env_map, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, compute_stage, sampled_access), vk14::use_of(*sky.ggx, VK_IMAGE_LAYOUT_GENERAL, compute_stage, storage_write)}); for (std::size_t level = 0; level < gl::ENVMAP_GGX_LEVELS; ++level) @@ -430,7 +431,7 @@ void vulkan14_renderer::measure_exposure(vk14::image &Image) { auto &ctx{*m_context}; auto &exposure{ctx.scene.exposure}; - auto const commands{ctx.commands}; + auto const commands{m_commands}; float data[4]; if (exposure.readback.pop(ctx.completed(), data, sizeof(data))) { diff --git a/rendering/vulkan14/vulkan14tasks.cpp b/rendering/vulkan14/vulkan14tasks.cpp new file mode 100644 index 00000000..0c35b54f --- /dev/null +++ b/rendering/vulkan14/vulkan14tasks.cpp @@ -0,0 +1,114 @@ +/* +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/vulkan14/vulkan14tasks.h" +#include "utilities/Logs.h" + +#include + +namespace vk14 +{ + +background_tasks::~background_tasks() +{ + stop(); +} + +void background_tasks::start(std::size_t const Threads) +{ + if (running()) + { + return; + } + { + std::lock_guard lock(m_mutex); + m_stop = false; + } + for (std::size_t i = 0; i < Threads; ++i) + { + m_threads.emplace_back([this]() { work(); }); + } +} + +void background_tasks::stop() +{ + { + std::lock_guard lock(m_mutex); + m_stop = true; + m_queue.clear(); + } + m_wake.notify_all(); + for (auto &thread : m_threads) + { + thread.join(); + } + m_threads.clear(); +} + +void background_tasks::add(std::function Task, bool const Urgent) +{ + { + std::lock_guard lock(m_mutex); + if (Urgent) + { + m_queue.emplace_front(std::move(Task)); + } + else + { + m_queue.emplace_back(std::move(Task)); + } + } + m_wake.notify_one(); +} + +void background_tasks::wait_idle() +{ + std::unique_lock lock(m_mutex); + m_idle.wait(lock, [this]() { return m_queue.empty() && (m_busy == 0); }); +} + +std::size_t background_tasks::pending() const +{ + std::lock_guard lock(m_mutex); + return m_queue.size() + m_busy; +} + +void background_tasks::work() +{ + std::unique_lock lock(m_mutex); + while (true) + { + m_wake.wait(lock, [this]() { return m_stop || (false == m_queue.empty()); }); + if (m_stop) + { + return; + } + auto task{std::move(m_queue.front())}; + m_queue.pop_front(); + ++m_busy; + lock.unlock(); + try + { + task(); + } + catch (std::exception const &Exception) + { + ErrorLog(std::string("vulkan: a background task failed: ") + Exception.what()); + } + lock.lock(); + --m_busy; + if (m_queue.empty() && (m_busy == 0)) + { + m_idle.notify_all(); + } + } +} + +} // namespace vk14 diff --git a/rendering/vulkan14/vulkan14tasks.h b/rendering/vulkan14/vulkan14tasks.h new file mode 100644 index 00000000..8de2741e --- /dev/null +++ b/rendering/vulkan14/vulkan14tasks.h @@ -0,0 +1,62 @@ +/* +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 background threads of the vulkan renderer (the stage M5 of the plan): the work which would hold up a frame, done while the +// frames go on. the pipelines of the scene and the textures made when a draw first needs them, the pipelines of the last session +// made ahead. what a task makes is taken over by the thread of the renderer when it's done (the frame after, as a rule); a task +// itself touches only what it was given and what is guarded for it + +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +namespace vk14 +{ + +class background_tasks +{ + public: + background_tasks() = default; + background_tasks(background_tasks const &) = delete; + background_tasks &operator=(background_tasks const &) = delete; + ~background_tasks(); + + void start(std::size_t Threads); + // the tasks under way are finished, the ones waiting dropped + void stop(); + bool running() const + { + return false == m_threads.empty(); + } + // Urgent: before the tasks waiting (what a draw waits for), otherwise after them (what's made ahead) + void add(std::function Task, bool Urgent = true); + // waits until there's nothing waiting and nothing under way + void wait_idle(); + // the tasks waiting and under way + std::size_t pending() const; + + private: + void work(); + + mutable std::mutex m_mutex; + std::condition_variable m_wake; + std::condition_variable m_idle; + std::deque> m_queue; + std::vector m_threads; + std::size_t m_busy{0}; + bool m_stop{false}; +}; + +} // namespace vk14 diff --git a/rendering/vulkan14/vulkan14texture.cpp b/rendering/vulkan14/vulkan14texture.cpp index 170d5597..dd5ba3e3 100644 --- a/rendering/vulkan14/vulkan14texture.cpp +++ b/rendering/vulkan14/vulkan14texture.cpp @@ -11,6 +11,7 @@ http://mozilla.org/MPL/2.0/. #include "rendering/vulkan14/vulkan14texture.h" #include "rendering/vulkan14/vulkan14commands.h" #include "rendering/vulkan14/vulkan14device.h" +#include "rendering/vulkan14/vulkan14tasks.h" #include "utilities/Globals.h" #include "utilities/Logs.h" @@ -28,8 +29,26 @@ void uploader::init(allocator const &Allocator, deletion_queue &Deletions, VkPhy m_physical = Physical; } +void uploader::reset() +{ + std::lock_guard lock(m_mutex); + m_allocator = nullptr; + m_deletions = nullptr; + m_physical = VK_NULL_HANDLE; + m_commands = nullptr; + m_value = 0; + m_recorded = false; + m_buffercopies = false; + m_pending.clear(); + m_uploaded = 0; + m_blittable.clear(); + m_hostcopy = false; + m_hostcopyable.clear(); +} + bool uploader::can_blit(VkFormat Format) const { + std::lock_guard lock(m_mutex); auto const lookup{m_blittable.find(Format)}; if (lookup != m_blittable.end()) { @@ -43,8 +62,48 @@ bool uploader::can_blit(VkFormat Format) const return result; } +bool uploader::can_host_copy(VkFormat Format) const +{ + if (false == m_hostcopy) + { + return false; + } + std::lock_guard lock(m_mutex); + auto const lookup{m_hostcopyable.find(Format)}; + if (lookup != m_hostcopyable.end()) + { + return lookup->second; + } + // the format filled by the host, and an image which can be is as fast for the gpu as one which can't + VkFormatProperties3 properties3{}; + properties3.sType = VK_STRUCTURE_TYPE_FORMAT_PROPERTIES_3; + VkFormatProperties2 properties{}; + properties.sType = VK_STRUCTURE_TYPE_FORMAT_PROPERTIES_2; + properties.pNext = &properties3; + vkGetPhysicalDeviceFormatProperties2(m_physical, Format, &properties); + auto result{(properties3.optimalTilingFeatures & VK_FORMAT_FEATURE_2_HOST_IMAGE_TRANSFER_BIT) != 0}; + if (result) + { + VkHostImageCopyDevicePerformanceQuery performance{}; + performance.sType = VK_STRUCTURE_TYPE_HOST_IMAGE_COPY_DEVICE_PERFORMANCE_QUERY; + VkImageFormatProperties2 imageproperties{}; + imageproperties.sType = VK_STRUCTURE_TYPE_IMAGE_FORMAT_PROPERTIES_2; + imageproperties.pNext = &performance; + VkPhysicalDeviceImageFormatInfo2 info{}; + info.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_IMAGE_FORMAT_INFO_2; + info.format = Format; + info.type = VK_IMAGE_TYPE_2D; + info.tiling = VK_IMAGE_TILING_OPTIMAL; + info.usage = VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_HOST_TRANSFER_BIT; + result = (vkGetPhysicalDeviceImageFormatProperties2(m_physical, &info, &imageproperties) == VK_SUCCESS) && (performance.optimalDeviceAccess == VK_TRUE); + } + m_hostcopyable.emplace(Format, result); + return result; +} + void uploader::begin_frame(command_stream *Commands, std::uint64_t Value) { + std::lock_guard lock(m_mutex); m_commands = Commands; m_value = Value; m_recorded = false; @@ -58,6 +117,7 @@ void uploader::begin_frame(command_stream *Commands, std::uint64_t Value) bool uploader::end_frame() { + std::lock_guard lock(m_mutex); if (m_buffercopies && (m_commands != nullptr)) { // the draws of the frame read what was copied @@ -100,6 +160,7 @@ bool uploader::upload_buffer(std::shared_ptr const &Target, void const * std::memcpy(upload.staging->mapped(), Data, static_cast(Size)); upload.staging->flush(0, Size); m_uploaded += Size; + std::lock_guard lock(m_mutex); if (m_commands != nullptr) { record(upload); @@ -113,19 +174,45 @@ bool uploader::upload_buffer(std::shared_ptr const &Target, void const * bool uploader::upload(std::shared_ptr const &Image, void const *Data, VkDeviceSize Size, std::vector const &Levels, bool const Mipmaps) { - job upload; - upload.target = Image; - upload.levels = Levels; - upload.mipmaps = Mipmaps; - upload.staging = std::make_shared(); - if ((false == upload.staging->create(*m_allocator, Size, VK_BUFFER_USAGE_TRANSFER_SRC_BIT, memory_kind::upload)) || (upload.staging->mapped() == nullptr)) + prepared upload; + if (false == prepare(Image, Data, Size, Levels, Mipmaps, upload)) { - log_error("no staging buffer of " + memory_size_string(Size) + " for an upload"); return false; } - std::memcpy(upload.staging->mapped(), Data, static_cast(Size)); - upload.staging->flush(0, Size); + submit(std::move(upload)); + return true; +} + +bool uploader::prepare(std::shared_ptr const &Image, void const *Data, VkDeviceSize Size, std::vector const &Levels, bool const Mipmaps, prepared &Upload) +{ + Upload.target = Image; + Upload.levels = Levels; + Upload.mipmaps = Mipmaps; + Upload.staging = std::make_shared(); + if ((false == Upload.staging->create(*m_allocator, Size, VK_BUFFER_USAGE_TRANSFER_SRC_BIT, memory_kind::upload)) || (Upload.staging->mapped() == nullptr)) + { + log_error("no staging buffer of " + memory_size_string(Size) + " for an upload"); + Upload = prepared{}; + return false; + } + std::memcpy(Upload.staging->mapped(), Data, static_cast(Size)); + Upload.staging->flush(0, Size); m_uploaded += Size; + return true; +} + +void uploader::submit(prepared &&Upload) +{ + if ((Upload.target == nullptr) || (Upload.staging == nullptr)) + { + return; + } + job upload; + upload.target = std::move(Upload.target); + upload.staging = std::move(Upload.staging); + upload.levels = std::move(Upload.levels); + upload.mipmaps = Upload.mipmaps; + std::lock_guard lock(m_mutex); if (m_commands != nullptr) { record(upload); @@ -134,7 +221,6 @@ bool uploader::upload(std::shared_ptr const &Image, void const *Data, VkD { m_pending.emplace_back(std::move(upload)); } - return true; } void uploader::record(job &Job) @@ -270,6 +356,7 @@ void uploader::record(job &Job) std::uint64_t image_table::add(std::shared_ptr const &Image, VkSampler Sampler) { + std::lock_guard lock(m_mutex); std::uint64_t id; if (false == m_free.empty()) { @@ -287,6 +374,7 @@ std::uint64_t image_table::add(std::shared_ptr const &Image, VkSampler Sa void image_table::replace(std::uint64_t Id, std::shared_ptr const &Image, VkSampler Sampler) { + std::lock_guard lock(m_mutex); if ((Id > 0) && (Id < m_entries.size())) { m_entries[Id] = entry{Image, Sampler}; @@ -295,6 +383,7 @@ void image_table::replace(std::uint64_t Id, std::shared_ptr const &Image, void image_table::remove(std::uint64_t Id, deletion_queue &Deletions, std::uint64_t Value) { + std::lock_guard lock(m_mutex); if ((Id == 0) || (Id >= m_entries.size()) || (m_entries[Id].target == nullptr)) { return; @@ -306,6 +395,7 @@ void image_table::remove(std::uint64_t Id, deletion_queue &Deletions, std::uint6 image_table::entry const *image_table::find(std::uint64_t Id) const { + std::lock_guard lock(m_mutex); if ((Id == 0) || (Id >= m_entries.size()) || (m_entries[Id].target == nullptr)) { return nullptr; @@ -315,6 +405,7 @@ image_table::entry const *image_table::find(std::uint64_t Id) const void image_table::clear() { + std::lock_guard lock(m_mutex); m_entries.assign(1, entry{}); m_free.clear(); } @@ -386,15 +477,184 @@ texture_format texture_format_of(GLint Format, GLint Hint) vulkan14_texture::~vulkan14_texture() { + // a background thread making the texture is done with it first + wait_job(); if (m_resources.pending != nullptr) { std::lock_guard lock(m_resources.pending->mutex); - auto &list{m_resources.pending->textures}; - list.erase(std::remove(list.begin(), list.end(), this), list.end()); + for (auto *list : {&m_resources.pending->textures, &m_resources.pending->made}) + { + list->erase(std::remove(list->begin(), list->end(), this), list->end()); + } + } +} + +void vulkan14_texture::wait_job() const +{ + while ((m_job == job_running) || (m_job == job_making)) + { + std::this_thread::yield(); + } +} + +bool vulkan14_texture::claim() +{ + auto const renderer{std::this_thread::get_id() == m_resources.thread}; + while (true) + { + auto state{m_job.load()}; + if (state == job_done) + { + if (false == renderer) + { + // what a background thread made is taken over by the thread of the renderer + return false; + } + publish(); + continue; + } + if (state != job_none) + { + // made by another thread at the moment (a background thread, or the walk of a pass on another thread) + std::this_thread::yield(); + continue; + } + if (m_job.compare_exchange_weak(state, job_making)) + { + return true; + } } } bool vulkan14_texture::create(bool const Static) +{ + if (m_made) + { + return true; + } + if ((type == "make:") && (std::this_thread::get_id() != m_resources.thread)) + { + // the request of the generated content goes from the thread of the renderer + return false; + } + if (false == claim()) + { + return false; + } + auto const result{make(false)}; + auto ready{true}; + switch (result) + { + case make_result::none: + ready = false; + break; + case make_result::done: + break; + case make_result::failed: + is_ready = true; + break; + case make_result::made: + if (type == "make:") + { + // for generated textures send a request to have the actual content of the texture generated + make_request(); + } + is_static = Static; + is_ready = true; + break; + } + m_job = job_none; + return ready; +} + +bool vulkan14_texture::request_background() +{ + if (m_made) + { + return true; + } + auto state{m_job.load()}; + if ((state == job_done) && (std::this_thread::get_id() == m_resources.thread)) + { + // made meanwhile by a background thread + publish(); + if (m_made) + { + return true; + } + state = m_job.load(); + } + if (state != job_none) + { + // being made, or made and waiting for the thread of the renderer + if (m_resources.pending != nullptr) + { + ++m_resources.pending->late_count; + } + return false; + } + if ((m_resources.background == nullptr) || (false == m_resources.background->running()) || (m_resources.pending == nullptr)) + { + return create(); + } + // asked for by one of the threads walking the passes at a time + if (false == m_job.compare_exchange_strong(state, job_running)) + { + ++m_resources.pending->late_count; + return false; + } + ++m_resources.pending->late_count; + // the texture outlives the job: its destruction waits for it (the background threads are stopped before the bank goes, too) + m_resources.background->add([this]() { + auto const result{make(true)}; + m_madefailed = (result == make_result::failed); + m_madeempty = (result == make_result::none); + std::lock_guard lock(m_resources.pending->mutex); + m_job = job_done; + m_resources.pending->made.push_back(this); + }); + return false; +} + +void vulkan14_texture::publish() +{ + int expected{job_done}; + if (false == m_job.compare_exchange_strong(expected, job_making)) + { + return; + } + if (m_madeimage != nullptr) + { + if (m_madestaged) + { + m_resources.uploads->submit(std::move(m_madeupload)); + } + m_image = std::move(m_madeimage); + m_id = m_resources.images->add(m_image, m_madesampler); + m_made = true; + if (type == "make:") + { + make_request(); + } + is_static = false; + is_ready = true; + ++m_resources.pending->made_count; + } + else if (m_madefailed) + { + // counted as made, without an image (on_failed()) + is_ready = true; + } + // otherwise there was no data yet: asked for again by the next draw + m_madeimage.reset(); + m_madeupload = uploader::prepared{}; + m_madestaged = false; + m_madefailed = false; + m_madeempty = false; + m_job = job_none; +} + +vulkan14_texture::make_result vulkan14_texture::make(bool const Background) { if ((data_state == resource_state::none) && (true == reload_on_use)) { @@ -408,12 +668,13 @@ bool vulkan14_texture::create(bool const Static) } if (m_made) { - return true; + // made before, or failed while the data was read (on_failed() of a texture without an id leaves the table alone) + return make_result::done; } if (data_state != resource_state::good) { // don't bother until we have useful texture data - return false; + return make_result::none; } auto const format{texture_format_of(data_format, components_hint != 0 ? components_hint : GL_SRGB_ALPHA)}; @@ -422,12 +683,14 @@ bool vulkan14_texture::create(bool const Static) { vkGetPhysicalDeviceFormatProperties(m_resources.gpu->physical(), format.format, &properties); } + auto const failed = [this]() { + on_failed(); + return make_result::failed; + }; if ((format.format == VK_FORMAT_UNDEFINED) || ((properties.optimalTilingFeatures & VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT) == 0)) { ErrorLog("Bad texture: the format of \"" + name + "\" can't be sampled by the gpu", logtype::texture); - on_failed(); - is_ready = true; - return true; + return failed(); } std::vector levels; @@ -478,41 +741,92 @@ bool vulkan14_texture::create(bool const Static) if (levels.empty() || (size > available) || (data_width == 0) || (data_height == 0)) { ErrorLog("Bad texture: \"" + name + "\" holds less data than its size needs", logtype::texture); - on_failed(); - is_ready = true; - return true; + return failed(); } // a single level is made into the whole chain on the gpu, as glGenerateMipmap() does for the opengl renderer auto const generate{(levels.size() == 1) && (false == format.compressed) && m_resources.uploads->can_blit(format.format)}; + // the levels the file gives are filled by the host where it can, by a background thread (what the gpu makes goes through the + // commands of a frame) + auto const hostcopy{Background && (false == generate) && m_resources.uploads->can_host_copy(format.format)}; image_desc desc; desc.format = format.format; desc.extent = {data_width, data_height, 1}; desc.mips = (generate ? static_cast(std::floor(std::log2(std::max(data_width, data_height)))) + 1 : static_cast(levels.size())); - desc.usage = VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT | (generate ? VK_IMAGE_USAGE_TRANSFER_SRC_BIT : 0); + desc.usage = VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT | (generate ? VK_IMAGE_USAGE_TRANSFER_SRC_BIT : 0) | (hostcopy ? VK_IMAGE_USAGE_HOST_TRANSFER_BIT : 0); desc.components = format.components; auto image{std::make_shared()}; if (false == image->create(*m_resources.memory, desc, name)) { ErrorLog("Bad texture: no gpu image for \"" + name + "\"", logtype::texture); - on_failed(); - is_ready = true; - return true; + return failed(); } - if (false == m_resources.uploads->upload(image, source, size, levels, generate)) + auto staged{false}; + if (hostcopy) + { + // straight to the layout the image is sampled in: no command buffer, nothing for the frames to wait for + auto const device{m_resources.gpu->handle()}; + VkHostImageLayoutTransitionInfo transition{}; + transition.sType = VK_STRUCTURE_TYPE_HOST_IMAGE_LAYOUT_TRANSITION_INFO; + transition.image = image->handle(); + transition.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED; + transition.newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; + transition.subresourceRange = {image->aspect(), 0, desc.mips, 0, desc.layers}; + std::vector regions(levels.size()); + for (std::size_t i = 0; i < levels.size(); ++i) + { + auto ®ion{regions[i]}; + region.sType = VK_STRUCTURE_TYPE_MEMORY_TO_IMAGE_COPY; + region.pHostPointer = source + levels[i].offset; + region.imageSubresource = {image->aspect(), static_cast(i), 0, desc.layers}; + region.imageExtent = levels[i].extent; + } + VkCopyMemoryToImageInfo copy{}; + copy.sType = VK_STRUCTURE_TYPE_COPY_MEMORY_TO_IMAGE_INFO; + copy.dstImage = image->handle(); + copy.dstImageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; + copy.regionCount = static_cast(regions.size()); + copy.pRegions = regions.data(); + if ((false == VK14_VERIFY(vkTransitionImageLayout(device, 1, &transition))) || (false == VK14_VERIFY(vkCopyMemoryToImage(device, ©)))) + { + ErrorLog("Bad texture: \"" + name + "\" couldn't be copied to its gpu image", logtype::texture); + return failed(); + } + image->state = image_state{VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT, VK_ACCESS_2_SHADER_SAMPLED_READ_BIT}; + } + else if (Background) + { + // the staging buffer filled here, the copies recorded by the thread of the renderer + if (false == m_resources.uploads->prepare(image, source, size, levels, generate, m_madeupload)) + { + ErrorLog("Bad texture: \"" + name + "\" couldn't be uploaded", logtype::texture); + return failed(); + } + staged = true; + } + else if (false == m_resources.uploads->upload(image, source, size, levels, generate)) { // never filled, so never sampled ErrorLog("Bad texture: \"" + name + "\" couldn't be uploaded", logtype::texture); - on_failed(); - is_ready = true; - return true; + return failed(); } // the traits of the file: clamped along s or t auto const clamped{(traits.find('s') != std::string::npos) || (traits.find('t') != std::string::npos)}; - m_image = image; - m_id = m_resources.images->add(image, clamped ? m_resources.sampling->clamp_aniso : m_resources.sampling->repeat_aniso); - m_made = true; + auto const sampler{clamped ? m_resources.sampling->clamp_aniso : m_resources.sampling->repeat_aniso}; + if (Background) + { + // the thread of the renderer puts it in the table (publish()) + m_madeimage = image; + m_madesampler = sampler; + m_madestaged = staged; + } + else + { + m_image = image; + m_id = m_resources.images->add(image, sampler); + m_made = true; + } if ((true == Global.ResourceMove) || (false == Global.ResourceSweep) || (true == streamed())) { @@ -520,14 +834,7 @@ bool vulkan14_texture::create(bool const Static) data = std::vector(); data_state = resource_state::none; } - if (type == "make:") - { - // for generated textures send a request to have the actual content of the texture generated - make_request(); - } - is_static = Static; - is_ready = true; - return true; + return make_result::made; } void vulkan14_texture::release() @@ -569,6 +876,20 @@ void vulkan14_texture::release() void vulkan14_texture::destroy() { + wait_job(); + if (m_job == job_done) + { + // made and not taken over: what was made goes now + if (m_resources.pending != nullptr) + { + std::lock_guard lock(m_resources.pending->mutex); + auto &list{m_resources.pending->made}; + list.erase(std::remove(list.begin(), list.end(), this), list.end()); + } + m_madeimage.reset(); + m_madeupload = uploader::prepared{}; + m_job = job_none; + } if (m_id != 0) { m_resources.images->remove(m_id, *m_resources.deletions, m_resources.uploads->current_value()); diff --git a/rendering/vulkan14/vulkan14texture.h b/rendering/vulkan14/vulkan14texture.h index 2dc97283..6e82598e 100644 --- a/rendering/vulkan14/vulkan14texture.h +++ b/rendering/vulkan14/vulkan14texture.h @@ -13,7 +13,12 @@ http://mozilla.org/MPL/2.0/. // the uploads are copies through staging buffers recorded into the command buffer of uploads of the frame being made, which goes // to the queue before the commands of the frame in the same submit; an upload asked for between the frames waits for the next // one. every image has an id in one table (decision K10): ITexture::get_id() of the textures, the ids of the images of the user -// interface and the font of imgui are all ImTextureID, so the user interface draws any of them +// interface and the font of imgui are all ImTextureID, so the user interface draws any of them. +// +// the textures the draws of the walks need first are made by the background threads (gfx.vulkan.asyncresources, the stage M5): +// the data read, the image made and filled by the host (hostImageCopy, where the device and the format allow it) or its staging +// buffer filled, then the thread of the renderer puts the image in the table, with the copies of the staging buffer recorded, before +// the next frame is walked. the draw which asked for it goes without it (the white texture) till then #pragma once @@ -21,6 +26,8 @@ http://mozilla.org/MPL/2.0/. #include "rendering/vulkan14/vulkan14descriptors.h" #include "model/Texture.h" +#include +#include #include #include #include @@ -29,8 +36,10 @@ namespace vk14 { class device; +class background_tasks; -// the uploads of data to the images +// the uploads of data to the images. asked for by any thread (the walks of the passes make the buffers of the geometry when they're +// first drawn); the staging buffers are made outside of the lock class uploader { public: @@ -39,8 +48,18 @@ class uploader VkDeviceSize offset{0}; // in the data VkExtent3D extent{1, 1, 1}; }; + // an upload to an image with its data in a staging buffer, not recorded yet + struct prepared + { + std::shared_ptr target; + std::shared_ptr staging; + std::vector levels; + bool mipmaps{false}; + }; void init(allocator const &Allocator, deletion_queue &Deletions, VkPhysicalDevice Physical); + // back to the state before init(), the uploads waiting dropped + void reset(); // the commands of the uploads go to Commands until end_frame(); Value: the value of the timeline the gpu reaches when it's done // with the frame. the uploads waiting from between the frames are recorded first void begin_frame(command_stream *Commands, std::uint64_t Value); @@ -54,6 +73,9 @@ class uploader // copies Data into Image (the levels given, the others made by blits of the first one when Mipmaps), the image left ready to be // sampled. Data is copied away. false: no staging memory, nothing will be copied bool upload(std::shared_ptr const &Image, void const *Data, VkDeviceSize Size, std::vector const &Levels, bool const Mipmaps); + // the same in two steps: the staging buffer made and filled (Upload), and the copies recorded. false: no staging memory + bool prepare(std::shared_ptr const &Image, void const *Data, VkDeviceSize Size, std::vector const &Levels, bool const Mipmaps, prepared &Upload); + void submit(prepared &&Upload); // copies Data into Target, done before the draws of the frame read it (and after the frames sent before did). Data holds the // pieces of Parts (offset in Target, size) one after another; all of Data goes to the start of Target when there are none. // false: no staging memory @@ -65,6 +87,13 @@ class uploader } // whether the format can be filtered by blits (the mipmaps made on the gpu) bool can_blit(VkFormat Format) const; + // whether images of the format are filled by the host (hostImageCopy) straight to the layout they're sampled in, without a loss of + // speed for the gpu. Copyable: the device does that for the layout (decided when the device is made) + void enable_host_copy(bool Copyable) + { + m_hostcopy = Copyable; + } + bool can_host_copy(VkFormat Format) const; private: struct job @@ -79,20 +108,24 @@ class uploader }; void record(job &Job); + mutable std::mutex m_mutex; allocator const *m_allocator{nullptr}; deletion_queue *m_deletions{nullptr}; VkPhysicalDevice m_physical{VK_NULL_HANDLE}; command_stream *m_commands{nullptr}; - std::uint64_t m_value{0}; + std::atomic m_value{0}; bool m_recorded{false}; // buffers were copied to in the frame: the barriers before the first copy and after the last one bool m_buffercopies{false}; std::vector m_pending; - std::uint64_t m_uploaded{0}; + std::atomic m_uploaded{0}; mutable std::unordered_map m_blittable; + bool m_hostcopy{false}; + mutable std::unordered_map m_hostcopyable; }; -// the images the shaders can be given by an id +// the images the shaders can be given by an id. added to by any thread drawing (a texture made when a draw needs it), removed from +// and replaced in by the thread of the renderer, found by any thread; an entry found stays where it is while the frame is made class image_table { public: @@ -110,21 +143,27 @@ class image_table void clear(); std::size_t size() const { + std::lock_guard lock(m_mutex); return m_entries.size() - m_free.size(); } private: - std::vector m_entries{entry{}}; + mutable std::mutex m_mutex; + std::deque m_entries{entry{}}; std::vector m_free; }; struct vulkan14_texture; -// the textures given content by other threads, waiting for the thread of the renderer +// the textures given content by other threads, waiting for the thread of the renderer; and the ones made by the background threads struct pending_textures { std::mutex mutex; std::vector textures; + std::vector made; + // the textures made by the background threads since the start, and how many draws went without one + std::atomic made_count{0}; + std::atomic late_count{0}; }; // what the images of the renderer need from it @@ -139,6 +178,8 @@ struct resources // the thread of the renderer: the content given by other threads waits for it (update_from_memory()) std::thread::id thread; pending_textures *pending{nullptr}; + // the threads making the textures the draws need first (gfx.vulkan.asyncresources), null: made by the draw + background_tasks *background{nullptr}; // the ray tracing is on (gfx.raytracing): the geometry is built into acceleration structures and read by its device address bool raytracing{false}; }; @@ -163,6 +204,11 @@ struct vulkan14_texture : public texture_source ~vulkan14_texture() override; bool create(bool const Static = false) override; + // the texture for a draw of the walks: true when it's made, otherwise it's made by the background threads (or made now, without + // them) and the draw goes without it + bool request_background(); + // the thread of the renderer takes over what a background thread made of the texture + void publish(); void release() override; // as the opengl textures give it: one not made yet has no id of 0, which the materials take for a texture which is missing size_t get_id() const override @@ -174,6 +220,11 @@ struct vulkan14_texture : public texture_source { return m_made; } + // a background thread makes it: its data isn't for the other threads to read + bool busy() const override + { + return m_job != job_none; + } void destroy() override; // the content given by update_from_memory() from another thread, uploaded by the thread of the renderer @@ -191,11 +242,43 @@ struct vulkan14_texture : public texture_source private: void upload_rgba(std::uint8_t const *Data, std::size_t Width, std::size_t Height); + // the data of the texture made into an image (Background: by a background thread, which leaves the images table and the uploads + // of the frames to the thread of the renderer): no data yet, made now, failed now, made (or failed) before + enum class make_result + { + none, + made, + failed, + done + }; + make_result make(bool const Background); + // waits for the thread making the texture, if there's one + void wait_job() const; + // the making of the texture taken by the calling thread (create()), after another thread making it is done; false when it was made by + // a background thread and waits for the thread of the renderer + bool claim(); resources const &m_resources; std::shared_ptr m_image; std::uint64_t m_id{0}; - bool m_made{false}; + std::atomic m_made{false}; + // the work on the texture: none, a background thread making it, made by it and waiting for publish(), a thread making it now (create(), + // publish(): the walks of the passes on several threads may need it at once, M2) + enum job_state : int + { + job_none, + job_running, + job_done, + job_making + }; + std::atomic m_job{job_none}; + // what the background thread made: the image and its sampler, its upload when the host didn't fill it, or that the texture failed + std::shared_ptr m_madeimage; + VkSampler m_madesampler{VK_NULL_HANDLE}; + uploader::prepared m_madeupload; + bool m_madestaged{false}; + bool m_madefailed{false}; + bool m_madeempty{false}; // content from another thread, waiting for the thread of the renderer std::mutex m_pendingmutex; std::vector m_pending; diff --git a/rendering/vulkan14/vulkan14walkers.cpp b/rendering/vulkan14/vulkan14walkers.cpp new file mode 100644 index 00000000..596c4819 --- /dev/null +++ b/rendering/vulkan14/vulkan14walkers.cpp @@ -0,0 +1,402 @@ +/* +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 passes walked by the walkers (the stage M2 of claude/vulkan14-renderer-plan.md, gfx.vulkan.parallelpasses). the main thread sets +// a pass up as it would walk it itself (the camera, the culling, the map it draws to with its barrier, the timestamps around it), hands +// the state of the frame over to a walker, splices the stream the walker records into the commands of the frame there and goes on with +// the frame; the walkers walk the passes at the same time as the main thread walks the colour pass of the main view, and are joined once +// it's recorded. what the walks share is guarded by gfx::walk_sync; what a walker's draws used is marked by the main renderer after the +// join. with the switch off, or for the other viewports, the passes are walked by the main thread as before + +#include "stdafx.h" +#include "rendering/vulkan14/vulkan14context.h" +#include "rendering/vulkan14/vulkan14commands.h" +#include "rendering/common/walksync.h" +#include "model/Model3d.h" +#include "utilities/Globals.h" +#include "utilities/Logs.h" + +#include +#include +#include +#include +#include +#include + +std::size_t vk14::walk_threads() +{ + // a pass for each of them: the three cascades, a lamp or two + auto threads{static_cast(Global.gfx_threads)}; + if (threads == 0) + { + auto const cores{static_cast(std::thread::hardware_concurrency())}; + threads = (cores > 3 ? cores - 2 : 1); + } + return std::clamp(threads, 1, 4); +} + +vulkan14_renderer::vulkan14_renderer(vulkan14_renderer &Main) : m_context{Main.m_context}, m_main{&Main}, m_geometry{Main.m_geometry}, m_banks{Main.m_banks}, m_materials{Main.m_materials} +{ + m_draw = std::make_unique(); + m_draw->textures.fill(null_handle); +} + +bool vulkan14_renderer::walks_wanted() const +{ + return (m_main == nullptr) && Global.gfx_vulkan_parallelpasses && m_context && m_context->walks.running() && (m_current_viewport != nullptr) && m_current_viewport->main && (m_commands != nullptr); +} + +void vulkan14_renderer::take_state(vulkan14_renderer const &Main) +{ + // the frame: its viewports, the passes set up for it, the light, the blocks of the scene and of the lights uploaded so far + m_viewports = Main.m_viewports; + m_current_viewport = (Main.m_current_viewport != nullptr) ? &m_viewports[static_cast(Main.m_current_viewport - Main.m_viewports.data())] : nullptr; + m_renderpass = Main.m_renderpass; + m_colorpass = Main.m_colorpass; + m_shadowpass = Main.m_shadowpass; + m_sunlight = Main.m_sunlight; + m_fogcolor = Main.m_fogcolor; + m_shadowcolor = Main.m_shadowcolor; + scene_ubs = Main.scene_ubs; + light_data = Main.light_data; + m_lightcount = Main.m_lightcount; + m_lightheaders = Main.m_lightheaders; + m_cabambient = Main.m_cabambient; + m_envmap_bound = Main.m_envmap_bound; + m_headlightconfig = Main.m_headlightconfig; + m_raytracingon = Main.m_raytracingon; + m_rtfoliage = Main.m_rtfoliage; + m_rtgather = false; + m_instancecullframe = Main.m_instancecullframe; + m_indirectcolourpending = false; + m_cubeshadows = false; + m_shadowcascade = -1; + m_lightshadowcull = nullptr; + // the textures and the materials the renderer draws with + m_invalid_material = Main.m_invalid_material; + m_suntexture = Main.m_suntexture; + m_moontexture = Main.m_moontexture; + m_smoketexture = Main.m_smoketexture; + m_headlightstexture = Main.m_headlightstexture; + m_precipitationtexture = Main.m_precipitationtexture; + // the walk + model_ubs = Main.model_ubs; + m_framestamp = Main.m_framestamp; + m_instancerecordsversion = Main.m_instancerecordsversion; + m_fogrange = Main.m_fogrange; + m_billboardgeometry = Main.m_billboardgeometry; + m_glaretexture = Main.m_glaretexture; + m_motionhistory = Main.m_motionhistory; + m_cabfuture = Main.m_cabfuture; + m_cablamplit = Main.m_cablamplit; + m_cabprobelit = Main.m_cabprobelit; + m_previewing = false; + m_cubescenery = false; + m_cubeskipinstance = nullptr; + m_shadowskipvehicle = nullptr; + m_renderme.clear(); + // the state of the thread of the main renderer, taken by the thread of the walker when it begins + m_forkmatrices = OpenGLMatrices; + m_forksubmodels = submodel_state{TSubModel::iInstance, TSubModel::iVariantSeed, TSubModel::ReplacableSkinId, TSubModel::iAlpha, TSubModel::fSquareDist, TSubModel::pRoot, TSubModel::pasText}; + // the draws: the offsets of the blocks of the pass set as the main renderer has them, the pass sets of the frame, the parts of the + // rings of its own + auto &ctx{*m_context}; + auto &draw{*m_draw}; + auto const &maindraw{*Main.m_draw}; + draw.scene_offset = maindraw.scene_offset; + draw.light_offset = maindraw.light_offset; + draw.cluster_offset = maindraw.cluster_offset; + draw.weather_offset = maindraw.weather_offset; + draw.pass_sets = ctx.scene.pass_sets; + draw.rendering = false; + draw.program = nullptr; + draw.textures.fill(null_handle); + draw.textures_dirty = true; + draw.cull_sets = nullptr; + draw.resume.valid = false; + draw.depthpass = false; + draw.probepass = false; + draw.async = true; + draw.lastprogram = nullptr; + draw.draws_cache.reset(&ctx.scene.draws.ring, vk14::draw_ring_part); + draw.instances_cache.reset(&ctx.scene.instances.ring, vk14::instance_ring_part); + draw.draws_window = {}; + draw.instances_window = {}; + draw.used_banks.clear(); + draw.used_textures.clear(); + m_walkfailed = false; +} + +void vulkan14_renderer::fork_walk(std::function Job, walk_kind const Kind) +{ + auto &ctx{*m_context}; + if (false == m_walkwindow) + { + // the pass sets of the frame as the walkers bind them (they don't make them anew), then the walk lock held by the main thread, + // which walks as well from now on + update_pass_sets(); + gfx::walk_sync::open(); + m_walkwindow = true; + } + auto const index{m_walkjobs++}; + while (m_walkers.size() <= index) + { + m_walkers.emplace_back(std::make_unique(*this)); + } + if (m_walkstreams.size() < ctx.frames.size()) + { + m_walkstreams.resize(ctx.frames.size()); + } + auto &streams{m_walkstreams[ctx.slot]}; + while (streams.size() <= index) + { + streams.emplace_back(std::make_unique()); + } + auto &stream{*streams[index]}; + auto &walker{*m_walkers[index]}; + // the stream of the frame which used it last was recorded into its command buffers before the slot of the frame came back + stream.clear(); + walker.take_state(*this); + walker.m_walkkind = Kind; + walker.m_commands = &stream; + walker.m_draw->commands = &stream; + // the pass goes to the gpu here, with the timestamps the main thread put around it + m_commands->splice(stream); + ctx.walks.add( + [&walker, &stream, job = std::move(Job)]() { + gfx::walk_sync::walk_scope scope; + // the state of the thread of the main renderer at the moment it handed the pass over + OpenGLMatrices = walker.m_forkmatrices; + auto const &submodels{walker.m_forksubmodels}; + TSubModel::iInstance = submodels.instance; + TSubModel::iVariantSeed = submodels.variantseed; + TSubModel::ReplacableSkinId = submodels.skins; + TSubModel::iAlpha = submodels.alpha; + TSubModel::fSquareDist = submodels.squaredistance; + TSubModel::pRoot = submodels.root; + TSubModel::pasText = submodels.text; + try + { + job(walker); + } + catch (std::exception const &Exception) + { + ErrorLog(std::string("vulkan: a walker failed to walk its pass, which is left out: ") + Exception.what()); + walker.m_walkfailed = true; + } + if (walker.m_walkfailed) + { + // whatever it left half done (a rendering under way) goes + stream.clear(); + } + walker.m_commands = nullptr; + walker.m_draw->commands = nullptr; + stream.finish(); + }, + false); +} + +void vulkan14_renderer::join_walks() +{ + if (false == m_walkwindow) + { + return; + } + auto &ctx{*m_context}; + auto const start{std::chrono::steady_clock::now()}; + // the main thread lets go of the walk lock before it waits, so the walkers can make what they need meanwhile + gfx::walk_sync::leave(); + ctx.walks.wait_idle(); + gfx::walk_sync::close(); + m_walkwindow = false; + auto const waited{std::chrono::duration(std::chrono::steady_clock::now() - start).count()}; + m_walkwait = (m_walkwait == 0.0 ? waited : m_walkwait * 0.9 + waited * 0.1); + // what the walkers' draws used marked as used, what they did counted + if (m_walkcountsframe != m_framestamp) + { + m_walkcountsframe = m_framestamp; + m_lastwalkcounts = m_walkcounts; + m_walkcounts = {}; + } + std::uint32_t lampdraws{0}; + std::uint32_t headlightdraws{0}; + for (std::size_t index = 0; index < m_walkjobs; ++index) + { + auto &walker{*m_walkers[index]}; + auto &draw{*walker.m_draw}; + for (auto const bank : draw.used_banks) + { + m_geometry.mark_used(gfx::geometry_handle{bank, 1}); + } + if (ctx.textures) + { + for (auto const texture : draw.used_textures) + { + ctx.textures->mark_as_used(texture); + } + } + draw.used_banks.clear(); + draw.used_textures.clear(); + if (walker.m_walkcountsframe == m_framestamp) + { + m_walkcounts.walked += walker.m_walkcounts.walked; + m_walkcounts.changing += walker.m_walkcounts.changing; + m_walkcounts.reused += walker.m_walkcounts.reused; + m_walkcounts.walktime += walker.m_walkcounts.walktime; + m_walkcounts.ordertime += walker.m_walkcounts.ordertime; + } + auto const drawcalls{static_cast(walker.m_renderpass.draw_stats.drawcalls)}; + switch (walker.m_walkkind) + { + case walk_kind::lamp: + lampdraws += drawcalls; + break; + case walk_kind::headlight: + headlightdraws += drawcalls; + break; + default: + break; + } + walker.Settle_Renderme(false); + } + ctx.scene.lamps.add_drawcalls(lampdraws, headlightdraws); + // the vehicles drawn by the colour pass keep what it set, as when the passes come one after another; then what the walks put off + Settle_Renderme(true); + Release_Deferred(); + m_walkpasses = m_walkjobs; + m_walkjobs = 0; +} + +// the walks of the groups of the cullings split among the helpers (the stage M3, gfx.vulkan.parallelmodels): a few groups are walked by +// the walk itself +bool vulkan14_renderer::walk_groups_parallel(std::size_t const Groups) +{ + m_helpgroups = Groups; + return Global.gfx_vulkan_parallelmodels && (m_context != nullptr) && m_context->models.running() && (Groups >= 16) && (false == m_previewing) && (false == m_rtgather); +} + +void vulkan14_renderer::take_helper_state(vulkan14_renderer const &Walk) +{ + // what the walk of the groups reads: the pass, the model block and the blending the walk is at, its recording of the draws + m_renderpass = Walk.m_renderpass; + m_renderpass.draw_stats = {}; + model_ubs = Walk.model_ubs; + m_framestamp = Walk.m_framestamp; + m_instancerecordsversion = Walk.m_instancerecordsversion; + m_fogrange = Walk.m_fogrange; + m_previewing = Walk.m_previewing; + m_motionhistory = Walk.m_motionhistory; + m_cabfuture = Walk.m_cabfuture; + m_invalid_material = Walk.m_invalid_material; + m_rtgather = false; + m_indirectrecording = true; + m_indirectentry = Walk.m_indirectentry; + m_indirectmaterial = Walk.m_indirectmaterial; + m_indirectsubmodel = Walk.m_indirectsubmodel; + m_indirectchanging = false; + m_forkmatrices = OpenGLMatrices; + m_forksubmodels = submodel_state{TSubModel::iInstance, TSubModel::iVariantSeed, TSubModel::ReplacableSkinId, TSubModel::iAlpha, TSubModel::fSquareDist, TSubModel::pRoot, TSubModel::pasText}; + auto &draw{*m_draw}; + draw.blending = Walk.m_draw->blending; + draw.rendering = false; + draw.depthpass = false; + draw.probepass = false; + draw.used_banks.clear(); + draw.used_textures.clear(); +} + +void vulkan14_renderer::walk_groups_run(std::size_t const Tasks, std::function const &Work) +{ + auto &ctx{*m_context}; + auto *main{m_main != nullptr ? m_main : this}; + // the helpers hold the models they walk (gfx::walk_sync): the window of the walks is opened for them when the main thread walks + // alone + auto const opened{(m_main == nullptr) && (false == gfx::walk_sync::parallel())}; + if (opened) + { + gfx::walk_sync::open(); + } + while (m_helpers.size() < Tasks) + { + m_helpers.emplace_back(std::make_unique(*main)); + } + for (std::size_t task = 0; task < Tasks; ++task) + { + m_helpers[task]->take_helper_state(*this); + } + std::mutex mutex; + std::condition_variable finished; + std::size_t left{Tasks}; + std::exception_ptr failure; + for (std::size_t task = 0; task < Tasks; ++task) + { + ctx.models.add( + [&, task]() { + { + // covered by the hold of the walk waiting for it + gfx::walk_sync::borrowed_scope scope; + auto &helper{*m_helpers[task]}; + OpenGLMatrices = helper.m_forkmatrices; + auto const &submodels{helper.m_forksubmodels}; + TSubModel::iInstance = submodels.instance; + TSubModel::iVariantSeed = submodels.variantseed; + TSubModel::ReplacableSkinId = submodels.skins; + TSubModel::iAlpha = submodels.alpha; + TSubModel::fSquareDist = submodels.squaredistance; + TSubModel::pRoot = submodels.root; + TSubModel::pasText = submodels.text; + try + { + Work(helper, task); + } + catch (...) + { + std::lock_guard lock(mutex); + failure = std::current_exception(); + } + } + std::lock_guard lock(mutex); + if (--left == 0) + { + finished.notify_all(); + } + }, + false); + } + { + std::unique_lock lock(mutex); + finished.wait(lock, [&]() { return left == 0; }); + } + // the banks the helpers' draws used, marked with those of the walk + for (std::size_t task = 0; task < Tasks; ++task) + { + auto &used{m_helpers[task]->m_draw->used_banks}; + for (auto const bank : used) + { + if (m_main != nullptr) + { + m_draw->used_banks.push_back(bank); + } + else + { + m_geometry.mark_used(gfx::geometry_handle{bank, 1}); + } + } + used.clear(); + } + if (opened) + { + gfx::walk_sync::close(); + } + ctx.helped += m_helpgroups; + if (failure) + { + std::rethrow_exception(failure); + } +} diff --git a/rendering/vulkan14/vulkan14weather.cpp b/rendering/vulkan14/vulkan14weather.cpp index 95b73b3b..e3e59ecc 100644 --- a/rendering/vulkan14/vulkan14weather.cpp +++ b/rendering/vulkan14/vulkan14weather.cpp @@ -146,6 +146,8 @@ bool vulkan14_renderer::init_weather(std::string &Reason) desc.extent = {static_cast(Size), static_cast(Size), Volume ? static_cast(Size) : 1u}; desc.mips = static_cast(std::floor(std::log2(static_cast(Size)))) + 1; desc.usage = VK_IMAGE_USAGE_STORAGE_BIT | VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT; + // read by the passes of the weather on the compute queue (the async compute of M6) + desc.shared = true; Image = std::make_shared(); if (false == Image->create(ctx.memory, desc, Name)) { @@ -164,6 +166,7 @@ bool vulkan14_renderer::init_weather(std::string &Reason) vk14::image_desc desc; desc.format = VK_FORMAT_R32_SFLOAT; desc.usage = VK_IMAGE_USAGE_STORAGE_BIT; + desc.shared = true; weather.depth_dummy = std::make_shared(); if (false == weather.depth_dummy->create(ctx.memory, desc, "clouds panorama depth")) { @@ -223,8 +226,10 @@ void vulkan14_renderer::destroy_weather() namespace { -// an image of the weather made anew: the old one goes once the gpu is done with the frames which used it (Value) -std::shared_ptr weather_image(vk14::allocator &Memory, vk14::deletion_queue &Deletions, std::uint64_t const Value, std::shared_ptr &Image, VkImageType const Type, VkFormat const Format, glm::ivec3 const &Size, char const *Name) +// an image of the weather made anew: the old one goes once the gpu is done with the frames which used it (Value). Shared: made by the +// passes of the weather on the compute queue (the async compute of M6) +std::shared_ptr weather_image(vk14::allocator &Memory, vk14::deletion_queue &Deletions, std::uint64_t const Value, std::shared_ptr &Image, VkImageType const Type, VkFormat const Format, glm::ivec3 const &Size, char const *Name, + bool const Shared = false) { if (Image) { @@ -236,6 +241,7 @@ std::shared_ptr weather_image(vk14::allocator &Memory, vk14::deleti desc.format = Format; desc.extent = {static_cast(std::max(Size.x, 1)), static_cast(std::max(Size.y, 1)), static_cast(std::max(Size.z, 1))}; desc.usage = VK_IMAGE_USAGE_STORAGE_BIT | VK_IMAGE_USAGE_SAMPLED_BIT; + desc.shared = Shared; Image = std::make_shared(); if (false == Image->create(Memory, desc, Name)) { @@ -251,7 +257,7 @@ void vulkan14_renderer::make_cloud_noise() { auto &ctx{*m_context}; auto &weather{ctx.scene.weather}; - auto const commands{ctx.commands}; + auto const commands{m_commands}; auto const device{ctx.device.handle()}; ctx.passes.begin_pass("clouds noise", {vk14::use_of(*weather.shape_noise, VK_IMAGE_LAYOUT_GENERAL, compute_stage, storage_write), vk14::use_of(*weather.detail_noise, VK_IMAGE_LAYOUT_GENERAL, compute_stage, storage_write), vk14::use_of(*weather.coverage_noise, VK_IMAGE_LAYOUT_GENERAL, compute_stage, storage_write)}); @@ -364,7 +370,7 @@ void vulkan14_renderer::update_weather() auto &ctx{*m_context}; auto &scene{ctx.scene}; auto &weather{scene.weather}; - auto const commands{ctx.commands}; + auto const commands{m_commands}; if (false == weather.noise_made) { make_cloud_noise(); @@ -376,7 +382,7 @@ void vulkan14_renderer::update_weather() if (weather.shadow_size != Global.gfx_clouds_shadows_size) { weather.shadow_size = Global.gfx_clouds_shadows_size; - weather_image(ctx.memory, ctx.deletions, ctx.uploads.current_value(), weather.shadow_map, VK_IMAGE_TYPE_2D, VK_FORMAT_R16_SFLOAT, glm::ivec3{weather.shadow_size, weather.shadow_size, 1}, "cloud shadow map"); + weather_image(ctx.memory, ctx.deletions, ctx.uploads.current_value(), weather.shadow_map, VK_IMAGE_TYPE_2D, VK_FORMAT_R16_SFLOAT, glm::ivec3{weather.shadow_size, weather.shadow_size, 1}, "cloud shadow map", true); if (weather.shadow_map) { // read before it's first drawn (the pass sets hold it) @@ -430,7 +436,7 @@ void vulkan14_renderer::update_weather() if ((weather.panorama_size != level.panorama) || (false == bool(weather.panorama))) { weather.panorama_size = level.panorama; - weather_image(ctx.memory, ctx.deletions, ctx.uploads.current_value(), weather.panorama, VK_IMAGE_TYPE_2D, VK_FORMAT_R16G16B16A16_SFLOAT, glm::ivec3{weather.panorama_size, 1}, "clouds panorama"); + weather_image(ctx.memory, ctx.deletions, ctx.uploads.current_value(), weather.panorama, VK_IMAGE_TYPE_2D, VK_FORMAT_R16G16B16A16_SFLOAT, glm::ivec3{weather.panorama_size, 1}, "clouds panorama", true); m_weatherstate.panorama_resized(); } if (weather.panorama) @@ -460,7 +466,7 @@ void vulkan14_renderer::render_weather_view(glm::mat4 const &Projection, glm::ma { auto &ctx{*m_context}; auto &weather{ctx.scene.weather}; - auto const commands{ctx.commands}; + auto const commands{m_commands}; auto const level{gfx::weather_state::quality(Global.gfx_clouds, Global.gfx_fog_quality)}; // the images of the clouds of the main view and of the fog volume, for the size and the quality auto const cloudsize{glm::max((Size + level.divisor - 1) / level.divisor, glm::ivec2{1})}; diff --git a/scene/scene.h b/scene/scene.h index a9278b1e..9b72f000 100644 --- a/scene/scene.h +++ b/scene/scene.h @@ -122,6 +122,10 @@ public: // legacy method, updates geometry for pieces in the animation list void RaAnimate( unsigned int const Framestamp ); + // whether RaAnimate() has anything to update in the frame + bool + animation_pending( unsigned int const Framestamp ) const { + return ( tTrackAnim != nullptr ) && ( Framestamp != m_framestamp ); } // sends content of the class to provided stream void serialize( std::ostream &Output ) const; @@ -405,6 +409,10 @@ public: // generates renderable version of held non-instanced geometry void create_geometry(); + // whether create_geometry() has anything to make or to load + bool + geometry_pending() const { + return ( false == m_terrain.empty() ) || ( false == m_geometrycreated ); } // loads geometry of the section kept in binary terrain files, if any is waiting void load_terrain(); diff --git a/utilities/Globals.cpp b/utilities/Globals.cpp index 331e7ccd..4852fd16 100644 --- a/utilities/Globals.cpp +++ b/utilities/Globals.cpp @@ -1830,6 +1830,26 @@ global_settings::ConfigParse_gfx( cParser &Parser, std::string_view const Token Parser >> gfx_threads; gfx_threads = std::clamp(gfx_threads, 0, 64); } + else if (Token == "gfx.vulkan.asyncresources") + { + Parser.getTokens(1); + Parser >> gfx_vulkan_asyncresources; + } + else if (Token == "gfx.vulkan.parallelpasses") + { + Parser.getTokens(1); + Parser >> gfx_vulkan_parallelpasses; + } + else if (Token == "gfx.vulkan.parallelmodels") + { + Parser.getTokens(1); + Parser >> gfx_vulkan_parallelmodels; + } + else if (Token == "gfx.vulkan.asynccompute") + { + Parser.getTokens(1); + Parser >> gfx_vulkan_asynccompute; + } else if (ConfigParse_gfx_sky(Parser, Token)) { @@ -2362,6 +2382,10 @@ global_settings::export_as_text( std::ostream &Output ) const { export_as_text( Output, "gfx.vulkan.framesinflight", gfx_vulkan_framesinflight ); export_as_text( Output, "gfx.vulkan.renderthread", gfx_vulkan_renderthread ); export_as_text( Output, "gfx.threads", gfx_threads ); + export_as_text( Output, "gfx.vulkan.asyncresources", gfx_vulkan_asyncresources ); + export_as_text( Output, "gfx.vulkan.parallelpasses", gfx_vulkan_parallelpasses ); + export_as_text( Output, "gfx.vulkan.parallelmodels", gfx_vulkan_parallelmodels ); + export_as_text( Output, "gfx.vulkan.asynccompute", gfx_vulkan_asynccompute ); export_as_text( Output, "gfx.shadow.angle.min", gfx_shadow_angle_min ); export_as_text( Output, "gfx.shadow.rank.cutoff", gfx_shadow_rank_cutoff ); export_as_text( Output, "gfx.shadow.sdsm", gfx_shadow_sdsm ); diff --git a/utilities/Globals.h b/utilities/Globals.h index 5ff6ae9b..0f9a66d1 100644 --- a/utilities/Globals.h +++ b/utilities/Globals.h @@ -425,6 +425,17 @@ struct global_settings { bool gfx_vulkan_renderthread { true }; // threads recording the command buffers of the vulkan renderer; 0: the cores less two, 1: one thread records them all int gfx_threads { 0 }; + // (vulkan) the pipelines and the textures a draw needs first made by the background threads, the draw left out till then (the + // cab waits for them); the pipelines of the last session made ahead at the start + bool gfx_vulkan_asyncresources { true }; + // (vulkan) the cascades of the sun shadows and the shadow maps of the lamps and the headlights walked by threads of their own while + // the main thread walks the colour pass + bool gfx_vulkan_parallelpasses { true }; + // (vulkan) the submodel trees of the instances culled on the gpu walked by the threads of gfx.threads, model by model + bool gfx_vulkan_parallelmodels { true }; + // (vulkan) the passes of the sky and the weather run on the compute queue, at the same time as the sun shadows, where the gpu has a + // compute family of its own; off until a measurement on such a gpu shows what it gains + bool gfx_vulkan_asynccompute { false }; bool vr = false; std::string vr_backend; diff --git a/vehicle/Train.cpp b/vehicle/Train.cpp index 7ab5e115..487a518b 100644 --- a/vehicle/Train.cpp +++ b/vehicle/Train.cpp @@ -12466,6 +12466,7 @@ void train_table::updateAsync(double dt) const int threads = std::max(1, Global.trainThreads); const size_t total = m_items.size(); const size_t chunkSize = (total + threads - 1) / threads; + auto const instance{TSubModel::iInstance}; std::vector workers; std::vector> results; // przekazują wyjątki z wątków do głównego wątku @@ -12480,7 +12481,12 @@ void train_table::updateAsync(double dt) if (start >= end) break; // brak więcej danych - std::packaged_task task([this, start, end, dt]() { update_trains(m_items, start, end, dt); }); + // the instance whose animations the gauges set (TSubModel::iInstance, per thread since the walks of the vulkan renderer went on + // several threads) is the one the main thread set for the driven vehicle + std::packaged_task task([this, start, end, dt, instance]() { + TSubModel::iInstance = instance; + update_trains(m_items, start, end, dt); + }); results.emplace_back(task.get_future()); workers.emplace_back(std::move(task)); }