mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-13 02:47:48 +00:00
ifcviewer: v15 — defer property metadata off the first-paint path
First-paint over a network is metadata-bound: the whole post-index metadata (~10 MB on a 118 MB model) had to download before any geometry. But ~25% of it — elements + string_table, the IFC element tree (names/GUIDs/hierarchy) — is used only for UI/picking, never for rendering (ViewportCore never touches it). v15 splits the post-index metadata into a render-CRITICAL block (meshes, instances, georef, chunk TOC) preceded by its byte length, then a DEFERRED block (elements + string_table). The web loader reads only the critical block before painting; the deferred block sits at a known, self-describing offset ([critical end, EOF)) and is fetched on demand. Desktop reads both (local). Web on-demand path is wired and complete (not yet called — no UI consumer): loadDeferredMetadataWeb(model_id) range-fetches + parses the deferred block into ModelGpuData.elements/string_table, at most once; the first consumer will be "show the selected object's name" on pick. No background prefetch — view-only sessions never download the property data (saves 2.64 MB on this model). parseSidecarTail split into parseSidecarCritical + parseSidecarDeferred (pure, unit-tested); StreamingSidecar gains the critical-block locator. Measured (118 MB model): critical metadata 10.35 -> 7.71 MB, deferred 2.64 MB off the path; first paint 10.5 -> 9.6 s @ 24 Mbps. (Instances still dominate the critical block — the next metadata lever.) Format -> v15, no back-compat; regenerate sidecars. 111/111 unit + 6/6 web smoke pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -201,35 +201,39 @@ TEST_CASE("parseSidecarHead validates magic / version / length", "[streaming]")
|
||||
REQUIRE_FALSE(parseSidecarHead(bad, sizeof(bad), got));
|
||||
}
|
||||
|
||||
TEST_CASE("parseSidecarTail rejects a truncated tail", "[streaming]") {
|
||||
// A valid full tail, then everything but its last byte must fail.
|
||||
fs::path dir = makeScratchDir("tailtrunc");
|
||||
TEST_CASE("v15 critical/deferred metadata split round-trips + rejects truncation",
|
||||
"[streaming]") {
|
||||
fs::path dir = makeScratchDir("v15split");
|
||||
fs::path ifc = dir / "model.ifc";
|
||||
SidecarData sd = buildFixture();
|
||||
sd.chunks = { {0, 1}, {1, 1} }; // a TOC, so the critical block carries chunks
|
||||
REQUIRE(writeSidecar(ifc.string(), sd));
|
||||
|
||||
// readSidecarMetadataOnly (desktop) reads BOTH blocks + records the locator.
|
||||
auto meta = readSidecarMetadataOnly(ifc.string());
|
||||
REQUIRE(meta.has_value());
|
||||
REQUIRE(meta->meta.meshes.size() == sd.meshes.size()); // critical
|
||||
REQUIRE(meta->meta.chunks.size() == sd.chunks.size()); // critical
|
||||
REQUIRE(meta->meta.elements.size() == sd.elements.size()); // deferred
|
||||
REQUIRE(meta->meta.string_table == sd.string_table); // deferred
|
||||
REQUIRE(meta->critical_meta_bytes > 0);
|
||||
|
||||
// Re-read the raw tail bytes from disk (offset = index section end).
|
||||
const uint64_t tail_off =
|
||||
meta->index_section_offset + meta->index_total_count * 4u;
|
||||
// Pull the raw critical block via the recorded locator and parse it alone —
|
||||
// exactly what the web loader does before painting.
|
||||
FILE* f = std::fopen((dir / "model.ifcview").string().c_str(), "rb");
|
||||
REQUIRE(f);
|
||||
std::fseek(f, 0, SEEK_END);
|
||||
const long end = std::ftell(f);
|
||||
const size_t tail_len = size_t(end - long(tail_off));
|
||||
std::vector<uint8_t> tail(tail_len);
|
||||
std::fseek(f, long(tail_off), SEEK_SET);
|
||||
REQUIRE(std::fread(tail.data(), 1, tail_len, f) == tail_len);
|
||||
std::vector<uint8_t> crit(size_t(meta->critical_meta_bytes));
|
||||
std::fseek(f, long(meta->critical_meta_offset), SEEK_SET);
|
||||
REQUIRE(std::fread(crit.data(), 1, crit.size(), f) == crit.size());
|
||||
std::fclose(f);
|
||||
|
||||
SidecarData full;
|
||||
REQUIRE(parseSidecarTail(tail.data(), tail.size(), full));
|
||||
REQUIRE(full.meshes.size() == sd.meshes.size());
|
||||
REQUIRE(full.string_table == sd.string_table);
|
||||
|
||||
SidecarData c;
|
||||
REQUIRE(parseSidecarCritical(crit.data(), crit.size(), c));
|
||||
REQUIRE(c.meshes.size() == sd.meshes.size());
|
||||
REQUIRE(c.chunks.size() == sd.chunks.size());
|
||||
REQUIRE(c.elements.empty()); // the critical block has no property data
|
||||
SidecarData chopped;
|
||||
REQUIRE_FALSE(parseSidecarTail(tail.data(), tail.size() - 1, chopped));
|
||||
REQUIRE_FALSE(parseSidecarCritical(crit.data(), crit.size() - 1, chopped));
|
||||
}
|
||||
|
||||
TEST_CASE("planSidecarReadRanges coalesces adjacent ranges, keeps far ones split",
|
||||
|
||||
Reference in New Issue
Block a user