ifcviewer-web: end-to-end rendering on Chrome + Firefox

Wire up wgpu init + scene load + RAF render so the embedded sample
sidecar paints on the canvas in both browsers.

Root-cause fix: BufferPool::addSubBuffer spin-waited on
PopErrorScope, which resolves via JS microtask on Dawn-web. The
spin blocked the JS event loop, so the microtask never fired and
the first allocation hung the page indefinitely. Skip the
error-scope dance on Emscripten; trust the buffer pointer.

ViewportCore: add initWgpuAsyncWeb (nested-callback adapter→device
chain with AllowSpontaneous mode, no spin) and loadSidecarFromPath
(Qt-free entry point). waitTickInstance becomes a no-op shim on
web; cull-threads / streaming_thread_ / wgpuSurfacePresent gated
off; chunk I/O runs inline.

main_web.cpp: AppState + initWgpuAsyncWeb → buildPipelines (+
HiZ/edge/pick) → loadSidecarFromPath → ready flag → Module._app_ptr
handoff. The RAF loop lives in shell.html (NOT here) because any
RAF helper called from inside Dawn-web's wgpu Promise.then chain
stalls the device callback.

CMakeLists.txt: EXIT_RUNTIME=0 + Module.noExitRuntime=true (shell)
keeps wasm alive past main() so the device promise lands; no
Asyncify; export _raf_tick_c so shell.html's RAF can call it.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Dion Moult
2026-06-09 17:16:14 +10:00
parent bb0e96b406
commit e8a6d2a92f
7 changed files with 417 additions and 100 deletions
+21
View File
@@ -257,6 +257,19 @@ public:
bool initWgpu(bool web_limits);
void shutdown();
#if defined(__EMSCRIPTEN__)
// Async-init driver for the web. The spin-wait pattern in initWgpu()
// doesn't work on Dawn-web — RequestDevice's callback never fires
// when the caller is parked inside an Asyncify spin loop, even with
// AllowSpontaneous + emscripten_sleep yields. The fix is to mirror
// the original main_web.cpp spike: nested callbacks, no spin.
// Fires `on_complete(true)` once instance + adapter + device + queue
// + pool + surface_format_ are all in place; on any failure, fires
// on_complete(false). Caller is responsible for calling
// buildPipelines + buildHiz/Edge/Pick + scene-load after on_complete.
void initWgpuAsyncWeb(std::function<void(bool ok)> on_complete);
#endif
// ---- Chunk residency (#84-n) ------------------------------------------
//
// Build the per-chunk WGPUBindGroup over its current pool slices +
@@ -315,6 +328,14 @@ public:
// freshly-loaded scene frames itself).
void applyCachedModel(std::uint32_t model_id, StreamingSidecar metadata);
// Qt-free sidecar load: readSidecarMetadataOnly + applyCachedModel.
// Used by the web build (and any other non-Qt embedder) so the
// public ViewportWindow::loadSidecar's QString + QFile triage
// tilde-expansion doesn't have to be replicated. Returns 0 on
// any failure (device not ready, file missing, magic / version
// mismatch) and the freshly-assigned model_id on success.
std::uint32_t loadSidecarFromPath(const std::string& path);
// Direct-load (bonsai-side) entry points. Bonsai's SceneLoader feeds
// the viewer one mesh + one instance at a time, then calls
// finalizeModel once everything's staged. The staging map lives on