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*/,
std::vector<PackedElementInfo> elements,
std::vector<ElementTableRecord> elements,
std::string string_table) {
auto string_from_table = [&](uint32_t offset, uint32_t length) -> QString {
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.model_id = packed_element.model_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.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);
@@ -115,7 +114,6 @@ void ElementRegistry::onStreamedElementsReady(uint32_t /*model_id*/, std::vector
info.object_id = element.object_id;
info.model_id = element.model_id;
info.ifc_id = element.ifc_id;
info.parent_id = element.parent_id;
info.guid = QString::fromStdString(element.guid);
info.name = QString::fromStdString(element.name);
info.type = QString::fromStdString(element.type);
+2 -3
View File
@@ -30,7 +30,7 @@
#include <vector>
class SceneLoader;
struct PackedElementInfo;
struct ElementTableRecord;
struct ElementInfo;
namespace bonsaiviewer {
@@ -39,7 +39,6 @@ struct BasicElementInfo {
uint32_t object_id = 0;
uint32_t model_id = 0;
int ifc_id = 0;
int parent_id = 0;
QString guid;
QString name;
QString type;
@@ -59,7 +58,7 @@ public:
private:
void onSidecarElementsReady(uint32_t model_id,
std::vector<PackedElementInfo> elements,
std::vector<ElementTableRecord> elements,
std::string string_table);
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
// 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
// (the inputs are all derivable from the resident MeshInfo + InstanceCpu
// (the inputs are all derivable from the resident MeshInfo + InstanceInfo
// data + ModelGeoref).
//
// 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;
}
// 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
// triangulations with per-face materials still render correctly.
// 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
// by T(+offset), which is mathematically the identity overall but moves
// 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,
const IfcGeom::TriangulationElement* elem,
const Eigen::Vector3d& offset) {
MeshChunk chunk;
chunk.model_id = model_id;
chunk.local_mesh_id = local_mesh_id;
StreamedMesh mesh;
mesh.model_id = model_id;
mesh.local_mesh_id = local_mesh_id;
const auto& geom = elem->geometry();
const auto& verts = geom.verts();
@@ -168,7 +168,7 @@ static MeshChunk buildMeshChunk(uint32_t model_id,
const auto& materials = geom.materials();
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_tris = faces.size() / 3;
@@ -184,8 +184,8 @@ static MeshChunk buildMeshChunk(uint32_t model_id,
std::unordered_map<uint64_t, uint32_t> remap;
remap.reserve(num_verts_src);
chunk.vertices.reserve(num_verts_src * INSTANCED_VERTEX_STRIDE_FLOATS);
chunk.indices.reserve(faces.size());
mesh.vertices.reserve(num_verts_src * INSTANCED_VERTEX_STRIDE_FLOATS);
mesh.indices.reserve(faces.size());
// Track local AABB as we emit vertices.
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;
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
// verts are far from origin and offset cancels the magnitude.
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 pz = static_cast<float>(verts[orig_idx * 3 + 2] - offset.z());
chunk.vertices.push_back(px);
chunk.vertices.push_back(py);
chunk.vertices.push_back(pz);
mesh.vertices.push_back(px);
mesh.vertices.push_back(py);
mesh.vertices.push_back(pz);
if (px < local_aabb_min[0]) local_aabb_min[0] = px;
if (px > local_aabb_max[0]) local_aabb_max[0] = px;
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 (orig_idx * 3 + 2 < normals.size()) {
chunk.vertices.push_back(static_cast<float>(normals[orig_idx * 3 + 0]));
chunk.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 + 0]));
mesh.vertices.push_back(static_cast<float>(normals[orig_idx * 3 + 1]));
mesh.vertices.push_back(static_cast<float>(normals[orig_idx * 3 + 2]));
} else {
chunk.vertices.push_back(0.0f);
chunk.vertices.push_back(1.0f);
chunk.vertices.push_back(0.0f);
mesh.vertices.push_back(0.0f);
mesh.vertices.push_back(1.0f);
mesh.vertices.push_back(0.0f);
}
MaterialInfo m;
@@ -235,7 +235,7 @@ static MeshChunk buildMeshChunk(uint32_t model_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);
mesh.vertices.push_back(packed_as_float);
remap.emplace(key, 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) {
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));
chunk.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 + 0]), mat_id));
mesh.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 + 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) {
chunk.local_aabb_min[a] = local_aabb_min[a];
chunk.local_aabb_max[a] = local_aabb_max[a];
mesh.local_aabb_min[a] = local_aabb_min[a];
mesh.local_aabb_max[a] = local_aabb_max[a];
}
return chunk;
return mesh;
}
// 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.name = tri_elem->name();
info.type = tri_elem->type();
info.parent_id = tri_elem->parent_id();
{
std::lock_guard<std::mutex> lock(elements_mutex_);
pending_elements_.push_back(std::move(info));
@@ -604,19 +603,19 @@ void GeometryStreamer::run(const std::string& path, int num_threads) {
}
}
MeshChunk mesh_chunk =
buildMeshChunk(model_id_, local_mesh_id, tri_elem, offset);
StreamedMesh streamed_mesh =
buildStreamedMesh(model_id_, local_mesh_id, tri_elem, offset);
MeshAabb mesh_aabb;
for (int a = 0; a < 3; ++a) {
mesh_aabb.lmin[a] = mesh_chunk.local_aabb_min[a];
mesh_aabb.lmax[a] = mesh_chunk.local_aabb_max[a];
mesh_aabb.lmin[a] = streamed_mesh.local_aabb_min[a];
mesh_aabb.lmax[a] = streamed_mesh.local_aabb_max[a];
mesh_aabb.offset[a] = offset[a];
}
mesh_aabb.has_offset = (offset.squaredNorm() > 0.0);
if (mesh_aabbs.size() <= local_mesh_id) mesh_aabbs.resize(local_mesh_id + 1);
mesh_aabbs[local_mesh_id] = mesh_aabb;
if (!mesh_chunk.indices.empty()) {
emit meshReady(std::move(mesh_chunk));
if (!streamed_mesh.indices.empty()) {
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;
}
InstanceChunk inst;
StreamedInstance inst;
inst.model_id = model_id_;
inst.local_mesh_id = local_mesh_id;
inst.object_id = object_id;
+2 -3
View File
@@ -41,7 +41,6 @@ struct ElementInfo {
std::string guid;
std::string name;
std::string type;
int parent_id;
};
class GeometryStreamer : public QObject {
@@ -70,8 +69,8 @@ public:
signals:
void progressChanged(int percent);
void meshReady(MeshChunk chunk);
void instanceReady(InstanceChunk chunk);
void meshReady(StreamedMesh mesh);
void instanceReady(StreamedInstance instance_record);
void finished();
void cancelled();
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;
const uint32_t instance_index = it->second;
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.mesh_id = instance.mesh_id;
std::memcpy(out.placement_transformation,
+1 -1
View File
@@ -66,7 +66,7 @@ void composeInstance(
// Result of a successful findInstance lookup. The placement_transformation
// 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.
struct InstanceLookup {
uint32_t model_id = 0;
+7 -7
View File
@@ -39,8 +39,8 @@
static constexpr int INSTANCED_VERTEX_STRIDE_BYTES = 12;
// Streamer-side intermediate format: 7 floats per vertex (pos3 + normal3 +
// color-as-float). GeometryStreamer writes this into MeshChunk.vertices;
// ViewportWindow::uploadMeshChunk quantizes it down to STRIDE_BYTES on the
// color-as-float). GeometryStreamer writes this into StreamedMesh.vertices;
// ViewportWindow::uploadStreamedMesh quantizes it down to STRIDE_BYTES on the
// way to the VBO. Not the GPU layout — purely a transfer convention.
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
// stage matrices are all identity (default), transform is the float rendering
// copy of placement_transformation.
struct InstanceCpu {
struct InstanceInfo {
uint32_t mesh_id = 0; // index into meshes array
uint32_t object_id = 0;
uint32_t color_override_rgba8 = 0;
@@ -120,12 +120,12 @@ struct InstanceCpu {
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
// geometry in local coords. `local_mesh_id` is the streamer-assigned id
// within this model.
struct MeshChunk {
struct StreamedMesh {
uint32_t model_id = 0;
uint32_t local_mesh_id = 0;
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
// 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.
struct InstanceChunk {
struct StreamedInstance {
uint32_t model_id = 0;
uint32_t local_mesh_id = 0;
uint32_t object_id = 0;
+17 -16
View File
@@ -34,7 +34,7 @@
#include "InstancedGeometry.h"
#include "BufferPool.h"
#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
// wgpu handles. Stage 2 only allocates and uploads the four core buffers;
@@ -305,26 +305,27 @@ struct ModelGpuData {
bool streaming_from_web = false;
// Web analog of streaming_file_path: which registered JS byte-source
// (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.
int web_source_id = 0;
// v15 deferred property metadata (web, on-demand). The IFC element tree
// (elements + string_table — names/GUIDs/hierarchy, for UI/picking, never
// v15 element metadata (web, on-demand). The IFC element metadata
// (elements + string_table — names/GUIDs, for UI/picking, never
// rendering) lives in a separate file block fetched only when a consumer
// asks, so first paint doesn't wait on it. Empty until
// loadDeferredMetadataWeb fetches [deferred_meta_offset, +bytes) and parses
// it; deferred_meta_loaded latches so it fetches at most once.
std::vector<PackedElementInfo> elements;
// loadElementMetadataWeb fetches [element_metadata_comp_offset, +bytes) and parses
// it; element_metadata_loaded latches so it fetches at most once.
std::vector<ElementTableRecord> elements;
std::string string_table;
// v16: the deferred block is a single zstd frame at deferred_comp_offset of
// deferred_comp_size bytes, expanding to deferred_raw_size.
uint64_t deferred_comp_offset = 0;
uint64_t deferred_comp_size = 0;
uint64_t deferred_raw_size = 0;
bool deferred_meta_loaded = false;
// v16: the element metadata block is a single zstd frame at
// element_metadata_comp_offset of element_metadata_comp_size bytes,
// expanding to element_metadata_raw_size.
uint64_t element_metadata_comp_offset = 0;
uint64_t element_metadata_comp_size = 0;
uint64_t element_metadata_raw_size = 0;
bool element_metadata_loaded = false;
// 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.
uint32_t object_id_base = 0;
@@ -378,10 +379,10 @@ struct ModelGpuData {
// CPU side, kept for cull / picking / federation recompose.
std::vector<MeshInfo> meshes;
std::vector<InstanceCpu> instances;
std::vector<InstanceInfo> instances;
// 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
// into the opaque or transparent draw partition: an instance with
// 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;
}
std::vector<PackedElementInfo> elements = std::move(d.elements);
std::vector<ElementTableRecord> elements = std::move(d.elements);
std::string stbl = std::move(d.string_table);
viewport_->applyCachedModel(mid, std::move(metadata));
@@ -331,24 +331,24 @@ void SceneLoader::onStreamerProgressChanged(int percent) {
emit progressChanged(percent);
}
void SceneLoader::onStreamerMeshReady(MeshChunk chunk) {
viewport_->uploadMeshChunk(chunk);
void SceneLoader::onStreamerMeshReady(StreamedMesh mesh) {
viewport_->uploadStreamedMesh(mesh);
if (loading_model_id_ != 0) {
auto it = models_.find(loading_model_id_);
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) {
auto it = models_.find(loading_model_id_);
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() {
+3 -3
View File
@@ -102,7 +102,7 @@ signals:
// packed element set. Consumer is responsible for decoding + tree/
// property-map population. Moved arguments — avoid unnecessary copies.
void sidecarElementsReady(uint32_t mid,
std::vector<PackedElementInfo> elements,
std::vector<ElementTableRecord> elements,
std::string string_table);
void loadedFromSidecar(uint32_t mid, qint64 elapsed_ms);
@@ -128,8 +128,8 @@ signals:
private slots:
void onStreamerProgressChanged(int percent);
void onStreamerMeshReady(MeshChunk chunk);
void onStreamerInstanceReady(InstanceChunk chunk);
void onStreamerMeshReady(StreamedMesh mesh);
void onStreamerInstanceReady(StreamedInstance instance_record);
void onStreamerFinished();
void onStreamerCancelled();
void onStreamerError(const QString& msg);
+23 -24
View File
@@ -37,14 +37,14 @@ SidecarBuilder::SidecarBuilder(QObject* parent)
{
}
void SidecarBuilder::onMeshReady(const MeshChunk& chunk) {
if (chunk.vertices.empty() || chunk.indices.empty()) return;
void SidecarBuilder::onMeshReady(const StreamedMesh& mesh) {
if (mesh.vertices.empty() || mesh.indices.empty()) return;
// 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
// way ViewportWindow::uploadMeshChunk does so the .ifcview byte layout
// way ViewportWindow::uploadStreamedMesh does so the .ifcview byte layout
// matches the live-render path.
float bmin[3] = { 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() };
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) {
if (vertex[a] < bmin[a]) bmin[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();
sidecar_data_.vertices.resize(vb_offset + n_verts * INSTANCED_VERTEX_STRIDE_BYTES);
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,
sidecar_data_.vertices.data() + vb_offset
+ i * INSTANCED_VERTEX_STRIDE_BYTES);
@@ -76,13 +76,13 @@ void SidecarBuilder::onMeshReady(const MeshChunk& chunk) {
const size_t ib_offset = sidecar_data_.indices.size();
sidecar_data_.indices.insert(sidecar_data_.indices.end(),
chunk.indices.begin(), chunk.indices.end());
mesh.indices.begin(), mesh.indices.end());
MeshInfo info;
info.vbo_byte_offset = static_cast<uint32_t>(vb_offset);
info.vertex_count = static_cast<uint32_t>(n_verts);
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) {
info.local_aabb_min[a] = bmin[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_index_count = 0;
if (sidecar_data_.meshes.size() <= chunk.local_mesh_id) {
sidecar_data_.meshes.resize(chunk.local_mesh_id + 1);
if (sidecar_data_.meshes.size() <= mesh.local_mesh_id) {
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) {
InstanceCpu instance;
instance.mesh_id = chunk.local_mesh_id;
instance.object_id = chunk.object_id;
instance.color_override_rgba8 = chunk.color_override_rgba8;
instance.model_id = chunk.model_id;
void SidecarBuilder::onInstanceReady(const StreamedInstance& instance_record) {
InstanceInfo instance;
instance.mesh_id = instance_record.local_mesh_id;
instance.object_id = instance_record.object_id;
instance.color_override_rgba8 = instance_record.color_override_rgba8;
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
// an identity-stage baseline; applyCachedModel recomposes from placement
// 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));
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_max, chunk.world_aabb_max, sizeof(instance.world_aabb_max));
std::memcpy(instance.world_aabb_min, instance_record.world_aabb_min, sizeof(instance.world_aabb_min));
std::memcpy(instance.world_aabb_max, instance_record.world_aabb_max, sizeof(instance.world_aabb_max));
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;
for (const auto& info : elements) {
PackedElementInfo packed;
ElementTableRecord packed;
packed.object_id = info.object_id;
packed.model_id = info.model_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_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
// streamer signals are delivered to.
void onMeshReady(const MeshChunk& chunk);
void onInstanceReady(const InstanceChunk& chunk);
void onMeshReady(const StreamedMesh& mesh);
void onInstanceReady(const StreamedInstance& instance_record);
// Finishes assembly using the georef + element batch the host collected
// during streaming. Returns the assembled SidecarData by move; the
@@ -76,4 +76,3 @@ private:
};
#endif // SIDECARBUILDER_H
+22 -22
View File
@@ -30,7 +30,7 @@
// MeshInfo[num_meshes]
//
// 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+)
// double[16] coordinate_operation_meters (v11+; column-major)
@@ -38,7 +38,7 @@
// double map_unit_to_meters (v11+)
//
// uint32_t num_elements
// PackedElementInfo[num_elements]
// ElementTableRecord[num_elements]
// uint32_t 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 (fseek(f, geom_end, SEEK_SET) != 0) { fclose(f); return false; }
// --- Critical metadata block (zstd): meshes, instances, georef, chunk TOC
std::vector<std::uint8_t> critical_metadata;
appendVec(critical_metadata, data.meshes);
appendVec(critical_metadata, data.instances);
appendBytes(critical_metadata, &data.has_coordinate_operation, 4);
appendBytes(critical_metadata, data.coordinate_operation_meters, sizeof(double) * 16);
appendBytes(critical_metadata, &data.project_length_to_meters, sizeof(double));
appendBytes(critical_metadata, &data.map_unit_to_meters, sizeof(double));
appendVec(critical_metadata, chunks);
if (!wrBlock(critical_metadata)) { fclose(f); return false; }
// --- Geometry metadata block (zstd): meshes, instances, georef, chunk TOC
std::vector<std::uint8_t> geometry_metadata;
appendVec(geometry_metadata, data.meshes);
appendVec(geometry_metadata, data.instances);
appendBytes(geometry_metadata, &data.has_coordinate_operation, 4);
appendBytes(geometry_metadata, data.coordinate_operation_meters, sizeof(double) * 16);
appendBytes(geometry_metadata, &data.project_length_to_meters, sizeof(double));
appendBytes(geometry_metadata, &data.map_unit_to_meters, sizeof(double));
appendVec(geometry_metadata, chunks);
if (!wrBlock(geometry_metadata)) { fclose(f); return false; }
// --- Deferred metadata block (zstd): element tree + string table ---------
std::vector<std::uint8_t> deferred_metadata;
appendVec(deferred_metadata, data.elements);
// --- Element metadata block (zstd): elements + string table --------------
std::vector<std::uint8_t> element_metadata;
appendVec(element_metadata, data.elements);
std::uint32_t stbl_len = static_cast<std::uint32_t>(data.string_table.size());
appendBytes(deferred_metadata, &stbl_len, 4);
appendBytes(deferred_metadata, data.string_table.data(), stbl_len);
if (!wrBlock(deferred_metadata)) { fclose(f); return false; }
appendBytes(element_metadata, &stbl_len, 4);
appendBytes(element_metadata, data.string_table.data(), stbl_len);
if (!wrBlock(element_metadata)) { fclose(f); return false; }
fclose(f);
return true;
@@ -286,12 +286,12 @@ std::optional<SidecarData> readSidecar(const std::string& ifc_path) {
out.assign(std::size_t(raw), 0);
return SidecarCompress::decompress(z.data(), z.size(), out.data(), out.size());
};
std::vector<std::uint8_t> critical_metadata, deferred_metadata;
if (!readBlock(critical_metadata) || !readBlock(deferred_metadata)) return fail();
std::vector<std::uint8_t> geometry_metadata, element_metadata;
if (!readBlock(geometry_metadata) || !readBlock(element_metadata)) return fail();
fclose(f);
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.instances)) 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.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;
std::uint32_t stbl_len = 0;
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
// (delete the sidecar to force a rebuild).
// 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
// raw streamer output; transform is the composed FederatedFalseOrigin ·
// 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
// IfcMapConversion do NOT invalidate the sidecar — delete the
// .ifcview manually if you change the source's georef parameters.
// v12 = InstanceCpu::placement_transformation is double[16], and
// InstanceChunk carries the streamer placement as double[16]. This keeps
// v12 = InstanceInfo::placement_transformation is double[16], and
// StreamedInstance carries the streamer placement as double[16]. This keeps
// large IFC placements exact until CoordinateOperation / FederatedFalseOrigin
// composition has reduced them to viewport-local float-sized values.
// 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
// wasm loader), so it must be baked in. No back-compat: v13 sidecars are
// rejected (regenerate them).
// v15 = The post-index metadata is split into a render-CRITICAL block (meshes,
// instances, georef, chunk TOC) followed by a DEFERRED block (elements +
// string_table — the IFC element tree, used for UI/picking, never for
// rendering), with the critical block's byte length written just after
// the index section. The web loader reads only the critical block before
// v15 = The post-index metadata is split into a geometry metadata block (meshes,
// instances, georef, chunk TOC) followed by an element metadata block (elements +
// string_table — IFC element metadata, used for UI/picking, never for
// rendering), with the geometry block's byte length written just after
// the index section. The web loader reads only the geometry block before
// 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.
// v16 = Geometry + metadata are zstd-COMPRESSED. Each chunk's vertex bytes and
// index bytes are stored as two independent zstd frames (so per-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 raw (decompressed) sizes. ~3-5x fewer bytes over the wire while
// keeping HTTP Range intact (unlike server Content-Encoding). No
// 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;
// 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
// into a separate string table.
struct PackedElementInfo {
struct ElementTableRecord {
uint32_t object_id;
uint32_t model_id;
int32_t ifc_id;
int32_t parent_id;
uint32_t guid_offset;
uint32_t guid_length;
uint32_t name_offset;
@@ -134,7 +135,7 @@ struct SidecarData {
// Mesh dictionary and per-instance data.
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
// load can apply georef without re-parsing the IFC source.
@@ -149,8 +150,8 @@ struct SidecarData {
double map_unit_to_meters = 1.0;
uint32_t has_coordinate_operation = 0;
// Element tree metadata.
std::vector<PackedElementInfo> elements;
// Element metadata.
std::vector<ElementTableRecord> elements;
std::string string_table;
// 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::uint32_t> new_indices; new_indices.reserve(sd.indices.size());
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
// 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.instance_count = std::uint32_t(insts_by_mesh[old].size());
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;
new_instances.push_back(instance);
}
+1 -1
View File
@@ -42,7 +42,7 @@
//
// 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,
// 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
// 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] index data <-- streaming skips
// 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 num_elements + PackedElementInfo[] <-- streaming reads
// uint32 num_elements + ElementTableRecord[] <-- streaming reads
// uint32 string_table_bytes + char[] <-- streaming reads
//
// 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;
}
bool parseSidecarCritical(const uint8_t* data, size_t n, SidecarData& out) {
// v15 render-critical block: meshes, instances, georef, chunk TOC.
bool parseSidecarGeometryMetadata(const uint8_t* data, size_t n, SidecarData& out) {
// v15 geometry metadata block: meshes, instances, georef, chunk TOC.
BufCursor c{data, n};
if (!c.takeVec(out.meshes)) 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;
}
bool parseSidecarDeferred(const uint8_t* data, size_t n, SidecarData& out) {
// v15 deferred block: element tree + string table (UI/picking, not rendered).
bool parseSidecarElementMetadata(const uint8_t* data, size_t n, SidecarData& out) {
// v15+ element metadata block: elements + string table (UI/picking, not rendered).
BufCursor c{data, n};
if (!c.takeVec(out.elements)) return false;
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());
};
std::vector<uint8_t> crit, def;
if (!readBlock(crit)) return fail();
if (!readBlock(def, &out.deferred_comp_offset, &out.deferred_comp_size,
&out.deferred_raw_size)) return fail();
std::vector<uint8_t> geometry_metadata, element_metadata;
if (!readBlock(geometry_metadata)) return fail();
if (!readBlock(element_metadata, &out.element_metadata_comp_offset, &out.element_metadata_comp_size,
&out.element_metadata_raw_size)) return fail();
std::fclose(f);
// Desktop reads both blocks up front; the web path reads only critical
// before painting and fetches the deferred block on demand.
if (!parseSidecarCritical(crit.data(), crit.size(), out.meta)) return std::nullopt;
if (!parseSidecarDeferred(def.data(), def.size(), out.meta)) return std::nullopt;
// Desktop reads both blocks up front; the web path reads only geometry
// metadata before painting and fetches the element metadata block on demand.
if (!parseSidecarGeometryMetadata(geometry_metadata.data(), geometry_metadata.size(), out.meta))
return std::nullopt;
if (!parseSidecarElementMetadata(element_metadata.data(), element_metadata.size(), out.meta))
return std::nullopt;
return out;
}
+16 -15
View File
@@ -46,7 +46,7 @@
struct StreamingSidecar {
// Everything except vertices + indices — same shape as SidecarData but
// 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;
// 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.
uint64_t geometry_section_offset = 0;
// v16 deferred (property) block locator: a single zstd frame at
// deferred_comp_offset of deferred_comp_size bytes → deferred_raw_size. The
// web loader fetches it on demand (elements/strings); desktop reads it up front.
uint64_t deferred_comp_offset = 0;
uint64_t deferred_comp_size = 0;
uint64_t deferred_raw_size = 0;
// v16 element metadata block locator: a single zstd frame at
// element_metadata_comp_offset of element_metadata_comp_size bytes →
// element_metadata_raw_size. The web loader fetches it on demand
// (elements/strings); desktop reads it up front.
uint64_t element_metadata_comp_offset = 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.
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,
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`
// 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.
bool parseSidecarCritical(const std::uint8_t* data, std::size_t n,
SidecarData& out_meta);
bool parseSidecarGeometryMetadata(const std::uint8_t* data, std::size_t n,
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
// demand. `data` points at the first deferred byte; `n` is its length.
bool parseSidecarDeferred(const std::uint8_t* data, std::size_t n,
SidecarData& out_meta);
// demand. `data` points at the first element metadata byte; `n` is its length.
bool parseSidecarElementMetadata(const std::uint8_t* data, std::size_t n,
SidecarData& out_meta);
// A coalesced read plan: a single contiguous source read whose bytes are
// 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:
// 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
// between ViewportWindow::uploadMeshChunk and SidecarBuilder so the
// between ViewportWindow::uploadStreamedMesh and SidecarBuilder so the
// on-disk format stays identical to what the viewport renders.
#ifndef VERTEXQUANTIZATION_H
+23 -23
View File
@@ -2858,8 +2858,8 @@ void ViewportCore::cullModelCpuUpload(ModelGpuData& m) {
}
// ===========================================================================
// Sidecar / direct load (#84-q): applyCachedModel + uploadMeshChunk +
// uploadInstanceChunk + finalizeModel
// Sidecar / direct load (#84-q): applyCachedModel + uploadStreamedMesh +
// uploadStreamedInstance + finalizeModel
// ===========================================================================
#include "ChunkPlanner.h"
@@ -3220,14 +3220,14 @@ void ViewportCore::applyCachedModel(std::uint32_t model_id,
host_->requestFrame();
}
void ViewportCore::uploadMeshChunk(const MeshChunk& chunk) {
if (chunk.vertices.empty() || chunk.indices.empty()) return;
SidecarData& s = getOrCreateDirectStaging(pending_direct_loads_, chunk.model_id);
void ViewportCore::uploadStreamedMesh(const StreamedMesh& mesh) {
if (mesh.vertices.empty() || mesh.indices.empty()) return;
SidecarData& s = getOrCreateDirectStaging(pending_direct_loads_, mesh.model_id);
// Streamer format: 7 floats / vertex (pos3 + normal3 + color-as-float).
// Same quantisation as SidecarBuilder::onMeshReady so direct-load and
// 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(),
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() };
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) {
if (vertex[a] < bmin[a]) bmin[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();
s.vertices.resize(vb_offset + n_verts * INSTANCED_VERTEX_STRIDE_BYTES);
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,
s.vertices.data() + vb_offset
+ i * INSTANCED_VERTEX_STRIDE_BYTES);
@@ -3259,13 +3259,13 @@ void ViewportCore::uploadMeshChunk(const MeshChunk& chunk) {
const std::size_t ib_offset = s.indices.size();
s.indices.insert(s.indices.end(),
chunk.indices.begin(), chunk.indices.end());
mesh.indices.begin(), mesh.indices.end());
MeshInfo info{};
info.vbo_byte_offset = std::uint32_t(vb_offset);
info.vertex_count = std::uint32_t(n_verts);
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) {
info.local_aabb_min[a] = bmin[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_index_count = 0;
if (s.meshes.size() <= chunk.local_mesh_id) {
s.meshes.resize(chunk.local_mesh_id + 1);
if (s.meshes.size() <= mesh.local_mesh_id) {
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) {
SidecarData& s = getOrCreateDirectStaging(pending_direct_loads_, chunk.model_id);
void ViewportCore::uploadStreamedInstance(const StreamedInstance& instance_record) {
SidecarData& s = getOrCreateDirectStaging(pending_direct_loads_, instance_record.model_id);
InstanceInfo instance{};
instance.mesh_id = chunk.local_mesh_id;
instance.object_id = chunk.object_id;
instance.color_override_rgba8 = chunk.color_override_rgba8;
instance.model_id = chunk.model_id;
std::memcpy(instance.placement_transformation, chunk.transform,
instance.mesh_id = instance_record.local_mesh_id;
instance.object_id = instance_record.object_id;
instance.color_override_rgba8 = instance_record.color_override_rgba8;
instance.model_id = instance_record.model_id;
std::memcpy(instance.placement_transformation, instance_record.transform,
sizeof(instance.placement_transformation));
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_max, chunk.world_aabb_max, sizeof(instance.world_aabb_max));
std::memcpy(instance.world_aabb_min, instance_record.world_aabb_min, sizeof(instance.world_aabb_min));
std::memcpy(instance.world_aabb_max, instance_record.world_aabb_max, sizeof(instance.world_aabb_max));
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
// finalizeModel once everything's staged. The staging map lives on
// ViewportCore so both halves can share it.
void uploadMeshChunk(const MeshChunk& chunk);
void uploadInstanceChunk(const InstanceChunk& chunk);
void uploadStreamedMesh(const StreamedMesh& mesh);
void uploadStreamedInstance(const StreamedInstance& instance_record);
void finalizeModel(std::uint32_t model_id);
// ---- Cross-chunk + screenshot capture (#84-v) -------------------------
@@ -1162,8 +1162,8 @@ private:
// completes.
std::string pending_screenshot_path_;
// Bonsai direct-load staging map. uploadMeshChunk +
// uploadInstanceChunk append into entries keyed by model_id; the
// Bonsai direct-load staging map. uploadStreamedMesh +
// uploadStreamedInstance append into entries keyed by model_id; the
// finalizeModel call moves the entry out, hands it to
// applyCachedModel, and uploads the chunk slices synchronously.
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 /
// uploadInstanceChunk / finalizeModel). Streamer pushes chunks; we stage
// Direct-IFC ingestion (mirrors GL ViewportWindow::uploadStreamedMesh /
// uploadStreamedInstance / finalizeModel). Streamer pushes transfer records; we stage
// them into a SidecarData-shaped buffer and commit at finalize via the
// same chunk planner the sidecar load uses.
// -----------------------------------------------------------------------------
// 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); }
+4 -4
View File
@@ -122,8 +122,8 @@ public:
struct StreamingSidecar metadata);
// Direct-IFC ingestion (mirrors GL ViewportWindow). The host (typically
// a GeometryStreamer running on a worker) calls uploadMeshChunk +
// uploadInstanceChunk once per representation / placement as the IFC
// a GeometryStreamer running on a worker) calls uploadStreamedMesh +
// uploadStreamedInstance once per representation / placement as the IFC
// triangulates; finalizeModel commits when the iterator finishes.
// Staged in CPU memory; finalizeModel runs the chunk planner over the
// 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
// model_id is passed through unchanged; the viewport's globally-unique
// object_id rebasing happens at finalize time.
void uploadMeshChunk(const struct MeshChunk& chunk);
void uploadInstanceChunk(const struct InstanceChunk& chunk);
void uploadStreamedMesh(const struct StreamedMesh& mesh);
void uploadStreamedInstance(const struct StreamedInstance& instance_record);
void finalizeModel(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,
double placement_tx) {
ModelGpuData m;
InstanceCpu inst{};
InstanceInfo inst{};
inst.mesh_id = mesh_id;
inst.object_id = object_id;
// Column-major identity with a tx for verification.
@@ -21,7 +21,7 @@
// vertex quantization used to fill it.
//
// 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
// byte-identical. The tests exercise that real implementation directly:
// - 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);
}
TEST_CASE("MeshChunk and InstanceChunk default-init to zeroed metadata", "[instgeom]") {
MeshChunk mc;
TEST_CASE("StreamedMesh and StreamedInstance default-init to zeroed metadata", "[instgeom]") {
StreamedMesh mc;
REQUIRE(mc.model_id == 0);
REQUIRE(mc.local_mesh_id == 0);
REQUIRE(mc.vertices.empty());
REQUIRE(mc.indices.empty());
InstanceChunk ic;
StreamedInstance ic;
REQUIRE(ic.model_id == 0);
REQUIRE(ic.local_mesh_id == 0);
REQUIRE(ic.object_id == 0);
+8 -7
View File
@@ -83,7 +83,7 @@ SidecarData buildFixture() {
sd.instances.resize(5);
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.object_id = uint32_t(100 + i);
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.elements.resize(3);
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.model_id = 1;
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.name_offset = 1; e.name_length = 4; // "Wall"
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;
}
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) {
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.
@@ -155,10 +154,12 @@ bool sidecarDataEqual(const SidecarData& a, const SidecarData& b) {
} // 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(InstanceInfo) == 232);
REQUIRE(sizeof(InstanceGpu) == 80);
REQUIRE(SIDECAR_VERSION == 16);
REQUIRE(sizeof(ElementTableRecord) == 36);
REQUIRE(SIDECAR_VERSION == 17);
REQUIRE(sizeof(SidecarChunk) == 56);
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 (int i = 0; i < N; ++i) {
if (k >= ninst(i)) continue;
InstanceCpu ic;
InstanceInfo ic;
ic.mesh_id = uint32_t(i); // authoritative
ic.object_id = obj++;
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);
InstSig s{};
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].model_id = 1;
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
// 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.
REQUIRE(meta->meta.chunks.size() == sd.chunks.size());
REQUIRE(meta->meta.chunks[0].v_comp_size > 0);
// The deferred (property) block locator is recorded for on-demand fetch.
REQUIRE(meta->deferred_comp_size > 0);
// The element metadata block locator is recorded for on-demand fetch.
REQUIRE(meta->element_metadata_comp_size > 0);
// Metadata round-trips.
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));
}
TEST_CASE("v16 deferred block: fetch via locator, decompress, parse", "[streaming]") {
fs::path dir = makeScratchDir("v16def");
TEST_CASE("v16 element metadata block: fetch via locator, decompress, parse", "[streaming]") {
fs::path dir = makeScratchDir("v16element");
fs::path ifc = dir / "model.ifc";
SidecarData sd = buildFixture();
REQUIRE(writeSidecar(ifc.string(), sd));
auto meta = readSidecarMetadataOnly(ifc.string());
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.elements.size() == sd.elements.size()); // desktop reads deferred too
REQUIRE(meta->deferred_comp_size > 0);
REQUIRE(meta->meta.elements.size() == sd.elements.size()); // desktop reads element metadata too
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.
FILE* f = std::fopen((dir / "model.ifcview").string().c_str(), "rb");
REQUIRE(f);
std::vector<uint8_t> cz(size_t(meta->deferred_comp_size));
std::fseek(f, long(meta->deferred_comp_offset), SEEK_SET);
std::vector<uint8_t> cz(size_t(meta->element_metadata_comp_size));
std::fseek(f, long(meta->element_metadata_comp_offset), SEEK_SET);
REQUIRE(std::fread(cz.data(), 1, cz.size(), f) == cz.size());
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()));
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.string_table == sd.string_table);
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",