ifcviewer: rename sidecar transfer/record types; drop unused element hierarchy (sidecar v17)

Rename the streamer/sidecar transfer and record types to describe what
they are rather than how they move:

  MeshChunk         -> StreamedMesh
  InstanceChunk     -> StreamedInstance
  InstanceCpu       -> InstanceInfo
  PackedElementInfo -> ElementTableRecord
  uploadMeshChunk   -> uploadStreamedMesh
  uploadInstanceChunk -> uploadStreamedInstance
  buildMeshChunk    -> buildStreamedMesh

and the two post-index sidecar metadata blocks:

  "critical" metadata -> "geometry" metadata  (meshes/instances/georef/TOC)
  "deferred" metadata -> "element"  metadata  (elements + string table)
  parseSidecarCritical -> parseSidecarGeometryMetadata
  parseSidecarDeferred -> parseSidecarElementMetadata

The one behavioural change: the element hierarchy (parent_id) was
carried through ElementInfo, ElementTableRecord, and the sidecar element
table but never consumed, so drop it and bump SIDECAR_VERSION 16 -> 17.
No back-compat: regenerate sidecars. sample.ifcview is regenerated at v17.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Dion Moult
2026-07-03 19:26:31 +10:00
parent da5c0b7991
commit 66d558ec2d
30 changed files with 240 additions and 240 deletions
+1 -3
View File
@@ -89,7 +89,7 @@ std::optional<express::Base> ElementRegistry::findEntity(uint32_t object_id) con
} }
void ElementRegistry::onSidecarElementsReady(uint32_t /*model_id*/, void ElementRegistry::onSidecarElementsReady(uint32_t /*model_id*/,
std::vector<PackedElementInfo> elements, std::vector<ElementTableRecord> elements,
std::string string_table) { std::string string_table) {
auto string_from_table = [&](uint32_t offset, uint32_t length) -> QString { auto string_from_table = [&](uint32_t offset, uint32_t length) -> QString {
if (length == 0 || offset + length > string_table.size()) return {}; if (length == 0 || offset + length > string_table.size()) return {};
@@ -101,7 +101,6 @@ void ElementRegistry::onSidecarElementsReady(uint32_t /*model_id*/,
info.object_id = packed_element.object_id; info.object_id = packed_element.object_id;
info.model_id = packed_element.model_id; info.model_id = packed_element.model_id;
info.ifc_id = packed_element.ifc_id; info.ifc_id = packed_element.ifc_id;
info.parent_id = packed_element.parent_id;
info.guid = string_from_table(packed_element.guid_offset, packed_element.guid_length); info.guid = string_from_table(packed_element.guid_offset, packed_element.guid_length);
info.name = string_from_table(packed_element.name_offset, packed_element.name_length); info.name = string_from_table(packed_element.name_offset, packed_element.name_length);
info.type = string_from_table(packed_element.type_offset, packed_element.type_length); info.type = string_from_table(packed_element.type_offset, packed_element.type_length);
@@ -115,7 +114,6 @@ void ElementRegistry::onStreamedElementsReady(uint32_t /*model_id*/, std::vector
info.object_id = element.object_id; info.object_id = element.object_id;
info.model_id = element.model_id; info.model_id = element.model_id;
info.ifc_id = element.ifc_id; info.ifc_id = element.ifc_id;
info.parent_id = element.parent_id;
info.guid = QString::fromStdString(element.guid); info.guid = QString::fromStdString(element.guid);
info.name = QString::fromStdString(element.name); info.name = QString::fromStdString(element.name);
info.type = QString::fromStdString(element.type); info.type = QString::fromStdString(element.type);
+2 -3
View File
@@ -30,7 +30,7 @@
#include <vector> #include <vector>
class SceneLoader; class SceneLoader;
struct PackedElementInfo; struct ElementTableRecord;
struct ElementInfo; struct ElementInfo;
namespace bonsaiviewer { namespace bonsaiviewer {
@@ -39,7 +39,6 @@ struct BasicElementInfo {
uint32_t object_id = 0; uint32_t object_id = 0;
uint32_t model_id = 0; uint32_t model_id = 0;
int ifc_id = 0; int ifc_id = 0;
int parent_id = 0;
QString guid; QString guid;
QString name; QString name;
QString type; QString type;
@@ -59,7 +58,7 @@ public:
private: private:
void onSidecarElementsReady(uint32_t model_id, void onSidecarElementsReady(uint32_t model_id,
std::vector<PackedElementInfo> elements, std::vector<ElementTableRecord> elements,
std::string string_table); std::string string_table);
void onStreamedElementsReady(uint32_t model_id, std::vector<ElementInfo> elements); void onStreamedElementsReady(uint32_t model_id, std::vector<ElementInfo> elements);
Binary file not shown.
+1 -1
View File
@@ -131,7 +131,7 @@ ModelGeoref computeModelGeoref(ifcopenshell::file* ifc_file);
// Build a FederatedFalseOrigin guess so that a model lands near the // Build a FederatedFalseOrigin guess so that a model lands near the
// federation origin instead of out at its surveyor coordinates. Designed // federation origin instead of out at its surveyor coordinates. Designed
// to work without an open IFC file so it's usable from sidecar-only loads // to work without an open IFC file so it's usable from sidecar-only loads
// (the inputs are all derivable from the resident MeshInfo + InstanceCpu // (the inputs are all derivable from the resident MeshInfo + InstanceInfo
// data + ModelGeoref). // data + ModelGeoref).
// //
// Position: `first_geometry_point_m` is a point that actually lies on the // Position: `first_geometry_point_m` is a point that actually lies on the
+33 -34
View File
@@ -144,7 +144,7 @@ std::vector<ElementInfo> GeometryStreamer::drainElements() {
return result; return result;
} }
// Build a mesh chunk (local coords, 28-byte interleaved vertices) from a // Build a streamed mesh record (local coords, 28-byte interleaved vertices) from a
// TriangulationElement. Per-vertex color is baked from material_ids so that // TriangulationElement. Per-vertex color is baked from material_ids so that
// triangulations with per-face materials still render correctly. // triangulations with per-face materials still render correctly.
// Vertex rebasing: when `offset` is non-zero, every vertex position is // Vertex rebasing: when `offset` is non-zero, every vertex position is
@@ -153,13 +153,13 @@ std::vector<ElementInfo> GeometryStreamer::drainElements() {
// compensates by post-multiplying each instance's PlacementTransformation // compensates by post-multiplying each instance's PlacementTransformation
// by T(+offset), which is mathematically the identity overall but moves // by T(+offset), which is mathematically the identity overall but moves
// the magnitude off the float-precision-sensitive vertex column. // the magnitude off the float-precision-sensitive vertex column.
static MeshChunk buildMeshChunk(uint32_t model_id, static StreamedMesh buildStreamedMesh(uint32_t model_id,
uint32_t local_mesh_id, uint32_t local_mesh_id,
const IfcGeom::TriangulationElement* elem, const IfcGeom::TriangulationElement* elem,
const Eigen::Vector3d& offset) { const Eigen::Vector3d& offset) {
MeshChunk chunk; StreamedMesh mesh;
chunk.model_id = model_id; mesh.model_id = model_id;
chunk.local_mesh_id = local_mesh_id; mesh.local_mesh_id = local_mesh_id;
const auto& geom = elem->geometry(); const auto& geom = elem->geometry();
const auto& verts = geom.verts(); const auto& verts = geom.verts();
@@ -168,7 +168,7 @@ static MeshChunk buildMeshChunk(uint32_t model_id,
const auto& materials = geom.materials(); const auto& materials = geom.materials();
const auto& material_ids = geom.material_ids(); const auto& material_ids = geom.material_ids();
if (verts.empty() || faces.empty()) return chunk; if (verts.empty() || faces.empty()) return mesh;
const size_t num_verts_src = verts.size() / 3; const size_t num_verts_src = verts.size() / 3;
const size_t num_tris = faces.size() / 3; const size_t num_tris = faces.size() / 3;
@@ -184,8 +184,8 @@ static MeshChunk buildMeshChunk(uint32_t model_id,
std::unordered_map<uint64_t, uint32_t> remap; std::unordered_map<uint64_t, uint32_t> remap;
remap.reserve(num_verts_src); remap.reserve(num_verts_src);
chunk.vertices.reserve(num_verts_src * INSTANCED_VERTEX_STRIDE_FLOATS); mesh.vertices.reserve(num_verts_src * INSTANCED_VERTEX_STRIDE_FLOATS);
chunk.indices.reserve(faces.size()); mesh.indices.reserve(faces.size());
// Track local AABB as we emit vertices. // Track local AABB as we emit vertices.
float local_aabb_min[3] = { std::numeric_limits<float>::max(), float local_aabb_min[3] = { std::numeric_limits<float>::max(),
@@ -201,16 +201,16 @@ static MeshChunk buildMeshChunk(uint32_t model_id,
if (it != remap.end()) return it->second; if (it != remap.end()) return it->second;
const uint32_t new_idx = static_cast<uint32_t>( const uint32_t new_idx = static_cast<uint32_t>(
chunk.vertices.size() / INSTANCED_VERTEX_STRIDE_FLOATS); mesh.vertices.size() / INSTANCED_VERTEX_STRIDE_FLOATS);
// Subtract in double, narrow to float — preserves precision when // Subtract in double, narrow to float — preserves precision when
// verts are far from origin and offset cancels the magnitude. // verts are far from origin and offset cancels the magnitude.
float px = static_cast<float>(verts[orig_idx * 3 + 0] - offset.x()); float px = static_cast<float>(verts[orig_idx * 3 + 0] - offset.x());
float py = static_cast<float>(verts[orig_idx * 3 + 1] - offset.y()); float py = static_cast<float>(verts[orig_idx * 3 + 1] - offset.y());
float pz = static_cast<float>(verts[orig_idx * 3 + 2] - offset.z()); float pz = static_cast<float>(verts[orig_idx * 3 + 2] - offset.z());
chunk.vertices.push_back(px); mesh.vertices.push_back(px);
chunk.vertices.push_back(py); mesh.vertices.push_back(py);
chunk.vertices.push_back(pz); mesh.vertices.push_back(pz);
if (px < local_aabb_min[0]) local_aabb_min[0] = px; if (px < local_aabb_min[0]) local_aabb_min[0] = px;
if (px > local_aabb_max[0]) local_aabb_max[0] = px; if (px > local_aabb_max[0]) local_aabb_max[0] = px;
if (py < local_aabb_min[1]) local_aabb_min[1] = py; if (py < local_aabb_min[1]) local_aabb_min[1] = py;
@@ -219,13 +219,13 @@ static MeshChunk buildMeshChunk(uint32_t model_id,
if (pz > local_aabb_max[2]) local_aabb_max[2] = pz; if (pz > local_aabb_max[2]) local_aabb_max[2] = pz;
if (orig_idx * 3 + 2 < normals.size()) { if (orig_idx * 3 + 2 < normals.size()) {
chunk.vertices.push_back(static_cast<float>(normals[orig_idx * 3 + 0])); mesh.vertices.push_back(static_cast<float>(normals[orig_idx * 3 + 0]));
chunk.vertices.push_back(static_cast<float>(normals[orig_idx * 3 + 1])); mesh.vertices.push_back(static_cast<float>(normals[orig_idx * 3 + 1]));
chunk.vertices.push_back(static_cast<float>(normals[orig_idx * 3 + 2])); mesh.vertices.push_back(static_cast<float>(normals[orig_idx * 3 + 2]));
} else { } else {
chunk.vertices.push_back(0.0f); mesh.vertices.push_back(0.0f);
chunk.vertices.push_back(1.0f); mesh.vertices.push_back(1.0f);
chunk.vertices.push_back(0.0f); mesh.vertices.push_back(0.0f);
} }
MaterialInfo m; MaterialInfo m;
@@ -235,7 +235,7 @@ static MeshChunk buildMeshChunk(uint32_t model_id,
uint32_t packed = packRGBA8(m); uint32_t packed = packRGBA8(m);
float packed_as_float; float packed_as_float;
std::memcpy(&packed_as_float, &packed, sizeof(float)); std::memcpy(&packed_as_float, &packed, sizeof(float));
chunk.vertices.push_back(packed_as_float); mesh.vertices.push_back(packed_as_float);
remap.emplace(key, new_idx); remap.emplace(key, new_idx);
return new_idx; return new_idx;
@@ -243,19 +243,19 @@ static MeshChunk buildMeshChunk(uint32_t model_id,
for (size_t t = 0; t < num_tris; ++t) { for (size_t t = 0; t < num_tris; ++t) {
const int mat_id = have_per_tri_material ? material_ids[t] : -1; const int mat_id = have_per_tri_material ? material_ids[t] : -1;
chunk.indices.push_back(emit_vertex(static_cast<uint32_t>(faces[t * 3 + 0]), mat_id)); mesh.indices.push_back(emit_vertex(static_cast<uint32_t>(faces[t * 3 + 0]), mat_id));
chunk.indices.push_back(emit_vertex(static_cast<uint32_t>(faces[t * 3 + 1]), mat_id)); mesh.indices.push_back(emit_vertex(static_cast<uint32_t>(faces[t * 3 + 1]), mat_id));
chunk.indices.push_back(emit_vertex(static_cast<uint32_t>(faces[t * 3 + 2]), mat_id)); mesh.indices.push_back(emit_vertex(static_cast<uint32_t>(faces[t * 3 + 2]), mat_id));
} }
if (chunk.vertices.empty()) { if (mesh.vertices.empty()) {
for (int a = 0; a < 3; ++a) local_aabb_min[a] = local_aabb_max[a] = 0.0f; for (int a = 0; a < 3; ++a) local_aabb_min[a] = local_aabb_max[a] = 0.0f;
} }
for (int a = 0; a < 3; ++a) { for (int a = 0; a < 3; ++a) {
chunk.local_aabb_min[a] = local_aabb_min[a]; mesh.local_aabb_min[a] = local_aabb_min[a];
chunk.local_aabb_max[a] = local_aabb_max[a]; mesh.local_aabb_max[a] = local_aabb_max[a];
} }
return chunk; return mesh;
} }
// Port of ifcopenshell.util.representation.get_prioritised_contexts: rank every // Port of ifcopenshell.util.representation.get_prioritised_contexts: rank every
@@ -562,7 +562,6 @@ void GeometryStreamer::run(const std::string& path, int num_threads) {
info.guid = tri_elem->guid(); info.guid = tri_elem->guid();
info.name = tri_elem->name(); info.name = tri_elem->name();
info.type = tri_elem->type(); info.type = tri_elem->type();
info.parent_id = tri_elem->parent_id();
{ {
std::lock_guard<std::mutex> lock(elements_mutex_); std::lock_guard<std::mutex> lock(elements_mutex_);
pending_elements_.push_back(std::move(info)); pending_elements_.push_back(std::move(info));
@@ -604,19 +603,19 @@ void GeometryStreamer::run(const std::string& path, int num_threads) {
} }
} }
MeshChunk mesh_chunk = StreamedMesh streamed_mesh =
buildMeshChunk(model_id_, local_mesh_id, tri_elem, offset); buildStreamedMesh(model_id_, local_mesh_id, tri_elem, offset);
MeshAabb mesh_aabb; MeshAabb mesh_aabb;
for (int a = 0; a < 3; ++a) { for (int a = 0; a < 3; ++a) {
mesh_aabb.lmin[a] = mesh_chunk.local_aabb_min[a]; mesh_aabb.lmin[a] = streamed_mesh.local_aabb_min[a];
mesh_aabb.lmax[a] = mesh_chunk.local_aabb_max[a]; mesh_aabb.lmax[a] = streamed_mesh.local_aabb_max[a];
mesh_aabb.offset[a] = offset[a]; mesh_aabb.offset[a] = offset[a];
} }
mesh_aabb.has_offset = (offset.squaredNorm() > 0.0); mesh_aabb.has_offset = (offset.squaredNorm() > 0.0);
if (mesh_aabbs.size() <= local_mesh_id) mesh_aabbs.resize(local_mesh_id + 1); if (mesh_aabbs.size() <= local_mesh_id) mesh_aabbs.resize(local_mesh_id + 1);
mesh_aabbs[local_mesh_id] = mesh_aabb; mesh_aabbs[local_mesh_id] = mesh_aabb;
if (!mesh_chunk.indices.empty()) { if (!streamed_mesh.indices.empty()) {
emit meshReady(std::move(mesh_chunk)); emit meshReady(std::move(streamed_mesh));
} }
} }
@@ -635,7 +634,7 @@ void GeometryStreamer::run(const std::string& path, int num_threads) {
mat_d.block<3, 1>(0, 3) += mat_d.block<3, 3>(0, 0) * mesh_rebase_offset; mat_d.block<3, 1>(0, 3) += mat_d.block<3, 3>(0, 0) * mesh_rebase_offset;
} }
InstanceChunk inst; StreamedInstance inst;
inst.model_id = model_id_; inst.model_id = model_id_;
inst.local_mesh_id = local_mesh_id; inst.local_mesh_id = local_mesh_id;
inst.object_id = object_id; inst.object_id = object_id;
+2 -3
View File
@@ -41,7 +41,6 @@ struct ElementInfo {
std::string guid; std::string guid;
std::string name; std::string name;
std::string type; std::string type;
int parent_id;
}; };
class GeometryStreamer : public QObject { class GeometryStreamer : public QObject {
@@ -70,8 +69,8 @@ public:
signals: signals:
void progressChanged(int percent); void progressChanged(int percent);
void meshReady(MeshChunk chunk); void meshReady(StreamedMesh mesh);
void instanceReady(InstanceChunk chunk); void instanceReady(StreamedInstance instance_record);
void finished(); void finished();
void cancelled(); void cancelled();
void errorOccurred(const QString& message); void errorOccurred(const QString& message);
+1 -1
View File
@@ -84,7 +84,7 @@ bool findInstanceInModels(
if (it == model_data.object_id_to_instance.end()) continue; if (it == model_data.object_id_to_instance.end()) continue;
const uint32_t instance_index = it->second; const uint32_t instance_index = it->second;
if (instance_index >= model_data.instances.size()) continue; if (instance_index >= model_data.instances.size()) continue;
const InstanceCpu& instance = model_data.instances[instance_index]; const InstanceInfo& instance = model_data.instances[instance_index];
out.model_id = model_id; out.model_id = model_id;
out.mesh_id = instance.mesh_id; out.mesh_id = instance.mesh_id;
std::memcpy(out.placement_transformation, std::memcpy(out.placement_transformation,
+1 -1
View File
@@ -66,7 +66,7 @@ void composeInstance(
// Result of a successful findInstance lookup. The placement_transformation // Result of a successful findInstance lookup. The placement_transformation
// is double[16] column-major (pre-CoordinateOperation / FederatedFalseOrigin // is double[16] column-major (pre-CoordinateOperation / FederatedFalseOrigin
// / ModelTransformation) — the same convention as InstanceCpu so the // / ModelTransformation) — the same convention as InstanceInfo so the
// measurement / picking tools can re-compose at need. // measurement / picking tools can re-compose at need.
struct InstanceLookup { struct InstanceLookup {
uint32_t model_id = 0; uint32_t model_id = 0;
+7 -7
View File
@@ -39,8 +39,8 @@
static constexpr int INSTANCED_VERTEX_STRIDE_BYTES = 12; static constexpr int INSTANCED_VERTEX_STRIDE_BYTES = 12;
// Streamer-side intermediate format: 7 floats per vertex (pos3 + normal3 + // Streamer-side intermediate format: 7 floats per vertex (pos3 + normal3 +
// color-as-float). GeometryStreamer writes this into MeshChunk.vertices; // color-as-float). GeometryStreamer writes this into StreamedMesh.vertices;
// ViewportWindow::uploadMeshChunk quantizes it down to STRIDE_BYTES on the // ViewportWindow::uploadStreamedMesh quantizes it down to STRIDE_BYTES on the
// way to the VBO. Not the GPU layout — purely a transfer convention. // way to the VBO. Not the GPU layout — purely a transfer convention.
static constexpr int INSTANCED_VERTEX_STRIDE_FLOATS = 7; static constexpr int INSTANCED_VERTEX_STRIDE_FLOATS = 7;
@@ -109,7 +109,7 @@ static_assert(sizeof(InstanceGpu) == 80, "InstanceGpu must be 80 bytes");
// uploaded to the SSBO, and used to compute world_aabb_*. When ViewportWindow's // uploaded to the SSBO, and used to compute world_aabb_*. When ViewportWindow's
// stage matrices are all identity (default), transform is the float rendering // stage matrices are all identity (default), transform is the float rendering
// copy of placement_transformation. // copy of placement_transformation.
struct InstanceCpu { struct InstanceInfo {
uint32_t mesh_id = 0; // index into meshes array uint32_t mesh_id = 0; // index into meshes array
uint32_t object_id = 0; uint32_t object_id = 0;
uint32_t color_override_rgba8 = 0; uint32_t color_override_rgba8 = 0;
@@ -120,12 +120,12 @@ struct InstanceCpu {
float world_aabb_max[3]{}; float world_aabb_max[3]{};
}; };
// Chunks emitted by the streamer to the viewport (main thread). // Transfer records emitted by the streamer to the viewport (main thread).
// Emitted the first time a representation id is seen. Carries the mesh // Emitted the first time a representation id is seen. Carries the mesh
// geometry in local coords. `local_mesh_id` is the streamer-assigned id // geometry in local coords. `local_mesh_id` is the streamer-assigned id
// within this model. // within this model.
struct MeshChunk { struct StreamedMesh {
uint32_t model_id = 0; uint32_t model_id = 0;
uint32_t local_mesh_id = 0; uint32_t local_mesh_id = 0;
std::vector<float> vertices; // 7 floats * N_verts (pos3+norm3+color1_packed) std::vector<float> vertices; // 7 floats * N_verts (pos3+norm3+color1_packed)
@@ -135,9 +135,9 @@ struct MeshChunk {
}; };
// Emitted for every placement (every triangulation element from the // Emitted for every placement (every triangulation element from the
// iterator). For the first instance of a mesh, the MeshChunk is emitted // iterator). For the first instance of a mesh, the StreamedMesh is emitted
// just before this. // just before this.
struct InstanceChunk { struct StreamedInstance {
uint32_t model_id = 0; uint32_t model_id = 0;
uint32_t local_mesh_id = 0; uint32_t local_mesh_id = 0;
uint32_t object_id = 0; uint32_t object_id = 0;
+17 -16
View File
@@ -34,7 +34,7 @@
#include "InstancedGeometry.h" #include "InstancedGeometry.h"
#include "BufferPool.h" #include "BufferPool.h"
#include "ChunkPlanner.h" // WGPU_CHUNK_VERTEX_BYTES_LIMIT (shared with bake) #include "ChunkPlanner.h" // WGPU_CHUNK_VERTEX_BYTES_LIMIT (shared with bake)
#include "SidecarCache.h" // PackedElementInfo (deferred property metadata) #include "SidecarCache.h" // ElementTableRecord (element metadata)
// Per-model wgpu state. Mirrors the GL backend's ModelGpuData but with // Per-model wgpu state. Mirrors the GL backend's ModelGpuData but with
// wgpu handles. Stage 2 only allocates and uploads the four core buffers; // wgpu handles. Stage 2 only allocates and uploads the four core buffers;
@@ -305,26 +305,27 @@ struct ModelGpuData {
bool streaming_from_web = false; bool streaming_from_web = false;
// Web analog of streaming_file_path: which registered JS byte-source // Web analog of streaming_file_path: which registered JS byte-source
// (Module.__ifcvSources[id] = a picked File or a remote URL) this model's // (Module.__ifcvSources[id] = a picked File or a remote URL) this model's
// chunk + deferred reads pull from. Lets several federated models stream // chunk + element metadata reads pull from. Lets several federated models stream
// from different files at once, mirroring the desktop per-model path. // from different files at once, mirroring the desktop per-model path.
int web_source_id = 0; int web_source_id = 0;
// v15 deferred property metadata (web, on-demand). The IFC element tree // v15 element metadata (web, on-demand). The IFC element metadata
// (elements + string_table — names/GUIDs/hierarchy, for UI/picking, never // (elements + string_table — names/GUIDs, for UI/picking, never
// rendering) lives in a separate file block fetched only when a consumer // rendering) lives in a separate file block fetched only when a consumer
// asks, so first paint doesn't wait on it. Empty until // asks, so first paint doesn't wait on it. Empty until
// loadDeferredMetadataWeb fetches [deferred_meta_offset, +bytes) and parses // loadElementMetadataWeb fetches [element_metadata_comp_offset, +bytes) and parses
// it; deferred_meta_loaded latches so it fetches at most once. // it; element_metadata_loaded latches so it fetches at most once.
std::vector<PackedElementInfo> elements; std::vector<ElementTableRecord> elements;
std::string string_table; std::string string_table;
// v16: the deferred block is a single zstd frame at deferred_comp_offset of // v16: the element metadata block is a single zstd frame at
// deferred_comp_size bytes, expanding to deferred_raw_size. // element_metadata_comp_offset of element_metadata_comp_size bytes,
uint64_t deferred_comp_offset = 0; // expanding to element_metadata_raw_size.
uint64_t deferred_comp_size = 0; uint64_t element_metadata_comp_offset = 0;
uint64_t deferred_raw_size = 0; uint64_t element_metadata_comp_size = 0;
bool deferred_meta_loaded = false; uint64_t element_metadata_raw_size = 0;
bool element_metadata_loaded = false;
// applyCachedModel rebases instance object_ids by this base to keep them // applyCachedModel rebases instance object_ids by this base to keep them
// globally unique across models; deferred elements carry the sidecar's // globally unique across models; element metadata records carry the sidecar's
// original (local) ids, so they're rebased by the same amount on load. // original (local) ids, so they're rebased by the same amount on load.
uint32_t object_id_base = 0; uint32_t object_id_base = 0;
@@ -378,10 +379,10 @@ struct ModelGpuData {
// CPU side, kept for cull / picking / federation recompose. // CPU side, kept for cull / picking / federation recompose.
std::vector<MeshInfo> meshes; std::vector<MeshInfo> meshes;
std::vector<InstanceCpu> instances; std::vector<InstanceInfo> instances;
// Per-mesh "any vertex has alpha < 255?" flag, indexed by mesh_id. // Per-mesh "any vertex has alpha < 255?" flag, indexed by mesh_id.
// Populated at uploadMeshChunk / applyStreamedChunk as vertex bytes // Populated at uploadStreamedMesh / applyStreamedChunk as vertex bytes
// become CPU-resident. Used at cull time to classify each instance // become CPU-resident. Used at cull time to classify each instance
// into the opaque or transparent draw partition: an instance with // into the opaque or transparent draw partition: an instance with
// color_override_rgba8==0 (the "use baked vertex color" sentinel) // color_override_rgba8==0 (the "use baked vertex color" sentinel)
+7 -7
View File
@@ -279,7 +279,7 @@ void SceneLoader::applySidecarData(uint32_t mid, StreamingSidecar metadata) {
model.has_georef = true; model.has_georef = true;
} }
std::vector<PackedElementInfo> elements = std::move(d.elements); std::vector<ElementTableRecord> elements = std::move(d.elements);
std::string stbl = std::move(d.string_table); std::string stbl = std::move(d.string_table);
viewport_->applyCachedModel(mid, std::move(metadata)); viewport_->applyCachedModel(mid, std::move(metadata));
@@ -331,24 +331,24 @@ void SceneLoader::onStreamerProgressChanged(int percent) {
emit progressChanged(percent); emit progressChanged(percent);
} }
void SceneLoader::onStreamerMeshReady(MeshChunk chunk) { void SceneLoader::onStreamerMeshReady(StreamedMesh mesh) {
viewport_->uploadMeshChunk(chunk); viewport_->uploadStreamedMesh(mesh);
if (loading_model_id_ != 0) { if (loading_model_id_ != 0) {
auto it = models_.find(loading_model_id_); auto it = models_.find(loading_model_id_);
if (it != models_.end() && it->second.sidecar_builder) { if (it != models_.end() && it->second.sidecar_builder) {
it->second.sidecar_builder->onMeshReady(chunk); it->second.sidecar_builder->onMeshReady(mesh);
} }
} }
} }
void SceneLoader::onStreamerInstanceReady(InstanceChunk chunk) { void SceneLoader::onStreamerInstanceReady(StreamedInstance instance_record) {
if (loading_model_id_ != 0) { if (loading_model_id_ != 0) {
auto it = models_.find(loading_model_id_); auto it = models_.find(loading_model_id_);
if (it != models_.end() && it->second.sidecar_builder) { if (it != models_.end() && it->second.sidecar_builder) {
it->second.sidecar_builder->onInstanceReady(chunk); it->second.sidecar_builder->onInstanceReady(instance_record);
} }
} }
viewport_->uploadInstanceChunk(chunk); viewport_->uploadStreamedInstance(instance_record);
} }
void SceneLoader::onElementPollTick() { void SceneLoader::onElementPollTick() {
+3 -3
View File
@@ -102,7 +102,7 @@ signals:
// packed element set. Consumer is responsible for decoding + tree/ // packed element set. Consumer is responsible for decoding + tree/
// property-map population. Moved arguments — avoid unnecessary copies. // property-map population. Moved arguments — avoid unnecessary copies.
void sidecarElementsReady(uint32_t mid, void sidecarElementsReady(uint32_t mid,
std::vector<PackedElementInfo> elements, std::vector<ElementTableRecord> elements,
std::string string_table); std::string string_table);
void loadedFromSidecar(uint32_t mid, qint64 elapsed_ms); void loadedFromSidecar(uint32_t mid, qint64 elapsed_ms);
@@ -128,8 +128,8 @@ signals:
private slots: private slots:
void onStreamerProgressChanged(int percent); void onStreamerProgressChanged(int percent);
void onStreamerMeshReady(MeshChunk chunk); void onStreamerMeshReady(StreamedMesh mesh);
void onStreamerInstanceReady(InstanceChunk chunk); void onStreamerInstanceReady(StreamedInstance instance_record);
void onStreamerFinished(); void onStreamerFinished();
void onStreamerCancelled(); void onStreamerCancelled();
void onStreamerError(const QString& msg); void onStreamerError(const QString& msg);
+23 -24
View File
@@ -37,14 +37,14 @@ SidecarBuilder::SidecarBuilder(QObject* parent)
{ {
} }
void SidecarBuilder::onMeshReady(const MeshChunk& chunk) { void SidecarBuilder::onMeshReady(const StreamedMesh& mesh) {
if (chunk.vertices.empty() || chunk.indices.empty()) return; if (mesh.vertices.empty() || mesh.indices.empty()) return;
// Streamer format: 7 floats/vertex (pos3 + normal3 + color-as-float). // Streamer format: 7 floats/vertex (pos3 + normal3 + color-as-float).
const size_t n_verts = chunk.vertices.size() / INSTANCED_VERTEX_STRIDE_FLOATS; const size_t n_verts = mesh.vertices.size() / INSTANCED_VERTEX_STRIDE_FLOATS;
// Recompute a tight local AABB from the actual vertex positions, same // Recompute a tight local AABB from the actual vertex positions, same
// way ViewportWindow::uploadMeshChunk does so the .ifcview byte layout // way ViewportWindow::uploadStreamedMesh does so the .ifcview byte layout
// matches the live-render path. // matches the live-render path.
float bmin[3] = { std::numeric_limits<float>::infinity(), float bmin[3] = { std::numeric_limits<float>::infinity(),
std::numeric_limits<float>::infinity(), std::numeric_limits<float>::infinity(),
@@ -53,7 +53,7 @@ void SidecarBuilder::onMeshReady(const MeshChunk& chunk) {
-std::numeric_limits<float>::infinity(), -std::numeric_limits<float>::infinity(),
-std::numeric_limits<float>::infinity() }; -std::numeric_limits<float>::infinity() };
for (size_t i = 0; i < n_verts; ++i) { for (size_t i = 0; i < n_verts; ++i) {
const float* vertex = chunk.vertices.data() + i * INSTANCED_VERTEX_STRIDE_FLOATS; const float* vertex = mesh.vertices.data() + i * INSTANCED_VERTEX_STRIDE_FLOATS;
for (int a = 0; a < 3; ++a) { for (int a = 0; a < 3; ++a) {
if (vertex[a] < bmin[a]) bmin[a] = vertex[a]; if (vertex[a] < bmin[a]) bmin[a] = vertex[a];
if (vertex[a] > bmax[a]) bmax[a] = vertex[a]; if (vertex[a] > bmax[a]) bmax[a] = vertex[a];
@@ -68,7 +68,7 @@ void SidecarBuilder::onMeshReady(const MeshChunk& chunk) {
const size_t vb_offset = sidecar_data_.vertices.size(); const size_t vb_offset = sidecar_data_.vertices.size();
sidecar_data_.vertices.resize(vb_offset + n_verts * INSTANCED_VERTEX_STRIDE_BYTES); sidecar_data_.vertices.resize(vb_offset + n_verts * INSTANCED_VERTEX_STRIDE_BYTES);
for (size_t i = 0; i < n_verts; ++i) { for (size_t i = 0; i < n_verts; ++i) {
quantizeVertex(chunk.vertices.data() + i * INSTANCED_VERTEX_STRIDE_FLOATS, quantizeVertex(mesh.vertices.data() + i * INSTANCED_VERTEX_STRIDE_FLOATS,
bmin, extent_recip, bmin, extent_recip,
sidecar_data_.vertices.data() + vb_offset sidecar_data_.vertices.data() + vb_offset
+ i * INSTANCED_VERTEX_STRIDE_BYTES); + i * INSTANCED_VERTEX_STRIDE_BYTES);
@@ -76,13 +76,13 @@ void SidecarBuilder::onMeshReady(const MeshChunk& chunk) {
const size_t ib_offset = sidecar_data_.indices.size(); const size_t ib_offset = sidecar_data_.indices.size();
sidecar_data_.indices.insert(sidecar_data_.indices.end(), sidecar_data_.indices.insert(sidecar_data_.indices.end(),
chunk.indices.begin(), chunk.indices.end()); mesh.indices.begin(), mesh.indices.end());
MeshInfo info; MeshInfo info;
info.vbo_byte_offset = static_cast<uint32_t>(vb_offset); info.vbo_byte_offset = static_cast<uint32_t>(vb_offset);
info.vertex_count = static_cast<uint32_t>(n_verts); info.vertex_count = static_cast<uint32_t>(n_verts);
info.ebo_byte_offset = static_cast<uint32_t>(ib_offset * sizeof(uint32_t)); info.ebo_byte_offset = static_cast<uint32_t>(ib_offset * sizeof(uint32_t));
info.index_count = static_cast<uint32_t>(chunk.indices.size()); info.index_count = static_cast<uint32_t>(mesh.indices.size());
for (int a = 0; a < 3; ++a) { for (int a = 0; a < 3; ++a) {
info.local_aabb_min[a] = bmin[a]; info.local_aabb_min[a] = bmin[a];
info.local_aabb_max[a] = bmax[a]; info.local_aabb_max[a] = bmax[a];
@@ -92,30 +92,30 @@ void SidecarBuilder::onMeshReady(const MeshChunk& chunk) {
info.lod1_ebo_byte_offset = 0; info.lod1_ebo_byte_offset = 0;
info.lod1_index_count = 0; info.lod1_index_count = 0;
if (sidecar_data_.meshes.size() <= chunk.local_mesh_id) { if (sidecar_data_.meshes.size() <= mesh.local_mesh_id) {
sidecar_data_.meshes.resize(chunk.local_mesh_id + 1); sidecar_data_.meshes.resize(mesh.local_mesh_id + 1);
} }
sidecar_data_.meshes[chunk.local_mesh_id] = info; sidecar_data_.meshes[mesh.local_mesh_id] = info;
} }
void SidecarBuilder::onInstanceReady(const InstanceChunk& chunk) { void SidecarBuilder::onInstanceReady(const StreamedInstance& instance_record) {
InstanceCpu instance; InstanceInfo instance;
instance.mesh_id = chunk.local_mesh_id; instance.mesh_id = instance_record.local_mesh_id;
instance.object_id = chunk.object_id; instance.object_id = instance_record.object_id;
instance.color_override_rgba8 = chunk.color_override_rgba8; instance.color_override_rgba8 = instance_record.color_override_rgba8;
instance.model_id = chunk.model_id; instance.model_id = instance_record.model_id;
// The streamer's chunk.transform is the double-precision // The streamer's instance transform is the double-precision
// placement_transformation. The cached float transform/world_aabb is only // placement_transformation. The cached float transform/world_aabb is only
// an identity-stage baseline; applyCachedModel recomposes from placement // an identity-stage baseline; applyCachedModel recomposes from placement
// against the consumer's stage matrices at load time. // against the consumer's stage matrices at load time.
std::memcpy(instance.placement_transformation, chunk.transform, std::memcpy(instance.placement_transformation, instance_record.transform,
sizeof(instance.placement_transformation)); sizeof(instance.placement_transformation));
for (int i = 0; i < 16; ++i) { for (int i = 0; i < 16; ++i) {
instance.transform[i] = static_cast<float>(chunk.transform[i]); instance.transform[i] = static_cast<float>(instance_record.transform[i]);
} }
std::memcpy(instance.world_aabb_min, chunk.world_aabb_min, sizeof(instance.world_aabb_min)); std::memcpy(instance.world_aabb_min, instance_record.world_aabb_min, sizeof(instance.world_aabb_min));
std::memcpy(instance.world_aabb_max, chunk.world_aabb_max, sizeof(instance.world_aabb_max)); std::memcpy(instance.world_aabb_max, instance_record.world_aabb_max, sizeof(instance.world_aabb_max));
sidecar_data_.instances.push_back(instance); sidecar_data_.instances.push_back(instance);
} }
@@ -140,11 +140,10 @@ SidecarData SidecarBuilder::finalize(const ModelGeoref& georef,
sidecar_data_.map_unit_to_meters = georef.units.map_unit_to_meters; sidecar_data_.map_unit_to_meters = georef.units.map_unit_to_meters;
for (const auto& info : elements) { for (const auto& info : elements) {
PackedElementInfo packed; ElementTableRecord packed;
packed.object_id = info.object_id; packed.object_id = info.object_id;
packed.model_id = info.model_id; packed.model_id = info.model_id;
packed.ifc_id = info.ifc_id; packed.ifc_id = info.ifc_id;
packed.parent_id = info.parent_id;
packed.guid_offset = static_cast<uint32_t>(sidecar_data_.string_table.size()); packed.guid_offset = static_cast<uint32_t>(sidecar_data_.string_table.size());
packed.guid_length = static_cast<uint32_t>(info.guid.size()); packed.guid_length = static_cast<uint32_t>(info.guid.size());
+2 -3
View File
@@ -58,8 +58,8 @@ public:
// Accumulator interface. Safe to call repeatedly from the same thread the // Accumulator interface. Safe to call repeatedly from the same thread the
// streamer signals are delivered to. // streamer signals are delivered to.
void onMeshReady(const MeshChunk& chunk); void onMeshReady(const StreamedMesh& mesh);
void onInstanceReady(const InstanceChunk& chunk); void onInstanceReady(const StreamedInstance& instance_record);
// Finishes assembly using the georef + element batch the host collected // Finishes assembly using the georef + element batch the host collected
// during streaming. Returns the assembled SidecarData by move; the // during streaming. Returns the assembled SidecarData by move; the
@@ -76,4 +76,3 @@ private:
}; };
#endif // SIDECARBUILDER_H #endif // SIDECARBUILDER_H
+22 -22
View File
@@ -30,7 +30,7 @@
// MeshInfo[num_meshes] // MeshInfo[num_meshes]
// //
// uint32_t num_instances // uint32_t num_instances
// InstanceCpu[num_instances] (already sorted by mesh_id; v13 layout) // InstanceInfo[num_instances] (already sorted by mesh_id; v13 layout)
// //
// uint32_t has_coordinate_operation (v11+) // uint32_t has_coordinate_operation (v11+)
// double[16] coordinate_operation_meters (v11+; column-major) // double[16] coordinate_operation_meters (v11+; column-major)
@@ -38,7 +38,7 @@
// double map_unit_to_meters (v11+) // double map_unit_to_meters (v11+)
// //
// uint32_t num_elements // uint32_t num_elements
// PackedElementInfo[num_elements] // ElementTableRecord[num_elements]
// uint32_t string_table_bytes // uint32_t string_table_bytes
// char[string_table_bytes] // char[string_table_bytes]
@@ -205,24 +205,24 @@ bool writeSidecar(const std::string& ifc_path, const SidecarData& data) {
if (!wrU64(std::uint64_t(geom_end - geom_start))) { fclose(f); return false; } if (!wrU64(std::uint64_t(geom_end - geom_start))) { fclose(f); return false; }
if (fseek(f, geom_end, SEEK_SET) != 0) { fclose(f); return false; } if (fseek(f, geom_end, SEEK_SET) != 0) { fclose(f); return false; }
// --- Critical metadata block (zstd): meshes, instances, georef, chunk TOC // --- Geometry metadata block (zstd): meshes, instances, georef, chunk TOC
std::vector<std::uint8_t> critical_metadata; std::vector<std::uint8_t> geometry_metadata;
appendVec(critical_metadata, data.meshes); appendVec(geometry_metadata, data.meshes);
appendVec(critical_metadata, data.instances); appendVec(geometry_metadata, data.instances);
appendBytes(critical_metadata, &data.has_coordinate_operation, 4); appendBytes(geometry_metadata, &data.has_coordinate_operation, 4);
appendBytes(critical_metadata, data.coordinate_operation_meters, sizeof(double) * 16); appendBytes(geometry_metadata, data.coordinate_operation_meters, sizeof(double) * 16);
appendBytes(critical_metadata, &data.project_length_to_meters, sizeof(double)); appendBytes(geometry_metadata, &data.project_length_to_meters, sizeof(double));
appendBytes(critical_metadata, &data.map_unit_to_meters, sizeof(double)); appendBytes(geometry_metadata, &data.map_unit_to_meters, sizeof(double));
appendVec(critical_metadata, chunks); appendVec(geometry_metadata, chunks);
if (!wrBlock(critical_metadata)) { fclose(f); return false; } if (!wrBlock(geometry_metadata)) { fclose(f); return false; }
// --- Deferred metadata block (zstd): element tree + string table --------- // --- Element metadata block (zstd): elements + string table --------------
std::vector<std::uint8_t> deferred_metadata; std::vector<std::uint8_t> element_metadata;
appendVec(deferred_metadata, data.elements); appendVec(element_metadata, data.elements);
std::uint32_t stbl_len = static_cast<std::uint32_t>(data.string_table.size()); std::uint32_t stbl_len = static_cast<std::uint32_t>(data.string_table.size());
appendBytes(deferred_metadata, &stbl_len, 4); appendBytes(element_metadata, &stbl_len, 4);
appendBytes(deferred_metadata, data.string_table.data(), stbl_len); appendBytes(element_metadata, data.string_table.data(), stbl_len);
if (!wrBlock(deferred_metadata)) { fclose(f); return false; } if (!wrBlock(element_metadata)) { fclose(f); return false; }
fclose(f); fclose(f);
return true; return true;
@@ -286,12 +286,12 @@ std::optional<SidecarData> readSidecar(const std::string& ifc_path) {
out.assign(std::size_t(raw), 0); out.assign(std::size_t(raw), 0);
return SidecarCompress::decompress(z.data(), z.size(), out.data(), out.size()); return SidecarCompress::decompress(z.data(), z.size(), out.data(), out.size());
}; };
std::vector<std::uint8_t> critical_metadata, deferred_metadata; std::vector<std::uint8_t> geometry_metadata, element_metadata;
if (!readBlock(critical_metadata) || !readBlock(deferred_metadata)) return fail(); if (!readBlock(geometry_metadata) || !readBlock(element_metadata)) return fail();
fclose(f); fclose(f);
SidecarData data; SidecarData data;
BufReader cr{ critical_metadata.data(), critical_metadata.size() }; BufReader cr{ geometry_metadata.data(), geometry_metadata.size() };
if (!cr.takeVec(data.meshes)) return std::nullopt; if (!cr.takeVec(data.meshes)) return std::nullopt;
if (!cr.takeVec(data.instances)) return std::nullopt; if (!cr.takeVec(data.instances)) return std::nullopt;
if (!cr.take(&data.has_coordinate_operation, 4)) return std::nullopt; if (!cr.take(&data.has_coordinate_operation, 4)) return std::nullopt;
@@ -300,7 +300,7 @@ std::optional<SidecarData> readSidecar(const std::string& ifc_path) {
if (!cr.take(&data.map_unit_to_meters, sizeof(double))) return std::nullopt; if (!cr.take(&data.map_unit_to_meters, sizeof(double))) return std::nullopt;
if (!cr.takeVec(data.chunks)) return std::nullopt; if (!cr.takeVec(data.chunks)) return std::nullopt;
BufReader dr{ deferred_metadata.data(), deferred_metadata.size() }; BufReader dr{ element_metadata.data(), element_metadata.size() };
if (!dr.takeVec(data.elements)) return std::nullopt; if (!dr.takeVec(data.elements)) return std::nullopt;
std::uint32_t stbl_len = 0; std::uint32_t stbl_len = 0;
if (!dr.take(&stbl_len, 4)) return std::nullopt; if (!dr.take(&stbl_len, 4)) return std::nullopt;
+17 -16
View File
@@ -46,7 +46,7 @@ static constexpr uint32_t SIDECAR_MAGIC = 0x49465657; // "IFVW"
// same cache serves either source format. Staleness is user-managed // same cache serves either source format. Staleness is user-managed
// (delete the sidecar to force a rebuild). // (delete the sidecar to force a rebuild).
// v9 = unused `reserved` field dropped from header (16 B -> 12 B). // v9 = unused `reserved` field dropped from header (16 B -> 12 B).
// v10 = InstanceCpu gains placement_transformation[16] alongside transform[16] // v10 = InstanceInfo gains placement_transformation[16] alongside transform[16]
// — record grew from 104 B to 168 B. placement_transformation is the // — record grew from 104 B to 168 B. placement_transformation is the
// raw streamer output; transform is the composed FederatedFalseOrigin · // raw streamer output; transform is the composed FederatedFalseOrigin ·
// ModelTransformation · CoordinateOperation · placement_transformation // ModelTransformation · CoordinateOperation · placement_transformation
@@ -59,8 +59,8 @@ static constexpr uint32_t SIDECAR_MAGIC = 0x49465657; // "IFVW"
// georef without re-parsing the IFC source. Edits to the IFC's // georef without re-parsing the IFC source. Edits to the IFC's
// IfcMapConversion do NOT invalidate the sidecar — delete the // IfcMapConversion do NOT invalidate the sidecar — delete the
// .ifcview manually if you change the source's georef parameters. // .ifcview manually if you change the source's georef parameters.
// v12 = InstanceCpu::placement_transformation is double[16], and // v12 = InstanceInfo::placement_transformation is double[16], and
// InstanceChunk carries the streamer placement as double[16]. This keeps // StreamedInstance carries the streamer placement as double[16]. This keeps
// large IFC placements exact until CoordinateOperation / FederatedFalseOrigin // large IFC placements exact until CoordinateOperation / FederatedFalseOrigin
// composition has reduced them to viewport-local float-sized values. // composition has reduced them to viewport-local float-sized values.
// v13 = Map unit scale in cached ModelGeoref is derived from // v13 = Map unit scale in cached ModelGeoref is derived from
@@ -73,23 +73,25 @@ static constexpr uint32_t SIDECAR_MAGIC = 0x49465657; // "IFVW"
// Morton quantisation isn't bit-identical across toolchains (x86 baker vs // Morton quantisation isn't bit-identical across toolchains (x86 baker vs
// wasm loader), so it must be baked in. No back-compat: v13 sidecars are // wasm loader), so it must be baked in. No back-compat: v13 sidecars are
// rejected (regenerate them). // rejected (regenerate them).
// v15 = The post-index metadata is split into a render-CRITICAL block (meshes, // v15 = The post-index metadata is split into a geometry metadata block (meshes,
// instances, georef, chunk TOC) followed by a DEFERRED block (elements + // instances, georef, chunk TOC) followed by an element metadata block (elements +
// string_table — the IFC element tree, used for UI/picking, never for // string_table — IFC element metadata, used for UI/picking, never for
// rendering), with the critical block's byte length written just after // rendering), with the geometry block's byte length written just after
// the index section. The web loader reads only the critical block before // the index section. The web loader reads only the geometry block before
// painting, so first geometry no longer waits on the property data; the // painting, so first geometry no longer waits on the property data; the
// deferred block is fetched lazily (or skipped where unused). Desktop // element block is fetched lazily (or skipped where unused). Desktop
// reads both. No back-compat: regenerate sidecars. // reads both. No back-compat: regenerate sidecars.
// v16 = Geometry + metadata are zstd-COMPRESSED. Each chunk's vertex bytes and // v16 = Geometry + metadata are zstd-COMPRESSED. Each chunk's vertex bytes and
// index bytes are stored as two independent zstd frames (so per-chunk // index bytes are stored as two independent zstd frames (so per-chunk
// Range streaming still works — you fetch + decompress just one chunk), // Range streaming still works — you fetch + decompress just one chunk),
// and the critical + deferred metadata blocks are single zstd frames. // and the geometry + element metadata blocks are single zstd frames.
// The chunk TOC records each chunk's compressed blob offsets/sizes plus // The chunk TOC records each chunk's compressed blob offsets/sizes plus
// the raw (decompressed) sizes. ~3-5x fewer bytes over the wire while // the raw (decompressed) sizes. ~3-5x fewer bytes over the wire while
// keeping HTTP Range intact (unlike server Content-Encoding). No // keeping HTTP Range intact (unlike server Content-Encoding). No
// back-compat: regenerate sidecars. // back-compat: regenerate sidecars.
static constexpr uint32_t SIDECAR_VERSION = 16; // v17 = Removes unused element hierarchy metadata. No back-compat: regenerate
// sidecars.
static constexpr uint32_t SIDECAR_VERSION = 17;
static constexpr uint32_t SIDECAR_ENDIAN = 0x01020304; static constexpr uint32_t SIDECAR_ENDIAN = 0x01020304;
// Chunk table-of-contents entry (v16). A chunk is a CONTIGUOUS range of meshes // Chunk table-of-contents entry (v16). A chunk is a CONTIGUOUS range of meshes
@@ -111,11 +113,10 @@ struct SidecarChunk {
// Fixed-size element record. Strings are stored as (offset, length) pairs // Fixed-size element record. Strings are stored as (offset, length) pairs
// into a separate string table. // into a separate string table.
struct PackedElementInfo { struct ElementTableRecord {
uint32_t object_id; uint32_t object_id;
uint32_t model_id; uint32_t model_id;
int32_t ifc_id; int32_t ifc_id;
int32_t parent_id;
uint32_t guid_offset; uint32_t guid_offset;
uint32_t guid_length; uint32_t guid_length;
uint32_t name_offset; uint32_t name_offset;
@@ -134,7 +135,7 @@ struct SidecarData {
// Mesh dictionary and per-instance data. // Mesh dictionary and per-instance data.
std::vector<MeshInfo> meshes; // indexed by local_mesh_id std::vector<MeshInfo> meshes; // indexed by local_mesh_id
std::vector<InstanceCpu> instances; // sorted by mesh_id std::vector<InstanceInfo> instances; // sorted by mesh_id
// CoordinateOperation cache (v11+). Mirrors ModelGeoref so a sidecar // CoordinateOperation cache (v11+). Mirrors ModelGeoref so a sidecar
// load can apply georef without re-parsing the IFC source. // load can apply georef without re-parsing the IFC source.
@@ -149,8 +150,8 @@ struct SidecarData {
double map_unit_to_meters = 1.0; double map_unit_to_meters = 1.0;
uint32_t has_coordinate_operation = 0; uint32_t has_coordinate_operation = 0;
// Element tree metadata. // Element metadata.
std::vector<PackedElementInfo> elements; std::vector<ElementTableRecord> elements;
std::string string_table; std::string string_table;
// Streaming chunk TOC. Always written on disk (v14); geometry is laid out // Streaming chunk TOC. Always written on disk (v14); geometry is laid out
+2 -2
View File
@@ -88,7 +88,7 @@ void reorderSidecarByMorton(SidecarData& sd) {
std::vector<std::uint8_t> new_vertices; new_vertices.reserve(sd.vertices.size()); std::vector<std::uint8_t> new_vertices; new_vertices.reserve(sd.vertices.size());
std::vector<std::uint32_t> new_indices; new_indices.reserve(sd.indices.size()); std::vector<std::uint32_t> new_indices; new_indices.reserve(sd.indices.size());
std::vector<MeshInfo> new_meshes(mesh_count); std::vector<MeshInfo> new_meshes(mesh_count);
std::vector<InstanceCpu> new_instances; new_instances.reserve(sd.instances.size()); std::vector<InstanceInfo> new_instances; new_instances.reserve(sd.instances.size());
// Pass A: vertices + LOD0 indices + instances, mesh-by-mesh in the new // Pass A: vertices + LOD0 indices + instances, mesh-by-mesh in the new
// order, recording the new offsets on each MeshInfo. // order, recording the new offsets on each MeshInfo.
@@ -112,7 +112,7 @@ void reorderSidecarByMorton(SidecarData& sd) {
new_mesh_info.first_instance = std::uint32_t(new_instances.size()); new_mesh_info.first_instance = std::uint32_t(new_instances.size());
new_mesh_info.instance_count = std::uint32_t(insts_by_mesh[old].size()); new_mesh_info.instance_count = std::uint32_t(insts_by_mesh[old].size());
for (std::uint32_t instance_index : insts_by_mesh[old]) { for (std::uint32_t instance_index : insts_by_mesh[old]) {
InstanceCpu instance = sd.instances[instance_index]; InstanceInfo instance = sd.instances[instance_index];
instance.mesh_id = new_mesh_index; instance.mesh_id = new_mesh_index;
new_instances.push_back(instance); new_instances.push_back(instance);
} }
+1 -1
View File
@@ -42,7 +42,7 @@
// //
// Pure transform (no Qt / no wgpu): meshes, vertices, indices (LOD0 + LOD1), // Pure transform (no Qt / no wgpu): meshes, vertices, indices (LOD0 + LOD1),
// and instances are all rebuilt in the new order with vbo/ebo/lod1 offsets, // and instances are all rebuilt in the new order with vbo/ebo/lod1 offsets,
// MeshInfo.first_instance, and InstanceCpu.mesh_id remapped consistently. // MeshInfo.first_instance, and InstanceInfo.mesh_id remapped consistently.
// Index values are mesh-local, so they move unchanged. Element/georef/string // Index values are mesh-local, so they move unchanged. Element/georef/string
// data is mesh-independent and untouched. No-op for < 2 meshes. // data is mesh-independent and untouched. No-op for < 2 meshes.
// //
+16 -14
View File
@@ -25,9 +25,9 @@
// uint32 num_indices // uint32 num_indices
// uint32[num_indices] index data <-- streaming skips // uint32[num_indices] index data <-- streaming skips
// uint32 num_meshes + MeshInfo[] <-- streaming reads // uint32 num_meshes + MeshInfo[] <-- streaming reads
// uint32 num_instances + InstanceCpu[] <-- streaming reads // uint32 num_instances + InstanceInfo[] <-- streaming reads
// uint32 has_coord_op + double[16] + 2× double <-- streaming reads // uint32 has_coord_op + double[16] + 2× double <-- streaming reads
// uint32 num_elements + PackedElementInfo[] <-- streaming reads // uint32 num_elements + ElementTableRecord[] <-- streaming reads
// uint32 string_table_bytes + char[] <-- streaming reads // uint32 string_table_bytes + char[] <-- streaming reads
// //
// Streaming reader returns offsets to the two skipped sections so chunks // Streaming reader returns offsets to the two skipped sections so chunks
@@ -100,8 +100,8 @@ bool parseSidecarHead(const uint8_t* data, size_t n, uint64_t& out_geom_bytes) {
return true; return true;
} }
bool parseSidecarCritical(const uint8_t* data, size_t n, SidecarData& out) { bool parseSidecarGeometryMetadata(const uint8_t* data, size_t n, SidecarData& out) {
// v15 render-critical block: meshes, instances, georef, chunk TOC. // v15 geometry metadata block: meshes, instances, georef, chunk TOC.
BufCursor c{data, n}; BufCursor c{data, n};
if (!c.takeVec(out.meshes)) return false; if (!c.takeVec(out.meshes)) return false;
if (!c.takeVec(out.instances)) return false; if (!c.takeVec(out.instances)) return false;
@@ -113,8 +113,8 @@ bool parseSidecarCritical(const uint8_t* data, size_t n, SidecarData& out) {
return true; return true;
} }
bool parseSidecarDeferred(const uint8_t* data, size_t n, SidecarData& out) { bool parseSidecarElementMetadata(const uint8_t* data, size_t n, SidecarData& out) {
// v15 deferred block: element tree + string table (UI/picking, not rendered). // v15+ element metadata block: elements + string table (UI/picking, not rendered).
BufCursor c{data, n}; BufCursor c{data, n};
if (!c.takeVec(out.elements)) return false; if (!c.takeVec(out.elements)) return false;
uint32_t stbl_len = 0; uint32_t stbl_len = 0;
@@ -166,16 +166,18 @@ std::optional<StreamingSidecar> readSidecarMetadataOnly(const std::string& ifc_p
return SidecarCompress::decompress(z.data(), z.size(), raw.data(), raw.size()); return SidecarCompress::decompress(z.data(), z.size(), raw.data(), raw.size());
}; };
std::vector<uint8_t> crit, def; std::vector<uint8_t> geometry_metadata, element_metadata;
if (!readBlock(crit)) return fail(); if (!readBlock(geometry_metadata)) return fail();
if (!readBlock(def, &out.deferred_comp_offset, &out.deferred_comp_size, if (!readBlock(element_metadata, &out.element_metadata_comp_offset, &out.element_metadata_comp_size,
&out.deferred_raw_size)) return fail(); &out.element_metadata_raw_size)) return fail();
std::fclose(f); std::fclose(f);
// Desktop reads both blocks up front; the web path reads only critical // Desktop reads both blocks up front; the web path reads only geometry
// before painting and fetches the deferred block on demand. // metadata before painting and fetches the element metadata block on demand.
if (!parseSidecarCritical(crit.data(), crit.size(), out.meta)) return std::nullopt; if (!parseSidecarGeometryMetadata(geometry_metadata.data(), geometry_metadata.size(), out.meta))
if (!parseSidecarDeferred(def.data(), def.size(), out.meta)) return std::nullopt; return std::nullopt;
if (!parseSidecarElementMetadata(element_metadata.data(), element_metadata.size(), out.meta))
return std::nullopt;
return out; return out;
} }
+16 -15
View File
@@ -46,7 +46,7 @@
struct StreamingSidecar { struct StreamingSidecar {
// Everything except vertices + indices — same shape as SidecarData but // Everything except vertices + indices — same shape as SidecarData but
// with empty vertices / indices vectors. The renderer uses meshes / // with empty vertices / indices vectors. The renderer uses meshes /
// instances / georef / chunks immediately (elements/strings deferred). // instances / georef / chunks immediately (elements/strings are element metadata).
SidecarData meta; SidecarData meta;
// v16: the compressed geometry section starts here. Each chunk's two zstd // v16: the compressed geometry section starts here. Each chunk's two zstd
@@ -54,12 +54,13 @@ struct StreamingSidecar {
// a per-chunk load fetches [that, +*_comp_size) and decompresses to *_raw_size. // a per-chunk load fetches [that, +*_comp_size) and decompresses to *_raw_size.
uint64_t geometry_section_offset = 0; uint64_t geometry_section_offset = 0;
// v16 deferred (property) block locator: a single zstd frame at // v16 element metadata block locator: a single zstd frame at
// deferred_comp_offset of deferred_comp_size bytes → deferred_raw_size. The // element_metadata_comp_offset of element_metadata_comp_size bytes →
// web loader fetches it on demand (elements/strings); desktop reads it up front. // element_metadata_raw_size. The web loader fetches it on demand
uint64_t deferred_comp_offset = 0; // (elements/strings); desktop reads it up front.
uint64_t deferred_comp_size = 0; uint64_t element_metadata_comp_offset = 0;
uint64_t deferred_raw_size = 0; uint64_t element_metadata_comp_size = 0;
uint64_t element_metadata_raw_size = 0;
// Resolved on-disk path so subsequent chunk reads can re-open / seek. // Resolved on-disk path so subsequent chunk reads can re-open / seek.
std::string file_path; std::string file_path;
@@ -104,18 +105,18 @@ inline constexpr std::size_t SIDECAR_HEAD_BYTES = 20;
bool parseSidecarHead(const std::uint8_t* data, std::size_t n, bool parseSidecarHead(const std::uint8_t* data, std::size_t n,
std::uint64_t& out_geom_bytes); std::uint64_t& out_geom_bytes);
// Parse the v15 render-CRITICAL metadata block (mesh dict, instance dict, // Parse the v15 geometry metadata block (mesh dict, instance dict,
// georef, chunk TOC) — everything needed to set up + draw the scene. `data` // georef, chunk TOC) — everything needed to set up + draw the scene. `data`
// points at the first critical byte; `n` is critical_meta_bytes. Returns false // points at the first geometry metadata byte; `n` is geometry_metadata_bytes. Returns false
// on any bounds overrun, leaving out_meta partially filled. // on any bounds overrun, leaving out_meta partially filled.
bool parseSidecarCritical(const std::uint8_t* data, std::size_t n, bool parseSidecarGeometryMetadata(const std::uint8_t* data, std::size_t n,
SidecarData& out_meta); SidecarData& out_meta);
// Parse the v15 DEFERRED metadata block (element table + string table — the // Parse the v15 element metadata block (element table + string table — the
// IFC property tree, used for UI/picking, never for rendering). Fetched on // IFC property tree, used for UI/picking, never for rendering). Fetched on
// demand. `data` points at the first deferred byte; `n` is its length. // demand. `data` points at the first element metadata byte; `n` is its length.
bool parseSidecarDeferred(const std::uint8_t* data, std::size_t n, bool parseSidecarElementMetadata(const std::uint8_t* data, std::size_t n,
SidecarData& out_meta); SidecarData& out_meta);
// A coalesced read plan: a single contiguous source read whose bytes are // A coalesced read plan: a single contiguous source read whose bytes are
// scattered into the destination at the recorded offsets. Merging adjacent // scattered into the destination at the recorded offsets. Merging adjacent
+1 -1
View File
@@ -20,7 +20,7 @@
// Inline helpers that turn streamer-format vertices (7 floats per vertex: // Inline helpers that turn streamer-format vertices (7 floats per vertex:
// pos3 + normal3 + color-as-float) into the 12 B quantized VBO layout used // pos3 + normal3 + color-as-float) into the 12 B quantized VBO layout used
// by both the viewport's GPU buffers and the .ifcview sidecar. Shared // by both the viewport's GPU buffers and the .ifcview sidecar. Shared
// between ViewportWindow::uploadMeshChunk and SidecarBuilder so the // between ViewportWindow::uploadStreamedMesh and SidecarBuilder so the
// on-disk format stays identical to what the viewport renders. // on-disk format stays identical to what the viewport renders.
#ifndef VERTEXQUANTIZATION_H #ifndef VERTEXQUANTIZATION_H
+23 -23
View File
@@ -2858,8 +2858,8 @@ void ViewportCore::cullModelCpuUpload(ModelGpuData& m) {
} }
// =========================================================================== // ===========================================================================
// Sidecar / direct load (#84-q): applyCachedModel + uploadMeshChunk + // Sidecar / direct load (#84-q): applyCachedModel + uploadStreamedMesh +
// uploadInstanceChunk + finalizeModel // uploadStreamedInstance + finalizeModel
// =========================================================================== // ===========================================================================
#include "ChunkPlanner.h" #include "ChunkPlanner.h"
@@ -3220,14 +3220,14 @@ void ViewportCore::applyCachedModel(std::uint32_t model_id,
host_->requestFrame(); host_->requestFrame();
} }
void ViewportCore::uploadMeshChunk(const MeshChunk& chunk) { void ViewportCore::uploadStreamedMesh(const StreamedMesh& mesh) {
if (chunk.vertices.empty() || chunk.indices.empty()) return; if (mesh.vertices.empty() || mesh.indices.empty()) return;
SidecarData& s = getOrCreateDirectStaging(pending_direct_loads_, chunk.model_id); SidecarData& s = getOrCreateDirectStaging(pending_direct_loads_, mesh.model_id);
// Streamer format: 7 floats / vertex (pos3 + normal3 + color-as-float). // Streamer format: 7 floats / vertex (pos3 + normal3 + color-as-float).
// Same quantisation as SidecarBuilder::onMeshReady so direct-load and // Same quantisation as SidecarBuilder::onMeshReady so direct-load and
// sidecar-load produce byte-identical GPU buffers. // sidecar-load produce byte-identical GPU buffers.
const std::size_t n_verts = chunk.vertices.size() / INSTANCED_VERTEX_STRIDE_FLOATS; const std::size_t n_verts = mesh.vertices.size() / INSTANCED_VERTEX_STRIDE_FLOATS;
float bmin[3] = { std::numeric_limits<float>::infinity(), float bmin[3] = { std::numeric_limits<float>::infinity(),
std::numeric_limits<float>::infinity(), std::numeric_limits<float>::infinity(),
@@ -3236,7 +3236,7 @@ void ViewportCore::uploadMeshChunk(const MeshChunk& chunk) {
-std::numeric_limits<float>::infinity(), -std::numeric_limits<float>::infinity(),
-std::numeric_limits<float>::infinity() }; -std::numeric_limits<float>::infinity() };
for (std::size_t i = 0; i < n_verts; ++i) { for (std::size_t i = 0; i < n_verts; ++i) {
const float* vertex = chunk.vertices.data() + i * INSTANCED_VERTEX_STRIDE_FLOATS; const float* vertex = mesh.vertices.data() + i * INSTANCED_VERTEX_STRIDE_FLOATS;
for (int a = 0; a < 3; ++a) { for (int a = 0; a < 3; ++a) {
if (vertex[a] < bmin[a]) bmin[a] = vertex[a]; if (vertex[a] < bmin[a]) bmin[a] = vertex[a];
if (vertex[a] > bmax[a]) bmax[a] = vertex[a]; if (vertex[a] > bmax[a]) bmax[a] = vertex[a];
@@ -3251,7 +3251,7 @@ void ViewportCore::uploadMeshChunk(const MeshChunk& chunk) {
const std::size_t vb_offset = s.vertices.size(); const std::size_t vb_offset = s.vertices.size();
s.vertices.resize(vb_offset + n_verts * INSTANCED_VERTEX_STRIDE_BYTES); s.vertices.resize(vb_offset + n_verts * INSTANCED_VERTEX_STRIDE_BYTES);
for (std::size_t i = 0; i < n_verts; ++i) { for (std::size_t i = 0; i < n_verts; ++i) {
quantizeVertex(chunk.vertices.data() + i * INSTANCED_VERTEX_STRIDE_FLOATS, quantizeVertex(mesh.vertices.data() + i * INSTANCED_VERTEX_STRIDE_FLOATS,
bmin, extent_recip, bmin, extent_recip,
s.vertices.data() + vb_offset s.vertices.data() + vb_offset
+ i * INSTANCED_VERTEX_STRIDE_BYTES); + i * INSTANCED_VERTEX_STRIDE_BYTES);
@@ -3259,13 +3259,13 @@ void ViewportCore::uploadMeshChunk(const MeshChunk& chunk) {
const std::size_t ib_offset = s.indices.size(); const std::size_t ib_offset = s.indices.size();
s.indices.insert(s.indices.end(), s.indices.insert(s.indices.end(),
chunk.indices.begin(), chunk.indices.end()); mesh.indices.begin(), mesh.indices.end());
MeshInfo info{}; MeshInfo info{};
info.vbo_byte_offset = std::uint32_t(vb_offset); info.vbo_byte_offset = std::uint32_t(vb_offset);
info.vertex_count = std::uint32_t(n_verts); info.vertex_count = std::uint32_t(n_verts);
info.ebo_byte_offset = std::uint32_t(ib_offset * sizeof(std::uint32_t)); info.ebo_byte_offset = std::uint32_t(ib_offset * sizeof(std::uint32_t));
info.index_count = std::uint32_t(chunk.indices.size()); info.index_count = std::uint32_t(mesh.indices.size());
for (int a = 0; a < 3; ++a) { for (int a = 0; a < 3; ++a) {
info.local_aabb_min[a] = bmin[a]; info.local_aabb_min[a] = bmin[a];
info.local_aabb_max[a] = bmax[a]; info.local_aabb_max[a] = bmax[a];
@@ -3275,27 +3275,27 @@ void ViewportCore::uploadMeshChunk(const MeshChunk& chunk) {
info.lod1_ebo_byte_offset = 0; info.lod1_ebo_byte_offset = 0;
info.lod1_index_count = 0; info.lod1_index_count = 0;
if (s.meshes.size() <= chunk.local_mesh_id) { if (s.meshes.size() <= mesh.local_mesh_id) {
s.meshes.resize(chunk.local_mesh_id + 1); s.meshes.resize(mesh.local_mesh_id + 1);
} }
s.meshes[chunk.local_mesh_id] = info; s.meshes[mesh.local_mesh_id] = info;
} }
void ViewportCore::uploadInstanceChunk(const InstanceChunk& chunk) { void ViewportCore::uploadStreamedInstance(const StreamedInstance& instance_record) {
SidecarData& s = getOrCreateDirectStaging(pending_direct_loads_, chunk.model_id); SidecarData& s = getOrCreateDirectStaging(pending_direct_loads_, instance_record.model_id);
InstanceInfo instance{}; InstanceInfo instance{};
instance.mesh_id = chunk.local_mesh_id; instance.mesh_id = instance_record.local_mesh_id;
instance.object_id = chunk.object_id; instance.object_id = instance_record.object_id;
instance.color_override_rgba8 = chunk.color_override_rgba8; instance.color_override_rgba8 = instance_record.color_override_rgba8;
instance.model_id = chunk.model_id; instance.model_id = instance_record.model_id;
std::memcpy(instance.placement_transformation, chunk.transform, std::memcpy(instance.placement_transformation, instance_record.transform,
sizeof(instance.placement_transformation)); sizeof(instance.placement_transformation));
for (int i = 0; i < 16; ++i) { for (int i = 0; i < 16; ++i) {
instance.transform[i] = float(chunk.transform[i]); instance.transform[i] = float(instance_record.transform[i]);
} }
std::memcpy(instance.world_aabb_min, chunk.world_aabb_min, sizeof(instance.world_aabb_min)); std::memcpy(instance.world_aabb_min, instance_record.world_aabb_min, sizeof(instance.world_aabb_min));
std::memcpy(instance.world_aabb_max, chunk.world_aabb_max, sizeof(instance.world_aabb_max)); std::memcpy(instance.world_aabb_max, instance_record.world_aabb_max, sizeof(instance.world_aabb_max));
s.instances.push_back(instance); s.instances.push_back(instance);
} }
+4 -4
View File
@@ -472,8 +472,8 @@ public:
// the viewer one mesh + one instance at a time, then calls // the viewer one mesh + one instance at a time, then calls
// finalizeModel once everything's staged. The staging map lives on // finalizeModel once everything's staged. The staging map lives on
// ViewportCore so both halves can share it. // ViewportCore so both halves can share it.
void uploadMeshChunk(const MeshChunk& chunk); void uploadStreamedMesh(const StreamedMesh& mesh);
void uploadInstanceChunk(const InstanceChunk& chunk); void uploadStreamedInstance(const StreamedInstance& instance_record);
void finalizeModel(std::uint32_t model_id); void finalizeModel(std::uint32_t model_id);
// ---- Cross-chunk + screenshot capture (#84-v) ------------------------- // ---- Cross-chunk + screenshot capture (#84-v) -------------------------
@@ -1162,8 +1162,8 @@ private:
// completes. // completes.
std::string pending_screenshot_path_; std::string pending_screenshot_path_;
// Bonsai direct-load staging map. uploadMeshChunk + // Bonsai direct-load staging map. uploadStreamedMesh +
// uploadInstanceChunk append into entries keyed by model_id; the // uploadStreamedInstance append into entries keyed by model_id; the
// finalizeModel call moves the entry out, hands it to // finalizeModel call moves the entry out, hands it to
// applyCachedModel, and uploads the chunk slices synchronously. // applyCachedModel, and uploads the chunk slices synchronously.
std::unordered_map<std::uint32_t, std::unique_ptr<SidecarData>> std::unordered_map<std::uint32_t, std::unique_ptr<SidecarData>>
+6 -4
View File
@@ -550,17 +550,19 @@ void ViewportWindow::applyCachedModel(uint32_t model_id, StreamingSidecar metada
} }
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
// Direct-IFC ingestion (mirrors GL ViewportWindow::uploadMeshChunk / // Direct-IFC ingestion (mirrors GL ViewportWindow::uploadStreamedMesh /
// uploadInstanceChunk / finalizeModel). Streamer pushes chunks; we stage // uploadStreamedInstance / finalizeModel). Streamer pushes transfer records; we stage
// them into a SidecarData-shaped buffer and commit at finalize via the // them into a SidecarData-shaped buffer and commit at finalize via the
// same chunk planner the sidecar load uses. // same chunk planner the sidecar load uses.
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
// getOrCreateDirectStaging moved to ViewportCore (anon namespace) (#84-q). // getOrCreateDirectStaging moved to ViewportCore (anon namespace) (#84-q).
void ViewportWindow::uploadMeshChunk(const MeshChunk& chunk) { core_.uploadMeshChunk(chunk); } void ViewportWindow::uploadStreamedMesh(const StreamedMesh& mesh) { core_.uploadStreamedMesh(mesh); }
void ViewportWindow::uploadInstanceChunk(const InstanceChunk& chunk) { core_.uploadInstanceChunk(chunk); } void ViewportWindow::uploadStreamedInstance(const StreamedInstance& instance_record) {
core_.uploadStreamedInstance(instance_record);
}
void ViewportWindow::finalizeModel(uint32_t model_id) { core_.finalizeModel(model_id); } void ViewportWindow::finalizeModel(uint32_t model_id) { core_.finalizeModel(model_id); }
+4 -4
View File
@@ -122,8 +122,8 @@ public:
struct StreamingSidecar metadata); struct StreamingSidecar metadata);
// Direct-IFC ingestion (mirrors GL ViewportWindow). The host (typically // Direct-IFC ingestion (mirrors GL ViewportWindow). The host (typically
// a GeometryStreamer running on a worker) calls uploadMeshChunk + // a GeometryStreamer running on a worker) calls uploadStreamedMesh +
// uploadInstanceChunk once per representation / placement as the IFC // uploadStreamedInstance once per representation / placement as the IFC
// triangulates; finalizeModel commits when the iterator finishes. // triangulates; finalizeModel commits when the iterator finishes.
// Staged in CPU memory; finalizeModel runs the chunk planner over the // Staged in CPU memory; finalizeModel runs the chunk planner over the
// staged data, allocates pool slices, and uploads — same render path // staged data, allocates pool slices, and uploads — same render path
@@ -131,8 +131,8 @@ public:
// every chunk lands `is_resident=true` immediately. The streamer's // every chunk lands `is_resident=true` immediately. The streamer's
// model_id is passed through unchanged; the viewport's globally-unique // model_id is passed through unchanged; the viewport's globally-unique
// object_id rebasing happens at finalize time. // object_id rebasing happens at finalize time.
void uploadMeshChunk(const struct MeshChunk& chunk); void uploadStreamedMesh(const struct StreamedMesh& mesh);
void uploadInstanceChunk(const struct InstanceChunk& chunk); void uploadStreamedInstance(const struct StreamedInstance& instance_record);
void finalizeModel(uint32_t model_id); void finalizeModel(uint32_t model_id);
void removeModel(uint32_t model_id); void removeModel(uint32_t model_id);
@@ -283,7 +283,7 @@ namespace {
ModelGpuData make_model_with_one_instance(uint32_t object_id, uint32_t mesh_id, ModelGpuData make_model_with_one_instance(uint32_t object_id, uint32_t mesh_id,
double placement_tx) { double placement_tx) {
ModelGpuData m; ModelGpuData m;
InstanceCpu inst{}; InstanceInfo inst{};
inst.mesh_id = mesh_id; inst.mesh_id = mesh_id;
inst.object_id = object_id; inst.object_id = object_id;
// Column-major identity with a tx for verification. // Column-major identity with a tx for verification.
@@ -21,7 +21,7 @@
// vertex quantization used to fill it. // vertex quantization used to fill it.
// //
// quantizeVertex / octEncodeNormal (VertexQuantization.h) are the shared // quantizeVertex / octEncodeNormal (VertexQuantization.h) are the shared
// production helpers: ViewportWindow::uploadMeshChunk and SidecarBuilder both // production helpers: ViewportWindow::uploadStreamedMesh and SidecarBuilder both
// route through them so the rendered VBO and the on-disk .ifcview record are // route through them so the rendered VBO and the on-disk .ifcview record are
// byte-identical. The tests exercise that real implementation directly: // byte-identical. The tests exercise that real implementation directly:
// - runtime size/alignment assertions (defense in depth for the static_asserts) // - runtime size/alignment assertions (defense in depth for the static_asserts)
@@ -245,14 +245,14 @@ TEST_CASE("quantizeVertex passes the packed color through unchanged", "[instgeom
REQUIRE(std::memcmp(dst + INSTANCED_VERTEX_COLOR_OFFSET, rgba, 4) == 0); REQUIRE(std::memcmp(dst + INSTANCED_VERTEX_COLOR_OFFSET, rgba, 4) == 0);
} }
TEST_CASE("MeshChunk and InstanceChunk default-init to zeroed metadata", "[instgeom]") { TEST_CASE("StreamedMesh and StreamedInstance default-init to zeroed metadata", "[instgeom]") {
MeshChunk mc; StreamedMesh mc;
REQUIRE(mc.model_id == 0); REQUIRE(mc.model_id == 0);
REQUIRE(mc.local_mesh_id == 0); REQUIRE(mc.local_mesh_id == 0);
REQUIRE(mc.vertices.empty()); REQUIRE(mc.vertices.empty());
REQUIRE(mc.indices.empty()); REQUIRE(mc.indices.empty());
InstanceChunk ic; StreamedInstance ic;
REQUIRE(ic.model_id == 0); REQUIRE(ic.model_id == 0);
REQUIRE(ic.local_mesh_id == 0); REQUIRE(ic.local_mesh_id == 0);
REQUIRE(ic.object_id == 0); REQUIRE(ic.object_id == 0);
+8 -7
View File
@@ -83,7 +83,7 @@ SidecarData buildFixture() {
sd.instances.resize(5); sd.instances.resize(5);
for (size_t i = 0; i < sd.instances.size(); ++i) { for (size_t i = 0; i < sd.instances.size(); ++i) {
InstanceCpu& inst = sd.instances[i]; InstanceInfo& inst = sd.instances[i];
inst.mesh_id = (i < 3) ? 0u : 1u; inst.mesh_id = (i < 3) ? 0u : 1u;
inst.object_id = uint32_t(100 + i); inst.object_id = uint32_t(100 + i);
inst.color_override_rgba8 = uint32_t(0xAA000000u | (i * 0x010203u)); inst.color_override_rgba8 = uint32_t(0xAA000000u | (i * 0x010203u));
@@ -109,11 +109,10 @@ SidecarData buildFixture() {
sd.string_table = std::string("\0Wall\0Slab\0", 11); // includes embedded NULs sd.string_table = std::string("\0Wall\0Slab\0", 11); // includes embedded NULs
sd.elements.resize(3); sd.elements.resize(3);
for (size_t i = 0; i < sd.elements.size(); ++i) { for (size_t i = 0; i < sd.elements.size(); ++i) {
PackedElementInfo& e = sd.elements[i]; ElementTableRecord& e = sd.elements[i];
e.object_id = uint32_t(100 + i); e.object_id = uint32_t(100 + i);
e.model_id = 1; e.model_id = 1;
e.ifc_id = int32_t(1000 + i); e.ifc_id = int32_t(1000 + i);
e.parent_id = (i == 0) ? -1 : int32_t(100);
e.guid_offset = 0; e.guid_length = 0; e.guid_offset = 0; e.guid_length = 0;
e.name_offset = 1; e.name_length = 4; // "Wall" e.name_offset = 1; e.name_length = 4; // "Wall"
e.type_offset = 6; e.type_length = 4; // "Slab" e.type_offset = 6; e.type_length = 4; // "Slab"
@@ -136,10 +135,10 @@ bool sidecarDataEqual(const SidecarData& a, const SidecarData& b) {
if (std::memcmp(&a.meshes[i], &b.meshes[i], sizeof(MeshInfo)) != 0) return false; if (std::memcmp(&a.meshes[i], &b.meshes[i], sizeof(MeshInfo)) != 0) return false;
} }
for (size_t i = 0; i < a.instances.size(); ++i) { for (size_t i = 0; i < a.instances.size(); ++i) {
if (std::memcmp(&a.instances[i], &b.instances[i], sizeof(InstanceCpu)) != 0) return false; if (std::memcmp(&a.instances[i], &b.instances[i], sizeof(InstanceInfo)) != 0) return false;
} }
for (size_t i = 0; i < a.elements.size(); ++i) { for (size_t i = 0; i < a.elements.size(); ++i) {
if (std::memcmp(&a.elements[i], &b.elements[i], sizeof(PackedElementInfo)) != 0) return false; if (std::memcmp(&a.elements[i], &b.elements[i], sizeof(ElementTableRecord)) != 0) return false;
} }
// v11 georef block. // v11 georef block.
@@ -155,10 +154,12 @@ bool sidecarDataEqual(const SidecarData& a, const SidecarData& b) {
} // namespace } // namespace
TEST_CASE("MeshInfo and InstanceCpu have stable layouts (sidecar wire format)", "[sidecar]") { TEST_CASE("MeshInfo and InstanceInfo have stable layouts (sidecar wire format)", "[sidecar]") {
REQUIRE(sizeof(MeshInfo) == 56); REQUIRE(sizeof(MeshInfo) == 56);
REQUIRE(sizeof(InstanceInfo) == 232);
REQUIRE(sizeof(InstanceGpu) == 80); REQUIRE(sizeof(InstanceGpu) == 80);
REQUIRE(SIDECAR_VERSION == 16); REQUIRE(sizeof(ElementTableRecord) == 36);
REQUIRE(SIDECAR_VERSION == 17);
REQUIRE(sizeof(SidecarChunk) == 56); REQUIRE(sizeof(SidecarChunk) == 56);
REQUIRE(SIDECAR_MAGIC == 0x49465657u); REQUIRE(SIDECAR_MAGIC == 0x49465657u);
} }
+2 -2
View File
@@ -81,7 +81,7 @@ SidecarData buildFixture() {
for (uint32_t k = 0; k < 3; ++k) { // outer loop = interleave for (uint32_t k = 0; k < 3; ++k) { // outer loop = interleave
for (int i = 0; i < N; ++i) { for (int i = 0; i < N; ++i) {
if (k >= ninst(i)) continue; if (k >= ninst(i)) continue;
InstanceCpu ic; InstanceInfo ic;
ic.mesh_id = uint32_t(i); // authoritative ic.mesh_id = uint32_t(i); // authoritative
ic.object_id = obj++; ic.object_id = obj++;
ic.model_id = 1; ic.model_id = 1;
@@ -114,7 +114,7 @@ struct InstSig {
} }
}; };
InstSig sigFor(const SidecarData& sd, const InstanceCpu& inst) { InstSig sigFor(const SidecarData& sd, const InstanceInfo& inst) {
const MeshInfo& m = sd.meshes.at(inst.mesh_id); const MeshInfo& m = sd.meshes.at(inst.mesh_id);
InstSig s{}; InstSig s{};
s.verts.assign(sd.vertices.begin() + m.vbo_byte_offset, s.verts.assign(sd.vertices.begin() + m.vbo_byte_offset,
+13 -14
View File
@@ -84,7 +84,6 @@ SidecarData buildFixture() {
sd.elements[i].object_id = uint32_t(100 + i); sd.elements[i].object_id = uint32_t(100 + i);
sd.elements[i].model_id = 1; sd.elements[i].model_id = 1;
sd.elements[i].ifc_id = int32_t(1000 + i); sd.elements[i].ifc_id = int32_t(1000 + i);
sd.elements[i].parent_id = (i == 0) ? -1 : int32_t(100);
} }
// v16 stores geometry per-chunk (compressed); a fixture with geometry needs // v16 stores geometry per-chunk (compressed); a fixture with geometry needs
// a chunk TOC covering its meshes (one chunk per mesh here). // a chunk TOC covering its meshes (one chunk per mesh here).
@@ -113,8 +112,8 @@ TEST_CASE("readSidecarMetadataOnly returns metadata, skips bulk geometry",
// Chunk TOC carries compressed blob locators for each chunk. // Chunk TOC carries compressed blob locators for each chunk.
REQUIRE(meta->meta.chunks.size() == sd.chunks.size()); REQUIRE(meta->meta.chunks.size() == sd.chunks.size());
REQUIRE(meta->meta.chunks[0].v_comp_size > 0); REQUIRE(meta->meta.chunks[0].v_comp_size > 0);
// The deferred (property) block locator is recorded for on-demand fetch. // The element metadata block locator is recorded for on-demand fetch.
REQUIRE(meta->deferred_comp_size > 0); REQUIRE(meta->element_metadata_comp_size > 0);
// Metadata round-trips. // Metadata round-trips.
REQUIRE(meta->meta.meshes.size() == sd.meshes.size()); REQUIRE(meta->meta.meshes.size() == sd.meshes.size());
@@ -197,37 +196,37 @@ TEST_CASE("parseSidecarHead validates magic / version, reads geom length", "[str
REQUIRE_FALSE(parseSidecarHead(bad, sizeof(bad), got)); REQUIRE_FALSE(parseSidecarHead(bad, sizeof(bad), got));
} }
TEST_CASE("v16 deferred block: fetch via locator, decompress, parse", "[streaming]") { TEST_CASE("v16 element metadata block: fetch via locator, decompress, parse", "[streaming]") {
fs::path dir = makeScratchDir("v16def"); fs::path dir = makeScratchDir("v16element");
fs::path ifc = dir / "model.ifc"; fs::path ifc = dir / "model.ifc";
SidecarData sd = buildFixture(); SidecarData sd = buildFixture();
REQUIRE(writeSidecar(ifc.string(), sd)); REQUIRE(writeSidecar(ifc.string(), sd));
auto meta = readSidecarMetadataOnly(ifc.string()); auto meta = readSidecarMetadataOnly(ifc.string());
REQUIRE(meta.has_value()); REQUIRE(meta.has_value());
REQUIRE(meta->meta.meshes.size() == sd.meshes.size()); // critical REQUIRE(meta->meta.meshes.size() == sd.meshes.size()); // geometry metadata
REQUIRE(meta->meta.chunks.size() == sd.chunks.size()); REQUIRE(meta->meta.chunks.size() == sd.chunks.size());
REQUIRE(meta->meta.elements.size() == sd.elements.size()); // desktop reads deferred too REQUIRE(meta->meta.elements.size() == sd.elements.size()); // desktop reads element metadata too
REQUIRE(meta->deferred_comp_size > 0); REQUIRE(meta->element_metadata_comp_size > 0);
// The on-demand path (web) fetches the compressed deferred frame via the // The on-demand path (web) fetches the compressed element metadata frame via the
// recorded locator and decompresses it — verify that round-trips. // recorded locator and decompresses it — verify that round-trips.
FILE* f = std::fopen((dir / "model.ifcview").string().c_str(), "rb"); FILE* f = std::fopen((dir / "model.ifcview").string().c_str(), "rb");
REQUIRE(f); REQUIRE(f);
std::vector<uint8_t> cz(size_t(meta->deferred_comp_size)); std::vector<uint8_t> cz(size_t(meta->element_metadata_comp_size));
std::fseek(f, long(meta->deferred_comp_offset), SEEK_SET); std::fseek(f, long(meta->element_metadata_comp_offset), SEEK_SET);
REQUIRE(std::fread(cz.data(), 1, cz.size(), f) == cz.size()); REQUIRE(std::fread(cz.data(), 1, cz.size(), f) == cz.size());
std::fclose(f); std::fclose(f);
std::vector<uint8_t> raw(size_t(meta->deferred_raw_size)); std::vector<uint8_t> raw(size_t(meta->element_metadata_raw_size));
REQUIRE(SidecarCompress::decompress(cz.data(), cz.size(), raw.data(), raw.size())); REQUIRE(SidecarCompress::decompress(cz.data(), cz.size(), raw.data(), raw.size()));
SidecarData d; SidecarData d;
REQUIRE(parseSidecarDeferred(raw.data(), raw.size(), d)); REQUIRE(parseSidecarElementMetadata(raw.data(), raw.size(), d));
REQUIRE(d.elements.size() == sd.elements.size()); REQUIRE(d.elements.size() == sd.elements.size());
REQUIRE(d.string_table == sd.string_table); REQUIRE(d.string_table == sd.string_table);
SidecarData chopped; SidecarData chopped;
REQUIRE_FALSE(parseSidecarDeferred(raw.data(), raw.size() - 1, chopped)); REQUIRE_FALSE(parseSidecarElementMetadata(raw.data(), raw.size() - 1, chopped));
} }
TEST_CASE("planSidecarReadRanges coalesces adjacent ranges, keeps far ones split", TEST_CASE("planSidecarReadRanges coalesces adjacent ranges, keeps far ones split",