/* 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 part of the shared walk of the scene which feeds the gpu culling of the instances and records the draws made from its // result (stage N6c of claude/vulkan14-renderer-plan.md): the records of the sleepers and of the models drawn on their own, the // queueing of buckets, the walks of the submodel trees of the culled groups and the order of the recorded draws #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 { namespace { // the group of the sleepers of a track in the gpu culling: their model, and the skin in the slot Render_Sleepers() sets it in scene::basic_cell::instance_bucket_key sleeper_key(TTrack const &Track) { scene::basic_cell::instance_bucket_key key; key.pModel = Track.m_sleeper_model; key.skins[1] = Track.m_sleeper_skin; return key; } } // namespace void scene_walk::Begin_Pass_Cull(gfx::instance_cull *Cull, bool const Mainview) { m_passcull = Cull; m_instancecullpass = false; if (m_passcull != nullptr) { m_instancecullpass = true; // the same choices as in Render_Instanced() auto const reflections{m_renderpass.draw_mode == rendermode::reflections}; gfx::instance_cull::pass_setup setup; setup.frame = m_framestamp; setup.camera = m_renderpass.pass_camera.position(); setup.distancecamera = (m_renderpass.draw_mode == rendermode::shadows ? m_renderpass.viewport_camera.position() : m_renderpass.pass_camera.position()); setup.frustum = &m_renderpass.pass_camera.frustum(); setup.distancescale = (reflections ? 1.f : static_cast(1.0 / (static_cast(Global.ZoomFactor) * static_cast(Global.ZoomFactor) * static_cast(Global.fDistanceFactor)))); setup.distanceoffset = (reflections ? static_cast(sq(EU07_REFLECTIONFIDELITYOFFSET)) : 0.f); setup.distancerange = (reflections ? static_cast(sq(Global.reflectiontune.range_instances)) : std::numeric_limits::max()); setup.drawrange = static_cast(sq(m_renderpass.draw_range + 250)); setup.zoom = Global.ZoomFactor; setup.drawing = (Global.gfx_gpucull == 1); m_passcull->begin_pass(setup); } // the sleepers of tracks go with the instances of the main view m_sleeperspass = (Mainview && (m_passcull != nullptr) && Global.gfx_gpucull_sleepers); if (m_sleeperspass && (false == m_passcull->drawing())) { for (auto &counts : m_sleeperbandcounts) { counts.second.clear(); } } // the models drawn on their own go with the instances of every pass culled on the gpu m_modelspass = (m_instancecullpass && Global.gfx_gpucull_models); } void scene_walk::Compare_Pass_Cull() { // the sleepers the cpu drew along the tracks, before the culling ran if (m_sleeperspass) { for (auto const &counts : m_sleeperbandcounts) { if (false == counts.second.empty()) { m_passcull->cpu_result(counts.first, counts.second, true); } } } // and the models drawn on their own for (auto const &model : m_modelcpudistances) { m_passcull->cpu_instance(model.first, model.second); } } void scene_walk::End_Pass_Cull() { if (m_instancecullpass) { m_passcull->end_pass(); m_instancecullpass = false; m_passcull = nullptr; } m_sleeperspass = false; m_modelspass = false; } bool scene_walk::walk_bucket(scene::basic_cell::instance_bucket_key const &Key, scene::basic_cell::instance_bucket const &Bucket, bool const Bucketrange, scene::bounding_area const &Area) { if (m_instancecullpass) { // the gpu tests every instance of the bucket, the way Render_Instanced() does. models with nothing to draw in the // sun shadows are left out of them, as Render_Instanced() leaves them if ((m_renderpass.draw_mode != rendermode::shadows) || Instances_Cast_Shadow(Key, Bucket)) { m_passcull->add_bucket(Key, Bucket, (Bucketrange ? Bucket_Area(Bucket) : Area)); } // and when the instances are drawn from its result, the cpu has nothing more to do with them if (m_passcull->drawing()) { return false; } } // the depth pass of the whole scene culled them already, the colour pass draws what it recorded if (m_indirectcolourpending) { return false; } return true; } void scene_walk::walk_bucket_culled(scene::basic_cell::instance_bucket const &Bucket) { if (m_instancecullpass) { m_passcull->skip_bucket(Bucket); } } bool scene_walk::walk_queue_model(TAnimModel *Instance, bool const Inrange) { return Queue_Model(Instance, Inrange); } bool scene_walk::walk_queue_sleepers(TTrack *Track) { if (m_sleeperspass) { // culled with the instances of the pass and drawn from the result, or drawn by the cpu as well to compare Queue_Sleepers(Track); if (m_passcull->drawing()) { return true; } } else if (m_indirectcolourpending && m_indirectcoloursleepers) { // the depth pass of the whole scene culled and recorded them, the colour pass draws what it recorded return true; } return false; } void scene_walk::walk_sleepers_drawn(TTrack const &Track, std::vector const &Lodbounds, std::vector> const &Drawn) { if (m_sleeperspass && m_instancecullpass && (false == m_passcull->drawing())) { // the comparison with the gpu culling: the sleepers drawn by lod band, the bands as the culling makes them, split by // the bounds of the model past 0 and short of the largest float auto const counted = [](float const Bound) { return (Bound > 0.f) && (Bound < std::numeric_limits::max()); }; auto &counts{m_sleeperbandcounts[sleeper_key(Track)]}; auto const bands{static_cast(std::count_if(Lodbounds.begin(), Lodbounds.end(), counted)) + 1}; if (counts.size() < bands) { counts.resize(bands, 0); } for (auto const &survivor : Drawn) { std::size_t band{0}; for (auto const bound : Lodbounds) { if (counted(bound) && (bound <= survivor.first)) { ++band; } } ++counts[band]; } } } void scene_walk::walk_instances_compare(scene::basic_cell::instance_bucket_key const &Key, std::vector const &Bandcounts) { m_passcull->cpu_result(Key, Bandcounts); } void scene_walk::Queue_Sleepers(TTrack *Track) { // the gates of Render_Sleepers(); the range is tested for the whole track if ((Track->m_sleeper_model == nullptr) || Track->m_sleeper_local_transforms.empty() || (Global.SleeperDistance <= 0.f)) { return; } auto const distsq{glm::length2(Track->location() - m_renderpass.pass_camera.position())}; auto const cutoffsq{static_cast(Global.SleeperDistance) * static_cast(Global.SleeperDistance)}; if (distsq > cutoffsq) { return; } auto &records{m_sleeperrecords[Track]}; auto const &transforms{Track->m_sleeper_local_transforms}; if ((records.bucket.recordsversion != m_instancerecordsversion) || (records.model != Track->m_sleeper_model) || (records.skin != Track->m_sleeper_skin) || (records.transforms != transforms.data()) || (records.count != transforms.size())) { // each sleeper is a record placed and turned by its transform, relative to the origin of the track (the centre of its // cell), with the sphere Render_Sleepers() tests against the frustum auto const radius{std::max(Track->m_sleeper_model->bounding_radius(), 2.0f)}; auto &bucket{records.bucket}; bucket.records.clear(); bucket.records.reserve(transforms.size()); glm::vec3 low{std::numeric_limits::max()}; glm::vec3 high{std::numeric_limits::lowest()}; for (auto const &local : transforms) { scene::basic_cell::instance_record record; glm::vec3 const position{local[3]}; record.position = packed_vec3{position}; record.radius = radius; record.rangesquaredmin = 0.f; record.rangesquaredmax = std::numeric_limits::max(); record.flags = scene::basic_cell::instance_visible | scene::basic_cell::instance_instanceable; for (int column = 0; column < 3; ++column) { record.rotationscale[column] = local[column]; } bucket.records.emplace_back(record); low = glm::min(low, position); high = glm::max(high, position); } bucket.recordorigin = Track->m_origin; bucket.lodrangesquared = std::numeric_limits::max(); bucket.recordsversion = m_instancerecordsversion; bucket.gpustale = true; records.key = sleeper_key(*Track); records.area = scene::bounding_area{Track->m_origin + glm::dvec3{(low + high) * 0.5f}, 0.5f * glm::length(high - low)}; records.model = Track->m_sleeper_model; records.skin = Track->m_sleeper_skin; records.transforms = transforms.data(); records.count = transforms.size(); } records.frame = m_framestamp; m_passcull->add_sleepers(records.key, records.bucket, records.area); } // queues a model instance drawn on its own in the gpu culling of the pass, as a bucket and a group of its own. the culling // makes the tests of Render( TAnimModel * ): visibility, frustum, the range of the instance, the draw range, the size on the // screen, and gives it the lod band of the bounds of its submodels (mindistance/maxdistance) its distance falls in, then // Walk_Indirect_Draws() animates, prepares and walks its submodel tree from its origin, like it does for a group of // instanced ones bool scene_walk::Queue_Model(TAnimModel *Instance, bool const Inrange) { // a billboard is turned to the camera from the modelview of the instance, which a walk from its origin doesn't have if ((false == Instance->m_cameraindependent) || (Instance->Model() == nullptr)) { return false; } if (false == m_modelspass) { // the depth pass of the whole scene culled and recorded it, the colour pass draws what it recorded return (m_indirectcolourpending && m_indirectcolourmodels); } auto const drawing{m_passcull->drawing()}; // a model hidden by an event, or in a cell wholly out of the range of the pass, would be left out by the culling if ((false == Instance->visible()) || (false == Inrange)) { return drawing; } auto &records{m_modelrecords[Instance]}; auto &bucket{records.bucket}; if ((bucket.recordsversion != m_instancerecordsversion) || (bucket.records.size() != 1)) { // the key, the instance and the record anew: the editor may have given the instance another model or skins, or // another instance may have taken the place of a deleted one auto &key{records.key}; key = {}; key.pModel = Instance->Model(); if (auto const *material{Instance->Material()}) { for (int skin = 0; skin < 5; ++skin) { key.skins[skin] = material->replacable_skins[skin]; } } key.instance = Instance; bucket.instances.assign(1, Instance); // the location of the instance as the origin of the record, which keeps the precision Update_Instance_Records(key.pModel, bucket, Instance->location()); // drawn by its bucket, though it isn't instanceable bucket.records.front().flags |= scene::basic_cell::instance_instanceable; records.area = scene::bounding_area{Instance->location(), 0.f}; } records.frame = m_framestamp; if (m_renderpass.draw_mode == rendermode::shadows) { // a model with nothing to draw in the shadows is left out of them, like the instances. the answer depends on the // model and the skins only, and is shared by the instances with them auto modelkey{records.key}; modelkey.instance = nullptr; if (false == Instances_Cast_Shadow(modelkey, bucket)) { return drawing; } } // out of the frustum of the pass, beyond its range or too small on the screen by its record: the culling would leave it out, // and the walk of Walk_Indirect_Draws() isn't made for it if (Global.gfx_gpucull_bucketrange && (false == EditorModeFlag) && Bucket_Culled(bucket)) { m_passcull->skip_bucket(bucket); return drawing; } m_passcull->add_bucket(records.key, bucket, records.area); if (false == drawing) { // the cpu draws it too, and tells what it drew for the comparison m_modelcomparekey = &records.key; } return drawing; } bool scene_walk::Instances_Cast_Shadow(scene::basic_cell::instance_bucket_key const &Key, scene::basic_cell::instance_bucket const &Bucket) { auto const lookup{m_shadowcasters.find(Key)}; if (lookup != m_shadowcasters.end()) { return lookup->second; } // the skins of the instance Render_Instanced() would take them from TAnimModel const *instance{nullptr}; for (auto const *candidate : Bucket.instances) { if (candidate != nullptr) { instance = candidate; break; } } auto const *skins{instance != nullptr && instance->Material() != nullptr ? instance->Material()->replacable_skins : nullptr}; auto castsshadow{false}; m_instance_lodpending.clear(); m_instance_lodpending.push_back(Key.pModel != nullptr ? Key.pModel->Root : nullptr); while ((false == castsshadow) && (false == m_instance_lodpending.empty())) { auto const *submodel = m_instance_lodpending.back(); m_instance_lodpending.pop_back(); if (submodel == nullptr) { continue; } if (submodel->eType < TP_ROTATOR) { auto const material{submodel->m_material >= 0 ? submodel->m_material : (skins != nullptr ? skins[-submodel->m_material] : null_handle)}; castsshadow = (walk_material_data(material).shadow_rank <= Global.gfx_shadow_rank_cutoff); } m_instance_lodpending.push_back(submodel->Child); m_instance_lodpending.push_back(submodel->Next); } m_shadowcasters.emplace(Key, castsshadow); return castsshadow; } void scene_walk::Walk_Indirect_Draws(gfx::instance_cull const &Cull, bool const Cascade) { m_indirectdraws.clear(); m_indirectcommands.clear(); auto const &lodbounds{Cull.lod_bounds()}; // the cascades of the sun shadows make the same walks: the mode, the lod band and the state of the models are the same in all // of them within a frame, only the instances differ, and those the gpu fills in. the walk of a group in a band is made by the // first cascade which draws it, the others copy its draws and commands with entries of their own auto const cascade{(m_renderpass.draw_mode == rendermode::shadows) && Cascade}; auto constexpr nowalk{std::numeric_limits::max()}; if (cascade && (m_cascaderecordframe != m_framestamp)) { m_cascaderecordframe = m_framestamp; m_cascadedraws.clear(); m_cascadecommands.clear(); if ((m_framestamp % 256) == 0) { // the groups not drawn for a while for (auto records{m_cascaderecords.begin()}; records != m_cascaderecords.end();) { if (m_framestamp - records->second.frame > 256) { records = m_cascaderecords.erase(records); } else { ++records; } } } } if (m_walkcountsframe != m_framestamp) { m_walkcountsframe = m_framestamp; m_lastwalkcounts = m_walkcounts; m_walkcounts = {}; } auto const walkstart{std::chrono::steady_clock::now()}; // the walks of earlier frames. they go when the switch is off, as the models may change meanwhile without the walks noticing // it the next time (the season), and when the shadows take other submodels auto const walkcache{Global.gfx_gpucull_walkcache && (false == m_previewing)}; if ((false == Global.gfx_gpucull_walkcache) || (m_walkrankcutoff != Global.gfx_shadow_rank_cutoff)) { m_walkrecords.clear(); m_walkrankcutoff = Global.gfx_shadow_rank_cutoff; } if (walkcache && ((m_framestamp % 256) == 0)) { // the groups not drawn for a while for (auto records{m_walkrecords.begin()}; records != m_walkrecords.end();) { if (m_framestamp - records->second.frame > 256) { records = m_walkrecords.erase(records); } else { ++records; } } } auto const shadows{m_renderpass.draw_mode == rendermode::shadows}; m_indirectrecording = true; // 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}; if (walkcache && (group.key->instance == nullptr)) { kept = &m_walkrecords[*group.key]; kept->frame = m_framestamp; auto &bands{kept->bands[shadows ? 1 : 0]}; if (bands.size() != group.lodcount + 1) { bands.clear(); bands.resize(group.lodcount + 1); } } 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) { // as Render_Sleepers() sets them up: the skin of the track, if any. like scenery models they pick no texture // variants of their own if (group.key->skins[1] != null_handle) { skins = group.key->skins.data(); } TSubModel::iVariantSeed = 0; } else { if (group.key->instance != nullptr) { // a model drawn on its own: its lights and animations, the way Render( TAnimModel * ) sets them up 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; }; // the walks of the group the cascades made in this frame. a group whose bands were all walked already isn't prepared cascade_record *walks{nullptr}; if (cascade) { walks = &m_cascaderecords[*group.key]; if ((walks->frame != m_framestamp) || (walks->bands.size() != group.lodcount + 1)) { walks->frame = m_framestamp; walks->bands.assign(group.lodcount + 1, cascade_band{nowalk, 0}); } } else if (kept == nullptr) { prepare(); } for (std::uint32_t band = 0; band <= group.lodcount; ++band) { // a band holds the squared distances from its lower bound up to the next one; any of them selects the same lod auto const bandmin{band == 0 ? 0.f : lodbounds[group.lodfirst + band - 1]}; auto const bandmax{band == group.lodcount ? std::numeric_limits::max() : lodbounds[group.lodfirst + band]}; if ((bandmin > group.distancemax) || (bandmax <= group.distancemin)) { continue; } m_indirectentry = group.entryfirst + band; if ((walks != nullptr) && (walks->bands[band].first != nowalk)) { // walked by another cascade: its draws, for the instances of this entry auto const &walk{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; } // as the walk would count them m_renderpass.draw_stats.submodels += static_cast(walk.count); m_renderpass.draw_stats.drawcalls += static_cast(walk.count); continue; } auto const walkfirst{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 == walk_blending_enabled())) { // walked in an earlier frame, with the model as it is now Replay_Walk(*keptwalk, shadows); ++m_walkcounts.reused; } else { if (false == prepared) { prepare(); } auto const blending{walk_blending_enabled()}; m_indirectchanging = false; ::glPushMatrix(); ::glLoadIdentity(); model->Root->fSquareDist = bandmin; model->Root->ReplacableSet(skins, alpha); model->Root->pRoot = model; Render(model->Root); ::glPopMatrix(); ++m_walkcounts.walked; if (m_indirectchanging) { ++m_walkcounts.changing; } if (keptwalk != nullptr) { keptwalk->valid = (false == m_indirectchanging); if (keptwalk->valid) { keptwalk->draws.assign(m_indirectdraws.begin() + walkfirst, m_indirectdraws.end()); keptwalk->bandmin = bandmin; keptwalk->modelversion = modelversion; keptwalk->blending = blending; } else { keptwalk->draws.clear(); } } } if (walks != nullptr) { 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()); } } } m_indirectrecording = false; 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 (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)}); } } void scene_walk::Replay_Walk(walk_band const &Walk, bool const Shadows) { auto &geometry{walk_geometry()}; for (auto const &walkdraw : Walk.draws) { // 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 == walk_indirect_source(walkdraw.geometry, source)) { continue; } m_indirectcommands.push_back({source.count, 0, source.first, static_cast(source.basevertex), 0, m_indirectentry, (source.indexed ? 1u : 0u), 0}); auto &draw{m_indirectdraws.emplace_back(walkdraw)}; draw.type = source.type; draw.group = geometry.draw_group(walkdraw.geometry); // the walk sets the placement of the submodel, and before each draw of the colour passes the parameters of its material // (the material bind); the rest of the block is what the pass has at the moment, as the walk would leave it auto const &walked{walkdraw.ubs}; draw.ubs = model_ubs; draw.ubs.modelview = walked.modelview; draw.ubs.modelviewnormal = walked.modelviewnormal; if (false == Shadows) { std::memcpy(&draw.ubs.param[0], &walked.param[0], sizeof(draw.ubs.param)); draw.ubs.opacity = walked.opacity; draw.ubs.alpha_mult = walked.alpha_mult; } } // as the walk would count them m_renderpass.draw_stats.submodels += static_cast(Walk.draws.size()); m_renderpass.draw_stats.drawcalls += static_cast(Walk.draws.size()); } texture_handle scene_walk::Indirect_Shadow_Texture(material_handle const Material) const { if (Material == null_handle) { return null_handle; } return walk_material_data(Material).textures[0]; } // the order of the recorded draws for multi draws. draws go in one multi draw when they follow one another with the same // state (material and its texture variant, geometry bank, primitive, blending; Indirect_Mergeable()). the draws of one entry, // the walk of one submodel tree in one lod band, keep their order: submodels of a model may lie on one another and rely on // the later one being drawn over the earlier one. the draws of different entries are drawn for different instances, which // the gpu draws in no particular order anyway, so they're free to move. the entries are queues of draws; the draws at the // heads of the queues which have the state taken next all go, each queue as far as its draws keep that state, then the // queues wait for the state of their next draw. the states go in the order in which the queues first reach them void scene_walk::Order_Indirect_Draws() { auto const count{m_indirectdraws.size()}; if (count < 2) { return; } // a number for each state the draws have, by sorting the states (the buffers are kept between frames) m_indirectstatekeys.clear(); // the sun shadows draw with the program and the texture of the alpha test, whatever the material auto const shadows{m_renderpass.draw_mode == rendermode::shadows}; for (std::size_t index = 0; index < count; ++index) { auto const &draw{m_indirectdraws[index]}; if (shadows) { m_indirectstatekeys.push_back({Indirect_Shadow_Texture(draw.material), 0u, draw.group, draw.type, m_indirectcommands[index].indexed, draw.blending, static_cast(index)}); continue; } auto variant{0u}; if (draw.material != null_handle) { auto const &material{walk_material_data(draw.material)}; if (false == material.texture_variants.empty()) { variant = TSubModel::VariantId(draw.submodel) % static_cast(material.texture_variants.size()); } } m_indirectstatekeys.push_back({draw.material, variant, draw.group, draw.type, m_indirectcommands[index].indexed, draw.blending, static_cast(index)}); } std::sort(std::begin(m_indirectstatekeys), std::end(m_indirectstatekeys), [](indirect_state_key const &Left, indirect_state_key const &Right) { return Left.tie() < Right.tie(); }); m_indirectdrawstates.resize(count); std::uint32_t statecount{0}; for (std::size_t index = 0; index < count; ++index) { if ((index > 0) && (m_indirectstatekeys[index].tie() != m_indirectstatekeys[index - 1].tie())) { ++statecount; } m_indirectdrawstates[m_indirectstatekeys[index].draw] = statecount; } ++statecount; if (statecount == count) { // no two draws share a state, nothing would go together return; } auto constexpr none{std::numeric_limits::max()}; m_indirectstates.assign(statecount, indirect_state{none, none}); m_indirectstateorder.clear(); m_indirectqueues.clear(); // the queue waits for the state of its next draw, behind the queues waiting for it already auto const enqueue = [this, none](std::uint32_t const Queue) { auto &queue{m_indirectqueues[Queue]}; auto &state{m_indirectstates[m_indirectdrawstates[queue.next]]}; queue.link = none; if (state.head == none) { state.head = Queue; m_indirectstateorder.push_back(m_indirectdrawstates[queue.next]); } else { m_indirectqueues[state.tail].link = Queue; } state.tail = Queue; }; for (std::size_t index = 0; index < count;) { auto end{index + 1}; while ((end < count) && (m_indirectcommands[end].entry == m_indirectcommands[index].entry)) { ++end; } m_indirectqueues.push_back({static_cast(index), static_cast(end), none}); index = end; } for (std::uint32_t queue = 0; queue < m_indirectqueues.size(); ++queue) { enqueue(queue); } m_indirectorder.clear(); for (std::size_t taken = 0; taken < m_indirectstateorder.size(); ++taken) { auto const stateindex{m_indirectstateorder[taken]}; auto queueindex{m_indirectstates[stateindex].head}; m_indirectstates[stateindex] = {none, none}; while (queueindex != none) { auto &queue{m_indirectqueues[queueindex]}; auto const nextqueue{queue.link}; while ((queue.next < queue.end) && (m_indirectdrawstates[queue.next] == stateindex)) { m_indirectorder.push_back(queue.next); ++queue.next; } if (queue.next < queue.end) { enqueue(queueindex); } queueindex = nextqueue; } } m_indirectdrawsordered.clear(); m_indirectcommandsordered.clear(); for (auto const index : m_indirectorder) { m_indirectdrawsordered.push_back(m_indirectdraws[index]); m_indirectcommandsordered.push_back(m_indirectcommands[index]); } m_indirectdraws.swap(m_indirectdrawsordered); m_indirectcommands.swap(m_indirectcommandsordered); } bool scene_walk::Indirect_Mergeable(std::size_t const First, std::size_t const Other) const { auto const &first{m_indirectdraws[First]}; auto const &other{m_indirectdraws[Other]}; if ((first.material != other.material) || (first.blending != other.blending) || (first.type != other.type) || (first.group != other.group) || (m_indirectcommands[First].indexed != m_indirectcommands[Other].indexed)) { return false; } if (first.material == null_handle) { return true; } // the texture set the material bind binds auto const &material{walk_material_data(first.material)}; if (material.texture_variants.empty()) { return true; } return (&material.GetTextures(TSubModel::VariantId(first.submodel)) == &material.GetTextures(TSubModel::VariantId(other.submodel))); } void scene_walk::Expire_Indirect_Records() { if ((m_framestamp % 256) == 0) { // the sleeper records of tracks out of sight for a while. their places in the record buffer of the gpu culling are // dropped when it starts over for (auto records{m_sleeperrecords.begin()}; records != m_sleeperrecords.end();) { if (m_framestamp - records->second.frame > 256) { records = m_sleeperrecords.erase(records); } else { ++records; } } // and so are the records of the models drawn on their own for (auto records{m_modelrecords.begin()}; records != m_modelrecords.end();) { if (m_framestamp - records->second.frame > 256) { records = m_modelrecords.erase(records); } else { ++records; } } } } void scene_walk::Clear_Indirect_Records() { m_sleeperrecords.clear(); m_modelrecords.clear(); m_cascaderecords.clear(); m_walkrecords.clear(); } } // namespace gfx