mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-13 02:47:48 +00:00
ifcviewer-web: stream user sidecars via Blob.slice byte ranges (#88)
Picked files are no longer copied whole into the wasm heap. The browser
File object stays in JS (Module.__ifcvFile) and is read lazily through
Blob.slice byte ranges, so a 200-500 MB sidecar never enters wasm linear
memory — only chunk-sized slices do.
Mechanism (web-only, #if __EMSCRIPTEN__):
- JS glue (EM_JS): ifcvFileSize + ifcvReadRangeInto — slice [off,off+n)
of the File and copy it into a caller-provided heap pointer, then call
back _ifcv_on_range_done. No malloc across the boundary; C pre-sizes
the destination from the read plan.
- webReadRangesAsync: reuses planSidecarReadRanges to coalesce a range
set into Blob.slice reads (1 MB gap — each slice is an async hop),
scatters them into a destination laid out in input order, and fires a
continuation when the whole set lands. An in-flight map keyed by id
survives unordered_map rehash (scratch buffers are heap-owned).
- loadSidecarFromBlobWeb: async metadata load — head (16 B) -> index
count -> tail-to-EOF -> parseSidecarHead/Tail -> applyCachedModel, then
tags the model streaming_from_blob and frames it.
- driveStreamingLoads: blob-sourced models route to beginWebChunkLoad
(async vertex+index range reads -> applyStreamedChunk in the callback),
holding is_loading until the bytes arrive. The embedded MEMFS sample
keeps the synchronous fopen path.
shell.html stashes the File and calls _load_sidecar_from_blob_c instead of
FS.writeFile'ing the whole thing; EXPORTED_RUNTIME_METHODS=['FS'] dropped.
Desktop is untouched (the new members + driveStreamingLoads branch are all
emscripten-guarded). Web links clean; desktop rebuilds; 107/107 unit tests
pass.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -41,10 +41,6 @@ namespace {
|
||||
// WebViewportHost selector below.
|
||||
constexpr const char* kCanvasSelector = "#viewer-canvas";
|
||||
|
||||
// MEMFS path the file-browse handler writes the picked sidecar to, and
|
||||
// that load_uploaded_model_c reads back. Must match shell.html.
|
||||
constexpr const char* kUploadPath = "/uploads/model.ifcview";
|
||||
|
||||
struct AppState {
|
||||
WebViewportHost host{ kCanvasSelector };
|
||||
ViewportCore core{ &host };
|
||||
@@ -156,26 +152,21 @@ extern "C" EMSCRIPTEN_KEEPALIVE void raf_tick_c(void* user) {
|
||||
}
|
||||
}
|
||||
|
||||
// Called from shell.html's file-browse handler after it has written the
|
||||
// picked file's bytes into MEMFS at kUploadPath. Replaces whatever is
|
||||
// currently loaded (the embedded sample on first use, or a prior upload)
|
||||
// with the new sidecar and frames it. Geometry becomes resident over the
|
||||
// next frames via render()'s inline driveStreamingLoads. Exported to JS
|
||||
// via EXPORTED_FUNCTIONS in CMakeLists.txt.
|
||||
extern "C" EMSCRIPTEN_KEEPALIVE void load_uploaded_model_c() {
|
||||
// Called from shell.html's file-browse handler after it has stashed the
|
||||
// picked File on Module.__ifcvFile. Replaces whatever is currently loaded
|
||||
// (the embedded sample on first use, or a prior pick) with the new sidecar.
|
||||
// Byte-range (#88): the whole file is NOT copied into the wasm heap — the
|
||||
// metadata is read via Blob.slice and chunk bytes stream per-chunk, so a
|
||||
// 500 MB sidecar stays in the browser File object. Asynchronous: this
|
||||
// returns immediately and the model frames itself from the JS completion
|
||||
// callback. Exported to JS via EXPORTED_FUNCTIONS in CMakeLists.txt.
|
||||
extern "C" EMSCRIPTEN_KEEPALIVE void load_sidecar_from_blob_c() {
|
||||
if (!g_app || !g_app->ready) return;
|
||||
|
||||
// resetScene drops the previous model's GPU resources so a fresh load
|
||||
// replaces rather than accumulates (loadSidecarFromPath appends).
|
||||
// replaces rather than accumulates (loadSidecar* appends).
|
||||
g_app->core.resetScene();
|
||||
const unsigned int mid = g_app->core.loadSidecarFromPath(kUploadPath);
|
||||
if (mid == 0) {
|
||||
Log::warn() << "ifcviewer-web: uploaded model load failed";
|
||||
return;
|
||||
}
|
||||
g_app->core.viewAll();
|
||||
g_app->host.requestFrame();
|
||||
Log::info() << "ifcviewer-web: loaded uploaded model (id " << mid << ")";
|
||||
g_app->core.loadSidecarFromBlobWeb();
|
||||
}
|
||||
|
||||
int main(int /*argc*/, char** /*argv*/) {
|
||||
@@ -199,8 +190,10 @@ int main(int /*argc*/, char** /*argv*/) {
|
||||
g_app->core.buildPickPipeline();
|
||||
|
||||
// Load the embedded sample sidecar (mounted into MEMFS via
|
||||
// --embed-file in CMakeLists.txt). Replaced by an
|
||||
// emscripten_fetch + Range backend in #88.
|
||||
// --embed-file in CMakeLists.txt). The sample stays on the
|
||||
// synchronous MEMFS read; user-picked files go through the
|
||||
// Blob.slice byte-range path (load_sidecar_from_blob_c) so large
|
||||
// sidecars never enter the wasm heap.
|
||||
if (!g_app->core.loadSidecarFromPath("/sample.ifcview")) {
|
||||
Log::warn() << "ifcviewer-web: sample sidecar load failed";
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user