From ec2122ab12a4a990956ab104298c12b6f1436c8d Mon Sep 17 00:00:00 2001 From: ecker Date: Tue, 5 May 2026 19:48:18 -0500 Subject: [PATCH] sanity fixes for gltf conversion (and LOD generation), (mostly) repaired streaming in mesh data (barycentric-deferred rendering would desync because of separated indirection buffers), streamed in mesh data picks the correct LOD (mostly working), squashed hidden bug where meshes were constantly destroying/creating the physics mesh and updating buffers --- bin/data/config.json | 6 +- bin/data/shaders/graph/cull/comp.glsl | 3 + engine/inc/uf/ext/meshopt/meshopt.h | 2 +- engine/src/engine/graph/graph.cpp | 125 ++++++++++++++---- engine/src/ext/gltf/gltf.cpp | 22 ++- engine/src/ext/gltf/processPrimitives.inl | 6 + engine/src/ext/meshopt/meshopt.cpp | 16 +-- engine/src/utils/io/file.cpp | 13 +- .../src/utils/math/physics/broadphase/bvh.cpp | 2 +- engine/src/utils/math/physics/impl.cpp | 3 + engine/src/utils/mesh/mesh.cpp | 6 +- 11 files changed, 154 insertions(+), 50 deletions(-) diff --git a/bin/data/config.json b/bin/data/config.json index 7887b566..82fdbe2e 100644 --- a/bin/data/config.json +++ b/bin/data/config.json @@ -1,7 +1,7 @@ { "engine": { "scenes": { - "start": "SourceEngine", + "start": "StartMenu", "matrix": { "reverseInfinite": true }, "meshes": { "interleaved": false }, "lights": { "enabled": false, @@ -109,7 +109,7 @@ "invariant": { "default stage buffers": true, "default defer buffer destroy": true, - "default command buffer immediate": false, + "default command buffer immediate": true, "multithreaded recording": true }, "pipelines": { @@ -118,7 +118,7 @@ "vsync": true, // vsync on vulkan side rather than engine-side "hdr": true, "vxgi": false, - "culling": true, + "culling": false, "bloom": true, "dof": true, "rt": false, diff --git a/bin/data/shaders/graph/cull/comp.glsl b/bin/data/shaders/graph/cull/comp.glsl index 93703253..fda95ebc 100644 --- a/bin/data/shaders/graph/cull/comp.glsl +++ b/bin/data/shaders/graph/cull/comp.glsl @@ -192,6 +192,9 @@ void main() { if ( projectedSize < 0.08 ) lodLevel = 2; if ( projectedSize < 0.02 ) lodLevel = 3; lodLevel = min(lodLevel, MAX_LODS - 1); + while ( lodLevel > 0 && lodMetadata[drawCommand.instanceID].levels[lodLevel].indices == 0 ) { + lodLevel--; + } LOD lod = lodMetadata[drawCommand.instanceID].levels[lodLevel]; diff --git a/engine/inc/uf/ext/meshopt/meshopt.h b/engine/inc/uf/ext/meshopt/meshopt.h index bc28ba15..d4b9ec2b 100644 --- a/engine/inc/uf/ext/meshopt/meshopt.h +++ b/engine/inc/uf/ext/meshopt/meshopt.h @@ -11,7 +11,7 @@ namespace ext { namespace meshopt { bool UF_API optimize( uf::Mesh&, float simplify = 1.0f, size_t = SIZE_MAX, bool verbose = false ); - uf::stl::vector computeLODs( size_t count, size_t maxLODs = 4, size_t minIndices = 32 ); + uf::stl::vector computeLODs( size_t count, size_t maxLODs = 4, size_t minIndices = 3 ); uf::stl::vector UF_API generateLODs( uf::Mesh&, const uf::stl::vector&, bool verbose = false ); template diff --git a/engine/src/engine/graph/graph.cpp b/engine/src/engine/graph/graph.cpp index dc445a3e..d56a6184 100644 --- a/engine/src/engine/graph/graph.cpp +++ b/engine/src/engine/graph/graph.cpp @@ -23,8 +23,10 @@ #define UF_DEBUG_TIMER_MULTITRACE_END(...) #endif -#define UF_GRAPH_SPARSE_READ_MESH 1 + #define UF_GRAPH_EXTENDED 1 +#define UF_GRAPH_SPARSE_READ_MESH 1 +// to-do: fix LOD1+ breaking, fix physics mesh not updating namespace { bool newGraphAdded = true; @@ -39,6 +41,14 @@ namespace { } return hash; } + inline uint64_t fnv1aHash(const uf::stl::vector& values) { + uint64_t hash = 1469598103934665603ULL; + for (bool v : values) { + hash ^= static_cast(static_cast(v)); + hash *= 1099511628211ULL; + } + return hash; + } size_t allocateObjectID( pod::Graph::Storage& storage ) { return storage.entities.keys.size(); @@ -1280,14 +1290,16 @@ void uf::graph::process( pod::Graph& graph, int32_t index, uf::Object& parent ) pod::Instance::Bounds bounds = {}; size_t baseInstanceID = ::allocateInstanceID( storage, graph.primitives[node.mesh] ); // setup instances - for ( auto i = 0; i < primitives.size(); ++i ) { - auto& primitive = primitives[i]; + for ( auto drawID = 0; drawID < primitives.size(); ++drawID ) { + auto& primitive = primitives[drawID]; auto& instance = primitive.instance; - size_t instanceID = baseInstanceID + i; + size_t instanceID = baseInstanceID + drawID; instance.objectID = node.object; instance.jointID = graphMetadataJson["renderer"]["skinned"].as() ? 0 : -1; + primitive.drawCommand.instanceID = instanceID; + bounds.min = uf::vector::min( bounds.min, instance.bounds.min ); bounds.max = uf::vector::max( bounds.max, instance.bounds.max ); @@ -1295,7 +1307,7 @@ void uf::graph::process( pod::Graph& graph, int32_t index, uf::Object& parent ) auto& attribute = mesh.indirect.attributes.front(); auto& buffer = mesh.buffers[mesh.isInterleaved(mesh.indirect.interleaved) ? mesh.indirect.interleaved : attribute.buffer]; pod::DrawCommand* drawCommands = (pod::DrawCommand*) buffer.data(); - auto& drawCommand = drawCommands[i]; + auto& drawCommand = drawCommands[drawID]; drawCommand.instanceID = instanceID; } } @@ -1615,6 +1627,7 @@ void uf::graph::reload( pod::Graph& graph, pod::Node& node ) { } } + bool meshUpdated = false; auto model = uf::transform::model( transform ); auto& mesh = storage.meshes.map[graph.meshes[node.mesh]]; auto& primitives = storage.primitives.map[graph.primitives[node.mesh]]; @@ -1623,6 +1636,11 @@ void uf::graph::reload( pod::Graph& graph, pod::Node& node ) { float radius = graph.settings.stream.radius; float radiusSquared = radius * radius; + // force update if entity isn't already bound to the graphic + if ( !entity.hasComponent() ) { + meshUpdated = true; + } + // disable if not tagged for streaming // to-do: check tag if ( graph.settings.stream.tag != "" && node.name != graph.settings.stream.tag ) { @@ -1640,7 +1658,7 @@ void uf::graph::reload( pod::Graph& graph, pod::Node& node ) { pod::DrawCommand* drawCommands = (pod::DrawCommand*) buffer.data(); // queues uf::stl::unordered_map> ranges; - uf::stl::vector queuedDrawIDs( primitives.size(), false ); // this is to maintain draw command order because apparently my code requires draw commands to stay in order + uf::stl::vector queuedLODs( primitives.size(), -1 ); // this is to maintain draw command order because apparently my code requires draw commands to stay in order // fallbacks for when no draw calls are requested (mainly for the collision mesh) float closestDistance = std::numeric_limits::max(); size_t closestDrawID = 0; @@ -1661,24 +1679,37 @@ void uf::graph::reload( pod::Graph& graph, pod::Node& node ) { closestDrawID = drawID; } // queue if we're within the radius - if ( (queuedDrawIDs[drawID] = distanceSquared <= radiusSquared) ) { + if ( distanceSquared <= radiusSquared ) { found = true; + + int8_t lodLevel = 0; + // deduce a simple ratio [0.0 to 1.0] of how far we are into the streaming radius + float distRatio = distanceSquared / radiusSquared; + if ( distRatio > 0.6f ) lodLevel = 3; + else if ( distRatio > 0.3f ) lodLevel = 2; + else if ( distRatio > 0.1f ) lodLevel = 1; + + while ( lodLevel > 0 && primitive.lod.levels[lodLevel].indices == 0 ) { + lodLevel--; + } + + queuedLODs[drawID] = lodLevel; } } // insert closest primitive if all are out of range (because of cringe logic) if ( !found ) { - queuedDrawIDs[closestDrawID] = true; + queuedLODs[closestDrawID] = 0; } // bail if no update is detected - auto drawCommandHash = ::fnv1aHash(queuedDrawIDs); + auto drawCommandHash = ::fnv1aHash(queuedLODs); graph.settings.stream.lastUpdate = uf::physics::time::current; - if ( drawCommandHash == graph.settings.stream.hash ) { return; } graph.settings.stream.hash = drawCommandHash; + meshUpdated = true; // read from disk #if UF_GRAPH_SPARSE_READ_MESH @@ -1707,14 +1738,24 @@ void uf::graph::reload( pod::Graph& graph, pod::Node& node ) { mesh.vertex.count = 0; mesh.index.count = 0; - for (size_t drawID = 0; drawID < queuedDrawIDs.size(); ++drawID) { - bool queued = queuedDrawIDs[drawID]; + for (size_t drawID = 0; drawID < queuedLODs.size(); ++drawID) { + auto lodLevel = queuedLODs[drawID]; auto& primitive = primitives[drawID]; auto& drawCommand = drawCommands[drawID]; + + // reset from LOD0 + //primitives[drawID].drawCommand.instances = 1; + primitives[drawID].drawCommand.indexID = primitives[drawID].lod.levels[0].indexID; + primitives[drawID].drawCommand.indices = primitives[drawID].lod.levels[0].indices; + primitives[drawID].drawCommand.vertexID = primitives[drawID].lod.levels[0].vertexID; + primitives[drawID].drawCommand.vertices = primitives[drawID].lod.levels[0].vertices; + + // copy from primitive + drawCommand = primitive.drawCommand; // disable draw call - if ( !queued ) { - drawCommand.instances = 0; + if ( lodLevel < 0 ) { + //drawCommand.instances = 0; drawCommand.vertices = 0; drawCommand.indices = 0; drawCommand.vertexID = 0; @@ -1722,26 +1763,33 @@ void uf::graph::reload( pod::Graph& graph, pod::Node& node ) { continue; } - // queue up ranges to read from disk + auto& lod = primitive.lod.levels[lodLevel]; + + // queue up ranges to read from disk using LOD bounds for (auto& attribute : mesh.index.attributes) { auto size = attribute.descriptor.size; ranges[attribute.buffer].emplace_back(pod::Range{ - primitive.drawCommand.indexID * size, - primitive.drawCommand.indices * size, + lod.indexID * size, + lod.indices * size, }); } for (auto& attribute : mesh.vertex.attributes) { auto size = attribute.descriptor.size; ranges[attribute.buffer].emplace_back(pod::Range{ - primitive.drawCommand.vertexID * size, - primitive.drawCommand.vertices * size, + lod.vertexID * size, + lod.vertices * size, }); } - // reset draw call and remap - drawCommand = primitive.drawCommand; + // reset draw call and remap to local compacted buffers + drawCommand.vertices = lod.vertices; + drawCommand.indices = lod.indices; drawCommand.vertexID = mesh.vertex.count; drawCommand.indexID = mesh.index.count; + + // synchronize primitive + primitives[drawID].drawCommand = drawCommands[drawID]; + // increment remap indices mesh.vertex.count += drawCommand.vertices; mesh.index.count += drawCommand.indices; @@ -1762,17 +1810,34 @@ void uf::graph::reload( pod::Graph& graph, pod::Node& node ) { #else // disable remaining draw commands for ( auto drawID = 0; drawID < primitives.size(); ++drawID ) { - bool queued = queuedDrawIDs[drawID]; - if ( !queued ) { - drawCommands[drawID].instances = 0; + int8_t lodLevel = queuedLODs[drawID]; + // reset from LOD0 + //primitives[drawID].drawCommand.instances = 1; + primitives[drawID].drawCommand.indexID = primitives[drawID].lod.levels[0].indexID; + primitives[drawID].drawCommand.indices = primitives[drawID].lod.levels[0].indices; + primitives[drawID].drawCommand.vertexID = primitives[drawID].lod.levels[0].vertexID; + primitives[drawID].drawCommand.vertices = primitives[drawID].lod.levels[0].vertices; + + // copy from primitive + drawCommands[drawID] = primitives[drawID].drawCommand; + + if ( lodLevel < 0 ) { + //drawCommands[drawID].instances = 0; drawCommands[drawID].vertices = 0; drawCommands[drawID].indices = 0; drawCommands[drawID].indexID = 0; drawCommands[drawID].vertexID = 0; } else { - drawCommands[drawID] = primitives[drawID].drawCommand; - // to-do: LOD pick here for OpenGL + auto& lod = primitives[drawID].lod.levels[lodLevel]; + + drawCommands[drawID].indexID = lod.indexID; + drawCommands[drawID].indices = lod.indices; + drawCommands[drawID].vertexID = lod.vertexID; + drawCommands[drawID].vertices = lod.vertices; } + + // synchronize primitive + primitives[drawID].drawCommand = drawCommands[drawID]; } #define STREAM_MESH_DATA( N ) \ @@ -1868,10 +1933,12 @@ void uf::graph::reload( pod::Graph& graph, pod::Node& node ) { } } } - } else { // this shouldn't be reached + } else { + // load mesh if not already loaded #define LOAD_MESH_DATA( N ) \ for ( auto& attribute : mesh.N.attributes ) {\ if ( !mesh.buffers[attribute.buffer].empty() || mesh.buffer_paths.empty() ) continue;\ + meshUpdated = true;\ uf::io::readAsBuffer( mesh.buffers[attribute.buffer], mesh.buffer_paths[attribute.buffer] );\ } @@ -1879,6 +1946,8 @@ void uf::graph::reload( pod::Graph& graph, pod::Node& node ) { LOAD_MESH_DATA( vertex ); } + if ( !meshUpdated ) return; + // in the event streamed in mesh data from any pathway isn't already converted { #if UF_ENV_DREAMCAST && GL_QUANTIZED_SHORT @@ -1908,6 +1977,8 @@ void uf::graph::reload( pod::Graph& graph, pod::Node& node ) { uf::renderer::states::rebuild = true; #endif + ::newGraphAdded = true; // force rebuffering the draw commands + // update graphic if ( graphMetadataJson["renderer"]["render"].as() ) { bool exists = entity.hasComponent(); diff --git a/engine/src/ext/gltf/gltf.cpp b/engine/src/ext/gltf/gltf.cpp index 80a82df7..eb1b8345 100644 --- a/engine/src/ext/gltf/gltf.cpp +++ b/engine/src/ext/gltf/gltf.cpp @@ -552,6 +552,8 @@ void ext::gltf::load( pod::Graph& graph, const uf::stl::string& filename, const } auto& mesh = storage.meshes[keyName]; + auto& primitives = storage.primitives[keyName]; + UF_MSG_DEBUG("Optimizing mesh at level {}: {}", level, keyName); if ( !ext::meshopt::optimize( mesh, simplify, level, print ) ) { UF_MSG_ERROR("Mesh optimization failed: {}", keyName ); @@ -563,9 +565,10 @@ void ext::gltf::load( pod::Graph& graph, const uf::stl::string& filename, const UF_MSG_ERROR("LOD generation failed: {}", keyName ); } else { UF_MSG_DEBUG("Generated {} LODs: {}", factors.size() - 1, keyName); - auto& primitives = storage.primitives[keyName]; UF_ASSERT( primitives.size() == lodMetadata.size() ); - for ( auto i = 0; i < primitives.size(); ++i ) primitives[i].lod = lodMetadata[i]; + for ( auto i = 0; i < primitives.size(); ++i ) { + primitives[i].lod = lodMetadata[i]; + } } } } @@ -573,6 +576,21 @@ void ext::gltf::load( pod::Graph& graph, const uf::stl::string& filename, const UF_MSG_DEBUG( "Optimized mesh" ); } #endif + { + // update primitive info + for ( auto& keyName : graph.meshes ) { + auto& mesh = storage.meshes[keyName]; + auto& primitives = storage.primitives[keyName]; + + UF_ASSERT( primitives.size() == mesh.indirect.count ); + auto& attribute = mesh.indirect.attributes.front(); + auto& buffer = mesh.buffers[mesh.isInterleaved(mesh.indirect.interleaved) ? mesh.indirect.interleaved : attribute.buffer]; + pod::DrawCommand* drawCommands = (pod::DrawCommand*) buffer.data(); + for ( auto drawID = 0; drawID < primitives.size(); ++drawID ) { + primitives[drawID].drawCommand = drawCommands[drawID]; + } + } + } if ( graph.metadata["exporter"]["enabled"].as() ) { #if !UF_ENV_DREAMCAST diff --git a/engine/src/ext/gltf/processPrimitives.inl b/engine/src/ext/gltf/processPrimitives.inl index 26bf7bfb..ddb2b9c2 100644 --- a/engine/src/ext/gltf/processPrimitives.inl +++ b/engine/src/ext/gltf/processPrimitives.inl @@ -385,6 +385,12 @@ if ( meshopt.should ) { meshlet.primitive.drawCommand.vertexID = vertexID; meshlet.primitive.drawCommand.vertices = meshlet.vertices.size(); + // copy to LOD metadata + meshlet.primitive.lod.levels[0].indexID = indexID; + meshlet.primitive.lod.levels[0].indices = meshlet.indices.size(); + meshlet.primitive.lod.levels[0].vertexID = vertexID; + meshlet.primitive.lod.levels[0].vertices = meshlet.vertices.size(); + drawCommands.emplace_back(meshlet.primitive.drawCommand); primitives.emplace_back( meshlet.primitive ); diff --git a/engine/src/ext/meshopt/meshopt.cpp b/engine/src/ext/meshopt/meshopt.cpp index c332952b..15ed8e1f 100644 --- a/engine/src/ext/meshopt/meshopt.cpp +++ b/engine/src/ext/meshopt/meshopt.cpp @@ -90,7 +90,7 @@ bool ext::meshopt::optimize( uf::Mesh& mesh, float simplify, size_t o, bool verb size_t optimizedIndexCount = srcIndexCount; if ( 0.0f < simplify && simplify < 1.0f ) { uf::stl::vector simplified(srcIndexCount); - float targetError = 1e-2f / simplify; + float targetError = FLT_MAX; // 1e-2f / simplify; float realError = 0.0f; optimizedIndexCount = meshopt_simplify( @@ -233,16 +233,18 @@ uf::stl::vector ext::meshopt::generateLODs( uf::Mesh& mesh, co // source from LOD0 auto& cmd0 = lodMetadata[cmdIdx].levels[0]; - size_t previousIndicesCount = lodMetadata[cmdIdx].levels[lodIdx - 1].indices; - uf::stl::vector baseIndices(cmd0.indices); - for ( size_t i = 0; i < cmd0.indices; ++i ) baseIndices[i] = outIndices[cmd0.indexID + i]; + // copy from LOD0 + lodMetadata[cmdIdx].levels[lodIdx] = lodMetadata[cmdIdx].levels[0]; // generate LOD if ( 0.0f < simplify && simplify < 1.0f ) { - float targetError = 1e-2f / simplify; + float targetError = FLT_MAX; // 1e-2f / simplify; float realError = 0.0f; size_t currentIndicesCount = cmd0.indices; + uf::stl::vector baseIndices(cmd0.indices); + for ( size_t i = 0; i < cmd0.indices; ++i ) baseIndices[i] = outIndices[cmd0.indexID + i]; + uf::stl::vector lodIndices = baseIndices; const float* basePositions = (const float*) (outVertices[posAttrIdx].data() + cmd0.vertexID * mesh.vertex.attributes[posAttrIdx].stride); @@ -254,6 +256,7 @@ uf::stl::vector ext::meshopt::generateLODs( uf::Mesh& mesh, co ); // couldn't simplify further, use previous LOD + size_t previousIndicesCount = lodMetadata[cmdIdx].levels[lodIdx - 1].indices; if ( currentIndicesCount == previousIndicesCount ) { lodMetadata[cmdIdx].levels[lodIdx] = lodMetadata[cmdIdx].levels[lodIdx - 1]; continue; @@ -288,9 +291,6 @@ uf::stl::vector ext::meshopt::generateLODs( uf::Mesh& mesh, co meshopt_remapVertexBuffer(packed.data(), srcPtr, cmd0.vertices, attr.stride, fetchRemap.data()); outVertices[a].insert(outVertices[a].end(), packed.begin(), packed.end()); } - } else { - // no simplification, just use LOD0 (shouldn't happen) - lodMetadata[cmdIdx].levels[lodIdx] = lodMetadata[cmdIdx].levels[0]; } } } diff --git a/engine/src/utils/io/file.cpp b/engine/src/utils/io/file.cpp index 272d5aea..84d0e9d3 100644 --- a/engine/src/utils/io/file.cpp +++ b/engine/src/utils/io/file.cpp @@ -189,13 +189,12 @@ uf::stl::vector& uf::io::readAsBuffer( uf::stl::vector& buffer buffer.resize(totalBytes); // Read each range - for (const auto& r : ranges) { - is.seekg(r.start, std::ios::beg); - size_t oldSize = buffer.size(); - buffer.resize(oldSize + r.len); - is.read(reinterpret_cast(buffer.data() + oldSize), r.len); - buffer.resize(oldSize + static_cast(is.gcount())); - } + size_t currentOffset = 0; + for (const auto& r : ranges) { + is.seekg(r.start, std::ios::beg); + is.read(reinterpret_cast(buffer.data() + currentOffset), r.len); + currentOffset += static_cast(is.gcount()); + } } uf::stl::string expected; diff --git a/engine/src/utils/math/physics/broadphase/bvh.cpp b/engine/src/utils/math/physics/broadphase/bvh.cpp index 330c32e9..154c6a0f 100644 --- a/engine/src/utils/math/physics/broadphase/bvh.cpp +++ b/engine/src/utils/math/physics/broadphase/bvh.cpp @@ -232,7 +232,7 @@ void impl::buildMeshBVH( pod::BVH& bvh, const uf::Mesh& mesh, pod::BVH::index_t auto aabb = impl::computeTriangleAABB( tri ); auto triID = triIndexID + (view.index.first / 3); - if ( triID != bounds.size() ) UF_MSG_DEBUG("triID={}, bounds.size()={}", triID, bounds.size()); + // if ( triID != bounds.size() ) UF_MSG_DEBUG("triID={}, bounds.size()={}", triID, bounds.size()); bounds.emplace_back( aabb ); bvh.indices.emplace_back( triID ); // triID => mesh.index.buffer[triID * 3]; diff --git a/engine/src/utils/math/physics/impl.cpp b/engine/src/utils/math/physics/impl.cpp index ca3edd30..9f2ea32b 100644 --- a/engine/src/utils/math/physics/impl.cpp +++ b/engine/src/utils/math/physics/impl.cpp @@ -516,6 +516,9 @@ void uf::physics::destroy( pod::PhysicsBody& body ) { if ( body.collider.type == pod::ShapeType::MESH ) { if ( body.collider.mesh.bvh ) delete body.collider.mesh.bvh; } + if ( body.collider.type == pod::ShapeType::CONVEX_HULL ) { + if ( body.collider.convexHull.bvh ) delete body.collider.convexHull.bvh; + } } pod::RayQuery uf::physics::rayCast( const pod::Ray& ray, const pod::PhysicsBody& body, float maxDistance ) { diff --git a/engine/src/utils/mesh/mesh.cpp b/engine/src/utils/mesh/mesh.cpp index 253f9aba..22a37a14 100644 --- a/engine/src/utils/mesh/mesh.cpp +++ b/engine/src/utils/mesh/mesh.cpp @@ -407,7 +407,11 @@ uf::Mesh::View uf::Mesh::makeView( size_t i, const uf::stl::vector uf::Mesh::makeViews( const uf::stl::vector& wanted, size_t lod ) const { uf::stl::vector views; if ( indirect.count > 0 ) { - for ( auto i = 0; i < indirect.count; i++ ) views.emplace_back(makeView(i, wanted, lod)); + for ( auto i = 0; i < indirect.count; i++ ) { + auto view = makeView( i, wanted, lod ); + if ( view.index.count == 0 && view.vertex.count == 0 ) continue; + views.emplace_back( view ); + } } else { views.emplace_back( makeView(wanted, lod) ); }