mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-09-19 14:41:25 +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:
Binary file not shown.
@@ -34,6 +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)
|
||||||
|
|
||||||
// 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;
|
||||||
@@ -287,6 +288,18 @@ struct ModelGpuData {
|
|||||||
// instead of the MEMFS sync read.
|
// instead of the MEMFS sync read.
|
||||||
bool streaming_from_web = false;
|
bool streaming_from_web = false;
|
||||||
|
|
||||||
|
// v15 deferred property metadata (web, on-demand). The IFC element tree
|
||||||
|
// (elements + string_table — names/GUIDs/hierarchy, 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;
|
||||||
|
std::string string_table;
|
||||||
|
uint64_t deferred_meta_offset = 0;
|
||||||
|
uint64_t deferred_meta_bytes = 0;
|
||||||
|
bool deferred_meta_loaded = false;
|
||||||
|
|
||||||
// For each mesh in meshes[], the chunk it lives in plus the chunk-local
|
// For each mesh in meshes[], the chunk it lives in plus the chunk-local
|
||||||
// offsets into that chunk's vertex_storage and index_buffer. Populated
|
// offsets into that chunk's vertex_storage and index_buffer. Populated
|
||||||
// at applyCachedModel time; consumed by cullModelCpuCompute when it
|
// at applyCachedModel time; consumed by cullModelCpuCompute when it
|
||||||
|
|||||||
@@ -96,9 +96,18 @@ bool writeSidecar(const std::string& ifc_path, const SidecarData& data) {
|
|||||||
|
|
||||||
if (!writeVec(f, data.vertices)) { fclose(f); return false; }
|
if (!writeVec(f, data.vertices)) { fclose(f); return false; }
|
||||||
if (!writeVec(f, data.indices)) { fclose(f); return false; }
|
if (!writeVec(f, data.indices)) { fclose(f); return false; }
|
||||||
|
|
||||||
|
// v15: a render-critical metadata block (meshes, instances, georef, chunk
|
||||||
|
// TOC) preceded by its byte length, then a deferred block (elements +
|
||||||
|
// string_table). The length lets the web loader read just the critical
|
||||||
|
// block before painting and fetch the property data lazily / not at all.
|
||||||
|
const long crit_len_pos = ftell(f);
|
||||||
|
uint64_t crit_bytes = 0;
|
||||||
|
if (fwrite(&crit_bytes, sizeof(crit_bytes), 1, f) != 1) { fclose(f); return false; }
|
||||||
|
const long crit_start = ftell(f);
|
||||||
|
|
||||||
if (!writeVec(f, data.meshes)) { fclose(f); return false; }
|
if (!writeVec(f, data.meshes)) { fclose(f); return false; }
|
||||||
if (!writeVec(f, data.instances)) { fclose(f); return false; }
|
if (!writeVec(f, data.instances)) { fclose(f); return false; }
|
||||||
|
|
||||||
// v11 georef block (148 B).
|
// v11 georef block (148 B).
|
||||||
if (fwrite(&data.has_coordinate_operation, 4, 1, f) != 1) { fclose(f); return false; }
|
if (fwrite(&data.has_coordinate_operation, 4, 1, f) != 1) { fclose(f); return false; }
|
||||||
if (fwrite(data.coordinate_operation_meters,
|
if (fwrite(data.coordinate_operation_meters,
|
||||||
@@ -107,18 +116,24 @@ bool writeSidecar(const std::string& ifc_path, const SidecarData& data) {
|
|||||||
sizeof(double), 1, f) != 1) { fclose(f); return false; }
|
sizeof(double), 1, f) != 1) { fclose(f); return false; }
|
||||||
if (fwrite(&data.map_unit_to_meters,
|
if (fwrite(&data.map_unit_to_meters,
|
||||||
sizeof(double), 1, f) != 1) { fclose(f); return false; }
|
sizeof(double), 1, f) != 1) { fclose(f); return false; }
|
||||||
|
if (!writeVec(f, data.chunks)) { fclose(f); return false; }
|
||||||
|
|
||||||
|
// Backpatch the critical-block length.
|
||||||
|
const long crit_end = ftell(f);
|
||||||
|
if (crit_start < 0 || crit_end < 0) { fclose(f); return false; }
|
||||||
|
crit_bytes = uint64_t(crit_end - crit_start);
|
||||||
|
if (fseek(f, crit_len_pos, SEEK_SET) != 0) { fclose(f); return false; }
|
||||||
|
if (fwrite(&crit_bytes, sizeof(crit_bytes), 1, f) != 1) { fclose(f); return false; }
|
||||||
|
if (fseek(f, crit_end, SEEK_SET) != 0) { fclose(f); return false; }
|
||||||
|
|
||||||
|
// Deferred block: element tree + string table (UI/picking, never rendered).
|
||||||
if (!writeVec(f, data.elements)) { fclose(f); return false; }
|
if (!writeVec(f, data.elements)) { fclose(f); return false; }
|
||||||
|
|
||||||
uint32_t stbl_len = static_cast<uint32_t>(data.string_table.size());
|
uint32_t stbl_len = static_cast<uint32_t>(data.string_table.size());
|
||||||
if (fwrite(&stbl_len, 4, 1, f) != 1) { fclose(f); return false; }
|
if (fwrite(&stbl_len, 4, 1, f) != 1) { fclose(f); return false; }
|
||||||
if (stbl_len > 0 && fwrite(data.string_table.data(), 1, stbl_len, f) != stbl_len) {
|
if (stbl_len > 0 && fwrite(data.string_table.data(), 1, stbl_len, f) != stbl_len) {
|
||||||
fclose(f); return false;
|
fclose(f); return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// v14 chunk TOC.
|
|
||||||
if (!writeVec(f, data.chunks)) { fclose(f); return false; }
|
|
||||||
|
|
||||||
fclose(f);
|
fclose(f);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -139,10 +154,15 @@ std::optional<SidecarData> readSidecar(const std::string& ifc_path) {
|
|||||||
SidecarData data;
|
SidecarData data;
|
||||||
if (!readVec(f, data.vertices)) return fail();
|
if (!readVec(f, data.vertices)) return fail();
|
||||||
if (!readVec(f, data.indices)) return fail();
|
if (!readVec(f, data.indices)) return fail();
|
||||||
|
|
||||||
|
// v15 critical-block length (consumed; only the streaming/web readers need
|
||||||
|
// it for a one-shot range read — here we read sequentially).
|
||||||
|
uint64_t crit_bytes = 0;
|
||||||
|
if (fread(&crit_bytes, sizeof(crit_bytes), 1, f) != 1) return fail();
|
||||||
|
|
||||||
|
// Critical block: meshes, instances, georef, chunk TOC.
|
||||||
if (!readVec(f, data.meshes)) return fail();
|
if (!readVec(f, data.meshes)) return fail();
|
||||||
if (!readVec(f, data.instances)) return fail();
|
if (!readVec(f, data.instances)) return fail();
|
||||||
|
|
||||||
// v11 georef block.
|
|
||||||
if (fread(&data.has_coordinate_operation, 4, 1, f) != 1) return fail();
|
if (fread(&data.has_coordinate_operation, 4, 1, f) != 1) return fail();
|
||||||
if (fread(data.coordinate_operation_meters,
|
if (fread(data.coordinate_operation_meters,
|
||||||
sizeof(double), 16, f) != 16) return fail();
|
sizeof(double), 16, f) != 16) return fail();
|
||||||
@@ -150,18 +170,16 @@ std::optional<SidecarData> readSidecar(const std::string& ifc_path) {
|
|||||||
sizeof(double), 1, f) != 1) return fail();
|
sizeof(double), 1, f) != 1) return fail();
|
||||||
if (fread(&data.map_unit_to_meters,
|
if (fread(&data.map_unit_to_meters,
|
||||||
sizeof(double), 1, f) != 1) return fail();
|
sizeof(double), 1, f) != 1) return fail();
|
||||||
|
if (!readVec(f, data.chunks)) return fail();
|
||||||
|
|
||||||
|
// Deferred block: element tree + string table.
|
||||||
if (!readVec(f, data.elements)) return fail();
|
if (!readVec(f, data.elements)) return fail();
|
||||||
|
|
||||||
uint32_t stbl_len;
|
uint32_t stbl_len;
|
||||||
if (fread(&stbl_len, 4, 1, f) != 1) return fail();
|
if (fread(&stbl_len, 4, 1, f) != 1) return fail();
|
||||||
data.string_table.resize(stbl_len);
|
data.string_table.resize(stbl_len);
|
||||||
if (stbl_len > 0 && fread(data.string_table.data(), 1, stbl_len, f) != stbl_len)
|
if (stbl_len > 0 && fread(data.string_table.data(), 1, stbl_len, f) != stbl_len)
|
||||||
return fail();
|
return fail();
|
||||||
|
|
||||||
// v14 chunk TOC.
|
|
||||||
if (!readVec(f, data.chunks)) return fail();
|
|
||||||
|
|
||||||
fclose(f);
|
fclose(f);
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -73,7 +73,15 @@ 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).
|
||||||
static constexpr uint32_t SIDECAR_VERSION = 14;
|
// 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
|
||||||
|
// painting, so first geometry no longer waits on the property data; the
|
||||||
|
// deferred block is fetched lazily (or skipped where unused). Desktop
|
||||||
|
// reads both. No back-compat: regenerate sidecars.
|
||||||
|
static constexpr uint32_t SIDECAR_VERSION = 15;
|
||||||
static constexpr uint32_t SIDECAR_ENDIAN = 0x01020304;
|
static constexpr uint32_t SIDECAR_ENDIAN = 0x01020304;
|
||||||
|
|
||||||
// Chunk table-of-contents entry (v14+). A chunk is a CONTIGUOUS range of
|
// Chunk table-of-contents entry (v14+). A chunk is a CONTIGUOUS range of
|
||||||
|
|||||||
@@ -99,26 +99,28 @@ bool parseSidecarHead(const uint8_t* data, size_t n, uint32_t& out_num_vertex_by
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool parseSidecarTail(const uint8_t* data, size_t n, SidecarData& out) {
|
bool parseSidecarCritical(const uint8_t* data, size_t n, SidecarData& out) {
|
||||||
|
// v15 render-critical 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;
|
||||||
|
|
||||||
// v11 georef block (148 bytes total).
|
|
||||||
if (!c.take(&out.has_coordinate_operation, 4)) return false;
|
if (!c.take(&out.has_coordinate_operation, 4)) return false;
|
||||||
if (!c.take(out.coordinate_operation_meters, sizeof(double) * 16)) return false;
|
if (!c.take(out.coordinate_operation_meters, sizeof(double) * 16)) return false;
|
||||||
if (!c.take(&out.project_length_to_meters, sizeof(double))) return false;
|
if (!c.take(&out.project_length_to_meters, sizeof(double))) return false;
|
||||||
if (!c.take(&out.map_unit_to_meters, sizeof(double))) return false;
|
if (!c.take(&out.map_unit_to_meters, sizeof(double))) return false;
|
||||||
|
if (!c.takeVec(out.chunks)) return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool parseSidecarDeferred(const uint8_t* data, size_t n, SidecarData& out) {
|
||||||
|
// v15 deferred block: element tree + string table (UI/picking, not rendered).
|
||||||
|
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;
|
||||||
if (!c.take(&stbl_len, 4)) return false;
|
if (!c.take(&stbl_len, 4)) return false;
|
||||||
if (stbl_len > c.remaining) return false;
|
if (stbl_len > c.remaining) return false;
|
||||||
out.string_table.resize(stbl_len);
|
out.string_table.resize(stbl_len);
|
||||||
if (stbl_len > 0 && !c.take(out.string_table.data(), stbl_len)) return false;
|
if (stbl_len > 0 && !c.take(out.string_table.data(), stbl_len)) return false;
|
||||||
|
|
||||||
// v14 chunk TOC.
|
|
||||||
if (!c.takeVec(out.chunks)) return false;
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -165,7 +167,22 @@ std::optional<StreamingSidecar> readSidecarMetadataOnly(const std::string& ifc_p
|
|||||||
return fail();
|
return fail();
|
||||||
std::fclose(f);
|
std::fclose(f);
|
||||||
|
|
||||||
if (!parseSidecarTail(tail.data(), tail.size(), out.meta)) return std::nullopt;
|
// Tail (v15) = [critical_meta_bytes (8)][critical block][deferred block].
|
||||||
|
// Desktop is local, so read both; the web path reads only the critical
|
||||||
|
// block before painting and the deferred block on demand.
|
||||||
|
if (tail.size() < sizeof(uint64_t)) return std::nullopt;
|
||||||
|
uint64_t crit_bytes = 0;
|
||||||
|
std::memcpy(&crit_bytes, tail.data(), sizeof(crit_bytes));
|
||||||
|
const size_t crit_off = sizeof(crit_bytes);
|
||||||
|
if (crit_off + crit_bytes > tail.size()) return std::nullopt;
|
||||||
|
out.critical_meta_offset = out.index_section_offset
|
||||||
|
+ uint64_t(num_indices) * 4u + crit_off;
|
||||||
|
out.critical_meta_bytes = crit_bytes;
|
||||||
|
if (!parseSidecarCritical(tail.data() + crit_off, size_t(crit_bytes), out.meta))
|
||||||
|
return std::nullopt;
|
||||||
|
if (!parseSidecarDeferred(tail.data() + crit_off + crit_bytes,
|
||||||
|
tail.size() - crit_off - size_t(crit_bytes), out.meta))
|
||||||
|
return std::nullopt;
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -58,6 +58,14 @@ struct StreamingSidecar {
|
|||||||
uint64_t index_section_offset = 0;
|
uint64_t index_section_offset = 0;
|
||||||
uint64_t index_total_count = 0; // u32 indices, NOT bytes
|
uint64_t index_total_count = 0; // u32 indices, NOT bytes
|
||||||
|
|
||||||
|
// v15 deferred-metadata locator. The render-critical metadata block starts
|
||||||
|
// at critical_meta_offset and is critical_meta_bytes long; the deferred
|
||||||
|
// block (elements + string_table) runs from there to EOF. The web loader
|
||||||
|
// reads only the critical block before painting and fetches the deferred
|
||||||
|
// block on demand from [critical_meta_offset + critical_meta_bytes, EOF).
|
||||||
|
uint64_t critical_meta_offset = 0;
|
||||||
|
uint64_t critical_meta_bytes = 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;
|
||||||
};
|
};
|
||||||
@@ -87,12 +95,18 @@ inline constexpr std::size_t SIDECAR_HEAD_BYTES = 16;
|
|||||||
bool parseSidecarHead(const std::uint8_t* data, std::size_t n,
|
bool parseSidecarHead(const std::uint8_t* data, std::size_t n,
|
||||||
std::uint32_t& out_num_vertex_bytes);
|
std::uint32_t& out_num_vertex_bytes);
|
||||||
|
|
||||||
// Parse the metadata tail (everything after the index section): mesh dict,
|
// Parse the v15 render-CRITICAL metadata block (mesh dict, instance dict,
|
||||||
// instance dict, georef block, element table, string table. `data` points at
|
// georef, chunk TOC) — everything needed to set up + draw the scene. `data`
|
||||||
// the first tail byte; `n` is the tail length (read to EOF). Returns false on
|
// points at the first critical byte; `n` is critical_meta_bytes. Returns false
|
||||||
// any bounds overrun (truncated buffer), leaving out_meta partially filled.
|
// on any bounds overrun, leaving out_meta partially filled.
|
||||||
bool parseSidecarTail(const std::uint8_t* data, std::size_t n,
|
bool parseSidecarCritical(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
|
||||||
|
// 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);
|
||||||
|
|
||||||
// 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
|
||||||
|
|||||||
@@ -3491,43 +3491,66 @@ void ViewportCore::loadSidecarMetadataWeb(std::string source_label) {
|
|||||||
}
|
}
|
||||||
std::uint32_t num_indices = 0;
|
std::uint32_t num_indices = 0;
|
||||||
std::memcpy(&num_indices, cnt.data(), 4);
|
std::memcpy(&num_indices, cnt.data(), 4);
|
||||||
const std::uint64_t isec = idx_count_off + 4;
|
const std::uint64_t isec = idx_count_off + 4;
|
||||||
const std::uint64_t tail_off = isec + std::uint64_t(num_indices) * 4u;
|
const std::uint64_t crit_size_off = isec + std::uint64_t(num_indices) * 4u;
|
||||||
if (double(tail_off) > fsize) {
|
if (double(crit_size_off + 8) > fsize) {
|
||||||
Log::warn() << "loadSidecarMetadataWeb: tail offset past EOF";
|
Log::warn() << "loadSidecarMetadataWeb: critical size past EOF";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const std::uint64_t tail_len = std::uint64_t(fsize) - tail_off;
|
// v15: read the 8-byte critical-block length, then the
|
||||||
|
// render-critical metadata only. The deferred block
|
||||||
webReadRangesAsync(0, {{tail_off, tail_len}},
|
// (elements/strings) is fetched on demand later — first
|
||||||
[this, vsec, nvb, isec, num_indices, source_label]
|
// paint no longer waits on the property tree.
|
||||||
(bool ok3, std::vector<std::uint8_t>&& tail) {
|
webReadRangesAsync(0, {{crit_size_off, 8}},
|
||||||
if (!ok3) {
|
[this, vsec, nvb, isec, num_indices, source_label, crit_size_off, fsize]
|
||||||
Log::warn() << "loadSidecarMetadataWeb: tail read failed";
|
(bool okc, std::vector<std::uint8_t>&& cb) {
|
||||||
|
if (!okc || cb.size() < 8) {
|
||||||
|
Log::warn() << "loadSidecarMetadataWeb: short critical size";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
StreamingSidecar sc;
|
std::uint64_t crit_bytes = 0;
|
||||||
sc.file_path = source_label;
|
std::memcpy(&crit_bytes, cb.data(), 8);
|
||||||
sc.vertex_section_offset = vsec;
|
const std::uint64_t crit_off = crit_size_off + 8;
|
||||||
sc.vertex_total_bytes = nvb;
|
if (double(crit_off + crit_bytes) > fsize) {
|
||||||
sc.index_section_offset = isec;
|
Log::warn() << "loadSidecarMetadataWeb: critical block past EOF";
|
||||||
sc.index_total_count = num_indices;
|
|
||||||
if (!parseSidecarTail(tail.data(), tail.size(), sc.meta)) {
|
|
||||||
Log::warn() << "loadSidecarMetadataWeb: bad metadata tail";
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const std::size_t n_meshes = sc.meta.meshes.size();
|
webReadRangesAsync(0, {{crit_off, crit_bytes}},
|
||||||
const std::size_t n_instances = sc.meta.instances.size();
|
[this, vsec, nvb, isec, num_indices, source_label,
|
||||||
const std::uint32_t mid = next_model_id_++;
|
crit_off, crit_bytes, fsize]
|
||||||
applyCachedModel(mid, std::move(sc));
|
(bool ok3, std::vector<std::uint8_t>&& crit) {
|
||||||
auto mit = models_gpu_.find(mid);
|
if (!ok3) {
|
||||||
if (mit != models_gpu_.end())
|
Log::warn() << "loadSidecarMetadataWeb: critical read failed";
|
||||||
mit->second.streaming_from_web = true;
|
return;
|
||||||
viewAll();
|
}
|
||||||
host_->requestFrame();
|
StreamingSidecar sc;
|
||||||
Log::info() << "ifcviewer-web: loaded sidecar (" << source_label
|
sc.file_path = source_label;
|
||||||
<< ", id " << mid << ", " << n_meshes << " meshes, "
|
sc.vertex_section_offset = vsec;
|
||||||
<< n_instances << " instances)";
|
sc.vertex_total_bytes = nvb;
|
||||||
|
sc.index_section_offset = isec;
|
||||||
|
sc.index_total_count = num_indices;
|
||||||
|
if (!parseSidecarCritical(crit.data(), crit.size(), sc.meta)) {
|
||||||
|
Log::warn() << "loadSidecarMetadataWeb: bad critical metadata";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const std::size_t n_meshes = sc.meta.meshes.size();
|
||||||
|
const std::size_t n_instances = sc.meta.instances.size();
|
||||||
|
const std::uint32_t mid = next_model_id_++;
|
||||||
|
applyCachedModel(mid, std::move(sc));
|
||||||
|
auto mit = models_gpu_.find(mid);
|
||||||
|
if (mit != models_gpu_.end()) {
|
||||||
|
mit->second.streaming_from_web = true;
|
||||||
|
// Deferred block: [end of critical, EOF).
|
||||||
|
mit->second.deferred_meta_offset = crit_off + crit_bytes;
|
||||||
|
mit->second.deferred_meta_bytes =
|
||||||
|
std::uint64_t(fsize) - (crit_off + crit_bytes);
|
||||||
|
}
|
||||||
|
viewAll();
|
||||||
|
host_->requestFrame();
|
||||||
|
Log::info() << "ifcviewer-web: loaded sidecar (" << source_label
|
||||||
|
<< ", id " << mid << ", " << n_meshes << " meshes, "
|
||||||
|
<< n_instances << " instances)";
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -3549,6 +3572,40 @@ void ViewportCore::loadSidecarFromUrlWeb(std::string url) {
|
|||||||
g_url_ready_core = this;
|
g_url_ready_core = this;
|
||||||
ifcvBeginUrlSource(url.c_str());
|
ifcvBeginUrlSource(url.c_str());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void ViewportCore::loadDeferredMetadataWeb(std::uint32_t model_id,
|
||||||
|
std::function<void(bool)> done) {
|
||||||
|
// On-demand fetch of the v15 deferred block (element tree + string table)
|
||||||
|
// for a web-streamed model — the property data a UI needs (tree, selected-
|
||||||
|
// object name, search) but rendering doesn't. Fetches at most once. Valid
|
||||||
|
// only for the currently-loaded model, since the JS byte-source
|
||||||
|
// (Module.__ifcvUrl/File) tracks the latest load.
|
||||||
|
auto it = models_gpu_.find(model_id);
|
||||||
|
if (it == models_gpu_.end()) { if (done) done(false); return; }
|
||||||
|
ModelGpuData& m = it->second;
|
||||||
|
if (m.deferred_meta_loaded || m.deferred_meta_bytes == 0) {
|
||||||
|
m.deferred_meta_loaded = true;
|
||||||
|
if (done) done(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
webReadRangesAsync(0, {{m.deferred_meta_offset, m.deferred_meta_bytes}},
|
||||||
|
[this, model_id, done](bool ok, std::vector<std::uint8_t>&& buf) {
|
||||||
|
auto mit = models_gpu_.find(model_id);
|
||||||
|
if (mit == models_gpu_.end()) { if (done) done(false); return; }
|
||||||
|
SidecarData tmp;
|
||||||
|
if (!ok || !parseSidecarDeferred(buf.data(), buf.size(), tmp)) {
|
||||||
|
Log::warn() << "loadDeferredMetadataWeb: read/parse failed";
|
||||||
|
if (done) done(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
mit->second.elements = std::move(tmp.elements);
|
||||||
|
mit->second.string_table = std::move(tmp.string_table);
|
||||||
|
mit->second.deferred_meta_loaded = true;
|
||||||
|
Log::info() << "ifcviewer-web: loaded deferred metadata ("
|
||||||
|
<< mit->second.elements.size() << " elements)";
|
||||||
|
if (done) done(true);
|
||||||
|
});
|
||||||
|
}
|
||||||
#endif // __EMSCRIPTEN__
|
#endif // __EMSCRIPTEN__
|
||||||
|
|
||||||
void ViewportCore::finalizeModel(std::uint32_t model_id) {
|
void ViewportCore::finalizeModel(std::uint32_t model_id) {
|
||||||
|
|||||||
@@ -371,6 +371,14 @@ public:
|
|||||||
void loadSidecarFromUrlWeb(std::string url);
|
void loadSidecarFromUrlWeb(std::string url);
|
||||||
void loadSidecarMetadataWeb(std::string source_label);
|
void loadSidecarMetadataWeb(std::string source_label);
|
||||||
|
|
||||||
|
// On-demand fetch of the v15 deferred property block (element tree + string
|
||||||
|
// table) for a web-streamed model — what a UI (object tree / selected-name
|
||||||
|
// / search) needs, fetched only when asked so first paint never waits on
|
||||||
|
// it. Populates ModelGpuData.elements/string_table; fires done(ok). At most
|
||||||
|
// one fetch per model. Currently unwired (no consumer yet) but complete.
|
||||||
|
void loadDeferredMetadataWeb(std::uint32_t model_id,
|
||||||
|
std::function<void(bool)> done = {});
|
||||||
|
|
||||||
// Kick off the async read of one chunk's vertex + index byte ranges (from
|
// Kick off the async read of one chunk's vertex + index byte ranges (from
|
||||||
// the active web source). applyStreamedChunk runs in the JS completion
|
// the active web source). applyStreamedChunk runs in the JS completion
|
||||||
// callback; c.is_loading is held until then. No-op if the model/chunk
|
// callback; c.is_loading is held until then. No-op if the model/chunk
|
||||||
|
|||||||
@@ -155,7 +155,7 @@ bool sidecarDataEqual(const SidecarData& a, const SidecarData& b) {
|
|||||||
TEST_CASE("MeshInfo and InstanceCpu have stable layouts (sidecar wire format)", "[sidecar]") {
|
TEST_CASE("MeshInfo and InstanceCpu have stable layouts (sidecar wire format)", "[sidecar]") {
|
||||||
REQUIRE(sizeof(MeshInfo) == 56);
|
REQUIRE(sizeof(MeshInfo) == 56);
|
||||||
REQUIRE(sizeof(InstanceGpu) == 80);
|
REQUIRE(sizeof(InstanceGpu) == 80);
|
||||||
REQUIRE(SIDECAR_VERSION == 14);
|
REQUIRE(SIDECAR_VERSION == 15);
|
||||||
REQUIRE(sizeof(SidecarChunk) == 8);
|
REQUIRE(sizeof(SidecarChunk) == 8);
|
||||||
REQUIRE(SIDECAR_MAGIC == 0x49465657u);
|
REQUIRE(SIDECAR_MAGIC == 0x49465657u);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -201,35 +201,39 @@ TEST_CASE("parseSidecarHead validates magic / version / length", "[streaming]")
|
|||||||
REQUIRE_FALSE(parseSidecarHead(bad, sizeof(bad), got));
|
REQUIRE_FALSE(parseSidecarHead(bad, sizeof(bad), got));
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST_CASE("parseSidecarTail rejects a truncated tail", "[streaming]") {
|
TEST_CASE("v15 critical/deferred metadata split round-trips + rejects truncation",
|
||||||
// A valid full tail, then everything but its last byte must fail.
|
"[streaming]") {
|
||||||
fs::path dir = makeScratchDir("tailtrunc");
|
fs::path dir = makeScratchDir("v15split");
|
||||||
fs::path ifc = dir / "model.ifc";
|
fs::path ifc = dir / "model.ifc";
|
||||||
SidecarData sd = buildFixture();
|
SidecarData sd = buildFixture();
|
||||||
|
sd.chunks = { {0, 1}, {1, 1} }; // a TOC, so the critical block carries chunks
|
||||||
REQUIRE(writeSidecar(ifc.string(), sd));
|
REQUIRE(writeSidecar(ifc.string(), sd));
|
||||||
|
|
||||||
|
// readSidecarMetadataOnly (desktop) reads BOTH blocks + records the locator.
|
||||||
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.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).
|
// Pull the raw critical block via the recorded locator and parse it alone —
|
||||||
const uint64_t tail_off =
|
// exactly what the web loader does before painting.
|
||||||
meta->index_section_offset + meta->index_total_count * 4u;
|
|
||||||
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::fseek(f, 0, SEEK_END);
|
std::vector<uint8_t> crit(size_t(meta->critical_meta_bytes));
|
||||||
const long end = std::ftell(f);
|
std::fseek(f, long(meta->critical_meta_offset), SEEK_SET);
|
||||||
const size_t tail_len = size_t(end - long(tail_off));
|
REQUIRE(std::fread(crit.data(), 1, crit.size(), f) == crit.size());
|
||||||
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::fclose(f);
|
std::fclose(f);
|
||||||
|
|
||||||
SidecarData full;
|
SidecarData c;
|
||||||
REQUIRE(parseSidecarTail(tail.data(), tail.size(), full));
|
REQUIRE(parseSidecarCritical(crit.data(), crit.size(), c));
|
||||||
REQUIRE(full.meshes.size() == sd.meshes.size());
|
REQUIRE(c.meshes.size() == sd.meshes.size());
|
||||||
REQUIRE(full.string_table == sd.string_table);
|
REQUIRE(c.chunks.size() == sd.chunks.size());
|
||||||
|
REQUIRE(c.elements.empty()); // the critical block has no property data
|
||||||
SidecarData chopped;
|
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",
|
TEST_CASE("planSidecarReadRanges coalesces adjacent ranges, keeps far ones split",
|
||||||
|
|||||||
Reference in New Issue
Block a user