From 1a1564fcb85fb67e23bee6c21f0207d333c98477 Mon Sep 17 00:00:00 2001 From: Dante Harper Date: Sun, 1 Nov 2026 17:56:35 +0100 Subject: [PATCH 1/6] shader thing a --- .../renderer/shaders/shader_compiler.cpp | 42 +++++++++---------- flake.lock | 6 +-- 2 files changed, 22 insertions(+), 26 deletions(-) diff --git a/engine/src/toast/renderer/shaders/shader_compiler.cpp b/engine/src/toast/renderer/shaders/shader_compiler.cpp index 565e3ee09..7629db828 100644 --- a/engine/src/toast/renderer/shaders/shader_compiler.cpp +++ b/engine/src/toast/renderer/shaders/shader_compiler.cpp @@ -142,7 +142,7 @@ void ShaderCompiler::setMaxParallelCompiles(size_t count) { auto ShaderCompiler::featureHash() -> uint64_t { // Debug compiles unoptimized with debug info so a release entry in the shared cache must read as stale and the reverse #if defined(NDEBUG) - const uint64_t k_build_profile = full_optimization.load(std::memory_order_relaxed) ? 0x0ull : 0x0f7d1e5ca1ab1e00ull; + const uint64_t k_build_profile = full_optimization.load(std::memory_order_relaxed) ? 0x0ull : 0x0f7d1e5ca1ab1e01ull; #else constexpr uint64_t k_build_profile = 0xd3b09c0de5a11e01ull; #endif @@ -167,35 +167,31 @@ static auto createSession(slang::IGlobalSession* global_session) -> Slang::ComPt std::array slang_targets {target}; std::vector compiler_options; - -#if !defined(NDEBUG) - TOAST_INFO("Render", "Configuring Slang for DEBUG: Optimizations disabled, debug symbols enabled."); - - slang::CompilerOptionEntry opt_entry {}; - opt_entry.name = slang::CompilerOptionName::Optimization; - opt_entry.value.kind = slang::CompilerOptionValueKind::Int; - opt_entry.value.intValue0 = SlangOptimizationLevel::SLANG_OPTIMIZATION_LEVEL_NONE; - - compiler_options.push_back(opt_entry); - - slang::CompilerOptionEntry dbg_entry {}; - dbg_entry.name = slang::CompilerOptionName::DebugInformation; - dbg_entry.value.kind = slang::CompilerOptionValueKind::Int; - dbg_entry.value.intValue0 = SlangDebugInfoLevel::SLANG_DEBUG_INFO_LEVEL_MAXIMAL; - compiler_options.push_back(dbg_entry); - +#if defined(NDEBUG) + const bool debug_shaders = !full_optimization.load(std::memory_order_relaxed); #else - const bool full = full_optimization.load(std::memory_order_relaxed); - TOAST_TRACE("Render", "Configuring Slang for RELEASE with {} optimization", full ? "maximal" : "default"); + constexpr bool debug_shaders = true; +#endif + TOAST_TRACE( + "Render", + "Configuring Slang with {} optimization and {} debug information", + debug_shaders ? "no" : "maximal", + debug_shaders ? "maximal" : "no" + ); slang::CompilerOptionEntry opt_entry {}; opt_entry.name = slang::CompilerOptionName::Optimization; opt_entry.value.kind = slang::CompilerOptionValueKind::Int; - opt_entry.value.intValue0 = - full ? SlangOptimizationLevel::SLANG_OPTIMIZATION_LEVEL_MAXIMAL : SlangOptimizationLevel::SLANG_OPTIMIZATION_LEVEL_DEFAULT; + opt_entry.value.intValue0 = debug_shaders ? SLANG_OPTIMIZATION_LEVEL_NONE : SLANG_OPTIMIZATION_LEVEL_MAXIMAL; compiler_options.push_back(opt_entry); -#endif + if (debug_shaders) { + slang::CompilerOptionEntry dbg_entry {}; + dbg_entry.name = slang::CompilerOptionName::DebugInformation; + dbg_entry.value.kind = slang::CompilerOptionValueKind::Int; + dbg_entry.value.intValue0 = SlangDebugInfoLevel::SLANG_DEBUG_INFO_LEVEL_MAXIMAL; + compiler_options.push_back(dbg_entry); + } // Always defined to 0 or 1 so shaders can #if TOAST_RAY_QUERY slang::CompilerOptionEntry ray_query_entry {}; diff --git a/flake.lock b/flake.lock index 2899d8110..908bdd143 100644 --- a/flake.lock +++ b/flake.lock @@ -2,11 +2,11 @@ "nodes": { "nixpkgs": { "locked": { - "lastModified": 1792094642, - "narHash": "sha256-zp8ToDMnqZcu4o4IH9pdwXFXGp7yLm/M0dEnNWnyd7I=", + "lastModified": 1793110635, + "narHash": "sha256-dhoW6VgFWlaodSY5jrBn+CF2FLmaUvDVgM7+C7T2+NM=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "fe13242281a67641c0102cdb1087f0ebe586c7df", + "rev": "31ecbaca1bdefa45dffc409f0250bd8039ea0010", "type": "github" }, "original": { From 43834cc725fa275d27b808de3a1ee4bc75e03213 Mon Sep 17 00:00:00 2001 From: Dante Harper Date: Tue, 3 Nov 2026 19:26:45 +0100 Subject: [PATCH 2/6] fixed thread issue with physics system --- engine/src/toast/physics/broad_phase.hpp | 5 + engine/src/toast/physics/overlap.cpp | 26 ++--- engine/src/toast/physics/query_snapshot.hpp | 76 ++++++++++++ engine/src/toast/physics/raycast.cpp | 36 ++++-- engine/src/toast/physics/raycast.hpp | 1 + engine/src/toast/physics/shape_query.cpp | 122 ++++++++++++++++++++ engine/src/toast/physics/simulator.cpp | 58 ++++++++++ engine/src/toast/physics/simulator.hpp | 8 ++ engine/src/toast/voxel/brick_pool.cpp | 26 ++++- engine/src/toast/voxel/brick_pool.hpp | 11 +- engine/src/toast/voxel/nodes/voxel_node.cpp | 2 + engine/src/toast/voxel/voxel_volume.cpp | 22 ++-- tests/physics/02-query_snapshot.cpp | 74 ++++++++++++ 13 files changed, 425 insertions(+), 42 deletions(-) create mode 100644 engine/src/toast/physics/query_snapshot.hpp create mode 100644 tests/physics/02-query_snapshot.cpp diff --git a/engine/src/toast/physics/broad_phase.hpp b/engine/src/toast/physics/broad_phase.hpp index 2b925ad82..9b0a6429f 100644 --- a/engine/src/toast/physics/broad_phase.hpp +++ b/engine/src/toast/physics/broad_phase.hpp @@ -69,6 +69,11 @@ class BroadPhase { [[nodiscard]] auto stats() const -> const BroadPhaseStats&; + [[nodiscard]] + auto snapshotTree() const -> AABBTree { + return m_tree; + } + private: struct ShapeLeaf { TreeNodeID node = null_node; diff --git a/engine/src/toast/physics/overlap.cpp b/engine/src/toast/physics/overlap.cpp index 7458f5a70..ce74ada55 100644 --- a/engine/src/toast/physics/overlap.cpp +++ b/engine/src/toast/physics/overlap.cpp @@ -1,3 +1,4 @@ +#include "query_snapshot.hpp" #include "raycast.hpp" #include "toast/physics/shape_query.hpp" #include "toast/physics/simulator.hpp" @@ -10,30 +11,17 @@ namespace physics { auto sphereOverlap(glm::vec3 position, float radius, std::optional layers) -> std::optional { ZoneScoped; - Simulator* simulator = Simulator::current(); - if (simulator == nullptr || !std::isfinite(radius) || radius <= 0.0f || !std::isfinite(position.x) || - !std::isfinite(position.y) || !std::isfinite(position.z)) { + const auto snapshot = Simulator::querySnapshot(); + if (!snapshot || !std::isfinite(radius) || radius <= 0.0f || !std::isfinite(position.x) || !std::isfinite(position.y) || + !std::isfinite(position.z)) { return std::nullopt; } - - std::vector contacts; - if (!simulator->overlapSphere(SphereShape {.radius = radius}, position, 0.0f, contacts, layers)) { - return std::nullopt; - } - - const QueryContact& deepest = *std::ranges::max_element(contacts, {}, &QueryContact::penetration); - return SphereHit { - .node = Simulator::nodeFor(deepest.body), - .position = deepest.point, - .normal = deepest.normal, - .penetration = deepest.penetration, - }; + return snapshotSphereOverlap(*snapshot, position, radius, layers); } auto overlapAABB(glm::vec3 min, glm::vec3 max) -> bool { - Simulator* simulator = Simulator::current(); - - return simulator->overlapAABB(AABB {.min = min, .max = max}); + const auto snapshot = Simulator::querySnapshot(); + return snapshot && snapshotAABBOverlap(*snapshot, AABB {.min = min, .max = max}); } } diff --git a/engine/src/toast/physics/query_snapshot.hpp b/engine/src/toast/physics/query_snapshot.hpp new file mode 100644 index 000000000..4c2bd6c5b --- /dev/null +++ b/engine/src/toast/physics/query_snapshot.hpp @@ -0,0 +1,76 @@ +#pragma once + +#include "aabb_tree.hpp" +#include "body.hpp" +#include "raycast.hpp" +#include "shape.hpp" +#include "voxel_shape_data.hpp" + +#include +#include +#include +#include + +namespace physics { + +/// All data used by gameplay collision queries from one completed physics tick. +struct QuerySnapshot { + AABBTree tree; + std::vector bodies; + std::vector shapes; + std::vector voxels; + std::vector> collider_nodes; + std::vector> body_nodes; + float fat_margin = 0.0f; + uint64_t tick = 0; + + template + void raycast(glm::vec3 origin, glm::vec3 inv_dir, float max_distance, Visit&& visit) const { + tree.raycast(origin, inv_dir, max_distance, std::forward(visit)); + } + + [[nodiscard]] + auto tryGetShape(ShapeID id) const -> const Shape* { + if (id.slot >= shapes.size()) { + return nullptr; + } + const ShapeSlot& slot = shapes[id.slot]; + return slot.occupied && slot.generation == id.generation ? &slot.shape : nullptr; + } + + [[nodiscard]] + auto tryGetBody(BodyID id) const -> const Body* { + if (id.slot >= bodies.size()) { + return nullptr; + } + const BodySlot& slot = bodies[id.slot]; + return slot.occupied && slot.generation == id.generation ? &slot.body : nullptr; + } + + [[nodiscard]] + auto tryGetVoxelDataConst(VoxelDataID id) const -> const VoxelShapeData* { + if (id.slot >= voxels.size()) { + return nullptr; + } + const VoxelShapeSlot& slot = voxels[id.slot]; + return slot.data && slot.generation == id.generation ? &*slot.data : nullptr; + } + + [[nodiscard]] + auto colliderFor(ShapeID id) const -> toast::Box { + return id.slot < collider_nodes.size() ? collider_nodes[id.slot] : toast::Box {}; + } + + [[nodiscard]] + auto nodeFor(BodyID id) const -> toast::Box { + return id.slot < body_nodes.size() ? body_nodes[id.slot] : toast::Box {}; + } +}; + +[[nodiscard]] +auto snapshotSphereOverlap(const QuerySnapshot& snapshot, glm::vec3 position, float radius, std::optional layers) + -> std::optional; +[[nodiscard]] +auto snapshotAABBOverlap(const QuerySnapshot& snapshot, const AABB& bounds) -> bool; + +} diff --git a/engine/src/toast/physics/raycast.cpp b/engine/src/toast/physics/raycast.cpp index 34ee42feb..d63cdb8cd 100644 --- a/engine/src/toast/physics/raycast.cpp +++ b/engine/src/toast/physics/raycast.cpp @@ -1,4 +1,5 @@ #include "nodes/rigidbody.hpp" +#include "query_snapshot.hpp" #include "toast/physics/broad_phase.hpp" #include "toast/physics/shape.hpp" #include "toast/physics/simulator.hpp" @@ -11,6 +12,7 @@ #include #include #include +#include #include #include @@ -172,19 +174,24 @@ namespace physics { template auto Simulator::castRay(const RayQuery& query, OnContact&& on_contact) const -> uint32_t { + return castRayUsing(*this, query, std::forward(on_contact)); +} + +template +auto Simulator::castRayUsing(const Source& source, const RayQuery& query, OnContact&& on_contact) -> uint32_t { const glm::vec3 inverse_direction = 1.0f / query.direction; float clip = query.max_distance; uint32_t tested = 0; - m_broad_phase.raycast(query.origin, inverse_direction, clip, [&](ShapeID shape_id, float) -> float { - const Shape* shape = tryGetShape(shape_id); + const auto visit = [&](ShapeID shape_id, float) -> float { + const Shape* shape = source.tryGetShape(shape_id); if (shape == nullptr || not shape->enabled || shape->owner == query.ignored) { return clip; } if (query.voxels_only && shape->type != ShapeType::voxel) { return clip; } - const Body* body = tryGetBody(shape->owner); + const Body* body = source.tryGetBody(shape->owner); if (body == nullptr || not body->enabled || (body->layers.included & query.layers) == 0) { return clip; } @@ -221,7 +228,7 @@ auto Simulator::castRay(const RayQuery& query, OnContact&& on_contact) const -> report_local(raycastCapsule(shape->capsule.radius, shape->capsule.height, local_origin, local_direction, clip)); break; case ShapeType::voxel: { - const VoxelShapeData* data = tryGetVoxelDataConst(shape->voxel.data); + const VoxelShapeData* data = source.tryGetVoxelDataConst(shape->voxel.data); if (data == nullptr || data->volume == nullptr) { break; } @@ -242,7 +249,12 @@ auto Simulator::castRay(const RayQuery& query, OnContact&& on_contact) const -> } } return clip; - }); + }; + if constexpr (std::is_same_v) { + source.m_broad_phase.raycast(query.origin, inverse_direction, clip, visit); + } else { + source.raycast(query.origin, inverse_direction, clip, visit); + } return tested; } @@ -313,13 +325,14 @@ auto Simulator::raycast(glm::vec3 origin, glm::vec3 direction, float max_distanc -> std::optional { ZoneScopedN("physics::Raycast"); const std::optional query = makeRayQuery(origin, direction, max_distance, ignored, layers); - if (instance == nullptr || not query) { + const auto snapshot = querySnapshot(); + if (not snapshot || not query) { return std::nullopt; } // Only nearest contact matters std::optional nearest; - const uint32_t tested = instance->castRay(*query, [&](const RayContact& contact) { + const uint32_t tested = castRayUsing(*snapshot, *query, [&](const RayContact& contact) { if (not nearest || contact.distance < nearest->distance) { nearest = contact; } @@ -330,7 +343,7 @@ auto Simulator::raycast(glm::vec3 origin, glm::vec3 direction, float max_distanc if (not nearest) { return std::nullopt; } - return makeRayHit(*query, *nearest, colliderFor(nearest->body, nearest->shape)); + return makeRayHit(*query, *nearest, snapshot->colliderFor(nearest->shape)); } auto Simulator::linecast(glm::vec3 from, glm::vec3 to, int max_hits, BodyID ignored, uint32_t layers) -> std::vector { @@ -345,7 +358,8 @@ auto Simulator::raymulticast( ) -> std::vector { ZoneScopedN("physics::RayMulticast"); const std::optional query = makeRayQuery(origin, direction, max_distance, ignored, layers); - if (instance == nullptr || not query || max_hits == 0) { + const auto snapshot = querySnapshot(); + if (not snapshot || not query || max_hits == 0) { return {}; } @@ -357,7 +371,7 @@ auto Simulator::raymulticast( contacts.reserve(limit + 1); } - const uint32_t tested = instance->castRay(*query, [&](const RayContact& contact) { + const uint32_t tested = castRayUsing(*snapshot, *query, [&](const RayContact& contact) { contacts.push_back(contact); if (not limited) { return query->max_distance; @@ -380,7 +394,7 @@ auto Simulator::raymulticast( for (const RayContact& contact : contacts) { auto known = std::ranges::find(nodes, contact.shape, &std::pair>::first); if (known == nodes.end()) { - nodes.emplace_back(contact.shape, colliderFor(contact.body, contact.shape)); + nodes.emplace_back(contact.shape, snapshot->colliderFor(contact.shape)); known = nodes.end() - 1; } hits.push_back(makeRayHit(*query, contact, known->second)); diff --git a/engine/src/toast/physics/raycast.hpp b/engine/src/toast/physics/raycast.hpp index cea332f45..4a9062b17 100644 --- a/engine/src/toast/physics/raycast.hpp +++ b/engine/src/toast/physics/raycast.hpp @@ -58,6 +58,7 @@ struct SphereHit { glm::vec3 position; glm::vec3 normal; float penetration; + ShapeType shape_type = ShapeType::sphere; }; /// The optional layer filter limits which bodies can overlap the sphere. diff --git a/engine/src/toast/physics/shape_query.cpp b/engine/src/toast/physics/shape_query.cpp index 2af099e96..bfc0954d6 100644 --- a/engine/src/toast/physics/shape_query.cpp +++ b/engine/src/toast/physics/shape_query.cpp @@ -1,6 +1,7 @@ #include "broad_phase.hpp" #include "manifold.hpp" #include "physics_settings.hpp" +#include "query_snapshot.hpp" #include "simulator.hpp" #include "voxel_query.hpp" @@ -145,6 +146,127 @@ auto Simulator::current() -> Simulator* { return instance; } +auto snapshotSphereOverlap(const QuerySnapshot& snapshot, glm::vec3 position, float radius, std::optional layers) + -> std::optional { + const Shape probe_shape = sphereProbe(SphereShape {.radius = radius}); + const Body probe_body = probeBody(position, glm::quat(1.0f, 0.0f, 0.0f, 0.0f)); + const AABB bounds = worldShapeBounds(probe_body, probe_shape).expanded(snapshot.fat_margin); + std::optional deepest; + std::vector manifolds; + for (ShapeID id : snapshot.tree.query(bounds)) { + const Shape* shape = snapshot.tryGetShape(id); + const Body* body = shape ? snapshot.tryGetBody(shape->owner) : nullptr; + if (!shape || !body || !shape->enabled || !body->enabled || (layers && !layersCollide(*layers, body->layers))) { + continue; + } + const BroadPhasePair pair { + .a = {}, + .b = {.body = shape->owner, .shape = id} + }; + const CollisionElement probe {probe_shape, probe_body}; + const CollisionElement other {*shape, *body}; + manifolds.clear(); + switch (shape->type) { + case ShapeType::sphere: + if (auto m = collideSpheres(pair, probe, other)) { + manifolds.push_back(*m); + } + break; + case ShapeType::box: + if (auto m = collideSphereBox(pair, probe, other)) { + manifolds.push_back(*m); + } + break; + case ShapeType::capsule: + if (auto m = collideSphereCapsule(pair, probe, other)) { + manifolds.push_back(*m); + } + break; + case ShapeType::voxel: + if (const auto* data = snapshot.tryGetVoxelDataConst(shape->voxel.data); data && data->volume) { + collideSphereVoxel(pair, probe, other, *data, manifolds); + } + break; + } + for (const Manifold& manifold : manifolds) { + for (const ContactPoint& point : std::span {manifold.contacts}.first(manifold.contact_count)) { + if (point.penetration > 0.0f && (!deepest || point.penetration > deepest->penetration)) { + deepest = QueryContact { + .body = shape->owner, + .shape = id, + .normal = -manifold.normal, + .point = point.position, + .penetration = point.penetration + }; + } + } + } + } + if (!deepest) { + return std::nullopt; + } + const Shape* shape = snapshot.tryGetShape(deepest->shape); + return SphereHit { + .node = snapshot.nodeFor(deepest->body), + .position = deepest->point, + .normal = deepest->normal, + .penetration = deepest->penetration, + .shape_type = shape->type + }; +} + +auto snapshotAABBOverlap(const QuerySnapshot& snapshot, const AABB& bounds) -> bool { + const glm::vec3 size = bounds.max - bounds.min; + if (!std::isfinite(bounds.min.x) || !std::isfinite(bounds.min.y) || !std::isfinite(bounds.min.z) || + !std::isfinite(bounds.max.x) || !std::isfinite(bounds.max.y) || !std::isfinite(bounds.max.z) || size.x < 0.0f || + size.y < 0.0f || size.z < 0.0f) { + return false; + } + const Shape probe_shape = boxProbe(bounds); + const Body probe_body = probeBody((bounds.min + bounds.max) * 0.5f, glm::quat(1.0f, 0.0f, 0.0f, 0.0f)); + std::vector manifolds; + for (ShapeID id : snapshot.tree.query(bounds.expanded(snapshot.fat_margin))) { + const Shape* shape = snapshot.tryGetShape(id); + const Body* body = shape ? snapshot.tryGetBody(shape->owner) : nullptr; + if (!shape || !body || !shape->enabled || !body->enabled) { + continue; + } + const BroadPhasePair pair { + .a = {}, + .b = {.body = shape->owner, .shape = id} + }; + const CollisionElement probe {probe_shape, probe_body}; + const CollisionElement other {*shape, *body}; + manifolds.clear(); + switch (shape->type) { + case ShapeType::sphere: + if (auto m = collideSphereBox(pair, other, probe)) { + manifolds.push_back(*m); + } + break; + case ShapeType::box: + if (auto m = collideBoxes(pair, probe, other)) { + manifolds.push_back(*m); + } + break; + case ShapeType::capsule: + if (auto m = collideCapsuleBox(pair, other, probe)) { + manifolds.push_back(*m); + } + break; + case ShapeType::voxel: + if (const auto* data = snapshot.tryGetVoxelDataConst(shape->voxel.data); data && data->volume) { + collideBoxVoxel(pair, probe, other, *data, manifolds); + } + break; + } + if (std::ranges::any_of(manifolds, [](const Manifold& m) { return m.contact_count != 0; })) { + return true; + } + } + return false; +} + auto Simulator::queryCandidates(BodyID ignored, const AABB& bounds, std::optional layers) const -> std::vector { std::vector result; diff --git a/engine/src/toast/physics/simulator.cpp b/engine/src/toast/physics/simulator.cpp index 74efba273..862a615c3 100644 --- a/engine/src/toast/physics/simulator.cpp +++ b/engine/src/toast/physics/simulator.cpp @@ -13,6 +13,7 @@ #include "nodes/sphere_collider.hpp" #include "nodes/trigger.hpp" #include "physics_settings.hpp" +#include "query_snapshot.hpp" #include "shatter.hpp" #include "toast/physics/body.hpp" #include "voxel_data_lock.hpp" @@ -238,6 +239,7 @@ Simulator::Simulator() { TOAST_ASSERT(not instance, "Physics", "Simulator can only be created once"); TOAST_INFO("Physics", "Simulator created"); instance = this; + published_query_snapshot.store(std::make_shared(), std::memory_order_release); m_listener.subscribe([this](event::VoxelPaletteAssetReloaded& e) { refreshPalette(e.uid.data()); @@ -283,6 +285,7 @@ void Simulator::resetWind() { Simulator::~Simulator() { TOAST_INFO("Physics", "Simulator destroyed"); + published_query_snapshot.store(nullptr, std::memory_order_release); clearDebris(); instance = nullptr; // Finish all requests before destroying @@ -292,6 +295,60 @@ Simulator::~Simulator() { all.pending.store(0, std::memory_order_release); } +auto Simulator::querySnapshot() -> std::shared_ptr { + return published_query_snapshot.load(std::memory_order_acquire); +} + +void Simulator::publishQuerySnapshot() { + ZoneScopedN("physics::PublishQuerySnapshot"); + auto snapshot = std::make_shared(); + snapshot->tree = m_broad_phase.snapshotTree(); + snapshot->bodies = m_bodies; + snapshot->shapes = m_shapes; + snapshot->fat_margin = tunables().broadphase_fat_margin; + snapshot->tick = m_tick_index; + snapshot->collider_nodes.resize(m_shapes.size()); + snapshot->body_nodes.resize(m_bodies.size()); + { + std::scoped_lock voxel_lock {voxelDataMutex()}; + snapshot->voxels.reserve(m_voxel_shapes.size()); + for (const VoxelShapeSlot& source : m_voxel_shapes) { + VoxelShapeSlot copy; + copy.generation = source.generation; + if (source.data) { + VoxelShapeData& data = copy.data.emplace(); + data.surface = source.data->surface; + data.palette = source.data->palette; + data.materials = source.data->materials; + if (source.data->volume) { + data.owned_volume = std::make_unique(voxel::Volume::instanceOf(*source.data->volume)); + data.volume = data.owned_volume.get(); + } + } + snapshot->voxels.push_back(std::move(copy)); + } + } + for (const NodeBinding& binding : m_node_bindings) { + if (binding.body.slot < snapshot->body_nodes.size()) { + snapshot->body_nodes[binding.body.slot] = binding.node; + } + for (const ColliderBinding& collider : binding.colliders) { + if (collider.shape.slot < snapshot->collider_nodes.size()) { + snapshot->collider_nodes[collider.shape.slot] = collider.node; + } + } + } + for (const VoxelNodeBinding& binding : m_voxel_bindings) { + if (binding.body.slot < snapshot->body_nodes.size()) { + snapshot->body_nodes[binding.body.slot] = binding.node; + } + if (binding.shape.slot < snapshot->collider_nodes.size()) { + snapshot->collider_nodes[binding.shape.slot] = binding.node; + } + } + published_query_snapshot.store(std::move(snapshot), std::memory_order_release); +} + void Simulator::registerRigidbody(Rigidbody& node) { ZoneScopedN("physics::RegisterRigidbody"); @@ -780,6 +837,7 @@ void Simulator::tick() { publishTransforms(); publishJoints(); publishVoxelRenderRecords(); + publishQuerySnapshot(); m_profile.tick_ms = elapsed_ms(tick_start, std::chrono::steady_clock::now()); publishProfile(islands); FrameMarkNamed("PhysicsStep"); diff --git a/engine/src/toast/physics/simulator.hpp b/engine/src/toast/physics/simulator.hpp index 7e38767ae..7a0dad9be 100644 --- a/engine/src/toast/physics/simulator.hpp +++ b/engine/src/toast/physics/simulator.hpp @@ -29,6 +29,7 @@ #include #include #include +#include #include #include #include @@ -53,6 +54,7 @@ class Joint3D; struct SimulatorTestAccess; struct SmashVolume; struct SmashPieces; +struct QuerySnapshot; } @@ -121,6 +123,8 @@ class TOAST_API Simulator { ) -> std::vector; static auto linecast(glm::vec3 from, glm::vec3 to, int max_hits = -1, BodyID ignored = {}, uint32_t layers = k_all_layers) -> std::vector; + [[nodiscard]] + static auto querySnapshot() -> std::shared_ptr; [[nodiscard]] static auto bodyFor(const toast::Node& node) -> BodyID; @@ -454,6 +458,8 @@ class TOAST_API Simulator { template auto castRay(const RayQuery& query, OnContact&& on_contact) const -> uint32_t; + template + static auto castRayUsing(const Source& source, const RayQuery& query, OnContact&& on_contact) -> uint32_t; /// @returns true if a static body is within @p distance of @p position along @p toward [[nodiscard]] @@ -706,6 +712,7 @@ class TOAST_API Simulator { ) const; inline static Simulator* instance = nullptr; + inline static std::atomic> published_query_snapshot; std::mutex m_wind_mutex; std::optional m_level_wind; @@ -785,6 +792,7 @@ class TOAST_API Simulator { uint64_t m_next_fragment_sequence = 1; uint64_t m_tick_index = 0; + void publishQuerySnapshot(); std::vector m_fragments; std::unordered_map m_fragment_index; std::vector m_pending_fragments; diff --git a/engine/src/toast/voxel/brick_pool.cpp b/engine/src/toast/voxel/brick_pool.cpp index 6f5dd1c7a..91912cc16 100644 --- a/engine/src/toast/voxel/brick_pool.cpp +++ b/engine/src/toast/voxel/brick_pool.cpp @@ -27,8 +27,9 @@ constexpr auto headTag(uint64_t head) noexcept -> uint32_t { BrickPool::BrickPool(uint32_t capacity) : m_capacity(capacity), m_free_head(packHead(k_invalid_brick, 0)), - m_next_free(std::make_unique[]>(capacity)), // NOLINT(modernize-avoid-c-arrays) - m_is_free(std::make_unique[]>(capacity)) { // NOLINT(modernize-avoid-c-arrays) + m_next_free(std::make_unique[]>(capacity)), // NOLINT(modernize-avoid-c-arrays) + m_is_free(std::make_unique[]>(capacity)), // NOLINT(modernize-avoid-c-arrays) + m_references(std::make_unique[]>(capacity)) { // NOLINT(modernize-avoid-c-arrays) assert(capacity < k_invalid_brick); // allocate clears every brick @@ -36,6 +37,9 @@ BrickPool::BrickPool(uint32_t capacity) static_cast(capacity) * k_brick_material_bytes ); // NOLINT(modernize-avoid-c-arrays) m_occupancy.assign(capacity, BrickOccupancy {}); + for (uint32_t id = 0; id < capacity; ++id) { + m_references[id].store(0, std::memory_order_relaxed); + } } auto BrickPool::allocate() -> uint32_t { @@ -45,6 +49,7 @@ auto BrickPool::allocate() -> uint32_t { const uint32_t next = m_next_free[id].load(std::memory_order_relaxed); if (m_free_head.compare_exchange_weak(head, packHead(next, headTag(head) + 1), std::memory_order_acquire)) { m_is_free[id].store(false, std::memory_order_relaxed); + m_references[id].store(1, std::memory_order_release); m_allocated.fetch_add(1, std::memory_order_relaxed); clearBrick(id); return id; @@ -59,12 +64,18 @@ auto BrickPool::allocate() -> uint32_t { } while (!m_next_unused.compare_exchange_weak(unused, unused + 1, std::memory_order_acq_rel)); m_allocated.fetch_add(1, std::memory_order_relaxed); + m_references[unused].store(1, std::memory_order_release); clearBrick(unused); return unused; } void BrickPool::free(uint32_t id) { assert(isValid(id)); + const uint32_t previous = m_references[id].fetch_sub(1, std::memory_order_acq_rel); + assert(previous != 0 && "brick released without an owner"); + if (previous != 1) { + return; + } assert(!m_is_free[id].exchange(true, std::memory_order_relaxed) && "brick freed twice"); uint64_t head = m_free_head.load(std::memory_order_relaxed); @@ -74,6 +85,17 @@ void BrickPool::free(uint32_t id) { m_allocated.fetch_sub(1, std::memory_order_relaxed); } +void BrickPool::retain(uint32_t id) { + assert(isValid(id)); + const uint32_t previous = m_references[id].fetch_add(1, std::memory_order_acq_rel); + assert(previous != 0 && "cannot retain a freed brick"); +} + +auto BrickPool::referenceCount(uint32_t id) const noexcept -> uint32_t { + assert(isValid(id)); + return m_references[id].load(std::memory_order_acquire); +} + void BrickPool::clearBrick(uint32_t id) { assert(id < m_capacity); const size_t base = static_cast(id) * k_brick_material_bytes; diff --git a/engine/src/toast/voxel/brick_pool.hpp b/engine/src/toast/voxel/brick_pool.hpp index b001d6004..1ba3140ec 100644 --- a/engine/src/toast/voxel/brick_pool.hpp +++ b/engine/src/toast/voxel/brick_pool.hpp @@ -33,6 +33,10 @@ class TOAST_API BrickPool { /// @brief The id must come from allocate and not already be freed void free(uint32_t id); + /// Retain a brick for another volume or a query snapshot. + void retain(uint32_t id); + [[nodiscard]] + auto referenceCount(uint32_t id) const noexcept -> uint32_t; [[nodiscard]] auto material(uint32_t id) -> std::span; @@ -76,9 +80,10 @@ class TOAST_API BrickPool { std::atomic m_allocated {0}; std::atomic m_free_head; - std::unique_ptr[]> m_next_free; // NOLINT(modernize-avoid-c-arrays) - std::unique_ptr[]> m_is_free; // NOLINT(modernize-avoid-c-arrays) - std::unique_ptr m_material; // NOLINT(modernize-avoid-c-arrays) + std::unique_ptr[]> m_next_free; // NOLINT(modernize-avoid-c-arrays) + std::unique_ptr[]> m_is_free; // NOLINT(modernize-avoid-c-arrays) + std::unique_ptr[]> m_references; // NOLINT(modernize-avoid-c-arrays) + std::unique_ptr m_material; // NOLINT(modernize-avoid-c-arrays) std::vector m_occupancy; }; diff --git a/engine/src/toast/voxel/nodes/voxel_node.cpp b/engine/src/toast/voxel/nodes/voxel_node.cpp index 7143ab4f1..b56f05e98 100644 --- a/engine/src/toast/voxel/nodes/voxel_node.cpp +++ b/engine/src/toast/voxel/nodes/voxel_node.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include #include @@ -397,6 +398,7 @@ void VoxelNode::postPhysics() { template void VoxelNode::edit(const voxel::EditBounds& local_bounds, bool grows, std::string_view operation, Kernel&& kernel) { + std::scoped_lock voxel_lock {physics::voxelDataMutex()}; if (usesParts()) { if (!m_warned_part_edit) { m_warned_part_edit = true; diff --git a/engine/src/toast/voxel/voxel_volume.cpp b/engine/src/toast/voxel/voxel_volume.cpp index a79e9c9d9..fb40e55b0 100644 --- a/engine/src/toast/voxel/voxel_volume.cpp +++ b/engine/src/toast/voxel/voxel_volume.cpp @@ -85,7 +85,8 @@ auto Volume::instanceOf(const Volume& source) -> Volume { out.m_texture_origin = source.m_texture_origin; out.m_entries = source.m_entries; for (BrickEntry& entry : out.m_entries) { - if (entry.tag() == BrickTag::owned) { + if (entry.isPooled()) { + source.m_pool->retain(entry.payload()); entry = BrickEntry::make(BrickTag::shared, entry.payload()); } } @@ -110,6 +111,9 @@ auto Volume::adoptResized(Volume& source, glm::ivec3 brick_offset, glm::uvec3 br } out.m_entries[out.entryIndex(target)] = entry; + if (entry.isPooled()) { + source.m_pool->retain(entry.payload()); + } if (entry.tag() == BrickTag::owned) { entry = BrickEntry::make(BrickTag::shared, entry.payload()); } @@ -122,7 +126,7 @@ void Volume::releaseOwned() { return; } for (BrickEntry& entry : m_entries) { - if (entry.tag() == BrickTag::owned) { + if (entry.isPooled()) { m_pool->free(entry.payload()); entry = BrickEntry {}; } @@ -193,7 +197,7 @@ auto Volume::neighbourhoodOf(glm::ivec3 brick) const noexcept -> BrickNeighbourh auto Volume::makeWritable(uint32_t entry_index) -> uint32_t { const BrickEntry entry = m_entries[entry_index]; - if (entry.tag() == BrickTag::owned) { + if (entry.tag() == BrickTag::owned && m_pool->referenceCount(entry.payload()) == 1) { return entry.payload(); } @@ -210,6 +214,7 @@ auto Volume::makeWritable(uint32_t entry_index) -> uint32_t { m_pool->occupancy(id) = k_full_brick; break; } + case BrickTag::owned: case BrickTag::shared: { const uint32_t source_id = entry.payload(); std::span source_bytes = m_pool->material(source_id); @@ -221,6 +226,9 @@ auto Volume::makeWritable(uint32_t entry_index) -> uint32_t { default: break; } + if (entry.isPooled()) { + m_pool->free(entry.payload()); + } m_entries[entry_index] = BrickEntry::make(BrickTag::owned, id); return id; } @@ -274,7 +282,7 @@ void Volume::setBrickUniform(glm::ivec3 brick, uint8_t material) { if (m_entries[index] == next) { return; } - if (m_entries[index].tag() == BrickTag::owned) { + if (m_entries[index].isPooled()) { m_pool->free(m_entries[index].payload()); } @@ -290,7 +298,7 @@ auto Volume::tryCollapseUniform(glm::ivec3 brick) -> bool { const uint32_t index = entryIndex(brick); const BrickEntry entry = m_entries[index]; - if (entry.tag() != BrickTag::owned) { + if (!entry.isPooled()) { return false; } @@ -373,7 +381,7 @@ void Volume::adoptBrick(glm::ivec3 brick, BrickEntry entry) { const uint32_t index = entryIndex(brick); assert(m_entries[index].tag() == BrickTag::empty); - if (m_entries[index].tag() == BrickTag::owned) { + if (m_entries[index].isPooled()) { m_pool->free(m_entries[index].payload()); } @@ -395,7 +403,7 @@ auto Volume::clearVoxels(glm::ivec3 brick, const BrickOccupancy& mask) -> bool { const uint32_t index = entryIndex(brick); if (affected == *occupancy) { - if (m_entries[index].tag() == BrickTag::owned) { + if (m_entries[index].isPooled()) { m_pool->free(m_entries[index].payload()); } m_entries[index] = BrickEntry {}; diff --git a/tests/physics/02-query_snapshot.cpp b/tests/physics/02-query_snapshot.cpp new file mode 100644 index 000000000..d07f3b078 --- /dev/null +++ b/tests/physics/02-query_snapshot.cpp @@ -0,0 +1,74 @@ +#include "test_registry.hpp" +#include "toast/world/world_test_access.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace toast; +using WorldTestAccess = toast::_detail::WorldTestAccess; + +TOAST_TEST_NAMED("physics", "physics/02-query_snapshot_concurrent", test_physics_02_query_snapshot_concurrent) { + WorldTestAccess::initThreadPool(); + auto world = WorldTestAccess::createWorld(); + physics::Simulator simulator; + physics::Simulator::bindToThisThread(); + auto body = WorldTestAccess::createTypedNode(*world, "physics::StaticRigidbody", "body"); + auto collider = WorldTestAccess::createTypedNode(*world, "physics::BoxCollider", "box"); + WorldTestAccess::attachChild(*body, *collider); + auto rigidbody = body.as(); + physics::Simulator::registerRigidbody(*rigidbody); + physics::Simulator::callTick(); + const auto hit = physics::Simulator::raycast({-2.0f, 0.0f, 0.0f}, {1.0f, 0.0f, 0.0f}, 5.0f); + assert(hit.has_value()); + assert(hit->distance > 1.4f && hit->distance < 1.6f); + assert(physics::sphereOverlap({}, 0.2f).has_value()); + assert(physics::overlapAABB(glm::vec3(-0.1f), glm::vec3(0.1f))); + + std::atomic running {true}; + std::thread reader([&] { + while (running.load(std::memory_order_relaxed)) { + (void)physics::Simulator::raycast({-2.0f, 0.0f, 0.0f}, {1.0f, 0.0f, 0.0f}, 5.0f); + (void)physics::sphereOverlap({}, 0.2f); + (void)physics::overlapAABB(glm::vec3(-0.1f), glm::vec3(0.1f)); + } + }); + for (int i = 0; i < 32; ++i) { + physics::Simulator::unregisterRigidbody(*rigidbody); + if (i == 0) { + assert(physics::Simulator::raycast({-2.0f, 0.0f, 0.0f}, {1.0f, 0.0f, 0.0f}, 5.0f)); + } + physics::Simulator::callTick(); + if (i == 0) { + assert(!physics::Simulator::raycast({-2.0f, 0.0f, 0.0f}, {1.0f, 0.0f, 0.0f}, 5.0f)); + } + physics::Simulator::registerRigidbody(*rigidbody); + physics::Simulator::callTick(); + } + running.store(false, std::memory_order_relaxed); + reader.join(); +} + +TOAST_TEST_NAMED("physics", "physics/03-voxel_snapshot_cow", test_physics_03_voxel_snapshot_cow) { + voxel::BrickPool pool(8); + { + voxel::Volume live(pool, {1, 1, 1}); + assert(live.setVoxel({0, 0, 0}, 3).changed); + voxel::Volume snapshot = voxel::Volume::instanceOf(live); + assert(pool.allocatedCount() == 1); + assert(live.setVoxel({0, 0, 0}, 4).changed); + assert(snapshot.materialAt({0, 0, 0}) == 3); + assert(live.materialAt({0, 0, 0}) == 4); + assert(pool.allocatedCount() == 2); + live.setBrickUniform({0, 0, 0}, 5); + assert(snapshot.materialAt({0, 0, 0}) == 3); + assert(pool.allocatedCount() == 1); + } + assert(pool.allocatedCount() == 0); +} From 543030ed30706848ddc3ecdd6445d42fa70f5bee Mon Sep 17 00:00:00 2001 From: Dante Harper <144488170+DanteDogDev@users.noreply.github.com> Date: Wed, 4 Nov 2026 10:43:13 +0100 Subject: [PATCH 3/6] Delete tests/physics/02-query_snapshot.cpp --- tests/physics/02-query_snapshot.cpp | 74 ----------------------------- 1 file changed, 74 deletions(-) delete mode 100644 tests/physics/02-query_snapshot.cpp diff --git a/tests/physics/02-query_snapshot.cpp b/tests/physics/02-query_snapshot.cpp deleted file mode 100644 index d07f3b078..000000000 --- a/tests/physics/02-query_snapshot.cpp +++ /dev/null @@ -1,74 +0,0 @@ -#include "test_registry.hpp" -#include "toast/world/world_test_access.hpp" - -#include -#include -#include -#include -#include -#include -#include -#include -#include - -using namespace toast; -using WorldTestAccess = toast::_detail::WorldTestAccess; - -TOAST_TEST_NAMED("physics", "physics/02-query_snapshot_concurrent", test_physics_02_query_snapshot_concurrent) { - WorldTestAccess::initThreadPool(); - auto world = WorldTestAccess::createWorld(); - physics::Simulator simulator; - physics::Simulator::bindToThisThread(); - auto body = WorldTestAccess::createTypedNode(*world, "physics::StaticRigidbody", "body"); - auto collider = WorldTestAccess::createTypedNode(*world, "physics::BoxCollider", "box"); - WorldTestAccess::attachChild(*body, *collider); - auto rigidbody = body.as(); - physics::Simulator::registerRigidbody(*rigidbody); - physics::Simulator::callTick(); - const auto hit = physics::Simulator::raycast({-2.0f, 0.0f, 0.0f}, {1.0f, 0.0f, 0.0f}, 5.0f); - assert(hit.has_value()); - assert(hit->distance > 1.4f && hit->distance < 1.6f); - assert(physics::sphereOverlap({}, 0.2f).has_value()); - assert(physics::overlapAABB(glm::vec3(-0.1f), glm::vec3(0.1f))); - - std::atomic running {true}; - std::thread reader([&] { - while (running.load(std::memory_order_relaxed)) { - (void)physics::Simulator::raycast({-2.0f, 0.0f, 0.0f}, {1.0f, 0.0f, 0.0f}, 5.0f); - (void)physics::sphereOverlap({}, 0.2f); - (void)physics::overlapAABB(glm::vec3(-0.1f), glm::vec3(0.1f)); - } - }); - for (int i = 0; i < 32; ++i) { - physics::Simulator::unregisterRigidbody(*rigidbody); - if (i == 0) { - assert(physics::Simulator::raycast({-2.0f, 0.0f, 0.0f}, {1.0f, 0.0f, 0.0f}, 5.0f)); - } - physics::Simulator::callTick(); - if (i == 0) { - assert(!physics::Simulator::raycast({-2.0f, 0.0f, 0.0f}, {1.0f, 0.0f, 0.0f}, 5.0f)); - } - physics::Simulator::registerRigidbody(*rigidbody); - physics::Simulator::callTick(); - } - running.store(false, std::memory_order_relaxed); - reader.join(); -} - -TOAST_TEST_NAMED("physics", "physics/03-voxel_snapshot_cow", test_physics_03_voxel_snapshot_cow) { - voxel::BrickPool pool(8); - { - voxel::Volume live(pool, {1, 1, 1}); - assert(live.setVoxel({0, 0, 0}, 3).changed); - voxel::Volume snapshot = voxel::Volume::instanceOf(live); - assert(pool.allocatedCount() == 1); - assert(live.setVoxel({0, 0, 0}, 4).changed); - assert(snapshot.materialAt({0, 0, 0}) == 3); - assert(live.materialAt({0, 0, 0}) == 4); - assert(pool.allocatedCount() == 2); - live.setBrickUniform({0, 0, 0}, 5); - assert(snapshot.materialAt({0, 0, 0}) == 3); - assert(pool.allocatedCount() == 1); - } - assert(pool.allocatedCount() == 0); -} From da564ba1500bfb2b86a731deba297ef723376d71 Mon Sep 17 00:00:00 2001 From: Dante Harper Date: Wed, 4 Nov 2026 11:44:19 +0100 Subject: [PATCH 4/6] fixed shader_compiler again --- .../renderer/shaders/shader_compiler.cpp | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/engine/src/toast/renderer/shaders/shader_compiler.cpp b/engine/src/toast/renderer/shaders/shader_compiler.cpp index 63cb2c335..7bcbb8028 100644 --- a/engine/src/toast/renderer/shaders/shader_compiler.cpp +++ b/engine/src/toast/renderer/shaders/shader_compiler.cpp @@ -132,22 +132,23 @@ void ShaderCompiler::setMaxParallelCompiles(size_t count) { } auto ShaderCompiler::featureHash() -> uint64_t { - // Debug compiles unoptimized with debug info so a release entry in the shared cache must read as stale and the reverse -#if defined(NDEBUG) - const uint64_t k_build_profile = full_optimization.load(std::memory_order_relaxed) ? 0x0ull : 0x0f7d1e5ca1ab1e01ull; +#if defined(__linux__) + constexpr bool is_optimized = false; #else - constexpr uint64_t k_build_profile = 0xd3b09c0de5a11e01ull; + const bool is_optimized = full_optimization.load(std::memory_order_relaxed); #endif - // TOAST_REVERSED_Z changes the SPIR-V so the other convention must read as stale + + const uint64_t k_build_profile = is_optimized ? 0x0ull : 0x0f7d1e5ca1ab1e01ull; constexpr uint64_t k_depth_convention = depth::k_reversed_z ? 0x5eed2e7e45edull : 0x0ull; const uint64_t k_trace_walk = traceRayQuery() ? 0x0ull : 0x7a11c0de5eedull; + #if defined(TOAST_DEV_TOOLS) constexpr uint64_t k_dev_tools = 0xde7700150000ull; #else constexpr uint64_t k_dev_tools = 0x0ull; #endif - return (ShaderCompiler::isRayQueryAvailable() ? 0x9e3779b97f4a7c15ull : 0x0ull) ^ k_build_profile ^ k_depth_convention ^ - k_trace_walk ^ k_dev_tools; + + return (ShaderCompiler::isRayQueryAvailable() ? 0x9e3779b97f4a7c15ull : 0x0ull) ^ k_build_profile ^ k_depth_convention ^ k_trace_walk ^ k_dev_tools; } static auto createSession(slang::IGlobalSession* global_session) -> Slang::ComPtr { @@ -159,10 +160,10 @@ static auto createSession(slang::IGlobalSession* global_session) -> Slang::ComPt std::array slang_targets {target}; std::vector compiler_options; -#if defined(NDEBUG) - const bool debug_shaders = !full_optimization.load(std::memory_order_relaxed); -#else +#if defined(__linux__) constexpr bool debug_shaders = true; +#else + const bool debug_shaders = !full_optimization.load(std::memory_order_relaxed); #endif TOAST_TRACE( "Render", From 203d9f9dc6ee630050a55c16835c07fe49c47b90 Mon Sep 17 00:00:00 2001 From: Xein Date: Wed, 4 Nov 2026 11:56:16 +0100 Subject: [PATCH 5/6] better mutex just in case voxelData is edited multiple times at once --- engine/src/toast/physics/voxel_data_lock.cpp | 2 +- engine/src/toast/physics/voxel_data_lock.hpp | 2 +- engine/src/toast/voxel/runtime_pool.cpp | 4 ++-- engine/src/toast/voxel/runtime_pool.hpp | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/engine/src/toast/physics/voxel_data_lock.cpp b/engine/src/toast/physics/voxel_data_lock.cpp index 3c73b9515..6a659a884 100644 --- a/engine/src/toast/physics/voxel_data_lock.cpp +++ b/engine/src/toast/physics/voxel_data_lock.cpp @@ -4,7 +4,7 @@ namespace physics { -auto voxelDataMutex() -> std::mutex& { +auto voxelDataMutex() -> std::recursive_mutex& { return voxel::runtimePoolMutex(); } diff --git a/engine/src/toast/physics/voxel_data_lock.hpp b/engine/src/toast/physics/voxel_data_lock.hpp index 33034066c..0fed92bce 100644 --- a/engine/src/toast/physics/voxel_data_lock.hpp +++ b/engine/src/toast/physics/voxel_data_lock.hpp @@ -13,6 +13,6 @@ namespace physics { [[nodiscard]] -TOAST_API auto voxelDataMutex() -> std::mutex&; +TOAST_API auto voxelDataMutex() -> std::recursive_mutex&; } diff --git a/engine/src/toast/voxel/runtime_pool.cpp b/engine/src/toast/voxel/runtime_pool.cpp index feb067042..1aadff402 100644 --- a/engine/src/toast/voxel/runtime_pool.cpp +++ b/engine/src/toast/voxel/runtime_pool.cpp @@ -8,9 +8,9 @@ auto runtimeBrickPool() -> BrickPool& { return *pool; } -auto runtimePoolMutex() -> std::mutex& { +auto runtimePoolMutex() -> std::recursive_mutex& { // Leaked like the pool - static std::mutex* mutex = new std::mutex; + static std::recursive_mutex* mutex = new std::recursive_mutex; return *mutex; } diff --git a/engine/src/toast/voxel/runtime_pool.hpp b/engine/src/toast/voxel/runtime_pool.hpp index 14ea55788..1816e9e68 100644 --- a/engine/src/toast/voxel/runtime_pool.hpp +++ b/engine/src/toast/voxel/runtime_pool.hpp @@ -21,6 +21,6 @@ TOAST_API auto runtimeBrickPool() -> BrickPool&; /// Held while physics writes runtime volumes and while the renderer packs them [[nodiscard]] -TOAST_API auto runtimePoolMutex() -> std::mutex&; +TOAST_API auto runtimePoolMutex() -> std::recursive_mutex&; } From 74002b2cbb2178567d720781b9e9fc76c345b462 Mon Sep 17 00:00:00 2001 From: Xein Date: Wed, 4 Nov 2026 12:05:54 +0100 Subject: [PATCH 6/6] clang format --- engine/src/toast/renderer/shaders/shader_compiler.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/engine/src/toast/renderer/shaders/shader_compiler.cpp b/engine/src/toast/renderer/shaders/shader_compiler.cpp index 7bcbb8028..3293ea8b2 100644 --- a/engine/src/toast/renderer/shaders/shader_compiler.cpp +++ b/engine/src/toast/renderer/shaders/shader_compiler.cpp @@ -148,7 +148,8 @@ auto ShaderCompiler::featureHash() -> uint64_t { constexpr uint64_t k_dev_tools = 0x0ull; #endif - return (ShaderCompiler::isRayQueryAvailable() ? 0x9e3779b97f4a7c15ull : 0x0ull) ^ k_build_profile ^ k_depth_convention ^ k_trace_walk ^ k_dev_tools; + return (ShaderCompiler::isRayQueryAvailable() ? 0x9e3779b97f4a7c15ull : 0x0ull) ^ k_build_profile ^ k_depth_convention ^ + k_trace_walk ^ k_dev_tools; } static auto createSession(slang::IGlobalSession* global_session) -> Slang::ComPtr {