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
@@ -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",