diff --git a/src/ifcviewer/BvhAccel.cpp b/src/ifcviewer/BvhAccel.cpp index e0b232a283..c285f1fbfe 100644 --- a/src/ifcviewer/BvhAccel.cpp +++ b/src/ifcviewer/BvhAccel.cpp @@ -23,7 +23,6 @@ #include #include #include -#include namespace { @@ -31,38 +30,36 @@ struct Centroid { float x, y, z; }; -Centroid computeCentroid(const ObjectDrawInfo& obj) { +Centroid computeCentroid(const BvhItem& it) { return { - (obj.aabb_min[0] + obj.aabb_max[0]) * 0.5f, - (obj.aabb_min[1] + obj.aabb_max[1]) * 0.5f, - (obj.aabb_min[2] + obj.aabb_max[2]) * 0.5f + (it.aabb_min[0] + it.aabb_max[0]) * 0.5f, + (it.aabb_min[1] + it.aabb_max[1]) * 0.5f, + (it.aabb_min[2] + it.aabb_max[2]) * 0.5f }; } -void computeAABB(const std::vector& draw_info, +void computeAABB(const std::vector& items, const uint32_t* indices, uint32_t count, float out_min[3], float out_max[3]) { out_min[0] = out_min[1] = out_min[2] = std::numeric_limits::max(); out_max[0] = out_max[1] = out_max[2] = -std::numeric_limits::max(); for (uint32_t i = 0; i < count; ++i) { - const auto& obj = draw_info[indices[i]]; + const auto& it = items[indices[i]]; for (int a = 0; a < 3; ++a) { - if (obj.aabb_min[a] < out_min[a]) out_min[a] = obj.aabb_min[a]; - if (obj.aabb_max[a] > out_max[a]) out_max[a] = obj.aabb_max[a]; + if (it.aabb_min[a] < out_min[a]) out_min[a] = it.aabb_min[a]; + if (it.aabb_max[a] > out_max[a]) out_max[a] = it.aabb_max[a]; } } } -// Recursive BVH builder. Writes nodes in pre-order DFS into mbvh.nodes. -// object_indices[start..start+count) are the indices to partition. void buildRecursive(ModelBvh& mbvh, - const std::vector& draw_info, + const std::vector& items, uint32_t start, uint32_t count) { uint32_t node_idx = static_cast(mbvh.nodes.size()); mbvh.nodes.emplace_back(); BvhNode& node = mbvh.nodes[node_idx]; - computeAABB(draw_info, &mbvh.object_indices[start], count, + computeAABB(items, &mbvh.item_indices[start], count, node.aabb_min, node.aabb_max); if (count <= BVH_MAX_LEAF_SIZE) { @@ -72,7 +69,6 @@ void buildRecursive(ModelBvh& mbvh, return; } - // Find longest axis of node AABB. float extent[3] = { node.aabb_max[0] - node.aabb_min[0], node.aabb_max[1] - node.aabb_min[1], @@ -82,145 +78,62 @@ void buildRecursive(ModelBvh& mbvh, if (extent[1] > extent[axis]) axis = 1; if (extent[2] > extent[axis]) axis = 2; - // Partition at median centroid on the chosen axis. uint32_t mid = count / 2; std::nth_element( - mbvh.object_indices.begin() + start, - mbvh.object_indices.begin() + start + mid, - mbvh.object_indices.begin() + start + count, + mbvh.item_indices.begin() + start, + mbvh.item_indices.begin() + start + mid, + mbvh.item_indices.begin() + start + count, [&](uint32_t a, uint32_t b) { - Centroid ca = computeCentroid(draw_info[a]); - Centroid cb = computeCentroid(draw_info[b]); + Centroid ca = computeCentroid(items[a]); + Centroid cb = computeCentroid(items[b]); return (&ca.x)[axis] < (&cb.x)[axis]; }); - node.count = 0; // interior + node.count = 0; node.axis = static_cast(axis); - // Left child is always node_idx + 1 (implicit in pre-order DFS). - // Build left subtree first. Note: &node is invalidated after this call - // because the vector may reallocate. - buildRecursive(mbvh, draw_info, start, mid); + buildRecursive(mbvh, items, start, mid); - // Right child is the next node written after the entire left subtree. uint32_t right_child_idx = static_cast(mbvh.nodes.size()); - buildRecursive(mbvh, draw_info, start + mid, count - mid); + buildRecursive(mbvh, items, start + mid, count - mid); - // Patch the right child index (left is implicit = node_idx + 1). mbvh.nodes[node_idx].right_or_first = right_child_idx; } +ModelBvh buildModelBvh(const std::vector& items, + const std::vector& model_item_indices, + uint32_t model_id) { + ModelBvh mbvh; + mbvh.model_id = model_id; + mbvh.item_indices = model_item_indices; + + uint32_t count = static_cast(model_item_indices.size()); + if (count == 0) return mbvh; + + mbvh.nodes.reserve(count * 2); + buildRecursive(mbvh, items, 0, count); + + assert(!mbvh.nodes.empty()); + return mbvh; +} + } // anonymous namespace -ModelBvh buildModelBvh(const std::vector& draw_info, - const std::vector& model_object_indices, - uint32_t model_id) { - ModelBvh mbvh; - mbvh.model_id = model_id; - mbvh.object_indices = model_object_indices; - - uint32_t count = static_cast(model_object_indices.size()); - if (count == 0) return mbvh; - - // Reserve a rough estimate: ~2*n nodes for a balanced binary tree. - mbvh.nodes.reserve(count * 2); - - buildRecursive(mbvh, draw_info, 0, count); - - // Verify: every object appears exactly once in the leaves. - assert(!mbvh.nodes.empty()); - - return mbvh; -} - -std::shared_ptr buildBvhSet(const std::vector& draw_info) { +std::shared_ptr buildBvhSet(const std::vector& items) { auto bvh_set = std::make_shared(); - // Group object indices by model_id. - std::unordered_map> model_objects; - for (uint32_t i = 0; i < static_cast(draw_info.size()); ++i) { - model_objects[draw_info[i].model_id].push_back(i); + std::unordered_map> model_items; + for (uint32_t i = 0; i < static_cast(items.size()); ++i) { + model_items[items[i].model_id].push_back(i); } - // Build per-model BVHs. - for (auto& [model_id, obj_indices] : model_objects) { - if (obj_indices.size() < BVH_MIN_OBJECTS) continue; + for (auto& [model_id, idxs] : model_items) { + if (idxs.size() < BVH_MIN_OBJECTS) continue; - ModelBvh mbvh = buildModelBvh(draw_info, obj_indices, model_id); + ModelBvh mbvh = buildModelBvh(items, idxs, model_id); bvh_set->bvh_model_ids.insert(model_id); bvh_set->models[model_id] = std::move(mbvh); } return bvh_set; } - -EboReorderResult reorderEbo(const BvhSet& bvh_set, - const std::vector& draw_info, - const std::vector& original_ebo) { - EboReorderResult result; - result.reordered_draw_info = draw_info; // copy; we'll update offsets - result.reordered_ebo.reserve(original_ebo.size()); - - // Track which draw_info entries have been placed. - std::vector placed(draw_info.size(), false); - - for (const auto& [model_id, mbvh] : bvh_set.models) { - // DFS traversal of BVH to visit leaves in order. - uint32_t stack[64]; - int sp = 0; - stack[sp++] = 0; - - while (sp > 0) { - uint32_t ni = stack[--sp]; - const BvhNode& node = mbvh.nodes[ni]; - - if (node.count > 0) { - // Leaf: emit objects in order. - for (uint32_t i = 0; i < node.count; ++i) { - uint32_t oi = mbvh.object_indices[node.right_or_first + i]; - if (placed[oi]) continue; - placed[oi] = true; - - const auto& old_info = draw_info[oi]; - uint32_t new_offset = static_cast( - result.reordered_ebo.size() * sizeof(uint32_t)); - - // Copy indices from original EBO. - uint32_t idx_start = old_info.index_offset / sizeof(uint32_t); - uint32_t idx_count = old_info.index_count; - for (uint32_t j = 0; j < idx_count; ++j) { - result.reordered_ebo.push_back(original_ebo[idx_start + j]); - } - - result.reordered_draw_info[oi].index_offset = new_offset; - } - } else { - // Interior: push left (=ni+1) last so it's processed first. - stack[sp++] = node.right_or_first; // right child - stack[sp++] = ni + 1; // left child - } - } - } - - // Append non-BVH objects (models too small for BVH). - for (uint32_t oi = 0; oi < static_cast(draw_info.size()); ++oi) { - if (placed[oi]) continue; - placed[oi] = true; - - const auto& old_info = draw_info[oi]; - uint32_t new_offset = static_cast( - result.reordered_ebo.size() * sizeof(uint32_t)); - - uint32_t idx_start = old_info.index_offset / sizeof(uint32_t); - uint32_t idx_count = old_info.index_count; - for (uint32_t j = 0; j < idx_count; ++j) { - result.reordered_ebo.push_back(original_ebo[idx_start + j]); - } - - result.reordered_draw_info[oi].index_offset = new_offset; - } - - assert(result.reordered_ebo.size() == original_ebo.size()); - - return result; -} diff --git a/src/ifcviewer/BvhAccel.h b/src/ifcviewer/BvhAccel.h index 21c57c2712..a2cb6a1316 100644 --- a/src/ifcviewer/BvhAccel.h +++ b/src/ifcviewer/BvhAccel.h @@ -26,22 +26,22 @@ #include #include -struct ObjectDrawInfo { - uint32_t index_offset; // byte offset into EBO - uint32_t index_count; // number of indices - uint32_t model_id; // which model this object belongs to - float aabb_min[3]; // world-space AABB - float aabb_max[3]; +// Generic BVH item — anything with a world AABB and a model_id. +// For the instanced renderer each item represents one InstanceCpu. +struct BvhItem { + float aabb_min[3]; + float aabb_max[3]; + uint32_t model_id; }; static constexpr uint32_t BVH_MAX_LEAF_SIZE = 8; -static constexpr uint32_t BVH_MIN_OBJECTS = 32; +static constexpr uint32_t BVH_MIN_OBJECTS = 32; struct BvhNode { - float aabb_min[3]; - float aabb_max[3]; - uint32_t right_or_first; // interior: right child index (left is always this_index+1); leaf: first object index - uint16_t count; // 0 = interior; >0 = leaf with this many objects + float aabb_min[3]; + float aabb_max[3]; + uint32_t right_or_first; // interior: right child index (left is always this_index+1); leaf: first item index + uint16_t count; // 0 = interior; >0 = leaf with this many items uint16_t axis; // split axis (0/1/2) for interior; unused for leaf }; static_assert(sizeof(BvhNode) == 32, "BvhNode must be 32 bytes for cache alignment and sidecar format"); @@ -49,7 +49,7 @@ static_assert(sizeof(BvhNode) == 32, "BvhNode must be 32 bytes for cache alignme struct ModelBvh { uint32_t model_id = 0; std::vector nodes; - std::vector object_indices; // indices into object_draw_info_ + std::vector item_indices; // indices into the model's InstanceCpu array }; struct BvhSet { @@ -57,19 +57,10 @@ struct BvhSet { std::unordered_set bvh_model_ids; }; -struct EboReorderResult { - std::vector reordered_ebo; - std::vector reordered_draw_info; -}; - -// Build BVH trees for all models in the given draw info snapshot. -// Only builds the tree structure; does not touch EBO data. -std::shared_ptr buildBvhSet(const std::vector& draw_info); - -// Reorder the EBO so objects within each BVH leaf are contiguous. -// Must be called with the CURRENT run's EBO and draw_info (not cached). -EboReorderResult reorderEbo(const BvhSet& bvh_set, - const std::vector& draw_info, - const std::vector& original_ebo); +// Build BVH trees for all models in the given item snapshot. +// Items are expected to already be grouped/filtered by caller if needed. +// item_indices in the result reference positions within the full `items` +// vector — callers providing a single model's items will see 0..N-1. +std::shared_ptr buildBvhSet(const std::vector& items); #endif // BVHACCEL_H diff --git a/src/ifcviewer/GeometryStreamer.cpp b/src/ifcviewer/GeometryStreamer.cpp index 54b37df70c..226fb0808c 100644 --- a/src/ifcviewer/GeometryStreamer.cpp +++ b/src/ifcviewer/GeometryStreamer.cpp @@ -20,16 +20,52 @@ #include "GeometryStreamer.h" #include "AppSettings.h" #include "../ifcgeom/hybrid_kernel.h" +#include "../ifcgeom/taxonomy.h" + +#include #include #include #include #include #include +#include #include #include +struct MaterialInfo { + float r = 0.75f, g = 0.75f, b = 0.78f, a = 1.0f; +}; + +static MaterialInfo materialFromStyle(const ifcopenshell::geometry::taxonomy::style::ptr& style) { + MaterialInfo m; + if (!style) return m; + const auto& color = style->get_color(); + if (color) { + m.r = static_cast(color.r()); + m.g = static_cast(color.g()); + m.b = static_cast(color.b()); + } + if (!std::isnan(style->transparency)) { + m.a = 1.0f - static_cast(style->transparency); + } + return m; +} + +static inline uint32_t packRGBA8(const MaterialInfo& m) { + auto to_byte = [](float v) -> uint32_t { + float c = std::clamp(v, 0.0f, 1.0f); + return static_cast(c * 255.0f + 0.5f); + }; + uint32_t r = to_byte(m.r); + uint32_t g = to_byte(m.g); + uint32_t b = to_byte(m.b); + uint32_t a = to_byte(m.a); + // Little-endian byte layout [r,g,b,a] for GL_UNSIGNED_BYTE * 4 normalized. + return r | (g << 8) | (b << 16) | (a << 24); +} + GeometryStreamer::GeometryStreamer(QObject* parent) : QObject(parent) { @@ -96,6 +132,130 @@ std::vector GeometryStreamer::drainElements() { return result; } +// Build a mesh chunk (local coords, 28-byte interleaved vertices) from a +// TriangulationElement. Per-vertex color is baked from material_ids so that +// triangulations with per-face materials still render correctly. +static MeshChunk buildMeshChunk(uint32_t model_id, + uint32_t local_mesh_id, + const IfcGeom::TriangulationElement* elem) { + MeshChunk chunk; + chunk.model_id = model_id; + chunk.local_mesh_id = local_mesh_id; + + const auto& geom = elem->geometry(); + const auto& verts = geom.verts(); + const auto& faces = geom.faces(); + const auto& normals = geom.normals(); + const auto& materials = geom.materials(); + const auto& material_ids = geom.material_ids(); + + if (verts.empty() || faces.empty()) return chunk; + + const size_t num_verts_src = verts.size() / 3; + const size_t num_tris = faces.size() / 3; + const bool have_per_tri_material = (material_ids.size() == num_tris); + + // Dedupe (original vertex index, material id) so vertices shared across + // triangles of the same material stay shared; vertices spanning multiple + // materials are split (per-face color demands it). + auto make_key = [](uint32_t orig_idx, int mat_id) -> uint64_t { + return (static_cast(orig_idx) << 32) | static_cast(mat_id); + }; + + std::unordered_map remap; + remap.reserve(num_verts_src); + + chunk.vertices.reserve(num_verts_src * INSTANCED_VERTEX_STRIDE_FLOATS); + chunk.indices.reserve(faces.size()); + + // Track local AABB as we emit vertices. + float amin[3] = { std::numeric_limits::max(), + std::numeric_limits::max(), + std::numeric_limits::max() }; + float amax[3] = { -std::numeric_limits::max(), + -std::numeric_limits::max(), + -std::numeric_limits::max() }; + + auto emit_vertex = [&](uint32_t orig_idx, int mat_id) -> uint32_t { + const uint64_t key = make_key(orig_idx, mat_id); + auto it = remap.find(key); + if (it != remap.end()) return it->second; + + const uint32_t new_idx = static_cast( + chunk.vertices.size() / INSTANCED_VERTEX_STRIDE_FLOATS); + + float px = static_cast(verts[orig_idx * 3 + 0]); + float py = static_cast(verts[orig_idx * 3 + 1]); + float pz = static_cast(verts[orig_idx * 3 + 2]); + chunk.vertices.push_back(px); + chunk.vertices.push_back(py); + chunk.vertices.push_back(pz); + if (px < amin[0]) amin[0] = px; if (px > amax[0]) amax[0] = px; + if (py < amin[1]) amin[1] = py; if (py > amax[1]) amax[1] = py; + if (pz < amin[2]) amin[2] = pz; if (pz > amax[2]) amax[2] = pz; + + if (orig_idx * 3 + 2 < normals.size()) { + chunk.vertices.push_back(static_cast(normals[orig_idx * 3 + 0])); + chunk.vertices.push_back(static_cast(normals[orig_idx * 3 + 1])); + chunk.vertices.push_back(static_cast(normals[orig_idx * 3 + 2])); + } else { + chunk.vertices.push_back(0.0f); + chunk.vertices.push_back(1.0f); + chunk.vertices.push_back(0.0f); + } + + MaterialInfo m; + if (mat_id >= 0 && mat_id < static_cast(materials.size())) { + m = materialFromStyle(materials[mat_id]); + } + uint32_t packed = packRGBA8(m); + float packed_as_float; + std::memcpy(&packed_as_float, &packed, sizeof(float)); + chunk.vertices.push_back(packed_as_float); + + remap.emplace(key, new_idx); + return new_idx; + }; + + for (size_t t = 0; t < num_tris; ++t) { + const int mat_id = have_per_tri_material ? material_ids[t] : -1; + chunk.indices.push_back(emit_vertex(static_cast(faces[t * 3 + 0]), mat_id)); + chunk.indices.push_back(emit_vertex(static_cast(faces[t * 3 + 1]), mat_id)); + chunk.indices.push_back(emit_vertex(static_cast(faces[t * 3 + 2]), mat_id)); + } + + if (chunk.vertices.empty()) { + for (int a = 0; a < 3; ++a) amin[a] = amax[a] = 0.0f; + } + for (int a = 0; a < 3; ++a) { + chunk.local_aabb_min[a] = amin[a]; + chunk.local_aabb_max[a] = amax[a]; + } + return chunk; +} + +// Compute the world-space AABB by transforming the 8 corners of the local +// AABB through the column-major 4x4 transform. +static void worldAabbFromLocal(const float local_min[3], + const float local_max[3], + const float M[16], + float out_min[3], float out_max[3]) { + out_min[0] = out_min[1] = out_min[2] = std::numeric_limits::max(); + out_max[0] = out_max[1] = out_max[2] = -std::numeric_limits::max(); + for (int c = 0; c < 8; ++c) { + float x = (c & 1) ? local_max[0] : local_min[0]; + float y = (c & 2) ? local_max[1] : local_min[1]; + float z = (c & 4) ? local_max[2] : local_min[2]; + // Column-major: world = M * [x,y,z,1]. + float wx = M[0]*x + M[4]*y + M[8]*z + M[12]; + float wy = M[1]*x + M[5]*y + M[9]*z + M[13]; + float wz = M[2]*x + M[6]*y + M[10]*z + M[14]; + if (wx < out_min[0]) out_min[0] = wx; if (wx > out_max[0]) out_max[0] = wx; + if (wy < out_min[1]) out_min[1] = wy; if (wy > out_max[1]) out_max[1] = wy; + if (wz < out_min[2]) out_min[2] = wz; if (wz > out_max[2]) out_max[2] = wz; + } +} + void GeometryStreamer::run(const std::string& path, int num_threads) { try { ifc_file_ = std::make_unique(path); @@ -105,7 +265,9 @@ void GeometryStreamer::run(const std::string& path, int num_threads) { } ifcopenshell::geometry::Settings settings; - settings.set("use-world-coords", true); + // Instancing path: geometry stays in local coords; the transform is + // applied on the GPU per instance. + settings.set("use-world-coords", false); settings.set("weld-vertices", false); settings.set("apply-default-materials", true); @@ -129,17 +291,14 @@ void GeometryStreamer::run(const std::string& path, int num_threads) { int last_progress = 0; - // Instancing analysis: count shapes grouped by representation id. - struct GeomStat { - uint32_t count = 0; - size_t vertex_count = 0; - size_t index_count = 0; - std::string example_type; - }; - std::unordered_map geom_stats; + // geom.id() → local_mesh_id within this model. + std::unordered_map geom_to_local_mesh_id; + // local_mesh_id → (local AABB) so we can derive world AABBs for later instances. + struct MeshAabb { float lmin[3], lmax[3]; }; + std::vector mesh_aabbs; + uint32_t total_shapes = 0; - size_t total_vertices = 0; - size_t total_indices = 0; + uint32_t total_meshes = 0; QElapsedTimer stream_timer; stream_timer.start(); @@ -152,9 +311,12 @@ void GeometryStreamer::run(const std::string& path, int num_threads) { const auto* tri_elem = dynamic_cast(elem); if (!tri_elem) continue; + const auto& geom = tri_elem->geometry(); + if (geom.verts().empty() || geom.faces().empty()) continue; + uint32_t object_id = next_object_id_++; - // Record element metadata + // Element metadata. ElementInfo info; info.object_id = object_id; info.model_id = model_id_; @@ -163,36 +325,62 @@ void GeometryStreamer::run(const std::string& path, int num_threads) { info.name = tri_elem->name(); info.type = tri_elem->type(); info.parent_id = tri_elem->parent_id(); - - // Instancing stats: key by representation id, count unique vs repeated. - const auto& geom = tri_elem->geometry(); - const std::string& geom_id = geom.id(); - size_t nv = geom.verts().size() / 3; - size_t ni = geom.faces().size(); - if (!geom_id.empty()) { - auto& gs = geom_stats[geom_id]; - gs.count++; - if (gs.count == 1) { - gs.vertex_count = nv; - gs.index_count = ni; - gs.example_type = info.type; - } - } - total_shapes++; - total_vertices += nv; - total_indices += ni; - { std::lock_guard lock(elements_mutex_); pending_elements_.push_back(std::move(info)); } - // Convert geometry to upload chunk - UploadChunk chunk = convertElement(tri_elem, object_id); - if (!chunk.indices.empty()) { - emit elementReady(std::move(chunk)); + // Representation dedup. + const std::string& geom_id = geom.id(); + uint32_t local_mesh_id; + bool first_sight = false; + if (geom_id.empty()) { + // No representation key — treat as unique. + local_mesh_id = total_meshes++; + first_sight = true; + } else { + auto it = geom_to_local_mesh_id.find(geom_id); + if (it == geom_to_local_mesh_id.end()) { + local_mesh_id = total_meshes++; + geom_to_local_mesh_id.emplace(geom_id, local_mesh_id); + first_sight = true; + } else { + local_mesh_id = it->second; + } } + if (first_sight) { + MeshChunk mesh_chunk = buildMeshChunk(model_id_, local_mesh_id, tri_elem); + MeshAabb ma; + for (int a = 0; a < 3; ++a) { + ma.lmin[a] = mesh_chunk.local_aabb_min[a]; + ma.lmax[a] = mesh_chunk.local_aabb_max[a]; + } + if (mesh_aabbs.size() <= local_mesh_id) mesh_aabbs.resize(local_mesh_id + 1); + mesh_aabbs[local_mesh_id] = ma; + if (!mesh_chunk.indices.empty()) { + emit meshReady(std::move(mesh_chunk)); + } + } + + // Transform (column-major 4x4, cast to float). + const Eigen::Matrix4d& mat_d = tri_elem->transformation().data()->ccomponents(); + InstanceChunk inst; + inst.model_id = model_id_; + inst.local_mesh_id = local_mesh_id; + inst.object_id = object_id; + inst.color_override_rgba8 = 0; // 0 = use baked vertex color + for (int i = 0; i < 16; ++i) { + inst.transform[i] = static_cast(mat_d.data()[i]); + } + + const MeshAabb& ma = mesh_aabbs[local_mesh_id]; + worldAabbFromLocal(ma.lmin, ma.lmax, inst.transform, + inst.world_aabb_min, inst.world_aabb_max); + + emit instanceReady(std::move(inst)); + total_shapes++; + int p = iterator->progress(); if (p != last_progress) { last_progress = p; @@ -204,188 +392,9 @@ void GeometryStreamer::run(const std::string& path, int num_threads) { progress_ = 100; emit progressChanged(100); - // === Instancing report === - { - size_t unique_geoms = geom_stats.size(); - size_t unique_vertices = 0; - size_t unique_indices = 0; - size_t repeated_shapes = 0; // total shapes that share a repr with another - for (const auto& [gid, gs] : geom_stats) { - unique_vertices += gs.vertex_count; - unique_indices += gs.index_count; - if (gs.count > 1) repeated_shapes += gs.count; - } - - // Bytes assuming current layout (32 B/vertex, 4 B/index). - size_t baked_vbo_bytes = total_vertices * 32; - size_t baked_ebo_bytes = total_indices * 4; - size_t instanced_vbo_bytes = unique_vertices * 32; - size_t instanced_ebo_bytes = unique_indices * 4; - // Per-instance data: 64 B transform + 8 B (object_id + color). - size_t per_instance_bytes = 72; - size_t instance_ssbo_bytes = total_shapes * per_instance_bytes; - - double dedup_ratio = unique_geoms > 0 - ? static_cast(total_shapes) / static_cast(unique_geoms) - : 1.0; - - qDebug("=== Instancing analysis: %s ===", path.c_str()); - qDebug(" Stream time: %.2f s", stream_timer.elapsed() / 1000.0); - qDebug(" Total shapes: %u", total_shapes); - qDebug(" Unique geometries: %zu (dedup ratio %.2fx)", - unique_geoms, dedup_ratio); - qDebug(" Repeated shapes: %zu (%.1f%% of total)", - repeated_shapes, - total_shapes > 0 ? 100.0 * repeated_shapes / total_shapes : 0.0); - qDebug(" Baked geometry: VBO %.1f MB + EBO %.1f MB = %.1f MB", - baked_vbo_bytes / (1024.0*1024.0), - baked_ebo_bytes / (1024.0*1024.0), - (baked_vbo_bytes + baked_ebo_bytes) / (1024.0*1024.0)); - qDebug(" If instanced: VBO %.1f MB + EBO %.1f MB + SSBO %.1f MB = %.1f MB", - instanced_vbo_bytes / (1024.0*1024.0), - instanced_ebo_bytes / (1024.0*1024.0), - instance_ssbo_bytes / (1024.0*1024.0), - (instanced_vbo_bytes + instanced_ebo_bytes + instance_ssbo_bytes) - / (1024.0*1024.0)); - size_t baked_total = baked_vbo_bytes + baked_ebo_bytes; - size_t inst_total = instanced_vbo_bytes + instanced_ebo_bytes + instance_ssbo_bytes; - if (inst_total > 0 && baked_total > inst_total) { - qDebug(" Potential savings: %.1f MB (%.1f%%)", - (baked_total - inst_total) / (1024.0*1024.0), - 100.0 * (baked_total - inst_total) / baked_total); - } else { - qDebug(" Potential savings: none (instance overhead exceeds dedup win)"); - } - - // Top-5 most duplicated representations. - std::vector> sorted(geom_stats.begin(), geom_stats.end()); - std::partial_sort(sorted.begin(), - sorted.begin() + std::min(5, sorted.size()), - sorted.end(), - [](const auto& a, const auto& b) { return a.second.count > b.second.count; }); - qDebug(" Top duplicated representations:"); - for (size_t i = 0; i < std::min(5, sorted.size()); ++i) { - const auto& [gid, gs] = sorted[i]; - qDebug(" [%zu] count=%u verts=%zu type=%s repr_id=%s", - i + 1, gs.count, gs.vertex_count, - gs.example_type.c_str(), gid.c_str()); - } - } -} - -static MaterialInfo materialFromStyle(const ifcopenshell::geometry::taxonomy::style::ptr& style) { - MaterialInfo m; - if (!style) return m; - - const auto& color = style->get_color(); - if (color) { - m.r = static_cast(color.r()); - m.g = static_cast(color.g()); - m.b = static_cast(color.b()); - } - if (!std::isnan(style->transparency)) { - m.a = 1.0f - static_cast(style->transparency); - } - return m; -} - -static inline uint32_t packRGBA8(const MaterialInfo& m) { - auto to_byte = [](float v) -> uint32_t { - float c = std::clamp(v, 0.0f, 1.0f); - return static_cast(c * 255.0f + 0.5f); - }; - uint32_t r = to_byte(m.r); - uint32_t g = to_byte(m.g); - uint32_t b = to_byte(m.b); - uint32_t a = to_byte(m.a); - // Layout in memory (little-endian) reads as bytes [r, g, b, a] which is - // what the GL_UNSIGNED_BYTE * 4 normalized vertex attribute expects. - return r | (g << 8) | (b << 16) | (a << 24); -} - -UploadChunk GeometryStreamer::convertElement(const IfcGeom::TriangulationElement* elem, uint32_t object_id) { - UploadChunk chunk; - chunk.object_id = object_id; - chunk.model_id = model_id_; - - const auto& geom = elem->geometry(); - const auto& verts = geom.verts(); - const auto& faces = geom.faces(); - const auto& normals = geom.normals(); - const auto& materials = geom.materials(); - const auto& material_ids = geom.material_ids(); - - if (verts.empty() || faces.empty()) return chunk; - - // Encode object_id as float bits for the vertex attribute - float id_as_float; - static_assert(sizeof(float) == sizeof(uint32_t)); - std::memcpy(&id_as_float, &object_id, sizeof(float)); - - const size_t num_verts = verts.size() / 3; - const size_t num_tris = faces.size() / 3; - const bool have_per_tri_material = (material_ids.size() == num_tris); - - // Per-vertex color requires that any vertex shared between triangles with - // *different* materials be split. We dedupe (orig_vert_idx, mat_id) pairs - // so vertices that are only ever used by one material stay shared. - auto make_key = [](uint32_t orig_idx, int mat_id) -> uint64_t { - return (static_cast(orig_idx) << 32) | - static_cast(mat_id); - }; - - std::unordered_map remap; - remap.reserve(num_verts); - - chunk.vertices.reserve(num_verts * 8); - chunk.indices.reserve(faces.size()); - - auto emit_vertex = [&](uint32_t orig_idx, int mat_id) -> uint32_t { - const uint64_t key = make_key(orig_idx, mat_id); - auto it = remap.find(key); - if (it != remap.end()) return it->second; - - const uint32_t new_idx = static_cast(chunk.vertices.size() / 8); - - // pos - chunk.vertices.push_back(static_cast(verts[orig_idx * 3 + 0])); - chunk.vertices.push_back(static_cast(verts[orig_idx * 3 + 1])); - chunk.vertices.push_back(static_cast(verts[orig_idx * 3 + 2])); - - // normal - if (orig_idx * 3 + 2 < normals.size()) { - chunk.vertices.push_back(static_cast(normals[orig_idx * 3 + 0])); - chunk.vertices.push_back(static_cast(normals[orig_idx * 3 + 1])); - chunk.vertices.push_back(static_cast(normals[orig_idx * 3 + 2])); - } else { - chunk.vertices.push_back(0.0f); - chunk.vertices.push_back(1.0f); - chunk.vertices.push_back(0.0f); - } - - // object_id (float bits) - chunk.vertices.push_back(id_as_float); - - // color (packed RGBA8 reinterpreted as float) - MaterialInfo m; - if (mat_id >= 0 && mat_id < static_cast(materials.size())) { - m = materialFromStyle(materials[mat_id]); - } - uint32_t packed = packRGBA8(m); - float packed_as_float; - std::memcpy(&packed_as_float, &packed, sizeof(float)); - chunk.vertices.push_back(packed_as_float); - - remap.emplace(key, new_idx); - return new_idx; - }; - - for (size_t t = 0; t < num_tris; ++t) { - const int mat_id = have_per_tri_material ? material_ids[t] : -1; - chunk.indices.push_back(emit_vertex(static_cast(faces[t * 3 + 0]), mat_id)); - chunk.indices.push_back(emit_vertex(static_cast(faces[t * 3 + 1]), mat_id)); - chunk.indices.push_back(emit_vertex(static_cast(faces[t * 3 + 2]), mat_id)); - } - - return chunk; + double dedup_ratio = total_meshes > 0 + ? static_cast(total_shapes) / static_cast(total_meshes) : 1.0; + qDebug("Streamer done: %s %.2fs shapes=%u unique_meshes=%u dedup=%.2fx", + path.c_str(), stream_timer.elapsed() / 1000.0, + total_shapes, total_meshes, dedup_ratio); } diff --git a/src/ifcviewer/GeometryStreamer.h b/src/ifcviewer/GeometryStreamer.h index 0d49a12ca7..f6201517ad 100644 --- a/src/ifcviewer/GeometryStreamer.h +++ b/src/ifcviewer/GeometryStreamer.h @@ -26,15 +26,13 @@ #include #include #include -#include #include #include -#include #include "../ifcparse/file.h" #include "../ifcgeom/Iterator.h" -#include "ViewportWindow.h" +#include "InstancedGeometry.h" struct ElementInfo { uint32_t object_id; @@ -67,15 +65,14 @@ public: signals: void progressChanged(int percent); - void elementReady(UploadChunk chunk); + void meshReady(MeshChunk chunk); + void instanceReady(InstanceChunk chunk); void finished(); void errorOccurred(const QString& message); private: void run(const std::string& path, int num_threads); - UploadChunk convertElement(const IfcGeom::TriangulationElement* elem, uint32_t object_id); - std::unique_ptr ifc_file_; std::unique_ptr worker_thread_; std::atomic running_{false}; @@ -85,7 +82,7 @@ private: std::mutex elements_mutex_; std::vector pending_elements_; - uint32_t next_object_id_ = 1; // 0 = no object + uint32_t next_object_id_ = 1; uint32_t model_id_ = 0; }; diff --git a/src/ifcviewer/InstancedGeometry.h b/src/ifcviewer/InstancedGeometry.h new file mode 100644 index 0000000000..1c027976ef --- /dev/null +++ b/src/ifcviewer/InstancedGeometry.h @@ -0,0 +1,103 @@ +/******************************************************************************** + * * + * This file is part of IfcOpenShell. * + * * + * IfcOpenShell is free software: you can redistribute it and/or modify * + * it under the terms of the Lesser GNU General Public License as published by * + * the Free Software Foundation, either version 3.0 of the License, or * + * (at your option) any later version. * + * * + * IfcOpenShell is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * Lesser GNU General Public License for more details. * + * * + * You should have received a copy of the Lesser GNU General Public License * + * along with this program. If not, see . * + * * + ********************************************************************************/ + +#ifndef INSTANCEDGEOMETRY_H +#define INSTANCEDGEOMETRY_H + +#include +#include +#include + +// Per-vertex layout for instanced meshes, stored in local coordinates. +// 28 bytes per vertex: +// pos(3 float) -- 12 B +// normal(3 float) -- 12 B +// color(4 bytes RGBA8, read as GL_UNSIGNED_BYTE*4 normalized) -- 4 B +static constexpr int INSTANCED_VERTEX_STRIDE_BYTES = 28; +static constexpr int INSTANCED_VERTEX_STRIDE_FLOATS = 7; + +// Per-mesh metadata on the CPU side. Meshes own a slice of the model's +// VBO and EBO (both local-coords/mesh-local indices). +struct MeshInfo { + uint32_t vbo_byte_offset = 0; // where this mesh's vertices start + uint32_t vertex_count = 0; + uint32_t ebo_byte_offset = 0; // where this mesh's indices start + uint32_t index_count = 0; + float local_aabb_min[3]{}; + float local_aabb_max[3]{}; + uint32_t first_instance = 0; // index into per-model instances array + uint32_t instance_count = 0; +}; +static_assert(sizeof(MeshInfo) == 48, "MeshInfo must be 48 bytes"); + +// Per-instance record uploaded to an SSBO and read by the vertex shader. +// Layout deliberately matches std430 expectations: +// mat4 transform (64 B column-major) +// uint object_id +// uint color_override_rgba8 -- 0 = use baked vertex color, else override +// uint _pad0, _pad1 -- align to 16 for std430 +struct alignas(16) InstanceGpu { + float transform[16]; + uint32_t object_id = 0; + uint32_t color_override_rgba8 = 0; + uint32_t _pad0 = 0; + uint32_t _pad1 = 0; +}; +static_assert(sizeof(InstanceGpu) == 80, "InstanceGpu must be 80 bytes"); + +// CPU-side per-instance data. The GPU record above is derived from this; +// we also retain the world AABB for BVH construction and the mesh_id. +struct InstanceCpu { + uint32_t mesh_id = 0; // index into meshes array + uint32_t object_id = 0; + uint32_t color_override_rgba8 = 0; + uint32_t model_id = 0; + float transform[16]{}; + float world_aabb_min[3]{}; + float world_aabb_max[3]{}; +}; + +// Chunks emitted by the streamer to the viewport (main thread). + +// Emitted the first time a representation id is seen. Carries the mesh +// geometry in local coords. `local_mesh_id` is the streamer-assigned id +// within this model. +struct MeshChunk { + uint32_t model_id = 0; + uint32_t local_mesh_id = 0; + std::vector vertices; // 7 floats * N_verts (pos3+norm3+color1_packed) + std::vector indices; + float local_aabb_min[3]{}; + float local_aabb_max[3]{}; +}; + +// Emitted for every placement (every triangulation element from the +// iterator). For the first instance of a mesh, the MeshChunk is emitted +// just before this. +struct InstanceChunk { + uint32_t model_id = 0; + uint32_t local_mesh_id = 0; + uint32_t object_id = 0; + uint32_t color_override_rgba8 = 0; + float transform[16]{}; + float world_aabb_min[3]{}; + float world_aabb_max[3]{}; +}; + +#endif // INSTANCEDGEOMETRY_H diff --git a/src/ifcviewer/MainWindow.cpp b/src/ifcviewer/MainWindow.cpp index b5ee3581c4..86a787a0e2 100644 --- a/src/ifcviewer/MainWindow.cpp +++ b/src/ifcviewer/MainWindow.cpp @@ -173,8 +173,10 @@ void MainWindow::addFiles(const QStringList& paths) { void MainWindow::connectStreamer(GeometryStreamer* streamer) { connect(streamer, &GeometryStreamer::progressChanged, this, &MainWindow::onProgressChanged, Qt::QueuedConnection); - connect(streamer, &GeometryStreamer::elementReady, - this, &MainWindow::onElementReady, Qt::QueuedConnection); + connect(streamer, &GeometryStreamer::meshReady, + this, &MainWindow::onMeshReady, Qt::QueuedConnection); + connect(streamer, &GeometryStreamer::instanceReady, + this, &MainWindow::onInstanceReady, Qt::QueuedConnection); connect(streamer, &GeometryStreamer::finished, this, &MainWindow::onStreamingFinished, Qt::QueuedConnection); connect(streamer, &GeometryStreamer::errorOccurred, this, [this](const QString& msg) { @@ -208,7 +210,7 @@ void MainWindow::startNextLoad() { qDebug(" Sidecar read: %lld ms (%s)", rt.elapsed(), ifc_path.c_str()); auto result = std::make_shared>(std::move(cached)); QMetaObject::invokeMethod(this, [this, mid, result]() { - if (*result && !(*result)->draw_info.empty()) { + if (*result && !(*result)->meshes.empty()) { applySidecarData(mid, std::move(**result)); } else { // No sidecar — fall back to streaming from IFC. @@ -227,51 +229,10 @@ void MainWindow::startNextLoad() { }); } -void MainWindow::applySidecarData(ModelId mid, SidecarData data) { - auto it = models_.find(mid); - if (it == models_.end()) return; - auto& model = it->second; - - QElapsedTimer t; - - qDebug("Sidecar hit: %s (%zu objects, %zu verts, %zu indices, %.1f MB)", - model.file_path.toStdString().c_str(), data.draw_info.size(), - data.vertices.size() / 8, data.indices.size(), - (data.vertices.size() * 4 + data.indices.size() * 4) / (1024.0 * 1024.0)); - - // GL upload — fast, single buffer copy. - t.start(); - viewport_->uploadBulk(mid, data.vertices, data.indices, - data.draw_info, std::move(data.bvh_set)); - qDebug(" GL upload: %lld ms", t.elapsed()); - - // Update next_object_id_ past all objects in this model. - for (const auto& elem : data.elements) { - if (elem.object_id >= next_object_id_) - next_object_id_ = elem.object_id + 1; - } - - // Suppress per-item layout recalcs while building the tree. - t.restart(); - element_tree_->setUpdatesEnabled(false); - populateTreeFromSidecar(model, data.elements, data.string_table); - element_tree_->setUpdatesEnabled(true); - qDebug(" Tree build: %lld ms (%zu elements)", t.elapsed(), data.elements.size()); - - progress_bar_->setVisible(false); - - qint64 ms = load_timer_.elapsed(); - QString elapsed = (ms >= 1000) - ? QString::number(ms / 1000.0, 'f', 2) + " s" - : QString::number(ms) + " ms"; - - status_label_->setText(QString("%1 elements across %2 model(s) — loaded from cache in %3") - .arg(element_map_.size()) - .arg(models_.size()) - .arg(elapsed)); - - loading_model_id_ = 0; - QTimer::singleShot(0, this, &MainWindow::startNextLoad); +void MainWindow::applySidecarData(ModelId /*mid*/, SidecarData /*data*/) { + // Commit A: readSidecar() always returns nullopt, so this is unreachable. + // Restored in Commit B along with the v4 on-disk format. + qWarning("applySidecarData called but sidecar is disabled in Commit A"); } void MainWindow::populateTreeFromSidecar(ModelHandle& model, @@ -325,8 +286,12 @@ void MainWindow::onProgressChanged(int percent) { progress_bar_->setValue(percent); } -void MainWindow::onElementReady(UploadChunk chunk) { - viewport_->uploadChunk(chunk); +void MainWindow::onMeshReady(MeshChunk chunk) { + viewport_->uploadMeshChunk(chunk); +} + +void MainWindow::onInstanceReady(InstanceChunk chunk) { + viewport_->uploadInstanceChunk(chunk); } void MainWindow::onStreamingFinished() { @@ -355,39 +320,10 @@ void MainWindow::onStreamingFinished() { .arg(num_models) .arg(elapsed)); - // Build BVH and write sidecar (geometry + metadata + BVH). + // Sort instances by mesh and upload the per-model instance SSBO. + // Sidecar write is stubbed in Commit A. if (loading_model_id_ != 0) { - auto it = models_.find(loading_model_id_); - if (it != models_.end()) { - std::string ifc_path = it->second.file_path.toStdString(); - QFileInfo fi(it->second.file_path); - uint64_t file_size = static_cast(fi.size()); - - // Pack element info for the sidecar (only this model's elements). - std::vector packed; - std::string stbl; - for (const auto& [oid, info] : element_map_) { - if (info.model_id != loading_model_id_) continue; - PackedElementInfo pe; - pe.object_id = info.object_id; - pe.model_id = info.model_id; - pe.ifc_id = info.ifc_id; - pe.parent_id = info.parent_id; - pe.guid_offset = static_cast(stbl.size()); - pe.guid_length = static_cast(info.guid.size()); - stbl += info.guid; - pe.name_offset = static_cast(stbl.size()); - pe.name_length = static_cast(info.name.size()); - stbl += info.name; - pe.type_offset = static_cast(stbl.size()); - pe.type_length = static_cast(info.type.size()); - stbl += info.type; - packed.push_back(pe); - } - - viewport_->buildBvhAsync(loading_model_id_, ifc_path, file_size, - std::move(packed), std::move(stbl)); - } + viewport_->finalizeModel(loading_model_id_); } // Start next model if queued. diff --git a/src/ifcviewer/MainWindow.h b/src/ifcviewer/MainWindow.h index f60da70b75..5270676af5 100644 --- a/src/ifcviewer/MainWindow.h +++ b/src/ifcviewer/MainWindow.h @@ -62,7 +62,8 @@ private slots: void onFileOpen(); void onFileSettings(); void onProgressChanged(int percent); - void onElementReady(UploadChunk chunk); + void onMeshReady(MeshChunk chunk); + void onInstanceReady(InstanceChunk chunk); void onStreamingFinished(); void onObjectPicked(uint32_t object_id); void onTreeSelectionChanged(); diff --git a/src/ifcviewer/SidecarCache.cpp b/src/ifcviewer/SidecarCache.cpp index d77095c922..be19c8698f 100644 --- a/src/ifcviewer/SidecarCache.cpp +++ b/src/ifcviewer/SidecarCache.cpp @@ -17,180 +17,20 @@ * * ********************************************************************************/ +// Commit A: sidecar cache is temporarily disabled. The on-disk format is +// being rewritten from v3 (monolithic world-coord geometry) to v4 (instanced +// meshes + per-instance records). Until v4 is finalised, loads always go +// through the streaming path and writes are no-ops. + #include "SidecarCache.h" -#include -#include - -// Binary layout (all multi-byte fields native-endian): -// -// SidecarHeader (16 bytes) -// uint64_t source_file_size -// -// uint32_t num_vertices (count of floats) -// float[num_vertices] vertex data -// -// uint32_t num_indices -// uint32_t[num_indices] index data -// -// uint32_t num_draw_infos -// ObjectDrawInfo[N] draw info array -// -// uint32_t num_elements -// PackedElementInfo[N] element records -// uint32_t string_table_bytes -// char[string_table_bytes] -// -// uint32_t num_bvh_models -// for each model: -// uint32_t model_id -// uint32_t num_nodes -// BvhNode[num_nodes] -// uint32_t num_object_indices -// uint32_t[num_object_indices] - -struct SidecarHeader { - uint32_t magic; - uint32_t version; - uint32_t endian; - uint32_t reserved; -}; - -static std::string sidecarPath(const std::string& ifc_path) { - return ifc_path + ".ifcview"; -} - -template -static bool writeVec(FILE* f, const std::vector& v) { - uint32_t n = static_cast(v.size()); - if (fwrite(&n, 4, 1, f) != 1) return false; - if (n > 0 && fwrite(v.data(), sizeof(T), n, f) != n) return false; +bool writeSidecar(const std::string& /*ifc_path*/, + const SidecarData& /*data*/, + uint64_t /*ifc_file_size*/) { return true; } -template -static bool readVec(FILE* f, std::vector& v) { - uint32_t n; - if (fread(&n, 4, 1, f) != 1) return false; - v.resize(n); - if (n > 0 && fread(v.data(), sizeof(T), n, f) != n) return false; - return true; -} - -bool writeSidecar(const std::string& ifc_path, - const SidecarData& data, - uint64_t ifc_file_size) { - std::string path = sidecarPath(ifc_path); - FILE* f = fopen(path.c_str(), "wb"); - if (!f) return false; - - // Header - SidecarHeader hdr = { SIDECAR_MAGIC, SIDECAR_VERSION, SIDECAR_ENDIAN, 0 }; - fwrite(&hdr, sizeof(hdr), 1, f); - fwrite(&ifc_file_size, 8, 1, f); - - // Geometry - if (!writeVec(f, data.vertices)) { fclose(f); return false; } - if (!writeVec(f, data.indices)) { fclose(f); return false; } - - // Draw info - if (!writeVec(f, data.draw_info)) { fclose(f); return false; } - - // Elements + string table - if (!writeVec(f, data.elements)) { fclose(f); return false; } - uint32_t stbl_len = static_cast(data.string_table.size()); - fwrite(&stbl_len, 4, 1, f); - if (stbl_len > 0) fwrite(data.string_table.data(), 1, stbl_len, f); - - // BVH - uint32_t num_bvh_models = data.bvh_set - ? static_cast(data.bvh_set->models.size()) : 0; - fwrite(&num_bvh_models, 4, 1, f); - - if (data.bvh_set) { - for (const auto& [model_id, mbvh] : data.bvh_set->models) { - fwrite(&model_id, 4, 1, f); - - uint32_t nn = static_cast(mbvh.nodes.size()); - fwrite(&nn, 4, 1, f); - if (nn > 0) fwrite(mbvh.nodes.data(), sizeof(BvhNode), nn, f); - - uint32_t no = static_cast(mbvh.object_indices.size()); - fwrite(&no, 4, 1, f); - if (no > 0) fwrite(mbvh.object_indices.data(), 4, no, f); - } - } - - fclose(f); - return true; -} - -std::optional readSidecar(const std::string& ifc_path, - uint64_t ifc_file_size) { - std::string path = sidecarPath(ifc_path); - FILE* f = fopen(path.c_str(), "rb"); - if (!f) return std::nullopt; - - auto fail = [&]() -> std::optional { fclose(f); return std::nullopt; }; - - // Header - SidecarHeader hdr; - if (fread(&hdr, sizeof(hdr), 1, f) != 1) return fail(); - if (hdr.magic != SIDECAR_MAGIC || - hdr.version != SIDECAR_VERSION || - hdr.endian != SIDECAR_ENDIAN) return fail(); - - uint64_t stored_size; - if (fread(&stored_size, 8, 1, f) != 1) return fail(); - if (stored_size != ifc_file_size) return fail(); - - SidecarData data; - - // Geometry - if (!readVec(f, data.vertices)) return fail(); - if (!readVec(f, data.indices)) return fail(); - - // Draw info - if (!readVec(f, data.draw_info)) return fail(); - - // Elements + string table - if (!readVec(f, data.elements)) return fail(); - uint32_t stbl_len; - if (fread(&stbl_len, 4, 1, f) != 1) return fail(); - data.string_table.resize(stbl_len); - if (stbl_len > 0 && fread(data.string_table.data(), 1, stbl_len, f) != stbl_len) - return fail(); - - // BVH - uint32_t num_bvh_models; - if (fread(&num_bvh_models, 4, 1, f) != 1) return fail(); - - if (num_bvh_models > 0) { - data.bvh_set = std::make_shared(); - for (uint32_t m = 0; m < num_bvh_models; ++m) { - uint32_t model_id; - if (fread(&model_id, 4, 1, f) != 1) return fail(); - - ModelBvh mbvh; - mbvh.model_id = model_id; - - uint32_t nn; - if (fread(&nn, 4, 1, f) != 1) return fail(); - mbvh.nodes.resize(nn); - if (nn > 0 && fread(mbvh.nodes.data(), sizeof(BvhNode), nn, f) != nn) - return fail(); - - uint32_t no; - if (fread(&no, 4, 1, f) != 1) return fail(); - mbvh.object_indices.resize(no); - if (no > 0 && fread(mbvh.object_indices.data(), 4, no, f) != no) - return fail(); - - data.bvh_set->bvh_model_ids.insert(model_id); - data.bvh_set->models[model_id] = std::move(mbvh); - } - } - - fclose(f); - return data; +std::optional readSidecar(const std::string& /*ifc_path*/, + uint64_t /*ifc_file_size*/) { + return std::nullopt; } diff --git a/src/ifcviewer/SidecarCache.h b/src/ifcviewer/SidecarCache.h index 49c36dba15..e14eb9d256 100644 --- a/src/ifcviewer/SidecarCache.h +++ b/src/ifcviewer/SidecarCache.h @@ -17,22 +17,28 @@ * * ********************************************************************************/ +// NOTE: Sidecar format v3 is being rewritten to v4 (instanced geometry layout). +// During the instancing rewrite (Commit A) the cache is a no-op: reads always +// miss and writes always succeed without producing a file. Commit B will +// re-introduce the on-disk format with MeshInfo + InstanceGpu sections. + #ifndef SIDECARCACHE_H #define SIDECARCACHE_H -#include "BvhAccel.h" +#include "InstancedGeometry.h" #include #include #include #include +#include static constexpr uint32_t SIDECAR_MAGIC = 0x49465657; // "IFVW" -static constexpr uint32_t SIDECAR_VERSION = 3; +static constexpr uint32_t SIDECAR_VERSION = 4; static constexpr uint32_t SIDECAR_ENDIAN = 0x01020304; -// Fixed-size element record for the sidecar. Strings are stored as -// (offset, length) pairs into a separate string table. +// Fixed-size element record. Strings are stored as (offset, length) pairs +// into a separate string table. struct PackedElementInfo { uint32_t object_id; uint32_t model_id; @@ -46,30 +52,27 @@ struct PackedElementInfo { uint32_t type_length; }; -// Everything the viewer needs to display a model without tessellating. +// Everything needed to display an already-tessellated model without +// re-running the iterator. v4 schema: instanced geometry. struct SidecarData { - // GPU geometry (ready to upload as-is) - std::vector vertices; // interleaved, 8 floats per vertex - std::vector indices; // global (already remapped) + // Per-model GPU geometry (local coords). 28 bytes/vertex. + std::vector vertices; + std::vector indices; - // Per-object metadata - std::vector draw_info; + // Mesh dictionary and per-instance data. + std::vector meshes; // indexed by local_mesh_id + std::vector instances; // sorted by mesh_id - // Element tree metadata + // Element tree metadata. std::vector elements; - std::string string_table; // concatenated UTF-8 - - // BVH acceleration - std::shared_ptr bvh_set; + std::string string_table; }; -// Write a full sidecar next to the IFC file. -// Returns true on success. +// v4 writer/reader are stubbed for Commit A — no disk I/O happens. bool writeSidecar(const std::string& ifc_path, const SidecarData& data, uint64_t ifc_file_size); -// Read a sidecar. Returns nullopt on any failure (missing, stale, corrupt). std::optional readSidecar(const std::string& ifc_path, uint64_t ifc_file_size); diff --git a/src/ifcviewer/ViewportWindow.cpp b/src/ifcviewer/ViewportWindow.cpp index 1c6ab78625..e264f990e4 100644 --- a/src/ifcviewer/ViewportWindow.cpp +++ b/src/ifcviewer/ViewportWindow.cpp @@ -18,7 +18,6 @@ ********************************************************************************/ #include "ViewportWindow.h" -#include "SidecarCache.h" #include #include @@ -31,33 +30,75 @@ #include #include -static const size_t INITIAL_VBO_SIZE = 64 * 1024 * 1024; // 64 MB -static const size_t INITIAL_EBO_SIZE = 32 * 1024 * 1024; // 32 MB -static const size_t MAX_BUFFER_SIZE = 4ull * 1024 * 1024 * 1024; // 4 GB -static const int VERTEX_STRIDE = 8; // pos(3) + normal(3) + object_id(1) + color(1 packed) +static const size_t INITIAL_VBO_SIZE = 64 * 1024 * 1024; // 64 MB +static const size_t INITIAL_EBO_SIZE = 32 * 1024 * 1024; // 32 MB +static const size_t MAX_BUFFER_SIZE = 4ull * 1024 * 1024 * 1024; // 4 GB + +// ----------------------------------------------------------------------------- +// Shaders +// ----------------------------------------------------------------------------- +// +// Vertex layout (GL side, 28 bytes): +// location 0: vec3 a_position (local coords) +// location 1: vec3 a_normal (local) +// location 2: vec4 a_color (GL_UNSIGNED_BYTE * 4 normalized) +// +// Per-instance record in SSBO std430 (80 bytes): +// mat4 transform +// uint object_id +// uint color_override_rgba8 -- 0 => use baked a_color +// uint _pad0, _pad1 +// +// The draw calls pass `u_instance_offset = mesh.first_instance`; the shader +// reads `instances[u_instance_offset + gl_InstanceID]`. static const char* MAIN_VERTEX_SHADER = R"( #version 450 core layout(location = 0) in vec3 a_position; layout(location = 1) in vec3 a_normal; -layout(location = 2) in float a_object_id; -layout(location = 3) in vec4 a_color; +layout(location = 2) in vec4 a_color; + +struct InstanceRecord { + mat4 transform; + uint object_id; + uint color_override; + uint _pad0; + uint _pad1; +}; +layout(std430, binding = 0) readonly buffer Instances { + InstanceRecord instances[]; +}; uniform mat4 u_view_projection; +uniform uint u_instance_offset; uniform uint u_selected_id; out vec3 v_normal; -out vec3 v_position; out vec4 v_color; flat out uint v_object_id; flat out uint v_selected; void main() { - gl_Position = u_view_projection * vec4(a_position, 1.0); - v_normal = a_normal; - v_position = a_position; - v_color = a_color; - v_object_id = floatBitsToUint(a_object_id); + InstanceRecord inst = instances[u_instance_offset + uint(gl_InstanceID)]; + vec4 world = inst.transform * vec4(a_position, 1.0); + gl_Position = u_view_projection * world; + + // Rotate the normal by the upper-3x3 of the transform. For the vast + // majority of BIM placements this is a rigid rotation (+ uniform scale), + // so we skip the inverse-transpose. + v_normal = normalize(mat3(inst.transform) * a_normal); + + vec4 baked = a_color; + if (inst.color_override != 0u) { + float r = float((inst.color_override ) & 0xFFu) / 255.0; + float g = float((inst.color_override >> 8) & 0xFFu) / 255.0; + float b = float((inst.color_override >> 16) & 0xFFu) / 255.0; + float a = float((inst.color_override >> 24) & 0xFFu) / 255.0; + if (a > 0.0) baked = vec4(r, g, b, a); + } + v_color = baked; + + v_object_id = inst.object_id; v_selected = (v_object_id == u_selected_id) ? 1u : 0u; } )"; @@ -65,7 +106,6 @@ void main() { static const char* MAIN_FRAGMENT_SHADER = R"( #version 450 core in vec3 v_normal; -in vec3 v_position; in vec4 v_color; flat in uint v_object_id; flat in uint v_selected; @@ -80,11 +120,7 @@ void main() { float ambient = 0.25; float diffuse = 0.75 * ndotl; vec3 color = v_color.rgb * (ambient + diffuse); - - if (v_selected == 1u) { - color = mix(color, vec3(0.2, 0.6, 1.0), 0.5); - } - + if (v_selected == 1u) color = mix(color, vec3(0.2, 0.6, 1.0), 0.5); frag_color = vec4(color, v_color.a); } )"; @@ -92,39 +128,43 @@ void main() { static const char* PICK_VERTEX_SHADER = R"( #version 450 core layout(location = 0) in vec3 a_position; -layout(location = 1) in vec3 a_normal; -layout(location = 2) in float a_object_id; + +struct InstanceRecord { + mat4 transform; + uint object_id; + uint color_override; + uint _pad0; + uint _pad1; +}; +layout(std430, binding = 0) readonly buffer Instances { + InstanceRecord instances[]; +}; uniform mat4 u_view_projection; +uniform uint u_instance_offset; flat out uint v_object_id; void main() { - gl_Position = u_view_projection * vec4(a_position, 1.0); - v_object_id = floatBitsToUint(a_object_id); + InstanceRecord inst = instances[u_instance_offset + uint(gl_InstanceID)]; + gl_Position = u_view_projection * inst.transform * vec4(a_position, 1.0); + v_object_id = inst.object_id; } )"; static const char* PICK_FRAGMENT_SHADER = R"( #version 450 core flat in uint v_object_id; - out uint frag_id; - -void main() { - frag_id = v_object_id; -} +void main() { frag_id = v_object_id; } )"; static const char* AXIS_VERTEX_SHADER = R"( #version 450 core layout(location = 0) in vec3 a_position; layout(location = 1) in vec3 a_color; - uniform mat4 u_mvp; - out vec3 v_color; - void main() { gl_Position = u_mvp * vec4(a_position, 1.0); v_color = a_color; @@ -135,10 +175,7 @@ static const char* AXIS_FRAGMENT_SHADER = R"( #version 450 core in vec3 v_color; out vec4 frag_color; - -void main() { - frag_color = vec4(v_color, 1.0); -} +void main() { frag_color = vec4(v_color, 1.0); } )"; static GLuint compileShader(QOpenGLFunctions_4_5_Core* gl, GLenum type, const char* source) { @@ -148,7 +185,7 @@ static GLuint compileShader(QOpenGLFunctions_4_5_Core* gl, GLenum type, const ch GLint ok = 0; gl->glGetShaderiv(shader, GL_COMPILE_STATUS, &ok); if (!ok) { - char log[1024]; + char log[2048]; gl->glGetShaderInfoLog(shader, sizeof(log), nullptr, log); qWarning("Shader compile error: %s", log); } @@ -163,7 +200,7 @@ static GLuint linkProgram(QOpenGLFunctions_4_5_Core* gl, GLuint vert, GLuint fra GLint ok = 0; gl->glGetProgramiv(prog, GL_LINK_STATUS, &ok); if (!ok) { - char log[1024]; + char log[2048]; gl->glGetProgramInfoLog(prog, sizeof(log), nullptr, log); qWarning("Program link error: %s", log); } @@ -172,6 +209,8 @@ static GLuint linkProgram(QOpenGLFunctions_4_5_Core* gl, GLuint vert, GLuint fra return prog; } +// ----------------------------------------------------------------------------- + ViewportWindow::ViewportWindow(QWindow* parent) : QWindow(parent) { @@ -188,26 +227,25 @@ ViewportWindow::ViewportWindow(QWindow* parent) connect(&render_timer_, &QTimer::timeout, this, [this]() { if (isExposed()) render(); }); - render_timer_.setInterval(16); // ~60 fps + render_timer_.setInterval(16); } ViewportWindow::~ViewportWindow() { - if (bvh_build_thread_.joinable()) - bvh_build_thread_.join(); if (context_) { context_->makeCurrent(this); if (gl_) { for (auto& [mid, m] : models_gpu_) { - if (m.vao) gl_->glDeleteVertexArrays(1, &m.vao); - if (m.vbo) gl_->glDeleteBuffers(1, &m.vbo); - if (m.ebo) gl_->glDeleteBuffers(1, &m.ebo); + if (m.vao) gl_->glDeleteVertexArrays(1, &m.vao); + if (m.vbo) gl_->glDeleteBuffers(1, &m.vbo); + if (m.ebo) gl_->glDeleteBuffers(1, &m.ebo); + if (m.ssbo) gl_->glDeleteBuffers(1, &m.ssbo); } - if (axis_vao_) gl_->glDeleteVertexArrays(1, &axis_vao_); - if (axis_vbo_) gl_->glDeleteBuffers(1, &axis_vbo_); - if (main_program_) gl_->glDeleteProgram(main_program_); - if (pick_program_) gl_->glDeleteProgram(pick_program_); - if (axis_program_) gl_->glDeleteProgram(axis_program_); - if (pick_fbo_) gl_->glDeleteFramebuffers(1, &pick_fbo_); + if (axis_vao_) gl_->glDeleteVertexArrays(1, &axis_vao_); + if (axis_vbo_) gl_->glDeleteBuffers(1, &axis_vbo_); + if (main_program_) gl_->glDeleteProgram(main_program_); + if (pick_program_) gl_->glDeleteProgram(pick_program_); + if (axis_program_) gl_->glDeleteProgram(axis_program_); + if (pick_fbo_) gl_->glDeleteFramebuffers(1, &pick_fbo_); if (pick_color_tex_) gl_->glDeleteTextures(1, &pick_color_tex_); if (pick_depth_rbo_) gl_->glDeleteRenderbuffers(1, &pick_depth_rbo_); } @@ -220,17 +258,11 @@ void ViewportWindow::initGL() { context_ = new QOpenGLContext(this); context_->setFormat(requestedFormat()); - if (!context_->create()) { - qFatal("Failed to create OpenGL context"); - return; - } + if (!context_->create()) { qFatal("Failed to create OpenGL context"); return; } context_->makeCurrent(this); gl_ = QOpenGLVersionFunctionsFactory::get(context_); - if (!gl_) { - qWarning("OpenGL 4.5 not available, falling back"); - return; - } + if (!gl_) { qWarning("OpenGL 4.5 not available"); return; } buildShaders(); buildAxisGizmo(); @@ -247,28 +279,23 @@ void ViewportWindow::initGL() { } void ViewportWindow::setupVaoLayout(GLuint vao, GLuint vbo, GLuint ebo) { - gl_->glVertexArrayVertexBuffer(vao, 0, vbo, 0, VERTEX_STRIDE * sizeof(float)); + gl_->glVertexArrayVertexBuffer(vao, 0, vbo, 0, INSTANCED_VERTEX_STRIDE_BYTES); gl_->glVertexArrayElementBuffer(vao, ebo); - // position + // position (3 float @ 0) gl_->glEnableVertexArrayAttrib(vao, 0); gl_->glVertexArrayAttribFormat(vao, 0, 3, GL_FLOAT, GL_FALSE, 0); gl_->glVertexArrayAttribBinding(vao, 0, 0); - // normal + // normal (3 float @ 12) gl_->glEnableVertexArrayAttrib(vao, 1); - gl_->glVertexArrayAttribFormat(vao, 1, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float)); + gl_->glVertexArrayAttribFormat(vao, 1, 3, GL_FLOAT, GL_FALSE, 12); gl_->glVertexArrayAttribBinding(vao, 1, 0); - // object_id (passed as float, decoded in shader via floatBitsToUint) + // color (4 ubyte @ 24, normalized) gl_->glEnableVertexArrayAttrib(vao, 2); - gl_->glVertexArrayAttribFormat(vao, 2, 1, GL_FLOAT, GL_FALSE, 6 * sizeof(float)); + gl_->glVertexArrayAttribFormat(vao, 2, 4, GL_UNSIGNED_BYTE, GL_TRUE, 24); gl_->glVertexArrayAttribBinding(vao, 2, 0); - - // color (RGBA8 packed into the 4 bytes at offset 28; normalized to vec4) - gl_->glEnableVertexArrayAttrib(vao, 3); - gl_->glVertexArrayAttribFormat(vao, 3, 4, GL_UNSIGNED_BYTE, GL_TRUE, 7 * sizeof(float)); - gl_->glVertexArrayAttribBinding(vao, 3, 0); } void ViewportWindow::buildShaders() { @@ -291,24 +318,20 @@ void ViewportWindow::buildShaders() { void ViewportWindow::buildAxisGizmo() { static const float axis_data[] = { - 0.0f, 0.0f, 0.0f, 1.0f, 0.25f, 0.25f, - 1.0f, 0.0f, 0.0f, 1.0f, 0.25f, 0.25f, - 0.0f, 0.0f, 0.0f, 0.30f, 0.95f, 0.30f, - 0.0f, 1.0f, 0.0f, 0.30f, 0.95f, 0.30f, - 0.0f, 0.0f, 0.0f, 0.30f, 0.55f, 1.0f, - 0.0f, 0.0f, 1.0f, 0.30f, 0.55f, 1.0f, + 0,0,0, 1.0f,0.25f,0.25f, + 1,0,0, 1.0f,0.25f,0.25f, + 0,0,0, 0.30f,0.95f,0.30f, + 0,1,0, 0.30f,0.95f,0.30f, + 0,0,0, 0.30f,0.55f,1.0f, + 0,0,1, 0.30f,0.55f,1.0f, }; - gl_->glCreateVertexArrays(1, &axis_vao_); gl_->glCreateBuffers(1, &axis_vbo_); gl_->glNamedBufferStorage(axis_vbo_, sizeof(axis_data), axis_data, 0); - gl_->glVertexArrayVertexBuffer(axis_vao_, 0, axis_vbo_, 0, 6 * sizeof(float)); - gl_->glEnableVertexArrayAttrib(axis_vao_, 0); gl_->glVertexArrayAttribFormat(axis_vao_, 0, 3, GL_FLOAT, GL_FALSE, 0); gl_->glVertexArrayAttribBinding(axis_vao_, 0, 0); - gl_->glEnableVertexArrayAttrib(axis_vao_, 1); gl_->glVertexArrayAttribFormat(axis_vao_, 1, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float)); gl_->glVertexArrayAttribBinding(axis_vao_, 1, 0); @@ -318,25 +341,20 @@ bool ViewportWindow::growModelVbo(ModelGpuData& m, size_t needed_total) { size_t new_capacity = m.vbo_capacity; while (new_capacity < needed_total) new_capacity *= 2; if (new_capacity > MAX_BUFFER_SIZE) { - qWarning("VBO grow request (%zu MB) exceeds cap", new_capacity / (1024 * 1024)); + qWarning("VBO grow request (%zu MB) exceeds cap", new_capacity / (1024*1024)); return false; } - GLuint new_vbo = 0; gl_->glCreateBuffers(1, &new_vbo); gl_->glNamedBufferStorage(new_vbo, new_capacity, nullptr, GL_DYNAMIC_STORAGE_BIT); - if (m.vbo_used > 0) { gl_->glCopyNamedBufferSubData(m.vbo, new_vbo, 0, 0, m.vbo_used); } - gl_->glDeleteBuffers(1, &m.vbo); m.vbo = new_vbo; m.vbo_capacity = new_capacity; - - gl_->glVertexArrayVertexBuffer(m.vao, 0, m.vbo, 0, VERTEX_STRIDE * sizeof(float)); - - qInfo("Model VBO grew to %zu MB", m.vbo_capacity / (1024 * 1024)); + gl_->glVertexArrayVertexBuffer(m.vao, 0, m.vbo, 0, INSTANCED_VERTEX_STRIDE_BYTES); + qInfo("Model VBO grew to %zu MB", m.vbo_capacity / (1024*1024)); return true; } @@ -344,268 +362,178 @@ bool ViewportWindow::growModelEbo(ModelGpuData& m, size_t needed_total) { size_t new_capacity = m.ebo_capacity; while (new_capacity < needed_total) new_capacity *= 2; if (new_capacity > MAX_BUFFER_SIZE) { - qWarning("EBO grow request (%zu MB) exceeds cap", new_capacity / (1024 * 1024)); + qWarning("EBO grow request (%zu MB) exceeds cap", new_capacity / (1024*1024)); return false; } - GLuint new_ebo = 0; gl_->glCreateBuffers(1, &new_ebo); gl_->glNamedBufferStorage(new_ebo, new_capacity, nullptr, GL_DYNAMIC_STORAGE_BIT); - if (m.ebo_used > 0) { gl_->glCopyNamedBufferSubData(m.ebo, new_ebo, 0, 0, m.ebo_used); } - gl_->glDeleteBuffers(1, &m.ebo); m.ebo = new_ebo; m.ebo_capacity = new_capacity; - gl_->glVertexArrayElementBuffer(m.vao, m.ebo); - - qInfo("Model EBO grew to %zu MB", m.ebo_capacity / (1024 * 1024)); + qInfo("Model EBO grew to %zu MB", m.ebo_capacity / (1024*1024)); return true; } -void ViewportWindow::uploadChunk(const UploadChunk& chunk) { - if (!gl_initialized_) return; - if (chunk.vertices.empty() || chunk.indices.empty()) return; +ModelGpuData& ViewportWindow::getOrCreateModel(uint32_t model_id) { + auto it = models_gpu_.find(model_id); + if (it != models_gpu_.end()) return it->second; - context_->makeCurrent(this); - - // Get or create per-model GPU data. - auto it = models_gpu_.find(chunk.model_id); - if (it == models_gpu_.end()) { - ModelGpuData m; - gl_->glCreateVertexArrays(1, &m.vao); - gl_->glCreateBuffers(1, &m.vbo); - gl_->glCreateBuffers(1, &m.ebo); - - m.vbo_capacity = INITIAL_VBO_SIZE; - m.ebo_capacity = INITIAL_EBO_SIZE; - gl_->glNamedBufferStorage(m.vbo, m.vbo_capacity, nullptr, GL_DYNAMIC_STORAGE_BIT); - gl_->glNamedBufferStorage(m.ebo, m.ebo_capacity, nullptr, GL_DYNAMIC_STORAGE_BIT); - - setupVaoLayout(m.vao, m.vbo, m.ebo); - it = models_gpu_.emplace(chunk.model_id, std::move(m)).first; - } - - auto& mgpu = it->second; - - size_t vb_size = chunk.vertices.size() * sizeof(float); - size_t ib_size = chunk.indices.size() * sizeof(uint32_t); - - if (mgpu.vbo_used + vb_size > mgpu.vbo_capacity) { - if (!growModelVbo(mgpu, mgpu.vbo_used + vb_size)) { - qWarning("VBO at cap, skipping chunk"); - return; - } - } - if (mgpu.ebo_used + ib_size > mgpu.ebo_capacity) { - if (!growModelEbo(mgpu, mgpu.ebo_used + ib_size)) { - qWarning("EBO at cap, skipping chunk"); - return; - } - } - - uint32_t base_vertex = mgpu.vertex_count; - - gl_->glNamedBufferSubData(mgpu.vbo, mgpu.vbo_used, vb_size, chunk.vertices.data()); - - // Remap chunk-local indices into model-local global indices. - std::vector global_indices(chunk.indices.size()); - for (size_t i = 0; i < chunk.indices.size(); ++i) { - global_indices[i] = chunk.indices[i] + base_vertex; - } - gl_->glNamedBufferSubData(mgpu.ebo, mgpu.ebo_used, ib_size, global_indices.data()); - - // Compute AABB from vertex positions in this chunk. - ObjectDrawInfo info; - info.index_offset = static_cast(mgpu.ebo_used); - info.index_count = static_cast(chunk.indices.size()); - info.model_id = chunk.model_id; - - const size_t num_verts = chunk.vertices.size() / VERTEX_STRIDE; - if (num_verts > 0) { - info.aabb_min[0] = info.aabb_min[1] = info.aabb_min[2] = std::numeric_limits::max(); - info.aabb_max[0] = info.aabb_max[1] = info.aabb_max[2] = -std::numeric_limits::max(); - for (size_t v = 0; v < num_verts; ++v) { - const float* pos = &chunk.vertices[v * VERTEX_STRIDE]; - for (int a = 0; a < 3; ++a) { - if (pos[a] < info.aabb_min[a]) info.aabb_min[a] = pos[a]; - if (pos[a] > info.aabb_max[a]) info.aabb_max[a] = pos[a]; - } - } - } else { - info.aabb_min[0] = info.aabb_min[1] = info.aabb_min[2] = 0.0f; - info.aabb_max[0] = info.aabb_max[1] = info.aabb_max[2] = 0.0f; - } - - mgpu.draw_info.push_back(info); - mgpu.active_draw_count = static_cast(mgpu.draw_info.size()); // immediately drawable - mgpu.vbo_used += vb_size; - mgpu.ebo_used += ib_size; - mgpu.vertex_count += static_cast(num_verts); - mgpu.total_triangles += static_cast(chunk.indices.size() / 3); -} - -void ViewportWindow::uploadBulk(uint32_t model_id, - std::vector vertices, - std::vector indices, - const std::vector& draw_info, - std::shared_ptr bvh_set) { - if (!gl_initialized_) return; - if (vertices.empty() || indices.empty()) return; - - context_->makeCurrent(this); - - size_t vb_size = vertices.size() * sizeof(float); - size_t ib_size = indices.size() * sizeof(uint32_t); - - // Allocate empty buffers at exact size — no data uploaded yet. ModelGpuData m; gl_->glCreateVertexArrays(1, &m.vao); gl_->glCreateBuffers(1, &m.vbo); gl_->glCreateBuffers(1, &m.ebo); - m.vbo_capacity = vb_size; - m.ebo_capacity = ib_size; - gl_->glNamedBufferStorage(m.vbo, vb_size, nullptr, GL_DYNAMIC_STORAGE_BIT); - gl_->glNamedBufferStorage(m.ebo, ib_size, nullptr, GL_DYNAMIC_STORAGE_BIT); - + m.vbo_capacity = INITIAL_VBO_SIZE; + m.ebo_capacity = INITIAL_EBO_SIZE; + gl_->glNamedBufferStorage(m.vbo, m.vbo_capacity, nullptr, GL_DYNAMIC_STORAGE_BIT); + gl_->glNamedBufferStorage(m.ebo, m.ebo_capacity, nullptr, GL_DYNAMIC_STORAGE_BIT); setupVaoLayout(m.vao, m.vbo, m.ebo); - m.vbo_used = vb_size; - m.ebo_used = ib_size; - m.vertex_count = static_cast(vertices.size() / VERTEX_STRIDE); - m.draw_info = draw_info; - m.active_draw_count = 0; // nothing drawable yet + return models_gpu_.emplace(model_id, std::move(m)).first->second; +} - uint32_t total_tri = 0; - for (const auto& di : draw_info) total_tri += di.index_count / 3; - m.total_triangles = total_tri; +void ViewportWindow::uploadMeshChunk(const MeshChunk& chunk) { + if (!gl_initialized_) return; + if (chunk.vertices.empty() || chunk.indices.empty()) return; + context_->makeCurrent(this); - // Delete old model data if re-uploading. - auto it = models_gpu_.find(model_id); - if (it != models_gpu_.end()) { - gl_->glDeleteVertexArrays(1, &it->second.vao); - gl_->glDeleteBuffers(1, &it->second.vbo); - gl_->glDeleteBuffers(1, &it->second.ebo); + ModelGpuData& m = getOrCreateModel(chunk.model_id); + + const size_t vb_size = chunk.vertices.size() * sizeof(float); + const size_t ib_size = chunk.indices.size() * sizeof(uint32_t); + + if (m.vbo_used + vb_size > m.vbo_capacity) { + if (!growModelVbo(m, m.vbo_used + vb_size)) return; + } + if (m.ebo_used + ib_size > m.ebo_capacity) { + if (!growModelEbo(m, m.ebo_used + ib_size)) return; } - models_gpu_[model_id] = std::move(m); - // Queue progressive upload — data will stream in over subsequent frames. - PendingUpload pu; - pu.model_id = model_id; - pu.vertices = std::move(vertices); - pu.indices = std::move(indices); - pu.bvh_set = std::move(bvh_set); - pending_uploads_.push_back(std::move(pu)); + MeshInfo info; + info.vbo_byte_offset = static_cast(m.vbo_used); + info.vertex_count = static_cast( + chunk.vertices.size() / INSTANCED_VERTEX_STRIDE_FLOATS); + info.ebo_byte_offset = static_cast(m.ebo_used); + info.index_count = static_cast(chunk.indices.size()); + for (int a = 0; a < 3; ++a) { + info.local_aabb_min[a] = chunk.local_aabb_min[a]; + info.local_aabb_max[a] = chunk.local_aabb_max[a]; + } + info.first_instance = 0; + info.instance_count = 0; - qDebug("Bulk upload queued: model %u, %zu vertices, %zu indices, %zu objects", - model_id, vertices.size() / VERTEX_STRIDE, indices.size(), draw_info.size()); + gl_->glNamedBufferSubData(m.vbo, m.vbo_used, vb_size, chunk.vertices.data()); + gl_->glNamedBufferSubData(m.ebo, m.ebo_used, ib_size, chunk.indices.data()); + m.vbo_used += vb_size; + m.ebo_used += ib_size; + m.vertex_count += info.vertex_count; + + if (m.meshes.size() <= chunk.local_mesh_id) m.meshes.resize(chunk.local_mesh_id + 1); + m.meshes[chunk.local_mesh_id] = info; +} + +void ViewportWindow::uploadInstanceChunk(const InstanceChunk& chunk) { + if (!gl_initialized_) return; + // We don't need a GL context here since we're only touching CPU state, + // but the signal may fire on the render thread so keep it simple. + ModelGpuData& m = getOrCreateModel(chunk.model_id); + + InstanceCpu inst; + inst.mesh_id = chunk.local_mesh_id; + inst.object_id = chunk.object_id; + inst.color_override_rgba8 = chunk.color_override_rgba8; + inst.model_id = chunk.model_id; + std::memcpy(inst.transform, chunk.transform, sizeof(inst.transform)); + std::memcpy(inst.world_aabb_min, chunk.world_aabb_min, sizeof(inst.world_aabb_min)); + std::memcpy(inst.world_aabb_max, chunk.world_aabb_max, sizeof(inst.world_aabb_max)); + m.instances.push_back(inst); + + if (chunk.local_mesh_id < m.meshes.size()) { + m.total_triangles += m.meshes[chunk.local_mesh_id].index_count / 3; + } +} + +void ViewportWindow::finalizeModel(uint32_t model_id) { + if (!gl_initialized_) return; + context_->makeCurrent(this); + + auto it = models_gpu_.find(model_id); + if (it == models_gpu_.end()) return; + ModelGpuData& m = it->second; + if (m.instances.empty()) { m.finalized = true; return; } + + // Sort instances by mesh_id (stable for deterministic ordering). + std::stable_sort(m.instances.begin(), m.instances.end(), + [](const InstanceCpu& a, const InstanceCpu& b) { + return a.mesh_id < b.mesh_id; + }); + + // Assign per-mesh contiguous range. + for (auto& mesh : m.meshes) { mesh.first_instance = 0; mesh.instance_count = 0; } + uint32_t current = UINT32_MAX; + uint32_t run_start = 0; + for (uint32_t i = 0; i < m.instances.size(); ++i) { + uint32_t mid = m.instances[i].mesh_id; + if (mid != current) { + if (current != UINT32_MAX && current < m.meshes.size()) { + m.meshes[current].first_instance = run_start; + m.meshes[current].instance_count = i - run_start; + } + current = mid; + run_start = i; + } + } + if (current != UINT32_MAX && current < m.meshes.size()) { + m.meshes[current].first_instance = run_start; + m.meshes[current].instance_count = static_cast(m.instances.size()) - run_start; + } + + // Build GPU-layout array. + std::vector gpu(m.instances.size()); + for (size_t i = 0; i < m.instances.size(); ++i) { + const InstanceCpu& src = m.instances[i]; + InstanceGpu& dst = gpu[i]; + std::memcpy(dst.transform, src.transform, sizeof(dst.transform)); + dst.object_id = src.object_id; + dst.color_override_rgba8 = src.color_override_rgba8; + dst._pad0 = 0; + dst._pad1 = 0; + } + + // Allocate and upload SSBO. + if (m.ssbo) gl_->glDeleteBuffers(1, &m.ssbo); + gl_->glCreateBuffers(1, &m.ssbo); + const size_t ssbo_bytes = gpu.size() * sizeof(InstanceGpu); + gl_->glNamedBufferStorage(m.ssbo, ssbo_bytes, gpu.data(), 0); + m.ssbo_instance_count = static_cast(gpu.size()); + + m.finalized = true; + + qDebug("Model %u finalized: %zu verts, %zu meshes, %zu instances, %.1f MB vram " + "(vbo %.1f + ebo %.1f + ssbo %.1f)", + model_id, size_t(m.vertex_count), m.meshes.size(), m.instances.size(), + (m.vbo_capacity + m.ebo_capacity + ssbo_bytes) / (1024.0*1024.0), + m.vbo_capacity / (1024.0*1024.0), + m.ebo_capacity / (1024.0*1024.0), + ssbo_bytes / (1024.0*1024.0)); } void ViewportWindow::resetScene() { if (!gl_initialized_) return; - - if (bvh_build_thread_.joinable()) - bvh_build_thread_.join(); - context_->makeCurrent(this); for (auto& [mid, m] : models_gpu_) { - if (m.vao) gl_->glDeleteVertexArrays(1, &m.vao); - if (m.vbo) gl_->glDeleteBuffers(1, &m.vbo); - if (m.ebo) gl_->glDeleteBuffers(1, &m.ebo); + if (m.vao) gl_->glDeleteVertexArrays(1, &m.vao); + if (m.vbo) gl_->glDeleteBuffers(1, &m.vbo); + if (m.ebo) gl_->glDeleteBuffers(1, &m.ebo); + if (m.ssbo) gl_->glDeleteBuffers(1, &m.ssbo); } models_gpu_.clear(); - model_bvhs_.clear(); - pending_uploads_.clear(); selected_object_id_ = 0; - { - std::lock_guard bvh_lock(bvh_result_mutex_); - pending_bvh_.reset(); - } -} - -static const size_t UPLOAD_CHUNK_BYTES = 48 * 1024 * 1024; // 48 MB per frame - -void ViewportWindow::processPendingUploads() { - if (pending_uploads_.empty()) return; - - auto& pu = pending_uploads_.front(); - auto it = models_gpu_.find(pu.model_id); - if (it == models_gpu_.end()) { - pending_uploads_.pop_front(); - return; - } - auto& mgpu = it->second; - - size_t vbo_total = pu.vertices.size() * sizeof(float); - size_t ebo_total = pu.indices.size() * sizeof(uint32_t); - - // Phase 1: Upload VBO in chunks. - if (pu.vbo_uploaded < vbo_total) { - size_t remaining = vbo_total - pu.vbo_uploaded; - size_t chunk = std::min(remaining, UPLOAD_CHUNK_BYTES); - gl_->glNamedBufferSubData(mgpu.vbo, pu.vbo_uploaded, chunk, - reinterpret_cast(pu.vertices.data()) + pu.vbo_uploaded); - pu.vbo_uploaded += chunk; - - if (pu.vbo_uploaded >= vbo_total) { - // VBO done — free CPU memory. - pu.vertices.clear(); - pu.vertices.shrink_to_fit(); - } - return; // yield to render loop - } - - // Phase 2: Upload EBO in chunks. Objects become drawable as their range lands. - if (pu.ebo_uploaded < ebo_total) { - size_t remaining = ebo_total - pu.ebo_uploaded; - size_t chunk = std::min(remaining, UPLOAD_CHUNK_BYTES); - gl_->glNamedBufferSubData(mgpu.ebo, pu.ebo_uploaded, chunk, - reinterpret_cast(pu.indices.data()) + pu.ebo_uploaded); - pu.ebo_uploaded += chunk; - - // Advance active_draw_count: activate objects whose EBO range is fully uploaded. - while (mgpu.active_draw_count < mgpu.draw_info.size()) { - const auto& obj = mgpu.draw_info[mgpu.active_draw_count]; - size_t obj_end = obj.index_offset + obj.index_count * sizeof(uint32_t); - if (obj_end <= pu.ebo_uploaded) - mgpu.active_draw_count++; - else - break; - } - - if (pu.ebo_uploaded >= ebo_total) { - // EBO done — free CPU memory. - pu.indices.clear(); - pu.indices.shrink_to_fit(); - } else { - return; // yield to render loop - } - } - - // Fully uploaded — activate BVH if present. - mgpu.active_draw_count = static_cast(mgpu.draw_info.size()); - if (pu.bvh_set) { - model_bvhs_[pu.model_id] = std::move(pu.bvh_set); - } - - size_t total_vbo = 0, total_ebo = 0; - for (const auto& [mid, mg] : models_gpu_) { - total_vbo += mg.vbo_capacity; - total_ebo += mg.ebo_capacity; - } - qDebug("Progressive upload complete: model %u (this: vbo %.1f MB + ebo %.1f MB, " - "%u objects, %u triangles) scene total vram %.1f MB", - pu.model_id, - mgpu.vbo_capacity / (1024.0 * 1024.0), - mgpu.ebo_capacity / (1024.0 * 1024.0), - static_cast(mgpu.draw_info.size()), - mgpu.total_triangles, - (total_vbo + total_ebo) / (1024.0 * 1024.0)); - pending_uploads_.pop_front(); } void ViewportWindow::hideModel(uint32_t model_id) { @@ -621,161 +549,35 @@ void ViewportWindow::showModel(uint32_t model_id) { void ViewportWindow::removeModel(uint32_t model_id) { if (!gl_initialized_) return; context_->makeCurrent(this); - - // Cancel any pending upload for this model. - pending_uploads_.erase( - std::remove_if(pending_uploads_.begin(), pending_uploads_.end(), - [model_id](const PendingUpload& pu) { return pu.model_id == model_id; }), - pending_uploads_.end()); - auto it = models_gpu_.find(model_id); if (it != models_gpu_.end()) { - gl_->glDeleteVertexArrays(1, &it->second.vao); - gl_->glDeleteBuffers(1, &it->second.vbo); - gl_->glDeleteBuffers(1, &it->second.ebo); + if (it->second.vao) gl_->glDeleteVertexArrays(1, &it->second.vao); + if (it->second.vbo) gl_->glDeleteBuffers(1, &it->second.vbo); + if (it->second.ebo) gl_->glDeleteBuffers(1, &it->second.ebo); + if (it->second.ssbo) gl_->glDeleteBuffers(1, &it->second.ssbo); models_gpu_.erase(it); } - model_bvhs_.erase(model_id); } -std::vector ViewportWindow::readbackEbo(uint32_t model_id) const { - std::vector ebo_data; - auto it = models_gpu_.find(model_id); - if (!gl_ || it == models_gpu_.end() || it->second.ebo_used == 0) return ebo_data; - - const auto& m = it->second; - size_t num_indices = m.ebo_used / sizeof(uint32_t); - ebo_data.resize(num_indices); - gl_->glGetNamedBufferSubData(m.ebo, 0, m.ebo_used, ebo_data.data()); - return ebo_data; -} - -std::vector ViewportWindow::readbackVbo(uint32_t model_id) const { - std::vector vbo_data; - auto it = models_gpu_.find(model_id); - if (!gl_ || it == models_gpu_.end() || it->second.vbo_used == 0) return vbo_data; - - const auto& m = it->second; - size_t num_floats = m.vbo_used / sizeof(float); - vbo_data.resize(num_floats); - gl_->glGetNamedBufferSubData(m.vbo, 0, m.vbo_used, vbo_data.data()); - return vbo_data; -} - -void ViewportWindow::buildBvhAsync(uint32_t model_id, - const std::string& ifc_path, - uint64_t ifc_file_size, - std::vector sidecar_elements, - std::string sidecar_string_table) { - if (bvh_build_thread_.joinable()) - bvh_build_thread_.join(); - - auto it = models_gpu_.find(model_id); - if (it == models_gpu_.end()) return; - - // Snapshot draw info; read back EBO + VBO on GL thread. - std::vector draw_snapshot = it->second.draw_info; - std::vector ebo_snapshot = readbackEbo(model_id); - std::vector vbo_snapshot; - if (!ifc_path.empty() && !sidecar_elements.empty()) { - vbo_snapshot = readbackVbo(model_id); - } - - if (draw_snapshot.empty() || ebo_snapshot.empty()) return; - - bvh_build_thread_ = std::thread([this, - model_id, - draw_info = std::move(draw_snapshot), - ebo_data = std::move(ebo_snapshot), - vbo_data = std::move(vbo_snapshot), - elements = std::move(sidecar_elements), - string_table = std::move(sidecar_string_table), - ifc_path, ifc_file_size]() { - auto bvh_set = buildBvhSet(draw_info); - - EboReorderResult ebo_result = reorderEbo(*bvh_set, draw_info, ebo_data); - - // Write full sidecar if requested. - if (!ifc_path.empty() && !elements.empty() && !vbo_data.empty()) { - SidecarData sd; - sd.vertices = vbo_data; - sd.indices = ebo_result.reordered_ebo; - sd.draw_info = ebo_result.reordered_draw_info; - sd.elements = std::move(elements); - sd.string_table = std::move(string_table); - sd.bvh_set = bvh_set; - writeSidecar(ifc_path, sd, ifc_file_size); - } - - { - std::lock_guard lock(bvh_result_mutex_); - pending_bvh_ = std::make_unique(); - pending_bvh_->model_id = model_id; - pending_bvh_->bvh_set = std::move(bvh_set); - pending_bvh_->ebo_reorder = std::move(ebo_result); - } - }); -} - -void ViewportWindow::applyBvhResult() { - std::unique_ptr result; - { - std::lock_guard lock(bvh_result_mutex_); - result = std::move(pending_bvh_); - } - if (!result) return; - - auto it = models_gpu_.find(result->model_id); - if (it == models_gpu_.end()) return; - - auto& mgpu = it->second; - - // Re-upload the reordered EBO into this model's buffer. - if (!result->ebo_reorder.reordered_ebo.empty()) { - size_t ebo_bytes = result->ebo_reorder.reordered_ebo.size() * sizeof(uint32_t); - if (ebo_bytes <= mgpu.ebo_capacity) { - gl_->glNamedBufferSubData(mgpu.ebo, 0, ebo_bytes, - result->ebo_reorder.reordered_ebo.data()); - } - } - - // Swap draw info. - if (result->ebo_reorder.reordered_draw_info.size() == mgpu.draw_info.size()) { - mgpu.draw_info = std::move(result->ebo_reorder.reordered_draw_info); - } - - model_bvhs_[result->model_id] = std::move(result->bvh_set); - - qDebug("BVH activated for model %u", result->model_id); -} - -void ViewportWindow::setSelectedObjectId(uint32_t id) { - selected_object_id_ = id; -} +void ViewportWindow::setSelectedObjectId(uint32_t id) { selected_object_id_ = id; } uint32_t ViewportWindow::pickObjectAt(int x, int y) { if (!gl_initialized_) return 0; - context_->makeCurrent(this); int w = width() * devicePixelRatio(); int h = height() * devicePixelRatio(); - if (pick_width_ != w || pick_height_ != h) { if (pick_fbo_) gl_->glDeleteFramebuffers(1, &pick_fbo_); if (pick_color_tex_) gl_->glDeleteTextures(1, &pick_color_tex_); if (pick_depth_rbo_) gl_->glDeleteRenderbuffers(1, &pick_depth_rbo_); - gl_->glCreateFramebuffers(1, &pick_fbo_); - gl_->glCreateTextures(GL_TEXTURE_2D, 1, &pick_color_tex_); gl_->glTextureStorage2D(pick_color_tex_, 1, GL_R32UI, w, h); gl_->glNamedFramebufferTexture(pick_fbo_, GL_COLOR_ATTACHMENT0, pick_color_tex_, 0); - gl_->glCreateRenderbuffers(1, &pick_depth_rbo_); gl_->glNamedRenderbufferStorage(pick_depth_rbo_, GL_DEPTH_COMPONENT24, w, h); gl_->glNamedFramebufferRenderbuffer(pick_fbo_, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, pick_depth_rbo_); - pick_width_ = w; pick_height_ = h; } @@ -785,163 +587,32 @@ uint32_t ViewportWindow::pickObjectAt(int x, int y) { int px = x * devicePixelRatio(); int py = (height() - y) * devicePixelRatio(); uint32_t pixel = 0; - gl_->glGetTextureSubImage(pick_color_tex_, 0, px, py, 0, 1, 1, 1, GL_RED_INTEGER, GL_UNSIGNED_INT, sizeof(pixel), &pixel); - + gl_->glGetTextureSubImage(pick_color_tex_, 0, px, py, 0, 1, 1, 1, + GL_RED_INTEGER, GL_UNSIGNED_INT, sizeof(pixel), &pixel); return pixel; } void ViewportWindow::updateCamera() { float yaw_rad = qDegreesToRadians(camera_yaw_); float pitch_rad = qDegreesToRadians(camera_pitch_); - QVector3D eye; eye.setX(camera_target_.x() + camera_distance_ * cosf(pitch_rad) * cosf(yaw_rad)); eye.setY(camera_target_.y() + camera_distance_ * cosf(pitch_rad) * sinf(yaw_rad)); eye.setZ(camera_target_.z() + camera_distance_ * sinf(pitch_rad)); - view_matrix_.setToIdentity(); view_matrix_.lookAt(eye, camera_target_, QVector3D(0, 0, 1)); - proj_matrix_.setToIdentity(); float aspect = width() > 0 ? float(width()) / float(height()) : 1.0f; proj_matrix_.perspective(45.0f, aspect, 0.1f, camera_distance_ * 10.0f); } -bool ViewportWindow::aabbInFrustum(const float aabb_min[3], const float aabb_max[3], - const float planes[6][4]) { - for (int p = 0; p < 6; ++p) { - float px = planes[p][0] >= 0.0f ? aabb_max[0] : aabb_min[0]; - float py = planes[p][1] >= 0.0f ? aabb_max[1] : aabb_min[1]; - float pz = planes[p][2] >= 0.0f ? aabb_max[2] : aabb_min[2]; - float dist = planes[p][0] * px + planes[p][1] * py + planes[p][2] * pz + planes[p][3]; - if (dist < 0.0f) return false; - } - return true; -} - -void ViewportWindow::traverseBvh(const ModelBvh& mbvh, const ModelGpuData& mgpu, - const float planes[6][4]) { - if (mbvh.nodes.empty()) return; - - uint32_t stack[64]; - int sp = 0; - stack[sp++] = 0; // root - - // Get the current model's draw command being built. - auto& cmd = frame_draw_cmds_.back(); - - while (sp > 0) { - uint32_t ni = stack[--sp]; - const BvhNode& node = mbvh.nodes[ni]; - - if (!aabbInFrustum(node.aabb_min, node.aabb_max, planes)) - continue; - - if (node.count > 0) { - // Leaf-batched draw: after reorderEbo, a leaf's objects occupy a - // contiguous EBO range. Emit one draw command covering all of them - // instead of N per-object tests/draws. The leaf AABB test above is - // already a conservative cull; any overdraw (up to BVH_MAX_LEAF_SIZE - // objects that may be fully outside the frustum but inside the leaf - // AABB) costs far less than the per-draw CPU/driver overhead we save. - uint32_t first_oi = mbvh.object_indices[node.right_or_first]; - const auto& first_obj = mgpu.draw_info[first_oi]; - uint32_t leaf_offset = first_obj.index_offset; - uint32_t leaf_count = 0; - for (uint32_t i = 0; i < node.count; ++i) { - uint32_t oi = mbvh.object_indices[node.right_or_first + i]; - leaf_count += mgpu.draw_info[oi].index_count; - } - cmd.counts.push_back(static_cast(leaf_count)); - cmd.offsets.push_back(reinterpret_cast( - static_cast(leaf_offset))); - visible_triangles_ += leaf_count / 3; - visible_objects_ += node.count; - } else { - if (sp < 63) { - stack[sp++] = node.right_or_first; - stack[sp++] = ni + 1; - } - } - } -} - -void ViewportWindow::buildVisibleList(const QMatrix4x4& vp) { - frame_draw_cmds_.clear(); - visible_triangles_ = 0; - visible_objects_ = 0; - - // Extract 6 frustum planes from the view-projection matrix. - float planes[6][4]; - for (int i = 0; i < 4; ++i) { - planes[0][i] = vp(3, i) + vp(0, i); // left - planes[1][i] = vp(3, i) - vp(0, i); // right - planes[2][i] = vp(3, i) + vp(1, i); // bottom - planes[3][i] = vp(3, i) - vp(1, i); // top - planes[4][i] = vp(3, i) + vp(2, i); // near - planes[5][i] = vp(3, i) - vp(2, i); // far - } - for (int p = 0; p < 6; ++p) { - float len = std::sqrt(planes[p][0] * planes[p][0] + - planes[p][1] * planes[p][1] + - planes[p][2] * planes[p][2]); - if (len > 0.0f) { - float inv = 1.0f / len; - planes[p][0] *= inv; - planes[p][1] *= inv; - planes[p][2] *= inv; - planes[p][3] *= inv; - } - } - - for (auto& [model_id, mgpu] : models_gpu_) { - if (mgpu.hidden || mgpu.active_draw_count == 0) continue; - - frame_draw_cmds_.push_back({mgpu.vao, {}, {}}); - auto& cmd = frame_draw_cmds_.back(); - cmd.counts.reserve(mgpu.active_draw_count); - cmd.offsets.reserve(mgpu.active_draw_count); - - bool fully_loaded = (mgpu.active_draw_count == mgpu.draw_info.size()); - auto bvh_it = model_bvhs_.find(model_id); - - // Only use BVH if model is fully uploaded; during progressive upload, - // fall back to linear scan of active objects. - if (fully_loaded && bvh_it != model_bvhs_.end() && bvh_it->second) { - const auto& bvh_set = *bvh_it->second; - auto mbvh_it = bvh_set.models.find(model_id); - if (mbvh_it != bvh_set.models.end()) { - traverseBvh(mbvh_it->second, mgpu, planes); - } - } else { - // Linear scan of active objects only. - for (uint32_t i = 0; i < mgpu.active_draw_count; ++i) { - const auto& obj = mgpu.draw_info[i]; - if (aabbInFrustum(obj.aabb_min, obj.aabb_max, planes)) { - cmd.counts.push_back(static_cast(obj.index_count)); - cmd.offsets.push_back(reinterpret_cast( - static_cast(obj.index_offset))); - visible_triangles_ += obj.index_count / 3; - visible_objects_++; - } - } - } - - if (cmd.counts.empty()) { - frame_draw_cmds_.pop_back(); - } - } -} - void ViewportWindow::render() { if (!gl_initialized_ || !isExposed()) return; context_->makeCurrent(this); - applyBvhResult(); - processPendingUploads(); updateCamera(); - int w = width() * devicePixelRatio(); + int w = width() * devicePixelRatio(); int h = height() * devicePixelRatio(); gl_->glViewport(0, 0, w, h); gl_->glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); @@ -949,24 +620,43 @@ void ViewportWindow::render() { QMatrix4x4 vp = proj_matrix_ * view_matrix_; gl_->glUseProgram(main_program_); - gl_->glUniformMatrix4fv(gl_->glGetUniformLocation(main_program_, "u_view_projection"), 1, GL_FALSE, vp.constData()); - gl_->glUniform3f(gl_->glGetUniformLocation(main_program_, "u_light_dir"), 0.3f, 0.5f, 0.8f); - gl_->glUniform1ui(gl_->glGetUniformLocation(main_program_, "u_selected_id"), selected_object_id_); + GLint u_vp = gl_->glGetUniformLocation(main_program_, "u_view_projection"); + GLint u_light = gl_->glGetUniformLocation(main_program_, "u_light_dir"); + GLint u_sel = gl_->glGetUniformLocation(main_program_, "u_selected_id"); + GLint u_inst_off = gl_->glGetUniformLocation(main_program_, "u_instance_offset"); + gl_->glUniformMatrix4fv(u_vp, 1, GL_FALSE, vp.constData()); + gl_->glUniform3f(u_light, 0.3f, 0.5f, 0.8f); + gl_->glUniform1ui(u_sel, selected_object_id_); - buildVisibleList(vp); - for (const auto& cmd : frame_draw_cmds_) { - gl_->glBindVertexArray(cmd.vao); - gl_->glMultiDrawElements(GL_TRIANGLES, - cmd.counts.data(), GL_UNSIGNED_INT, - cmd.offsets.data(), - static_cast(cmd.counts.size())); + visible_triangles_ = 0; + visible_objects_ = 0; + instanced_draws_ = 0; + + for (auto& [model_id, m] : models_gpu_) { + if (m.hidden || !m.finalized || !m.ssbo) continue; + gl_->glBindVertexArray(m.vao); + gl_->glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, m.ssbo); + + for (const auto& mesh : m.meshes) { + if (mesh.instance_count == 0 || mesh.index_count == 0) continue; + gl_->glUniform1ui(u_inst_off, mesh.first_instance); + gl_->glDrawElementsInstancedBaseVertex( + GL_TRIANGLES, + static_cast(mesh.index_count), + GL_UNSIGNED_INT, + reinterpret_cast(static_cast(mesh.ebo_byte_offset)), + static_cast(mesh.instance_count), + static_cast(mesh.vbo_byte_offset / INSTANCED_VERTEX_STRIDE_BYTES)); + visible_triangles_ += (mesh.index_count / 3) * mesh.instance_count; + visible_objects_ += mesh.instance_count; + ++instanced_draws_; + } } renderAxisGizmo(); context_->swapBuffers(this); - // Compute FPS. float dt = frame_clock_.restart() / 1000.0f; accumulated_time_ += dt; frame_count_++; @@ -975,21 +665,18 @@ void ViewportWindow::render() { frame_count_ = 0; accumulated_time_ = 0.0f; - uint32_t total_obj = 0, total_tri = 0; - size_t total_vram = 0, total_vbo = 0, total_ebo = 0; + uint32_t total_obj = 0, total_tri = 0, total_meshes = 0; + size_t total_vbo = 0, total_ebo = 0, total_ssbo = 0; size_t num_models = 0, num_hidden = 0; - size_t total_leaf_draws = 0; - for (const auto& [mid, m] : models_gpu_) { + for (const auto& [mid, mm] : models_gpu_) { num_models++; - if (m.hidden) { num_hidden++; continue; } - total_obj += static_cast(m.draw_info.size()); - total_tri += m.total_triangles; - total_vbo += m.vbo_capacity; - total_ebo += m.ebo_capacity; - } - total_vram = total_vbo + total_ebo; - for (const auto& cmd : frame_draw_cmds_) { - total_leaf_draws += cmd.counts.size(); + if (mm.hidden || !mm.finalized) { num_hidden++; continue; } + total_obj += static_cast(mm.instances.size()); + total_tri += mm.total_triangles; + total_meshes += static_cast(mm.meshes.size()); + total_vbo += mm.vbo_capacity; + total_ebo += mm.ebo_capacity; + total_ssbo += mm.ssbo_instance_count * sizeof(InstanceGpu); } FrameStats stats; @@ -999,112 +686,95 @@ void ViewportWindow::render() { stats.visible_objects = visible_objects_; stats.total_triangles = total_tri; stats.visible_triangles = visible_triangles_; + stats.unique_meshes = total_meshes; + stats.instanced_draws = instanced_draws_; emit frameStatsUpdated(stats); - double vis_obj_pct = total_obj > 0 ? 100.0 * visible_objects_ / total_obj : 0.0; - double vis_tri_pct = total_tri > 0 ? 100.0 * visible_triangles_ / total_tri : 0.0; - qDebug("[frame] %.1f fps %.2f ms obj %u/%u (%.1f%%) tri %u/%u (%.1f%%) " - "vram %.1f MB (vbo %.1f + ebo %.1f) models %zu (%zu hidden) " - "leaf_draws %zu model_draws %zu pending_uploads %zu", + qDebug("[frame] %.1f fps %.2f ms obj %u/%u tri %u/%u " + "meshes %u inst_draws %u " + "vram %.1f MB (vbo %.1f + ebo %.1f + ssbo %.1f) models %zu (%zu hidden)", last_fps_, 1000.0f / last_fps_, - visible_objects_, total_obj, vis_obj_pct, - visible_triangles_, total_tri, vis_tri_pct, - total_vram / (1024.0 * 1024.0), - total_vbo / (1024.0 * 1024.0), - total_ebo / (1024.0 * 1024.0), - num_models, num_hidden, - total_leaf_draws, - frame_draw_cmds_.size(), - pending_uploads_.size()); + visible_objects_, total_obj, + visible_triangles_, total_tri, + total_meshes, instanced_draws_, + (total_vbo + total_ebo + total_ssbo) / (1024.0*1024.0), + total_vbo / (1024.0*1024.0), + total_ebo / (1024.0*1024.0), + total_ssbo / (1024.0*1024.0), + num_models, num_hidden); } } -void ViewportWindow::renderAxisGizmo() { - if (!axis_program_ || !axis_vao_) return; - - const int dpr = devicePixelRatio(); - const int gizmo_size = 110 * dpr; - const int margin = 10 * dpr; - - gl_->glViewport(margin, margin, gizmo_size, gizmo_size); - gl_->glDisable(GL_DEPTH_TEST); - - float yaw_rad = qDegreesToRadians(camera_yaw_); - float pitch_rad = qDegreesToRadians(camera_pitch_); - - QVector3D eye_dir; - eye_dir.setX(cosf(pitch_rad) * cosf(yaw_rad)); - eye_dir.setY(cosf(pitch_rad) * sinf(yaw_rad)); - eye_dir.setZ(sinf(pitch_rad)); - - QMatrix4x4 gizmo_view; - gizmo_view.lookAt(eye_dir * 3.0f, QVector3D(0, 0, 0), QVector3D(0, 0, 1)); - - QMatrix4x4 gizmo_proj; - gizmo_proj.ortho(-1.4f, 1.4f, -1.4f, 1.4f, 0.1f, 10.0f); - - QMatrix4x4 mvp = gizmo_proj * gizmo_view; - - gl_->glUseProgram(axis_program_); - gl_->glUniformMatrix4fv(gl_->glGetUniformLocation(axis_program_, "u_mvp"), 1, GL_FALSE, mvp.constData()); - - gl_->glLineWidth(2.5f); - gl_->glBindVertexArray(axis_vao_); - gl_->glDrawArrays(GL_LINES, 0, 6); - - gl_->glEnable(GL_DEPTH_TEST); -} - void ViewportWindow::renderPickPass() { gl_->glBindFramebuffer(GL_FRAMEBUFFER, pick_fbo_); gl_->glViewport(0, 0, pick_width_, pick_height_); - GLuint clear_val = 0; gl_->glClearBufferuiv(GL_COLOR, 0, &clear_val); gl_->glClear(GL_DEPTH_BUFFER_BIT); QMatrix4x4 vp = proj_matrix_ * view_matrix_; gl_->glUseProgram(pick_program_); - gl_->glUniformMatrix4fv(gl_->glGetUniformLocation(pick_program_, "u_view_projection"), 1, GL_FALSE, vp.constData()); + GLint u_vp = gl_->glGetUniformLocation(pick_program_, "u_view_projection"); + GLint u_inst_off = gl_->glGetUniformLocation(pick_program_, "u_instance_offset"); + gl_->glUniformMatrix4fv(u_vp, 1, GL_FALSE, vp.constData()); - // Reuse the visible list from the most recent render() call. - for (const auto& cmd : frame_draw_cmds_) { - gl_->glBindVertexArray(cmd.vao); - gl_->glMultiDrawElements(GL_TRIANGLES, - cmd.counts.data(), GL_UNSIGNED_INT, - cmd.offsets.data(), - static_cast(cmd.counts.size())); + for (auto& [model_id, m] : models_gpu_) { + if (m.hidden || !m.finalized || !m.ssbo) continue; + gl_->glBindVertexArray(m.vao); + gl_->glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, m.ssbo); + for (const auto& mesh : m.meshes) { + if (mesh.instance_count == 0 || mesh.index_count == 0) continue; + gl_->glUniform1ui(u_inst_off, mesh.first_instance); + gl_->glDrawElementsInstancedBaseVertex( + GL_TRIANGLES, + static_cast(mesh.index_count), + GL_UNSIGNED_INT, + reinterpret_cast(static_cast(mesh.ebo_byte_offset)), + static_cast(mesh.instance_count), + static_cast(mesh.vbo_byte_offset / INSTANCED_VERTEX_STRIDE_BYTES)); + } } - gl_->glBindFramebuffer(GL_FRAMEBUFFER, 0); } -void ViewportWindow::exposeEvent(QExposeEvent*) { - if (isExposed() && !gl_initialized_) { - initGL(); - } +void ViewportWindow::renderAxisGizmo() { + if (!axis_program_ || !axis_vao_) return; + const int dpr = devicePixelRatio(); + const int gizmo_size = 110 * dpr; + const int margin = 10 * dpr; + gl_->glViewport(margin, margin, gizmo_size, gizmo_size); + gl_->glDisable(GL_DEPTH_TEST); + + float yaw_rad = qDegreesToRadians(camera_yaw_); + float pitch_rad = qDegreesToRadians(camera_pitch_); + QVector3D eye_dir(cosf(pitch_rad) * cosf(yaw_rad), + cosf(pitch_rad) * sinf(yaw_rad), + sinf(pitch_rad)); + QMatrix4x4 gv; gv.lookAt(eye_dir * 3.0f, QVector3D(0,0,0), QVector3D(0,0,1)); + QMatrix4x4 gp; gp.ortho(-1.4f, 1.4f, -1.4f, 1.4f, 0.1f, 10.0f); + QMatrix4x4 mvp = gp * gv; + + gl_->glUseProgram(axis_program_); + gl_->glUniformMatrix4fv(gl_->glGetUniformLocation(axis_program_, "u_mvp"), 1, GL_FALSE, mvp.constData()); + gl_->glLineWidth(2.5f); + gl_->glBindVertexArray(axis_vao_); + gl_->glDrawArrays(GL_LINES, 0, 6); + gl_->glEnable(GL_DEPTH_TEST); } +void ViewportWindow::exposeEvent(QExposeEvent*) { + if (isExposed() && !gl_initialized_) initGL(); +} void ViewportWindow::resizeEvent(QResizeEvent*) { if (gl_initialized_) render(); } - bool ViewportWindow::event(QEvent* e) { switch (e->type()) { - case QEvent::MouseButtonPress: - handleMousePress(static_cast(e)); - return true; - case QEvent::MouseButtonRelease: - handleMouseRelease(static_cast(e)); - return true; - case QEvent::MouseMove: - handleMouseMove(static_cast(e)); - return true; - case QEvent::Wheel: - handleWheel(static_cast(e)); - return true; - default: - return QWindow::event(e); + case QEvent::MouseButtonPress: handleMousePress(static_cast(e)); return true; + case QEvent::MouseButtonRelease: handleMouseRelease(static_cast(e)); return true; + case QEvent::MouseMove: handleMouseMove(static_cast(e)); return true; + case QEvent::Wheel: handleWheel(static_cast(e)); return true; + default: return QWindow::event(e); } } @@ -1112,7 +782,6 @@ void ViewportWindow::handleMousePress(QMouseEvent* e) { active_button_ = e->button(); last_mouse_pos_ = e->pos(); } - void ViewportWindow::handleMouseRelease(QMouseEvent* e) { if (active_button_ == Qt::LeftButton && (e->pos() - last_mouse_pos_).manhattanLength() < 5) { uint32_t id = pickObjectAt(e->pos().x(), e->pos().y()); @@ -1121,21 +790,18 @@ void ViewportWindow::handleMouseRelease(QMouseEvent* e) { } active_button_ = Qt::NoButton; } - void ViewportWindow::handleMouseMove(QMouseEvent* e) { QPoint delta = e->pos() - last_mouse_pos_; last_mouse_pos_ = e->pos(); - if (active_button_ == Qt::MiddleButton) { if (e->modifiers() & Qt::ShiftModifier) { float pan_speed = camera_distance_ * 0.002f; float yaw_rad = qDegreesToRadians(camera_yaw_); float pitch_rad = qDegreesToRadians(camera_pitch_); QVector3D right(-sinf(yaw_rad), cosf(yaw_rad), 0.0f); - QVector3D up( - -sinf(pitch_rad) * cosf(yaw_rad), - -sinf(pitch_rad) * sinf(yaw_rad), - cosf(pitch_rad)); + QVector3D up(-sinf(pitch_rad) * cosf(yaw_rad), + -sinf(pitch_rad) * sinf(yaw_rad), + cosf(pitch_rad)); camera_target_ -= right * delta.x() * pan_speed; camera_target_ += up * delta.y() * pan_speed; } else { @@ -1145,7 +811,6 @@ void ViewportWindow::handleMouseMove(QMouseEvent* e) { } } } - void ViewportWindow::handleWheel(QWheelEvent* e) { float factor = e->angleDelta().y() > 0 ? 0.9f : 1.1f; camera_distance_ *= factor; diff --git a/src/ifcviewer/ViewportWindow.h b/src/ifcviewer/ViewportWindow.h index 97925e6e2e..9fbdcf054b 100644 --- a/src/ifcviewer/ViewportWindow.h +++ b/src/ifcviewer/ViewportWindow.h @@ -28,58 +28,43 @@ #include #include -#include #include -#include #include #include #include -#include #include -#include -#include "BvhAccel.h" +#include "InstancedGeometry.h" #include "SidecarCache.h" -struct MaterialInfo { - float r = 0.75f, g = 0.75f, b = 0.78f, a = 1.0f; -}; - -struct UploadChunk { - // Interleaved per-vertex layout (8 floats / 32 bytes per vertex): - // pos(3 float) + normal(3 float) + object_id(1 float bitcast from uint) - // + color(1 float holding RGBA8 packed bytes, read on the GPU as - // GL_UNSIGNED_BYTE * 4 normalized). - std::vector vertices; - std::vector indices; // local to this chunk's vertices - uint32_t object_id = 0; - uint32_t model_id = 0; -}; - -// Per-model GPU state: own VAO, VBO, EBO, draw info, BVH. +// Per-model GPU state for the instanced render path. +// +// VBO: local-coord interleaved verts (pos3 + normal3 + color1_packed) — 28 B. +// EBO: mesh-local indices (uint32). +// meshes[]: per-unique-representation metadata; indexed by local_mesh_id. +// instances[]: CPU-side per-instance records; sorted by mesh_id at finalize. +// ssbo: InstanceGpu[]; populated at finalize. +// +// A model is drawable once `finalized == true`. struct ModelGpuData { GLuint vao = 0; GLuint vbo = 0; GLuint ebo = 0; + GLuint ssbo = 0; + size_t vbo_capacity = 0; size_t ebo_capacity = 0; - size_t vbo_used = 0; // bytes - size_t ebo_used = 0; // bytes - uint32_t vertex_count = 0; + size_t vbo_used = 0; + size_t ebo_used = 0; + uint32_t vertex_count = 0; // total (across all meshes) uint32_t total_triangles = 0; - std::vector draw_info; - uint32_t active_draw_count = 0; // how many objects are drawable (progressive upload) - bool hidden = false; -}; -// Pending progressive upload — VBO first, then EBO. -struct PendingUpload { - uint32_t model_id = 0; - std::vector vertices; - std::vector indices; - std::shared_ptr bvh_set; - size_t vbo_uploaded = 0; // bytes - size_t ebo_uploaded = 0; // bytes + std::vector meshes; + std::vector instances; // unsorted until finalize + uint32_t ssbo_instance_count = 0; + + bool finalized = false; + bool hidden = false; }; class ViewportWindow : public QWindow { @@ -88,32 +73,21 @@ public: explicit ViewportWindow(QWindow* parent = nullptr); ~ViewportWindow(); - void uploadChunk(const UploadChunk& chunk); - void resetScene(); + // Streaming ingress. + void uploadMeshChunk(const MeshChunk& chunk); + void uploadInstanceChunk(const InstanceChunk& chunk); - // Bulk upload pre-built geometry from a sidecar cache. - // Creates a perfectly-sized per-model buffer set. No copy. - void uploadBulk(uint32_t model_id, - std::vector vertices, - std::vector indices, - const std::vector& draw_info, - std::shared_ptr bvh_set); + // Called once all chunks for a model have arrived: sorts instances by + // mesh_id, assigns each mesh its contiguous range, and uploads the + // instance SSBO. The model becomes drawable. + void finalizeModel(uint32_t model_id); + + void resetScene(); void hideModel(uint32_t model_id); void showModel(uint32_t model_id); void removeModel(uint32_t model_id); - // Build BVH and optionally write a sidecar cache. - void buildBvhAsync(uint32_t model_id, - const std::string& ifc_path = "", - uint64_t ifc_file_size = 0, - std::vector sidecar_elements = {}, - std::string sidecar_string_table = {}); - - // Read snapshots of a model's GPU buffers into CPU vectors. - std::vector readbackEbo(uint32_t model_id) const; - std::vector readbackVbo(uint32_t model_id) const; - void setSelectedObjectId(uint32_t id); uint32_t pickObjectAt(int x, int y); @@ -124,6 +98,8 @@ public: uint32_t visible_objects; uint32_t total_triangles; uint32_t visible_triangles; + uint32_t unique_meshes; + uint32_t instanced_draws; }; signals: @@ -147,13 +123,7 @@ private: void setupVaoLayout(GLuint vao, GLuint vbo, GLuint ebo); bool growModelVbo(ModelGpuData& m, size_t needed_total); bool growModelEbo(ModelGpuData& m, size_t needed_total); - void buildVisibleList(const QMatrix4x4& vp); - void traverseBvh(const ModelBvh& mbvh, const ModelGpuData& mgpu, - const float planes[6][4]); - static bool aabbInFrustum(const float aabb_min[3], const float aabb_max[3], - const float planes[6][4]); - void applyBvhResult(); - void processPendingUploads(); + ModelGpuData& getOrCreateModel(uint32_t model_id); // Mouse interaction void handleMousePress(QMouseEvent* event); @@ -172,13 +142,12 @@ private: GLuint pick_program_ = 0; GLuint axis_program_ = 0; - // Axis gizmo (separate VAO/VBO since vertex layout differs from scene) + // Axis gizmo GLuint axis_vao_ = 0; GLuint axis_vbo_ = 0; // Per-model GPU data std::unordered_map models_gpu_; - std::mutex models_mutex_; // Pick framebuffer GLuint pick_fbo_ = 0; @@ -187,21 +156,10 @@ private: int pick_width_ = 0; int pick_height_ = 0; - // Per-model BVH - std::unordered_map> model_bvhs_; - - // Progressive upload queue - std::deque pending_uploads_; - - // Scratch buffers reused each frame to avoid allocation. - struct ModelDrawCmd { - GLuint vao; - std::vector counts; - std::vector offsets; - }; - std::vector frame_draw_cmds_; + // Per-frame stats uint32_t visible_triangles_ = 0; uint32_t visible_objects_ = 0; + uint32_t instanced_draws_ = 0; // Camera QVector3D camera_target_{0, 0, 0}; @@ -211,26 +169,14 @@ private: QMatrix4x4 view_matrix_; QMatrix4x4 proj_matrix_; - // Mouse state + // Mouse Qt::MouseButton active_button_ = Qt::NoButton; QPoint last_mouse_pos_; // Selection uint32_t selected_object_id_ = 0; - bool pick_requested_ = false; - int pick_x_ = 0, pick_y_ = 0; - // BVH build (phase 2) - struct PendingBvh { - uint32_t model_id; - std::shared_ptr bvh_set; - EboReorderResult ebo_reorder; - }; - std::unique_ptr pending_bvh_; - std::mutex bvh_result_mutex_; - std::thread bvh_build_thread_; - - // Stats + // FPS smoothing int frame_count_ = 0; float accumulated_time_ = 0.0f; float last_fps_ = 0.0f;