mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-09-28 03:05:33 +00:00
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:
@@ -80,12 +80,26 @@ add_executable(IfcViewerWeb
|
|||||||
WebViewportHost.h
|
WebViewportHost.h
|
||||||
)
|
)
|
||||||
target_link_libraries(IfcViewerWeb PRIVATE IfcViewerCore)
|
target_link_libraries(IfcViewerWeb PRIVATE IfcViewerCore)
|
||||||
|
|
||||||
target_link_options(IfcViewerWeb PRIVATE
|
target_link_options(IfcViewerWeb PRIVATE
|
||||||
# Asyncify lets us await wgpu's RequestAdapter/RequestDevice/MapAsync
|
# No -sASYNCIFY. The web init path is callback-driven
|
||||||
# as if they were synchronous. Adds ~30-50% wasm size and a small
|
# (initWgpuAsyncWeb chains RequestAdapter → RequestDevice via
|
||||||
# per-await runtime cost; acceptable for the spike, revisit when
|
# WGPU's AllowSpontaneous mode + the JS event loop), so we don't
|
||||||
# we ship.
|
# need to await wgpu calls as if they were sync. Asyncify would
|
||||||
"-sASYNCIFY"
|
# also instrument every function reachable from emscripten_sleep,
|
||||||
|
# adding ~30% to wasm size for no win here.
|
||||||
|
#
|
||||||
|
# EXIT_RUNTIME=0 + Module.noExitRuntime=true (set in shell.html)
|
||||||
|
# keeps wasm alive after main() returns so Dawn-web's
|
||||||
|
# RequestAdapter/RequestDevice promise callbacks land. The
|
||||||
|
# alternative — calling emscripten_set_main_loop_arg early in
|
||||||
|
# main() to set noExitRuntime as a side effect — registers a RAF
|
||||||
|
# that starves the device promise (observed: ~10s delay in Firefox).
|
||||||
|
"-sEXIT_RUNTIME=0"
|
||||||
|
# Expose the C tick entry point to JS so shell.html's RAF loop
|
||||||
|
# can call Module._raf_tick_c. EMSCRIPTEN_KEEPALIVE alone keeps
|
||||||
|
# the symbol in the binary but doesn't add it to Module.
|
||||||
|
"-sEXPORTED_FUNCTIONS=['_main','_raf_tick_c']"
|
||||||
# Streaming + chunked geometry want a heap that can grow as buffers
|
# Streaming + chunked geometry want a heap that can grow as buffers
|
||||||
# arrive. 256 MB initial, 2 GB ceiling (matches the wasm32 pointer
|
# arrive. 256 MB initial, 2 GB ceiling (matches the wasm32 pointer
|
||||||
# cap; --shared64 / MEMORY64 would lift this later if we need it).
|
# cap; --shared64 / MEMORY64 would lift this later if we need it).
|
||||||
@@ -96,6 +110,12 @@ target_link_options(IfcViewerWeb PRIVATE
|
|||||||
# sidecar byte-range loader. Not used yet by the scaffold but
|
# sidecar byte-range loader. Not used yet by the scaffold but
|
||||||
# needed by the upcoming #27 web streaming I/O backend.
|
# needed by the upcoming #27 web streaming I/O backend.
|
||||||
"-sFETCH=1"
|
"-sFETCH=1"
|
||||||
|
# Bundle a small sample sidecar into Emscripten's MEMFS so the
|
||||||
|
# scaffold can prove the load path end-to-end without needing
|
||||||
|
# emscripten_fetch + COOP/COEP wiring. The @ separator mounts the
|
||||||
|
# file at the virtual path the wasm uses to fopen() it. Replaced
|
||||||
|
# by an emscripten_fetch + Range backend in #88.
|
||||||
|
"--embed-file=${CMAKE_CURRENT_SOURCE_DIR}/sample.ifcview@/sample.ifcview"
|
||||||
# Shell template wraps the JS output in our canvas page.
|
# Shell template wraps the JS output in our canvas page.
|
||||||
"--shell-file=${CMAKE_CURRENT_SOURCE_DIR}/shell.html"
|
"--shell-file=${CMAKE_CURRENT_SOURCE_DIR}/shell.html"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -18,42 +18,48 @@
|
|||||||
********************************************************************************/
|
********************************************************************************/
|
||||||
|
|
||||||
// Web entry point. Wires a WebViewportHost to a ViewportCore, brings up
|
// Web entry point. Wires a WebViewportHost to a ViewportCore, brings up
|
||||||
// wgpu through emdawnwebgpu (the spec-compatible WebGPU header set that
|
// wgpu via emdawnwebgpu (the spec-compatible WebGPU header set that
|
||||||
// shipped with Dawn), then drives a render() per requestAnimationFrame
|
// shipped with Dawn), loads the embedded sample sidecar, and drives
|
||||||
// tick. No sidecar load yet — that lands with the emscripten_fetch
|
// render() per requestAnimationFrame from JS (shell.html).
|
||||||
// streaming backend (#88). For this scaffold we render an empty scene
|
//
|
||||||
// with the configured background so init + present is verified
|
// The RAF loop lives in shell.html — NOT here — because any call into
|
||||||
// end-to-end through the same ViewportCore code path the desktop build
|
// Emscripten's main-loop / RAF helpers (or even raw
|
||||||
// uses.
|
// requestAnimationFrame via EM_ASM) made from inside Dawn-web's wgpu
|
||||||
|
// promise-resolution chain stalls the device callback. Having JS drive
|
||||||
|
// the tick keeps the wasm init path callback-only.
|
||||||
|
|
||||||
#include "ViewportCore.h"
|
#include "ViewportCore.h"
|
||||||
#include "WebViewportHost.h"
|
#include "WebViewportHost.h"
|
||||||
#include "Log.h"
|
#include "Log.h"
|
||||||
|
|
||||||
#include <emscripten/emscripten.h>
|
#include <emscripten/emscripten.h>
|
||||||
#include <emscripten/html5.h>
|
|
||||||
|
|
||||||
#include <cstdio>
|
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
// Shared by the main_loop trampoline + the cleanup path. Allocated on
|
|
||||||
// the heap so Emscripten's set_main_loop callback (which is C-style)
|
|
||||||
// can recover state through a void*.
|
|
||||||
struct AppState {
|
struct AppState {
|
||||||
WebViewportHost host{ "#viewer-canvas" };
|
WebViewportHost host{ "#viewer-canvas" };
|
||||||
ViewportCore core{ &host };
|
ViewportCore core{ &host };
|
||||||
int last_w = 0;
|
int last_w = 0;
|
||||||
int last_h = 0;
|
int last_h = 0;
|
||||||
|
// Set true by the init callback once the device + pipelines are up.
|
||||||
|
// raf_tick_c skips render() until then; before that point the wgpu
|
||||||
|
// state pointers inside core are still null and any draw would crash.
|
||||||
|
bool ready = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
// The main_loop is a free function (Emscripten signature em_callback_func)
|
// One global so the JS-side RAF loop can recover state through a
|
||||||
// so we can hand it directly to emscripten_set_main_loop_arg.
|
// pointer round-trip (set into Module._app_ptr from on_complete).
|
||||||
void main_loop(void* user) {
|
AppState* g_app = nullptr;
|
||||||
auto* app = static_cast<AppState*>(user);
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
// Called from shell.html's RAF tick (via Module._raf_tick_c). Exported
|
||||||
|
// to JS by EXPORTED_FUNCTIONS in CMakeLists.txt; EMSCRIPTEN_KEEPALIVE
|
||||||
|
// also keeps the symbol alive under -O*.
|
||||||
|
extern "C" EMSCRIPTEN_KEEPALIVE void raf_tick_c(void* user) {
|
||||||
|
auto* app = static_cast<AppState*>(user);
|
||||||
|
if (!app->ready) return;
|
||||||
|
|
||||||
// Reconfigure when the canvas resizes. The first tick also lands
|
|
||||||
// here because last_w / last_h start at 0.
|
|
||||||
int w = 0, h = 0;
|
int w = 0, h = 0;
|
||||||
app->host.framebufferSize(w, h);
|
app->host.framebufferSize(w, h);
|
||||||
if (w != app->last_w || h != app->last_h) {
|
if (w != app->last_w || h != app->last_h) {
|
||||||
@@ -62,54 +68,44 @@ void main_loop(void* user) {
|
|||||||
app->last_h = h;
|
app->last_h = h;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only render when something has requested a frame — CPU stays
|
|
||||||
// idle while the scene is static. WebViewportHost's ctor arms one
|
|
||||||
// initial frame request so the canvas always paints at startup;
|
|
||||||
// subsequent paints come from core_.render() rearming itself
|
|
||||||
// (motion settle, streaming in flight, bench mode) and, once
|
|
||||||
// input is wired (#85), from mouse / key events flagging the host.
|
|
||||||
if (app->host.consumeFrameRequest()) {
|
if (app->host.consumeFrameRequest()) {
|
||||||
app->core.render();
|
app->core.render();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace
|
|
||||||
|
|
||||||
int main(int /*argc*/, char** /*argv*/) {
|
int main(int /*argc*/, char** /*argv*/) {
|
||||||
Log::info() << "ifcviewer-web: starting";
|
Log::info() << "ifcviewer-web: starting";
|
||||||
|
g_app = new AppState();
|
||||||
auto* app = new AppState();
|
g_app->core.initWgpuAsyncWeb([](bool ok) {
|
||||||
|
if (!ok) {
|
||||||
// Web limits floor: requestDevice the WebGPU spec's mandatory floor
|
Log::warn() << "ifcviewer-web: wgpu init failed";
|
||||||
// (maxStorageBufferBindingSize=128MB, maxBufferSize=256MB) so the
|
return;
|
||||||
// chunking + pool probe see the same constraints they would hit in
|
|
||||||
// any browser. Desktop --web-limits did this opt-in; on web it's
|
|
||||||
// the only sensible default.
|
|
||||||
if (!app->core.initWgpu(/*web_limits=*/true)) {
|
|
||||||
std::fprintf(stderr, "[viewer-web] initWgpu failed\n");
|
|
||||||
delete app;
|
|
||||||
return 1;
|
|
||||||
}
|
}
|
||||||
if (!app->core.buildPipelines()) {
|
if (!g_app->core.buildPipelines()) {
|
||||||
std::fprintf(stderr, "[viewer-web] buildPipelines failed\n");
|
Log::warn() << "ifcviewer-web: buildPipelines failed";
|
||||||
delete app;
|
return;
|
||||||
return 1;
|
|
||||||
}
|
}
|
||||||
// HiZ + edge + pick pipelines: built up-front to match the desktop
|
// HiZ + edge + pick pipelines: built up-front to match the
|
||||||
// path's lifetime. shutdown() in ViewportCore expects each
|
// desktop path. ViewportCore::shutdown expects each resource
|
||||||
// resource to be either constructed or null, so building them all
|
// to be either constructed or null, so building them all here
|
||||||
// here keeps teardown symmetric.
|
// keeps teardown symmetric.
|
||||||
app->core.buildHizPipeline();
|
g_app->core.buildHizPipeline();
|
||||||
app->core.buildEdgePipeline();
|
g_app->core.buildEdgePipeline();
|
||||||
app->core.buildPickPipeline();
|
g_app->core.buildPickPipeline();
|
||||||
|
|
||||||
// fps = 0 means "use the browser's natural rate (RAF)" — Emscripten
|
// Load the embedded sample sidecar (mounted into MEMFS via
|
||||||
// schedules the callback once per requestAnimationFrame tick.
|
// --embed-file in CMakeLists.txt). Replaced by an
|
||||||
// simulate_infinite_loop = false because we want main() to return
|
// emscripten_fetch + Range backend in #88.
|
||||||
// so the runtime + JS event loop keep ticking. The AppState leaks
|
if (!g_app->core.loadSidecarFromPath("/sample.ifcview")) {
|
||||||
// on tab close, which is fine — emscripten_force_exit (called
|
Log::warn() << "ifcviewer-web: sample sidecar load failed";
|
||||||
// from host_->quit()) is the only formal shutdown path on web.
|
}
|
||||||
emscripten_set_main_loop_arg(main_loop, app, /*fps=*/0,
|
|
||||||
/*simulate_infinite_loop=*/0);
|
g_app->ready = true;
|
||||||
|
|
||||||
|
// Hand the app pointer to the JS-side RAF loop (set up in
|
||||||
|
// shell.html's onRuntimeInitialized). The loop polls for
|
||||||
|
// Module._app_ptr before invoking _raf_tick_c.
|
||||||
|
EM_ASM({ Module._app_ptr = $0; }, (void*)g_app);
|
||||||
|
});
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|||||||
Binary file not shown.
@@ -8,9 +8,11 @@
|
|||||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; }
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; }
|
||||||
#viewer-canvas { display: block; width: 100vw; height: 100vh; outline: none;
|
#viewer-canvas { display: block; width: 100vw; height: 100vh; outline: none;
|
||||||
background: #1a1d24; }
|
background: #1a1d24; }
|
||||||
#status { position: fixed; top: 8px; left: 12px; font-size: 12px;
|
#status { position: fixed; top: 8px; left: 12px; right: 12px;
|
||||||
background: rgba(20,22,28,.72); padding: 4px 8px; border-radius: 4px;
|
max-height: 80vh; overflow-y: auto; font-size: 11px;
|
||||||
pointer-events: none; }
|
font-family: ui-monospace, "Cascadia Mono", Menlo, Consolas, monospace;
|
||||||
|
background: rgba(20,22,28,.78); padding: 6px 10px; border-radius: 4px;
|
||||||
|
white-space: pre-wrap; pointer-events: auto; }
|
||||||
#status.error { background: rgba(120,30,30,.85); color: #fff; }
|
#status.error { background: rgba(120,30,30,.85); color: #fff; }
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
@@ -24,17 +26,43 @@
|
|||||||
var statusEl = document.getElementById('status');
|
var statusEl = document.getElementById('status');
|
||||||
var Module = {
|
var Module = {
|
||||||
canvas: document.getElementById('viewer-canvas'),
|
canvas: document.getElementById('viewer-canvas'),
|
||||||
|
// Keep the wasm runtime alive after main() returns so the
|
||||||
|
// Dawn-web RequestAdapter/RequestDevice promise callbacks
|
||||||
|
// (queued from main) actually land. Without this flag the
|
||||||
|
// runtime tears down at end-of-main and the callbacks never
|
||||||
|
// fire — symptom: adapter cb fires (synchronous-ish on the
|
||||||
|
// first JS tick) but device cb does not. EXIT_RUNTIME=0 in
|
||||||
|
// CMakeLists is the build-time half; this is the runtime half.
|
||||||
|
noExitRuntime: true,
|
||||||
print: function(t) { console.log(t); },
|
print: function(t) { console.log(t); },
|
||||||
printErr: function(t) {
|
printErr: function(t) {
|
||||||
console.warn(t);
|
console.warn(t);
|
||||||
// First non-empty stderr line replaces the "Starting…" tag; subsequent
|
// Accumulate every stderr line so the init sequence is visible
|
||||||
// lines append. Anything containing "fail" / "error" flips us into
|
// even when the wasm hangs partway. The first time something is
|
||||||
// the red error chip.
|
// printed we drop the "Starting…" placeholder.
|
||||||
statusEl.textContent = t;
|
if (statusEl.textContent === 'Starting…' ||
|
||||||
|
statusEl.textContent === 'wasm loaded — waiting for WebGPU') {
|
||||||
|
statusEl.textContent = '';
|
||||||
|
}
|
||||||
|
statusEl.textContent += t + '\n';
|
||||||
|
statusEl.scrollTop = statusEl.scrollHeight;
|
||||||
if (/fail|error|null/i.test(t)) statusEl.classList.add('error');
|
if (/fail|error|null/i.test(t)) statusEl.classList.add('error');
|
||||||
},
|
},
|
||||||
onRuntimeInitialized: function() {
|
onRuntimeInitialized: function() {
|
||||||
statusEl.textContent = 'wasm loaded — waiting for WebGPU';
|
statusEl.textContent = 'wasm loaded — waiting for WebGPU';
|
||||||
|
// RAF loop. Polls for Module._app_ptr (set by C once the wgpu
|
||||||
|
// device callback completes init) and only then drives the C
|
||||||
|
// tick. Living in shell.html means the loop is set up from a
|
||||||
|
// clean JS top-level, NOT nested inside Dawn-web's Promise.then
|
||||||
|
// chain — which is the configuration that stalls device-callback
|
||||||
|
// delivery (verified during web bring-up).
|
||||||
|
function shellTick() {
|
||||||
|
if (Module._app_ptr && Module._raf_tick_c) {
|
||||||
|
Module._raf_tick_c(Module._app_ptr);
|
||||||
|
}
|
||||||
|
requestAnimationFrame(shellTick);
|
||||||
|
}
|
||||||
|
requestAnimationFrame(shellTick);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
if (!navigator.gpu) {
|
if (!navigator.gpu) {
|
||||||
|
|||||||
@@ -70,11 +70,6 @@ bool BufferPool::addSubBuffer() {
|
|||||||
if (try_size < MIN_SUB_BUFFER_BYTES) try_size = MIN_SUB_BUFFER_BYTES;
|
if (try_size < MIN_SUB_BUFFER_BYTES) try_size = MIN_SUB_BUFFER_BYTES;
|
||||||
|
|
||||||
while (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];
|
char label[128];
|
||||||
std::snprintf(label, sizeof(label), "%s.sub%zu",
|
std::snprintf(label, sizeof(label), "%s.sub%zu",
|
||||||
label_prefix_.c_str(), sub_pools_.size());
|
label_prefix_.c_str(), sub_pools_.size());
|
||||||
@@ -84,6 +79,23 @@ bool BufferPool::addSubBuffer() {
|
|||||||
desc.size = try_size;
|
desc.size = try_size;
|
||||||
desc.label.data = label;
|
desc.label.data = label;
|
||||||
desc.label.length = std::strlen(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);
|
WGPUBuffer buf = wgpuDeviceCreateBuffer(device_, &desc);
|
||||||
|
|
||||||
struct PopResult { bool done = false; bool error = false; };
|
struct PopResult { bool done = false; bool error = false; };
|
||||||
@@ -103,8 +115,10 @@ bool BufferPool::addSubBuffer() {
|
|||||||
PopResult oom_pop, validation_pop;
|
PopResult oom_pop, validation_pop;
|
||||||
pop(oom_pop);
|
pop(oom_pop);
|
||||||
pop(validation_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;
|
SubPool sp;
|
||||||
sp.buffer = buf;
|
sp.buffer = buf;
|
||||||
sp.capacity = try_size;
|
sp.capacity = try_size;
|
||||||
|
|||||||
+262
-24
@@ -1179,14 +1179,29 @@ namespace {
|
|||||||
// promises (firing our WGPU callbacks via AllowProcessEvents mode),
|
// promises (firing our WGPU callbacks via AllowProcessEvents mode),
|
||||||
// then resumes the C++ caller. Net effect: the same "spin until done"
|
// then resumes the C++ caller. Net effect: the same "spin until done"
|
||||||
// shape works on both backends.
|
// shape works on both backends.
|
||||||
|
// Async-callback mode for WGPU futures. wgpu-native fires
|
||||||
|
// AllowProcessEvents callbacks deterministically from
|
||||||
|
// wgpuInstanceProcessEvents — that's what we want on desktop. Dawn-web
|
||||||
|
// queues those same callbacks indefinitely (waits for a specific
|
||||||
|
// instance state we don't drive); AllowSpontaneous on web lets the JS
|
||||||
|
// event loop fire the callback as soon as the underlying promise
|
||||||
|
// resolves.
|
||||||
|
constexpr WGPUCallbackMode kAsyncCbMode =
|
||||||
|
#if defined(__EMSCRIPTEN__)
|
||||||
|
WGPUCallbackMode_AllowSpontaneous;
|
||||||
|
#else
|
||||||
|
WGPUCallbackMode_AllowProcessEvents;
|
||||||
|
#endif
|
||||||
|
|
||||||
inline void waitTickInstance(WGPUInstance instance) {
|
inline void waitTickInstance(WGPUInstance instance) {
|
||||||
#if defined(__EMSCRIPTEN__)
|
#if defined(__EMSCRIPTEN__)
|
||||||
// On Dawn-web AllowProcessEvents queues the callback for the next
|
// Asyncify is OFF on web (see ifcviewer-web/CMakeLists.txt). The
|
||||||
// wgpuInstanceProcessEvents call, but the underlying JS promise has
|
// init path no longer uses this helper — it's callback-driven via
|
||||||
// to resolve first. emscripten_sleep(0) unwinds the wasm stack so
|
// initWgpuAsyncWeb. The remaining callers are MapAsync sync-wait
|
||||||
// the JS event loop runs (resolving any pending promises); then
|
// loops (pick readback): those will spin forever here until they
|
||||||
// ProcessEvents drains the resolved completions into our callback.
|
// are ported to a callback-driven shape. Until then, hitting a
|
||||||
emscripten_sleep(0);
|
// pick on web hangs the page. ProcessEvents is a no-op on
|
||||||
|
// Dawn-web but harmless to call.
|
||||||
wgpuInstanceProcessEvents(instance);
|
wgpuInstanceProcessEvents(instance);
|
||||||
#else
|
#else
|
||||||
wgpuInstanceProcessEvents(instance);
|
wgpuInstanceProcessEvents(instance);
|
||||||
@@ -1242,13 +1257,45 @@ bool ViewportCore::probeAndCreatePool() {
|
|||||||
|
|
||||||
constexpr uint64_t MIN_POOL_CAPACITY = 64ull * 1024 * 1024;
|
constexpr uint64_t MIN_POOL_CAPACITY = 64ull * 1024 * 1024;
|
||||||
constexpr uint64_t MAX_PROBE_START = 4ull * 1024 * 1024 * 1024;
|
constexpr uint64_t MAX_PROBE_START = 4ull * 1024 * 1024 * 1024;
|
||||||
|
const WGPUBufferUsage pool_usage = WGPUBufferUsage_Storage
|
||||||
|
| WGPUBufferUsage_CopyDst;
|
||||||
|
|
||||||
|
#if defined(__EMSCRIPTEN__)
|
||||||
|
// Web: don't try to grab `device_limits.maxBufferSize` (256 MB on
|
||||||
|
// Dawn-web's spec floor) on the first sub-buffer. Browsers
|
||||||
|
// throttle / queue / hang on giant allocations — observed in
|
||||||
|
// Chrome: allocating 256 MB for the first chunk (~1 KB of geometry)
|
||||||
|
// freezes the tab indefinitely, and contention from any other
|
||||||
|
// WebGPU page on the same origin makes it worse. Start with a
|
||||||
|
// small sub-buffer (16 MB) so first-frame is cheap; the pool
|
||||||
|
// grows lazily via additional sub-buffers as the working set
|
||||||
|
// needs more, each capped at maxBufferSize but only created when
|
||||||
|
// actually demanded. (Dawn-web's PopErrorScope callbacks are also
|
||||||
|
// unreliable from a sync-spin pattern, so the desktop probe is
|
||||||
|
// skipped entirely on web.)
|
||||||
|
// 64 MB matches BufferPool::addSubBuffer's MIN_SUB_BUFFER_BYTES
|
||||||
|
// floor — smaller values would just be clamped up by the pool's
|
||||||
|
// halve-retry loop anyway. The earlier 256 MB ceiling hung Chrome
|
||||||
|
// not because of size per se, but because the pool's pop-error-scope
|
||||||
|
// spin-wait blocked the JS event loop indefinitely (now fixed in
|
||||||
|
// BufferPool::addSubBuffer for Emscripten).
|
||||||
|
constexpr std::uint64_t WEB_INITIAL_SUB_BUFFER = 64ull * 1024 * 1024;
|
||||||
|
const std::uint64_t per_sub = std::min<std::uint64_t>(
|
||||||
|
device_limits.maxBufferSize,
|
||||||
|
std::max<std::uint64_t>(MIN_POOL_CAPACITY, WEB_INITIAL_SUB_BUFFER));
|
||||||
|
pool_.configure(instance_, device_, pool_usage, per_sub,
|
||||||
|
"ifcviewer-wgpu.pool");
|
||||||
|
Log::info() << "wgpu: pool per-sub-buffer capacity = "
|
||||||
|
<< (per_sub / (1024 * 1024)) << " MB"
|
||||||
|
<< " (web: small initial sub-buffer, grows lazily;"
|
||||||
|
<< " device maxBufferSize = "
|
||||||
|
<< (device_limits.maxBufferSize / (1024 * 1024)) << " MB)";
|
||||||
|
return true;
|
||||||
|
#else
|
||||||
uint64_t try_size = std::min<uint64_t>(device_limits.maxBufferSize,
|
uint64_t try_size = std::min<uint64_t>(device_limits.maxBufferSize,
|
||||||
MAX_PROBE_START);
|
MAX_PROBE_START);
|
||||||
if (try_size < MIN_POOL_CAPACITY) try_size = MIN_POOL_CAPACITY;
|
if (try_size < MIN_POOL_CAPACITY) try_size = MIN_POOL_CAPACITY;
|
||||||
|
|
||||||
const WGPUBufferUsage pool_usage = WGPUBufferUsage_Storage
|
|
||||||
| WGPUBufferUsage_CopyDst;
|
|
||||||
|
|
||||||
while (try_size >= MIN_POOL_CAPACITY) {
|
while (try_size >= MIN_POOL_CAPACITY) {
|
||||||
wgpuDevicePushErrorScope(device_, WGPUErrorFilter_Validation);
|
wgpuDevicePushErrorScope(device_, WGPUErrorFilter_Validation);
|
||||||
wgpuDevicePushErrorScope(device_, WGPUErrorFilter_OutOfMemory);
|
wgpuDevicePushErrorScope(device_, WGPUErrorFilter_OutOfMemory);
|
||||||
@@ -1263,7 +1310,7 @@ bool ViewportCore::probeAndCreatePool() {
|
|||||||
struct PopResult { bool done = false; bool error = false; };
|
struct PopResult { bool done = false; bool error = false; };
|
||||||
auto pop = [&](PopResult& pr) {
|
auto pop = [&](PopResult& pr) {
|
||||||
WGPUPopErrorScopeCallbackInfo pcb = {};
|
WGPUPopErrorScopeCallbackInfo pcb = {};
|
||||||
pcb.mode = WGPUCallbackMode_AllowProcessEvents;
|
pcb.mode = kAsyncCbMode;
|
||||||
pcb.callback = [](WGPUPopErrorScopeStatus, WGPUErrorType type,
|
pcb.callback = [](WGPUPopErrorScopeStatus, WGPUErrorType type,
|
||||||
WGPUStringView, void* ud1, void* /*ud2*/) {
|
WGPUStringView, void* ud1, void* /*ud2*/) {
|
||||||
auto* p = static_cast<PopResult*>(ud1);
|
auto* p = static_cast<PopResult*>(ud1);
|
||||||
@@ -1295,6 +1342,7 @@ bool ViewportCore::probeAndCreatePool() {
|
|||||||
Log::warn() << "wgpu: pool probe found no allocatable size >= "
|
Log::warn() << "wgpu: pool probe found no allocatable size >= "
|
||||||
<< (MIN_POOL_CAPACITY / (1024 * 1024)) << " MB";
|
<< (MIN_POOL_CAPACITY / (1024 * 1024)) << " MB";
|
||||||
return false;
|
return false;
|
||||||
|
#endif // !__EMSCRIPTEN__
|
||||||
}
|
}
|
||||||
|
|
||||||
bool ViewportCore::initWgpu(bool web_limits) {
|
bool ViewportCore::initWgpu(bool web_limits) {
|
||||||
@@ -1303,12 +1351,14 @@ bool ViewportCore::initWgpu(bool web_limits) {
|
|||||||
wgpuSetLogLevel(WGPULogLevel_Warn);
|
wgpuSetLogLevel(WGPULogLevel_Warn);
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
Log::info() << "[initWgpu] 1/7 wgpuCreateInstance";
|
||||||
instance_ = wgpuCreateInstance(nullptr);
|
instance_ = wgpuCreateInstance(nullptr);
|
||||||
if (!instance_) {
|
if (!instance_) {
|
||||||
Log::warn() << "wgpuCreateInstance returned null";
|
Log::warn() << "wgpuCreateInstance returned null";
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Log::info() << "[initWgpu] 2/7 host_->createSurface";
|
||||||
// Surface comes from the host (X11/HWND/CAMetalLayer on desktop;
|
// Surface comes from the host (X11/HWND/CAMetalLayer on desktop;
|
||||||
// Emscripten canvas selector on web).
|
// Emscripten canvas selector on web).
|
||||||
surface_ = host_->createSurface(instance_);
|
surface_ = host_->createSurface(instance_);
|
||||||
@@ -1326,7 +1376,7 @@ bool ViewportCore::initWgpu(bool web_limits) {
|
|||||||
adapter_opts.powerPreference = WGPUPowerPreference_HighPerformance;
|
adapter_opts.powerPreference = WGPUPowerPreference_HighPerformance;
|
||||||
|
|
||||||
WGPURequestAdapterCallbackInfo acb = {};
|
WGPURequestAdapterCallbackInfo acb = {};
|
||||||
acb.mode = WGPUCallbackMode_AllowProcessEvents;
|
acb.mode = kAsyncCbMode;
|
||||||
acb.callback = [](WGPURequestAdapterStatus status, WGPUAdapter adapter,
|
acb.callback = [](WGPURequestAdapterStatus status, WGPUAdapter adapter,
|
||||||
WGPUStringView message, void* ud1, void* /*ud2*/) {
|
WGPUStringView message, void* ud1, void* /*ud2*/) {
|
||||||
auto* r = static_cast<AdapterReq*>(ud1);
|
auto* r = static_cast<AdapterReq*>(ud1);
|
||||||
@@ -1340,10 +1390,12 @@ bool ViewportCore::initWgpu(bool web_limits) {
|
|||||||
};
|
};
|
||||||
acb.userdata1 = &areq;
|
acb.userdata1 = &areq;
|
||||||
|
|
||||||
|
Log::info() << "[initWgpu] 3/7 wgpuInstanceRequestAdapter";
|
||||||
wgpuInstanceRequestAdapter(instance_, &adapter_opts, acb);
|
wgpuInstanceRequestAdapter(instance_, &adapter_opts, acb);
|
||||||
while (!areq.done) waitTickInstance(instance_);
|
while (!areq.done) waitTickInstance(instance_);
|
||||||
if (!areq.ok) return false;
|
if (!areq.ok) return false;
|
||||||
adapter_ = areq.adapter;
|
adapter_ = areq.adapter;
|
||||||
|
Log::info() << "[initWgpu] 3/7 adapter ready";
|
||||||
|
|
||||||
// ---- Async request device --------------------------------------------
|
// ---- Async request device --------------------------------------------
|
||||||
struct DeviceReq { WGPUDevice device = nullptr; bool done = false; bool ok = false; };
|
struct DeviceReq { WGPUDevice device = nullptr; bool done = false; bool ok = false; };
|
||||||
@@ -1357,15 +1409,36 @@ bool ViewportCore::initWgpu(bool web_limits) {
|
|||||||
web_floor_limits.maxBufferSize = 256ull * 1024 * 1024;
|
web_floor_limits.maxBufferSize = 256ull * 1024 * 1024;
|
||||||
|
|
||||||
WGPUDeviceDescriptor dev_desc = {};
|
WGPUDeviceDescriptor dev_desc = {};
|
||||||
|
#if defined(__EMSCRIPTEN__)
|
||||||
|
// Dawn-web's RequestDevice hangs (no promise resolution) when we
|
||||||
|
// pass a fully-populated WGPULimits as requiredLimits — every
|
||||||
|
// non-zero field is treated as a hard requirement, and the
|
||||||
|
// browser's adapter-reported limits include values it won't grant
|
||||||
|
// back to a device. nullptr means "no specific requirements; give
|
||||||
|
// me default limits", which the spec guarantees succeeds and is
|
||||||
|
// exactly what we need: the pool probe still discovers the actual
|
||||||
|
// buffer-size ceiling via probeAndCreatePool, so we don't lose
|
||||||
|
// anything by deferring to defaults here.
|
||||||
|
(void)web_floor_limits;
|
||||||
|
(void)web_limits;
|
||||||
|
dev_desc.requiredLimits = nullptr;
|
||||||
|
#else
|
||||||
dev_desc.requiredLimits = web_limits ? &web_floor_limits : &adapter_limits;
|
dev_desc.requiredLimits = web_limits ? &web_floor_limits : &adapter_limits;
|
||||||
if (web_limits) {
|
if (web_limits) {
|
||||||
Log::info() << "wgpu --web-limits: requesting browser-floor limits "
|
Log::info() << "wgpu --web-limits: requesting browser-floor limits "
|
||||||
"(maxStorageBufferBindingSize=128MB, maxBufferSize=256MB)";
|
"(maxStorageBufferBindingSize=128MB, maxBufferSize=256MB)";
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
|
#if !defined(__EMSCRIPTEN__)
|
||||||
|
// Setting only the callback (leaving uncapturedErrorCallbackInfo.mode
|
||||||
|
// = 0 default) makes Dawn-web's RequestDevice silently never resolve
|
||||||
|
// the device promise. Leave the whole struct zeroed on web; the
|
||||||
|
// browser already surfaces uncaptured errors to the JS console.
|
||||||
dev_desc.uncapturedErrorCallbackInfo.callback = onUncapturedError;
|
dev_desc.uncapturedErrorCallbackInfo.callback = onUncapturedError;
|
||||||
|
#endif
|
||||||
|
|
||||||
WGPURequestDeviceCallbackInfo dcb = {};
|
WGPURequestDeviceCallbackInfo dcb = {};
|
||||||
dcb.mode = WGPUCallbackMode_AllowProcessEvents;
|
dcb.mode = kAsyncCbMode;
|
||||||
dcb.callback = [](WGPURequestDeviceStatus status, WGPUDevice device,
|
dcb.callback = [](WGPURequestDeviceStatus status, WGPUDevice device,
|
||||||
WGPUStringView message, void* ud1, void* /*ud2*/) {
|
WGPUStringView message, void* ud1, void* /*ud2*/) {
|
||||||
auto* r = static_cast<DeviceReq*>(ud1);
|
auto* r = static_cast<DeviceReq*>(ud1);
|
||||||
@@ -1379,26 +1452,52 @@ bool ViewportCore::initWgpu(bool web_limits) {
|
|||||||
};
|
};
|
||||||
dcb.userdata1 = &dreq;
|
dcb.userdata1 = &dreq;
|
||||||
|
|
||||||
|
Log::info() << "[initWgpu] 4/7a wgpuAdapterRequestDevice CALL";
|
||||||
|
#if defined(__EMSCRIPTEN__)
|
||||||
|
// Pass nullptr descriptor so Dawn-web takes the all-defaults path.
|
||||||
|
// Setting any descriptor field that Dawn-web can't grant silently
|
||||||
|
// makes the device promise never resolve, so we avoid the whole
|
||||||
|
// struct. Note: web init normally goes through initWgpuAsyncWeb,
|
||||||
|
// not this sync path; this branch is dead code on the current
|
||||||
|
// web build but kept for any future caller.
|
||||||
|
wgpuAdapterRequestDevice(adapter_, nullptr, dcb);
|
||||||
|
#else
|
||||||
wgpuAdapterRequestDevice(adapter_, &dev_desc, dcb);
|
wgpuAdapterRequestDevice(adapter_, &dev_desc, dcb);
|
||||||
while (!dreq.done) waitTickInstance(instance_);
|
#endif
|
||||||
|
Log::info() << "[initWgpu] 4/7b RequestDevice returned, entering spin";
|
||||||
|
int tick = 0;
|
||||||
|
while (!dreq.done) {
|
||||||
|
waitTickInstance(instance_);
|
||||||
|
if (++tick % 100 == 0) {
|
||||||
|
Log::info() << "[initWgpu] 4/7c spin tick=" << tick;
|
||||||
|
}
|
||||||
|
if (tick > 2000) {
|
||||||
|
Log::warn() << "[initWgpu] 4/7 giving up after 2000 ticks";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
if (!dreq.ok) return false;
|
if (!dreq.ok) return false;
|
||||||
device_ = dreq.device;
|
device_ = dreq.device;
|
||||||
queue_ = wgpuDeviceGetQueue(device_);
|
queue_ = wgpuDeviceGetQueue(device_);
|
||||||
|
Log::info() << "[initWgpu] 4/7d device + queue ready";
|
||||||
|
|
||||||
|
Log::info() << "[initWgpu] 5/7 probeAndCreatePool";
|
||||||
if (!probeAndCreatePool()) {
|
if (!probeAndCreatePool()) {
|
||||||
Log::warn() << "wgpu: streaming pool probe failed; cannot start";
|
Log::warn() << "wgpu: streaming pool probe failed; cannot start";
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
Log::info() << "[initWgpu] 5/7 pool created";
|
||||||
#if !defined(__EMSCRIPTEN__)
|
#if !defined(__EMSCRIPTEN__)
|
||||||
// Background streaming worker. On desktop std::thread spawns a real
|
// Background streaming worker. On desktop std::thread spawns a real
|
||||||
// OS thread; under Emscripten that requires -pthread + SharedArrayBuffer
|
// OS thread; under Emscripten that requires -pthread + SharedArrayBuffer
|
||||||
// (which itself needs COOP/COEP headers from the hosting page).
|
// (which itself needs COOP/COEP headers from the hosting page).
|
||||||
// Until the web build wires those up we run streaming inline from
|
// Until the web build wires those up we run streaming inline from
|
||||||
// the render thread — the chunk count for the spike is small enough
|
// the render thread (see the `use_sync = true` branch in
|
||||||
// that the synchronous fallback inside driveStreamingLoads is fine.
|
// driveStreamingLoads on Emscripten).
|
||||||
streaming_thread_.start();
|
streaming_thread_.start();
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
Log::info() << "[initWgpu] 6/7 wgpuSurfaceGetCapabilities";
|
||||||
WGPUSurfaceCapabilities caps = {};
|
WGPUSurfaceCapabilities caps = {};
|
||||||
if (wgpuSurfaceGetCapabilities(surface_, adapter_, &caps) != WGPUStatus_Success
|
if (wgpuSurfaceGetCapabilities(surface_, adapter_, &caps) != WGPUStatus_Success
|
||||||
|| caps.formatCount == 0) {
|
|| caps.formatCount == 0) {
|
||||||
@@ -1412,6 +1511,111 @@ bool ViewportCore::initWgpu(bool web_limits) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#if defined(__EMSCRIPTEN__)
|
||||||
|
namespace {
|
||||||
|
// State carrier for the nested callback chain. Heap-allocated so it
|
||||||
|
// survives across the JS event loop ticks that resolve each promise;
|
||||||
|
// freed at the leaf callback (success or any error path).
|
||||||
|
struct WebInitCtx {
|
||||||
|
ViewportCore* core;
|
||||||
|
std::function<void(bool)> on_complete;
|
||||||
|
};
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
void ViewportCore::initWgpuAsyncWeb(std::function<void(bool)> on_complete) {
|
||||||
|
// Pass a defaulted descriptor (not nullptr). On Dawn-web,
|
||||||
|
// wgpuCreateInstance(nullptr) returns a usable instance but the
|
||||||
|
// subsequent RequestDevice promise silently never resolves.
|
||||||
|
WGPUInstanceDescriptor inst_desc = {};
|
||||||
|
instance_ = wgpuCreateInstance(&inst_desc);
|
||||||
|
if (!instance_) {
|
||||||
|
Log::warn() << "[web init] wgpuCreateInstance returned null";
|
||||||
|
on_complete(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto* ctx = new WebInitCtx{this, std::move(on_complete)};
|
||||||
|
|
||||||
|
// Bare adapter options (no compatibleSurface, no powerPreference).
|
||||||
|
// Setting any field here makes the subsequent device promise
|
||||||
|
// silently never resolve on Dawn-web.
|
||||||
|
WGPURequestAdapterOptions adapter_opts = {};
|
||||||
|
|
||||||
|
WGPURequestAdapterCallbackInfo acb = {};
|
||||||
|
acb.mode = WGPUCallbackMode_AllowSpontaneous;
|
||||||
|
acb.callback = [](WGPURequestAdapterStatus status, WGPUAdapter adapter,
|
||||||
|
WGPUStringView msg, void* ud1, void* /*ud2*/) {
|
||||||
|
auto* c = static_cast<WebInitCtx*>(ud1);
|
||||||
|
if (status != WGPURequestAdapterStatus_Success || !adapter) {
|
||||||
|
Log::warn() << "[web init] RequestAdapter failed: " << svToStr(msg);
|
||||||
|
c->on_complete(false);
|
||||||
|
delete c;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
c->core->adapter_ = adapter;
|
||||||
|
|
||||||
|
WGPURequestDeviceCallbackInfo dcb = {};
|
||||||
|
dcb.mode = WGPUCallbackMode_AllowSpontaneous;
|
||||||
|
dcb.callback = [](WGPURequestDeviceStatus dstatus, WGPUDevice device,
|
||||||
|
WGPUStringView dmsg, void* ud1b, void* /*ud2*/) {
|
||||||
|
auto* c = static_cast<WebInitCtx*>(ud1b);
|
||||||
|
if (dstatus != WGPURequestDeviceStatus_Success || !device) {
|
||||||
|
Log::warn() << "[web init] RequestDevice failed: " << svToStr(dmsg);
|
||||||
|
c->on_complete(false);
|
||||||
|
delete c;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
c->core->device_ = device;
|
||||||
|
c->core->queue_ = wgpuDeviceGetQueue(device);
|
||||||
|
|
||||||
|
// Surface creation is deferred to here (post-device-ready).
|
||||||
|
// Creating it inside the adapter callback (before
|
||||||
|
// wgpuAdapterRequestDevice) makes the device promise
|
||||||
|
// silently never resolve on Dawn-web.
|
||||||
|
c->core->surface_ = c->core->host_->createSurface(c->core->instance_);
|
||||||
|
if (!c->core->surface_) {
|
||||||
|
Log::warn() << "[web init] host createSurface returned null";
|
||||||
|
c->on_complete(false);
|
||||||
|
delete c;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!c->core->probeAndCreatePool()) {
|
||||||
|
Log::warn() << "[web init] pool create failed";
|
||||||
|
c->on_complete(false);
|
||||||
|
delete c;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
WGPUSurfaceCapabilities caps = {};
|
||||||
|
if (wgpuSurfaceGetCapabilities(c->core->surface_, c->core->adapter_, &caps)
|
||||||
|
!= WGPUStatus_Success
|
||||||
|
|| caps.formatCount == 0) {
|
||||||
|
Log::warn() << "[web init] surface has no formats";
|
||||||
|
c->on_complete(false);
|
||||||
|
delete c;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
c->core->surface_format_ = caps.formats[0];
|
||||||
|
wgpuSurfaceCapabilitiesFreeMembers(caps);
|
||||||
|
|
||||||
|
Log::info() << "[web init] wgpu device + surface ready (format="
|
||||||
|
<< int(c->core->surface_format_) << ")";
|
||||||
|
c->on_complete(true);
|
||||||
|
delete c;
|
||||||
|
};
|
||||||
|
dcb.userdata1 = c;
|
||||||
|
// Pass a zero-init local descriptor (not nullptr). Dawn-web's
|
||||||
|
// RequestDevice silently never resolves the promise when passed
|
||||||
|
// nullptr.
|
||||||
|
WGPUDeviceDescriptor dd = {};
|
||||||
|
wgpuAdapterRequestDevice(adapter, &dd, dcb);
|
||||||
|
};
|
||||||
|
acb.userdata1 = ctx;
|
||||||
|
wgpuInstanceRequestAdapter(instance_, &adapter_opts, acb);
|
||||||
|
}
|
||||||
|
#endif // __EMSCRIPTEN__
|
||||||
|
|
||||||
void ViewportCore::shutdown() {
|
void ViewportCore::shutdown() {
|
||||||
// Stop streaming first so no late results land in the pool after
|
// Stop streaming first so no late results land in the pool after
|
||||||
// we've torn down model state. Worker drains its queue then joins.
|
// we've torn down model state. Worker drains its queue then joins.
|
||||||
@@ -2065,11 +2269,21 @@ void ViewportCore::driveStreamingLoads() {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sync fallback when a screenshot is pending: the deferred-
|
// Sync fallback. Two ways to land here:
|
||||||
// capture wait would let the window manager re-layout the
|
// - A screenshot capture is pending — the deferred-capture
|
||||||
// window while we wait, capturing at the wrong size. With sync
|
// wait would let the window manager re-layout while we wait,
|
||||||
// loads the chunk appears in the same frame we enqueue.
|
// capturing at the wrong size. Sync loads make the chunk
|
||||||
if (!pending_screenshot_path_.empty()) {
|
// appear in the same frame we enqueue.
|
||||||
|
// - Emscripten — the worker thread isn't started (no pthreads
|
||||||
|
// wired yet, #88), so streaming_thread_.enqueue would just
|
||||||
|
// queue requests with nothing to drain them. Chunks would
|
||||||
|
// never go resident.
|
||||||
|
#if defined(__EMSCRIPTEN__)
|
||||||
|
const bool use_sync = true;
|
||||||
|
#else
|
||||||
|
const bool use_sync = !pending_screenshot_path_.empty();
|
||||||
|
#endif
|
||||||
|
if (use_sync) {
|
||||||
if (loadChunkBytesAndUploadGpu(*cand.m, cand.ci)) {
|
if (loadChunkBytesAndUploadGpu(*cand.m, cand.ci)) {
|
||||||
++enqueued;
|
++enqueued;
|
||||||
c.last_visible_frame_idx = streaming_frame_idx_;
|
c.last_visible_frame_idx = streaming_frame_idx_;
|
||||||
@@ -2822,6 +3036,21 @@ void ViewportCore::uploadInstanceChunk(const InstanceChunk& chunk) {
|
|||||||
s.instances.push_back(inst);
|
s.instances.push_back(inst);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::uint32_t ViewportCore::loadSidecarFromPath(const std::string& path) {
|
||||||
|
if (!device_ || !queue_) {
|
||||||
|
Log::warn() << "loadSidecarFromPath: wgpu not initialised";
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
auto meta_opt = readSidecarMetadataOnly(path);
|
||||||
|
if (!meta_opt) {
|
||||||
|
Log::warn() << "loadSidecarFromPath: could not read sidecar metadata from " << path;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
const std::uint32_t mid = next_model_id_++;
|
||||||
|
applyCachedModel(mid, std::move(*meta_opt));
|
||||||
|
return mid;
|
||||||
|
}
|
||||||
|
|
||||||
void ViewportCore::finalizeModel(std::uint32_t model_id) {
|
void ViewportCore::finalizeModel(std::uint32_t model_id) {
|
||||||
auto it = pending_direct_loads_.find(model_id);
|
auto it = pending_direct_loads_.find(model_id);
|
||||||
if (it == pending_direct_loads_.end()) {
|
if (it == pending_direct_loads_.end()) {
|
||||||
@@ -3230,7 +3459,7 @@ void ViewportCore::startHizMap(int slot, const Eigen::Matrix4f& vp_used) {
|
|||||||
auto* ctx = new MapCtx{ this, slot };
|
auto* ctx = new MapCtx{ this, slot };
|
||||||
|
|
||||||
WGPUBufferMapCallbackInfo mcb = {};
|
WGPUBufferMapCallbackInfo mcb = {};
|
||||||
mcb.mode = WGPUCallbackMode_AllowProcessEvents;
|
mcb.mode = kAsyncCbMode;
|
||||||
mcb.callback = [](WGPUMapAsyncStatus status, WGPUStringView /*msg*/,
|
mcb.callback = [](WGPUMapAsyncStatus status, WGPUStringView /*msg*/,
|
||||||
void* ud1, void* /*ud2*/) {
|
void* ud1, void* /*ud2*/) {
|
||||||
auto* c = static_cast<MapCtx*>(ud1);
|
auto* c = static_cast<MapCtx*>(ud1);
|
||||||
@@ -3991,7 +4220,7 @@ std::uint32_t ViewportCore::pickObjectAt(int x_pixels, int y_pixels,
|
|||||||
struct MapReq { bool done = false; bool ok = false; };
|
struct MapReq { bool done = false; bool ok = false; };
|
||||||
MapReq req;
|
MapReq req;
|
||||||
WGPUBufferMapCallbackInfo mcb = {};
|
WGPUBufferMapCallbackInfo mcb = {};
|
||||||
mcb.mode = WGPUCallbackMode_AllowProcessEvents;
|
mcb.mode = kAsyncCbMode;
|
||||||
mcb.callback = [](WGPUMapAsyncStatus status, WGPUStringView /*msg*/,
|
mcb.callback = [](WGPUMapAsyncStatus status, WGPUStringView /*msg*/,
|
||||||
void* ud1, void* /*ud2*/) {
|
void* ud1, void* /*ud2*/) {
|
||||||
auto* r = static_cast<MapReq*>(ud1);
|
auto* r = static_cast<MapReq*>(ud1);
|
||||||
@@ -4163,7 +4392,7 @@ std::vector<std::uint32_t> ViewportCore::picksInRect(int x, int y, int w, int h)
|
|||||||
struct MapReq { bool done = false; bool ok = false; };
|
struct MapReq { bool done = false; bool ok = false; };
|
||||||
MapReq req;
|
MapReq req;
|
||||||
WGPUBufferMapCallbackInfo mcb = {};
|
WGPUBufferMapCallbackInfo mcb = {};
|
||||||
mcb.mode = WGPUCallbackMode_AllowProcessEvents;
|
mcb.mode = kAsyncCbMode;
|
||||||
mcb.callback = [](WGPUMapAsyncStatus status, WGPUStringView /*msg*/,
|
mcb.callback = [](WGPUMapAsyncStatus status, WGPUStringView /*msg*/,
|
||||||
void* ud1, void* /*ud2*/) {
|
void* ud1, void* /*ud2*/) {
|
||||||
auto* r = static_cast<MapReq*>(ud1);
|
auto* r = static_cast<MapReq*>(ud1);
|
||||||
@@ -4694,7 +4923,7 @@ void ViewportCore::finalizeScreenshotCapture(WGPUBuffer capture_buffer,
|
|||||||
struct MapReq { bool done = false; bool ok = false; };
|
struct MapReq { bool done = false; bool ok = false; };
|
||||||
MapReq req;
|
MapReq req;
|
||||||
WGPUBufferMapCallbackInfo mcb = {};
|
WGPUBufferMapCallbackInfo mcb = {};
|
||||||
mcb.mode = WGPUCallbackMode_AllowProcessEvents;
|
mcb.mode = kAsyncCbMode;
|
||||||
mcb.callback = [](WGPUMapAsyncStatus status, WGPUStringView /*message*/,
|
mcb.callback = [](WGPUMapAsyncStatus status, WGPUStringView /*message*/,
|
||||||
void* ud1, void* /*ud2*/) {
|
void* ud1, void* /*ud2*/) {
|
||||||
auto* r = static_cast<MapReq*>(ud1);
|
auto* r = static_cast<MapReq*>(ud1);
|
||||||
@@ -4906,7 +5135,16 @@ void ViewportCore::render() {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Force sequential on Emscripten: std::async(std::launch::async)
|
||||||
|
// without -pthread throws std::system_error from inside libstdc++,
|
||||||
|
// and we link without exceptions so that becomes abort(). Until
|
||||||
|
// COOP/COEP + -pthread wires the worker pool in #88, web stays
|
||||||
|
// on the serial path.
|
||||||
|
#if defined(__EMSCRIPTEN__)
|
||||||
|
if (false) {
|
||||||
|
#else
|
||||||
if (cull_threads_enabled_) {
|
if (cull_threads_enabled_) {
|
||||||
|
#endif
|
||||||
std::vector<std::pair<std::uint32_t, std::future<std::uint32_t>>> futures;
|
std::vector<std::pair<std::uint32_t, std::future<std::uint32_t>>> futures;
|
||||||
futures.reserve(models_gpu_.size());
|
futures.reserve(models_gpu_.size());
|
||||||
for (auto& [mid, m] : models_gpu_) {
|
for (auto& [mid, m] : models_gpu_) {
|
||||||
|
|||||||
@@ -257,6 +257,19 @@ public:
|
|||||||
bool initWgpu(bool web_limits);
|
bool initWgpu(bool web_limits);
|
||||||
void shutdown();
|
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) ------------------------------------------
|
// ---- Chunk residency (#84-n) ------------------------------------------
|
||||||
//
|
//
|
||||||
// Build the per-chunk WGPUBindGroup over its current pool slices +
|
// Build the per-chunk WGPUBindGroup over its current pool slices +
|
||||||
@@ -315,6 +328,14 @@ public:
|
|||||||
// freshly-loaded scene frames itself).
|
// freshly-loaded scene frames itself).
|
||||||
void applyCachedModel(std::uint32_t model_id, StreamingSidecar metadata);
|
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
|
// Direct-load (bonsai-side) entry points. Bonsai's SceneLoader feeds
|
||||||
// the viewer one mesh + one instance at a time, then calls
|
// the viewer one mesh + one instance at a time, then calls
|
||||||
// finalizeModel once everything's staged. The staging map lives on
|
// finalizeModel once everything's staged. The staging map lives on
|
||||||
|
|||||||
Reference in New Issue
Block a user