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
+20 -6
View File
@@ -70,11 +70,6 @@ bool BufferPool::addSubBuffer() {
if (try_size < MIN_SUB_BUFFER_BYTES) try_size = MIN_SUB_BUFFER_BYTES;
while (try_size >= MIN_SUB_BUFFER_BYTES) {
// wgpu-native classifies "Not enough memory left" as Validation,
// not OutOfMemory. Nested scopes: OOM inner, Validation outer.
wgpuDevicePushErrorScope(device_, WGPUErrorFilter_Validation);
wgpuDevicePushErrorScope(device_, WGPUErrorFilter_OutOfMemory);
char label[128];
std::snprintf(label, sizeof(label), "%s.sub%zu",
label_prefix_.c_str(), sub_pools_.size());
@@ -84,6 +79,23 @@ bool BufferPool::addSubBuffer() {
desc.size = try_size;
desc.label.data = label;
desc.label.length = std::strlen(label);
#if defined(__EMSCRIPTEN__)
// Web: skip the error-scope dance. PopErrorScope on Dawn-web
// resolves via JS microtask, and the spin-wait below (the
// desktop path) would block the JS event loop indefinitely —
// microtask never gets to fire, page hangs. We just trust the
// browser: if createBuffer succeeded the buffer is usable,
// and if it failed `buf` is null and we halve-retry as
// before. A real OOM still surfaces as `buf == nullptr` here.
WGPUBuffer buf = wgpuDeviceCreateBuffer(device_, &desc);
const bool ok = (buf != nullptr);
#else
// wgpu-native classifies "Not enough memory left" as Validation,
// not OutOfMemory. Nested scopes: OOM inner, Validation outer.
wgpuDevicePushErrorScope(device_, WGPUErrorFilter_Validation);
wgpuDevicePushErrorScope(device_, WGPUErrorFilter_OutOfMemory);
WGPUBuffer buf = wgpuDeviceCreateBuffer(device_, &desc);
struct PopResult { bool done = false; bool error = false; };
@@ -103,8 +115,10 @@ bool BufferPool::addSubBuffer() {
PopResult oom_pop, validation_pop;
pop(oom_pop);
pop(validation_pop);
const bool ok = buf && !oom_pop.error && !validation_pop.error;
#endif
if (buf && !oom_pop.error && !validation_pop.error) {
if (ok) {
SubPool sp;
sp.buffer = buf;
sp.capacity = try_size;