From 2b43e6f7e0044cddd4b67f13d04e2b3590ff670f Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Fri, 5 Jun 2026 18:06:25 +1000 Subject: [PATCH] ifcviewer: move initWgpu + probeAndCreatePool + shutdown into ViewportCore (#84-l) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The instance/adapter/device/queue/pool/surface-format wgpu lifecycle now lives in ViewportCore — including the OOM-scoped pool size probe and the worker-thread startup. ViewportWindow::initWgpu becomes a Qt shell that handles env-var tuning + nav-button preset wiring, then delegates to core_.initWgpu(); the VW-only pipeline builders (HiZ, edge, overlays, pick) still run after. ViewportWindow::shutdown drops the VW-only resources (depth, msaa, hiz, edge, overlays, pick) and lets core_.shutdown() release the shared wgpu handles it now owns. The wgpu-native log callback (wgpuSetLogCallback / WGPULogLevel) is gated on !__EMSCRIPTEN__: it's not part of the W3C spec header, and the emdawnwebgpu port doesn't ship wgpu.h — validation errors there land in the browser console regardless. Drive-by: update test_federation to compare HomeView::target as Eigen::Vector3f (left stale by #79 when QVector3D was retired). --- src/ifcviewer/ViewportCore.cpp | 255 ++++++++++++++ src/ifcviewer/ViewportCore.h | 27 ++ src/ifcviewer/ViewportWindow.cpp | 436 +++++------------------- src/ifcviewer/ViewportWindow.h | 5 - src/ifcviewer/tests/test_federation.cpp | 8 +- 5 files changed, 364 insertions(+), 367 deletions(-) diff --git a/src/ifcviewer/ViewportCore.cpp b/src/ifcviewer/ViewportCore.cpp index 917b3dbc85..b6f1763e65 100644 --- a/src/ifcviewer/ViewportCore.cpp +++ b/src/ifcviewer/ViewportCore.cpp @@ -26,6 +26,14 @@ #include #include +// wgpu-native extensions (log callback, MULTI_DRAW_INDIRECT). The web +// build (emdawnwebgpu / Dawn) doesn't ship this header — validation +// errors there go to the browser console, so the log-callback path +// simply compiles out under __EMSCRIPTEN__. +#if !defined(__EMSCRIPTEN__) +# include +#endif + #include "CameraMath.h" #include "InstanceCompose.h" #include "Log.h" @@ -1112,3 +1120,250 @@ void ViewportCore::uploadSelectionFlagsIfDirty() { selection_flags_scratch_.size() * sizeof(uint32_t)); selection_.markClean(); } + +// =========================================================================== +// Lifecycle (#84-l): initWgpu + probeAndCreatePool + shutdown +// =========================================================================== + +namespace { +// String-view → std::string for log output. WGPU_STRLEN is the +// sentinel meaning "nul-terminated", in which case strlen() gives the +// length. +std::string svToStr(WGPUStringView s) { + if (!s.data) return {}; + const std::size_t len = (s.length == WGPU_STRLEN) + ? std::strlen(s.data) + : s.length; + return std::string(s.data, len); +} + +#if !defined(__EMSCRIPTEN__) +// wgpu-native callback: route every log line to Log::warn / Log::info +// so backend init problems surface in the console instead of being +// swallowed by the native runtime. Not available on emdawnwebgpu — +// see the include guard above. +void onWgpuLog(WGPULogLevel level, WGPUStringView message, void* /*userdata*/) { + const std::string m = svToStr(message); + switch (level) { + case WGPULogLevel_Error: Log::warn() << "[wgpu err] " << m; break; + case WGPULogLevel_Warn: Log::warn() << "[wgpu warn] " << m; break; + case WGPULogLevel_Info: Log::info() << "[wgpu info] " << m; break; + case WGPULogLevel_Debug: Log::info() << "[wgpu dbg] " << m; break; + case WGPULogLevel_Trace: Log::info() << "[wgpu trace] " << m; break; + default: break; + } +} +#endif + +// Per-device uncaptured-error callback. Validation failures land here +// when no error scope is open. Surfacing them into Log::warn makes +// otherwise-silent driver complaints attributable. +void onUncapturedError(WGPUDevice const* /*device*/, + WGPUErrorType type, WGPUStringView message, + void* /*ud1*/, void* /*ud2*/) { + Log::warn() << "[wgpu device error " << int(type) << "] " << svToStr(message); +} +} // namespace + +bool ViewportCore::probeAndCreatePool() { + // Discover the largest single buffer the runtime will grant. Each + // attempt sits inside OOM + Validation error scopes so a failed + // allocation doesn't surface as a noisy uncaptured-error warning. + WGPULimits device_limits = {}; + wgpuDeviceGetLimits(device_, &device_limits); + + constexpr uint64_t MIN_POOL_CAPACITY = 64ull * 1024 * 1024; + constexpr uint64_t MAX_PROBE_START = 4ull * 1024 * 1024 * 1024; + uint64_t try_size = std::min(device_limits.maxBufferSize, + MAX_PROBE_START); + 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) { + wgpuDevicePushErrorScope(device_, WGPUErrorFilter_Validation); + wgpuDevicePushErrorScope(device_, WGPUErrorFilter_OutOfMemory); + + WGPUBufferDescriptor desc = {}; + desc.usage = pool_usage; + desc.size = try_size; + desc.label.data = "ifcviewer-wgpu.pool_probe"; + desc.label.length = std::strlen("ifcviewer-wgpu.pool_probe"); + WGPUBuffer probe_buf = wgpuDeviceCreateBuffer(device_, &desc); + + struct PopResult { bool done = false; bool error = false; }; + auto pop = [&](PopResult& pr) { + WGPUPopErrorScopeCallbackInfo pcb = {}; + pcb.mode = WGPUCallbackMode_AllowProcessEvents; + pcb.callback = [](WGPUPopErrorScopeStatus, WGPUErrorType type, + WGPUStringView, void* ud1, void* /*ud2*/) { + auto* p = static_cast(ud1); + p->done = true; + p->error = (type != WGPUErrorType_NoError); + }; + pcb.userdata1 = ≺ + wgpuDevicePopErrorScope(device_, pcb); + while (!pr.done) wgpuInstanceProcessEvents(instance_); + }; + PopResult oom_pop, validation_pop; + pop(oom_pop); + pop(validation_pop); + + if (probe_buf) wgpuBufferRelease(probe_buf); + if (probe_buf && !oom_pop.error && !validation_pop.error) { + pool_.configure(instance_, device_, pool_usage, try_size, + "ifcviewer-wgpu.pool"); + Log::info() << "wgpu: pool per-sub-buffer capacity = " + << (try_size / (1024 * 1024)) << " MB" + << " (device maxBufferSize = " + << (device_limits.maxBufferSize / (1024 * 1024)) + << " MB); pool will grow on demand"; + return true; + } + try_size /= 2; + } + + Log::warn() << "wgpu: pool probe found no allocatable size >= " + << (MIN_POOL_CAPACITY / (1024 * 1024)) << " MB"; + return false; +} + +bool ViewportCore::initWgpu(bool web_limits) { +#if !defined(__EMSCRIPTEN__) + wgpuSetLogCallback(onWgpuLog, nullptr); + wgpuSetLogLevel(WGPULogLevel_Warn); +#endif + + instance_ = wgpuCreateInstance(nullptr); + if (!instance_) { + Log::warn() << "wgpuCreateInstance returned null"; + return false; + } + + // Surface comes from the host (X11/HWND/CAMetalLayer on desktop; + // Emscripten canvas selector on web). + surface_ = host_->createSurface(instance_); + if (!surface_) { + Log::warn() << "host createSurface returned null"; + return false; + } + + // ---- Async request adapter ------------------------------------------- + struct AdapterReq { WGPUAdapter adapter = nullptr; bool done = false; bool ok = false; }; + AdapterReq areq; + + WGPURequestAdapterOptions adapter_opts = {}; + adapter_opts.compatibleSurface = surface_; + adapter_opts.powerPreference = WGPUPowerPreference_HighPerformance; + + WGPURequestAdapterCallbackInfo acb = {}; + acb.mode = WGPUCallbackMode_AllowProcessEvents; + acb.callback = [](WGPURequestAdapterStatus status, WGPUAdapter adapter, + WGPUStringView message, void* ud1, void* /*ud2*/) { + auto* r = static_cast(ud1); + r->done = true; + if (status == WGPURequestAdapterStatus_Success) { + r->adapter = adapter; + r->ok = true; + } else { + Log::warn() << "RequestAdapter failed: " << svToStr(message); + } + }; + acb.userdata1 = &areq; + + wgpuInstanceRequestAdapter(instance_, &adapter_opts, acb); + while (!areq.done) wgpuInstanceProcessEvents(instance_); + if (!areq.ok) return false; + adapter_ = areq.adapter; + + // ---- Async request device -------------------------------------------- + struct DeviceReq { WGPUDevice device = nullptr; bool done = false; bool ok = false; }; + DeviceReq dreq; + + WGPULimits adapter_limits = {}; + wgpuAdapterGetLimits(adapter_, &adapter_limits); + + WGPULimits web_floor_limits = adapter_limits; + web_floor_limits.maxStorageBufferBindingSize = 128ull * 1024 * 1024; + web_floor_limits.maxBufferSize = 256ull * 1024 * 1024; + + WGPUDeviceDescriptor dev_desc = {}; + dev_desc.requiredLimits = web_limits ? &web_floor_limits : &adapter_limits; + if (web_limits) { + Log::info() << "wgpu --web-limits: requesting browser-floor limits " + "(maxStorageBufferBindingSize=128MB, maxBufferSize=256MB)"; + } + dev_desc.uncapturedErrorCallbackInfo.callback = onUncapturedError; + + WGPURequestDeviceCallbackInfo dcb = {}; + dcb.mode = WGPUCallbackMode_AllowProcessEvents; + dcb.callback = [](WGPURequestDeviceStatus status, WGPUDevice device, + WGPUStringView message, void* ud1, void* /*ud2*/) { + auto* r = static_cast(ud1); + r->done = true; + if (status == WGPURequestDeviceStatus_Success) { + r->device = device; + r->ok = true; + } else { + Log::warn() << "RequestDevice failed: " << svToStr(message); + } + }; + dcb.userdata1 = &dreq; + + wgpuAdapterRequestDevice(adapter_, &dev_desc, dcb); + while (!dreq.done) wgpuInstanceProcessEvents(instance_); + if (!dreq.ok) return false; + device_ = dreq.device; + queue_ = wgpuDeviceGetQueue(device_); + + if (!probeAndCreatePool()) { + Log::warn() << "wgpu: streaming pool probe failed; cannot start"; + return false; + } + streaming_thread_.start(); + + WGPUSurfaceCapabilities caps = {}; + if (wgpuSurfaceGetCapabilities(surface_, adapter_, &caps) != WGPUStatus_Success + || caps.formatCount == 0) { + Log::warn() << "wgpuSurfaceGetCapabilities returned no formats"; + return false; + } + surface_format_ = caps.formats[0]; + wgpuSurfaceCapabilitiesFreeMembers(caps); + + Log::info() << "wgpu init OK; surface format = " << int(surface_format_); + return true; +} + +void ViewportCore::shutdown() { + // Stop streaming first so no late results land in the pool after + // we've torn down model state. Worker drains its queue then joins. + streaming_thread_.stop(); + + for (auto& [mid, m] : models_gpu_) releaseWgpuModelGpuData(m, pool_); + models_gpu_.clear(); + + if (frame_bind_group_) { wgpuBindGroupRelease(frame_bind_group_); frame_bind_group_ = nullptr; } + if (frame_uniform_buffer_) { wgpuBufferRelease(frame_uniform_buffer_); frame_uniform_buffer_ = nullptr; } + if (selection_flags_buffer_) { wgpuBufferRelease(selection_flags_buffer_); selection_flags_buffer_ = nullptr; } + selection_flags_capacity_ = 0; + if (main_pipeline_) { wgpuRenderPipelineRelease(main_pipeline_); main_pipeline_ = nullptr; } + if (main_pipeline_transparent_) { wgpuRenderPipelineRelease(main_pipeline_transparent_); main_pipeline_transparent_ = nullptr; } + if (main_shader_module_) { wgpuShaderModuleRelease(main_shader_module_); main_shader_module_ = nullptr; } + if (pipeline_layout_) { wgpuPipelineLayoutRelease(pipeline_layout_); pipeline_layout_ = nullptr; } + if (model_bgl_) { wgpuBindGroupLayoutRelease(model_bgl_); model_bgl_ = nullptr; } + if (frame_bgl_) { wgpuBindGroupLayoutRelease(frame_bgl_); frame_bgl_ = nullptr; } + + // Destroy the streaming pool while device_ is still alive — it owns + // the underlying WGPUBuffer. + pool_.destroy(); + + if (queue_) { wgpuQueueRelease(queue_); queue_ = nullptr; } + if (device_) { wgpuDeviceRelease(device_); device_ = nullptr; } + if (adapter_) { wgpuAdapterRelease(adapter_); adapter_ = nullptr; } + if (surface_) { wgpuSurfaceRelease(surface_); surface_ = nullptr; } + if (instance_) { wgpuInstanceRelease(instance_); instance_ = nullptr; } + wgpu_initialized_ = false; + surface_configured_ = false; +} diff --git a/src/ifcviewer/ViewportCore.h b/src/ifcviewer/ViewportCore.h index e67cfaf4e2..5428c58691 100644 --- a/src/ifcviewer/ViewportCore.h +++ b/src/ifcviewer/ViewportCore.h @@ -223,6 +223,33 @@ public: void ensureSelectionFlagsBuffer(); void uploadSelectionFlagsIfDirty(); + // ---- wgpu lifecycle ---------------------------------------------------- + // + // initWgpu brings up the wgpu instance, gets the platform surface + // from host_->createSurface, requests adapter + device + queue, + // probes the streaming pool capacity, starts the background loader + // thread, and picks the swap-chain surface format. Does NOT build + // pipelines — the host runs the pipeline construction after this + // (so VW can still keep its HiZ / edge / pick / overlay builders + // co-located). + // + // `web_limits` forces the WebGPU spec mandatory floor (128 MB max + // storage binding, 256 MB max buffer) instead of the adapter's + // actual maximum — used by --web-limits to verify chunking fits + // through browser constraints. + // + // shutdown tears down everything ViewportCore owns. The host's own + // teardown (depth/MSAA/HiZ/edge/pick attachments + overlays) must + // run BEFORE this call so its device-owned resources release + // against a still-live device. + bool initWgpu(bool web_limits); + void shutdown(); + +private: + bool probeAndCreatePool(); + +public: + // Friend access for ViewportWindow's reference proxies. As each // render method moves into ViewportCore it stops needing these // (it touches the fields directly); once everything has migrated diff --git a/src/ifcviewer/ViewportWindow.cpp b/src/ifcviewer/ViewportWindow.cpp index 21a5db2c9c..0b107ad114 100644 --- a/src/ifcviewer/ViewportWindow.cpp +++ b/src/ifcviewer/ViewportWindow.cpp @@ -163,24 +163,6 @@ static QString sv(WGPUStringView s) { return QString::fromUtf8(s.data, len); } -static void onWgpuLog(WGPULogLevel level, WGPUStringView message, void* /*userdata*/) { - const QString m = sv(message); - switch (level) { - case WGPULogLevel_Error: Log::warn().noquote() << "[wgpu err]" << m; break; - case WGPULogLevel_Warn: Log::warn().noquote() << "[wgpu warn]" << m; break; - case WGPULogLevel_Info: Log::info() << "[wgpu info] " << m; break; - case WGPULogLevel_Debug: Log::info() << "[wgpu dbg] " << m; break; - case WGPULogLevel_Trace: Log::info() << "[wgpu trace] " << m; break; - default: break; - } -} - -static void onUncapturedError(WGPUDevice const* /*device*/, - WGPUErrorType type, WGPUStringView message, - void* /*ud1*/, void* /*ud2*/) { - Log::warn().noquote() << "[wgpu device error" << int(type) << "]" << sv(message); -} - // Allocate a wgpu buffer of `size_bytes` with the given usage, and upload // `data` into it via the queue. Returns nullptr when size_bytes == 0 (wgpu // rejects zero-sized buffer creation). `label` is informational; it shows up @@ -328,13 +310,65 @@ ViewportWindow::~ViewportWindow() { // forwards to the existing Q_SIGNAL so bonsai-side consumers see no // change. -WGPUSurface ViewportWindow::createSurface(WGPUInstance /*instance*/) { - // initWgpu() still drives surface creation through the private - // no-arg createSurface() helper that populates surface_. Once - // ViewportCore takes over the wgpu init flow it'll call this - // override instead and the private helper goes away; for now the - // override is a getter. - return surface_; +// Platform-specific WGPUSurface creation. Called by ViewportCore::initWgpu +// (#84-l) once the wgpu instance is up. The platform branches reach into +// Qt's QNativeInterface to fish out the native window handle (X11 +// Display + Window, Win32 HWND, or NSView wrapped in CAMetalLayer) and +// wrap each in the corresponding WGPUSurfaceSource* descriptor. The +// returned WGPUSurface is owned by the caller (ViewportCore stores it +// on `core_.surface_`). +WGPUSurface ViewportWindow::createSurface(WGPUInstance instance) { + WGPUSurfaceDescriptor surface_desc = {}; + +#if defined(Q_OS_LINUX) + const QString platform = QGuiApplication::platformName(); + if (platform == "xcb") { +# if __has_include() + auto* x11 = qApp->nativeInterface(); + if (!x11 || !x11->display()) { + Log::warn() << "Could not get X11 Display* from Qt"; + return nullptr; + } + WGPUSurfaceSourceXlibWindow xlib = {}; + xlib.chain.sType = WGPUSType_SurfaceSourceXlibWindow; + xlib.display = x11->display(); + xlib.window = static_cast(winId()); + surface_desc.nextInChain = &xlib.chain; + return wgpuInstanceCreateSurface(instance, &surface_desc); +# else + Log::warn() << "Built without Xlib headers; cannot create X11 surface"; + return nullptr; +# endif + } else if (platform == "wayland") { + Log::warn() << "Wayland wgpu surface creation not yet wired (stage 1.5)"; + return nullptr; + } else { + Log::warn().noquote() << "Unsupported Qt platform for wgpu surface:" << platform; + return nullptr; + } +#elif defined(Q_OS_WIN) + WGPUSurfaceSourceWindowsHWND hwndsrc = {}; + hwndsrc.chain.sType = WGPUSType_SurfaceSourceWindowsHWND; + hwndsrc.hinstance = ::GetModuleHandleW(nullptr); + hwndsrc.hwnd = reinterpret_cast(static_cast(winId())); + surface_desc.nextInChain = &hwndsrc.chain; + return wgpuInstanceCreateSurface(instance, &surface_desc); +#elif defined(Q_OS_MAC) + void* nsview = reinterpret_cast(static_cast(winId())); + void* layer = wgpu_macos_attach_metal_layer(nsview); + if (!layer) { + Log::warn() << "Could not attach CAMetalLayer to the Qt NSView"; + return nullptr; + } + WGPUSurfaceSourceMetalLayer metalsrc = {}; + metalsrc.chain.sType = WGPUSType_SurfaceSourceMetalLayer; + metalsrc.layer = layer; + surface_desc.nextInChain = &metalsrc.chain; + return wgpuInstanceCreateSurface(instance, &surface_desc); +#else + Log::warn() << "wgpu surface creation not yet wired for this platform"; + return nullptr; +#endif } void ViewportWindow::framebufferSize(int& width_px, int& height_px) const { @@ -1127,17 +1161,13 @@ bool ViewportWindow::event(QEvent* event) { // ----------------------------------------------------------------------------- bool ViewportWindow::initWgpu() { - // Optional: log everything wgpu-native says at warn+ so backend init - // problems surface in the console rather than being swallowed. - wgpuSetLogCallback(onWgpuLog, nullptr); - wgpuSetLogLevel(WGPULogLevel_Warn); - - // Env-var overrides for contribution-cull thresholds. wgpu uses - // view-Z (perspective-divide-correct) for projected_px, whereas the - // GL backend uses euclidean distance — so for off-axis instances - // wgpu computes a larger projected_px and is less aggressive at the - // same numeric threshold. These knobs exist to let us sweep matching - // values during the perf-parity push without rebuilding. + // ---- Env-var tuning + nav binding setup ----------------------------- + // + // These mutate VW-side state (cull thresholds, hiz_enabled_, nav + // button bindings) so they stay in the Qt-bound shell. ViewportCore + // doesn't know about Qt::MouseButton enums or the still-in-VW cull + // tuning fields. Once those move (later #84 steps + #85 for input) + // this whole prologue migrates with them. if (const char* s = std::getenv("WGPU_MIN_PX")) { const float v = float(std::atof(s)); if (v >= 0.0f) min_pixel_radius_ = v; @@ -1165,9 +1195,6 @@ bool ViewportWindow::initWgpu() { } } if (const char* s = std::getenv("WGPU_CULL_THREADS")) { - // "0" disables std::async dispatch — every model is culled on the - // main thread, sequentially. Used to measure speedup vs the - // parallel-per-model path. Any non-"0" value keeps parallelism on. cull_threads_enabled_ = (s[0] != '0'); Log::info().noquote().nospace() << "[wgpu cull] WGPU_CULL_THREADS=" << s @@ -1179,8 +1206,6 @@ bool ViewportWindow::initWgpu() { Log::info() << "[wgpu fly] WGPU_FLY_DEBUG=1 — per-frame [fly] dt log enabled"; } } - // Mouse-nav preset (matches GL AppSettings::NavPreset). blender default, - // rhino or revit as alternatives. Selection always stays on LMB. const char* nav_env = std::getenv("WGPU_NAV_PRESET"); applyNavPreset(nav_env ? nav_env : "blender"); Log::info().noquote().nospace() @@ -1193,119 +1218,11 @@ bool ViewportWindow::initWgpu() { << (pan_mods_ & Qt::ShiftModifier ? "+Shift" : "") << ")"; - instance_ = wgpuCreateInstance(nullptr); - if (!instance_) { - Log::warn() << "wgpuCreateInstance returned null"; - return false; - } - - if (!createSurface()) return false; - - // ---- Async request adapter ------------------------------------------- - struct AdapterReq { WGPUAdapter adapter = nullptr; bool done = false; bool ok = false; }; - AdapterReq areq; - - WGPURequestAdapterOptions adapter_opts = {}; - adapter_opts.compatibleSurface = surface_; - adapter_opts.powerPreference = WGPUPowerPreference_HighPerformance; - - WGPURequestAdapterCallbackInfo acb = {}; - acb.mode = WGPUCallbackMode_AllowProcessEvents; - acb.callback = [](WGPURequestAdapterStatus status, WGPUAdapter adapter, - WGPUStringView message, void* ud1, void* /*ud2*/) { - auto* r = static_cast(ud1); - r->done = true; - if (status == WGPURequestAdapterStatus_Success) { - r->adapter = adapter; - r->ok = true; - } else { - Log::warn().noquote() << "RequestAdapter failed:" << sv(message); - } - }; - acb.userdata1 = &areq; - - wgpuInstanceRequestAdapter(instance_, &adapter_opts, acb); - while (!areq.done) wgpuInstanceProcessEvents(instance_); - if (!areq.ok) return false; - adapter_ = areq.adapter; - - // ---- Async request device -------------------------------------------- - struct DeviceReq { WGPUDevice device = nullptr; bool done = false; bool ok = false; }; - DeviceReq dreq; - - // Pick the limits to request on the device. Default = the adapter's - // actual maximum so large native scenes get all the headroom the GPU - // can give. --web-limits forces the WebGPU spec mandatory floor - // (128 MB max storage binding, 256 MB max buffer) so we can verify on - // desktop that the renderer's chunking actually fits through browser - // constraints — turns "trust me, web will work" into a hard test. - WGPULimits adapter_limits = {}; - wgpuAdapterGetLimits(adapter_, &adapter_limits); - - WGPULimits web_floor_limits = adapter_limits; - // Override just the two that BIM scenes typically blow past. Everything - // else stays at adapter max (no point making the device weaker than it - // could be on facets we know browsers grant generously, e.g. workgroup - // sizes — those are texture / compute limits and we don't hit them). - web_floor_limits.maxStorageBufferBindingSize = 128ull * 1024 * 1024; - web_floor_limits.maxBufferSize = 256ull * 1024 * 1024; - - WGPUDeviceDescriptor dev_desc = {}; - dev_desc.requiredLimits = web_limits_ ? &web_floor_limits : &adapter_limits; - if (web_limits_) { - Log::info() << "wgpu --web-limits: requesting browser-floor limits" - << "(maxStorageBufferBindingSize=128MB, maxBufferSize=256MB)"; - } - // Surface uncaptured errors (validation failures etc.) into Log::warn so - // they're attributable rather than silently swallowed. - dev_desc.uncapturedErrorCallbackInfo.callback = onUncapturedError; - - WGPURequestDeviceCallbackInfo dcb = {}; - dcb.mode = WGPUCallbackMode_AllowProcessEvents; - dcb.callback = [](WGPURequestDeviceStatus status, WGPUDevice device, - WGPUStringView message, void* ud1, void* /*ud2*/) { - auto* r = static_cast(ud1); - r->done = true; - if (status == WGPURequestDeviceStatus_Success) { - r->device = device; - r->ok = true; - } else { - Log::warn().noquote() << "RequestDevice failed:" << sv(message); - } - }; - dcb.userdata1 = &dreq; - - wgpuAdapterRequestDevice(adapter_, &dev_desc, dcb); - while (!dreq.done) wgpuInstanceProcessEvents(instance_); - if (!dreq.ok) return false; - device_ = dreq.device; - queue_ = wgpuDeviceGetQueue(device_); - - // ---- Probe streaming pool capacity ---------------------------------- - // Ask the device for the largest single buffer it'll actually give us. - // Replaces the per-machine "guess the OOM ceiling" knob: now the - // runtime answers the question. Failure here is fatal — without any - // pool we can't load chunks. - if (!probeAndCreatePool()) { - Log::warn() << "wgpu: streaming pool probe failed; cannot start"; - return false; - } - - // Background loader for streaming reads — must outlive any - // applyCachedModel call so we can drain results into the - // pool. Stopped in shutdown() before pool_.destroy(). - streaming_thread_.start(); - - // ---- Pick a surface format ------------------------------------------- - WGPUSurfaceCapabilities caps = {}; - if (wgpuSurfaceGetCapabilities(surface_, adapter_, &caps) != WGPUStatus_Success - || caps.formatCount == 0) { - Log::warn() << "wgpuSurfaceGetCapabilities returned no formats"; - return false; - } - surface_format_ = caps.formats[0]; // preferred format per wgpu docs - wgpuSurfaceCapabilitiesFreeMembers(caps); + // ---- ViewportCore handles instance/adapter/device/queue/pool/format - + if (!core_.initWgpu(web_limits_)) return false; + // ---- Pipelines + overlays (still VW-side; HiZ/edge/pick + overlay + // init haven't migrated yet) ------------------------------------- if (!buildPipelines()) return false; if (!buildHizPipeline()) return false; if (!buildEdgePipeline()) return false; @@ -1314,101 +1231,9 @@ bool ViewportWindow::initWgpu() { return false; } if (!buildPickPipeline()) return false; - - Log::info() << "wgpu init OK; surface format =" << int(surface_format_); return true; } -bool ViewportWindow::probeAndCreatePool() { - // Discover the largest single buffer the runtime will grant. We - // descend from the device's advertised maxBufferSize because the - // adapter promises that much per binding, but the underlying - // allocator (gpu-alloc-rs on Vulkan, Metal heap manager, browser - // internals) may refuse anything above an undocumented per-system - // ceiling. The probe answers the question honestly. - // - // Each attempt is wrapped in an OOM error scope so a failed - // allocation doesn't surface to onUncapturedError as a noisy - // validation warning — the scope captures the OOM cleanly and we - // simply halve and retry. - - WGPULimits device_limits = {}; - wgpuDeviceGetLimits(device_, &device_limits); - - // 64 MB lower bound: below this the viewer is unusable for any real - // dataset, so we'd rather fail init than limp along. - constexpr uint64_t MIN_POOL_CAPACITY = 64ull * 1024 * 1024; - // 4 GB starting cap: this is the largest single buffer the WebGPU - // ecosystem realistically supports today (browsers stay well below; - // desktop drivers vary). Asking for the device's full advertised - // maxBufferSize first is wasteful — on wgpu-native it can be 1 TB - // (a sentinel meaning "no spec floor"), which always fails and - // forces ~10 halving steps before we land somewhere sensible. - constexpr uint64_t MAX_PROBE_START = 4ull * 1024 * 1024 * 1024; - uint64_t try_size = std::min(device_limits.maxBufferSize, - MAX_PROBE_START); - 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) { - // wgpu-native classifies "Not enough memory left" as Validation, - // not OutOfMemory — so we need both filters. Nested scopes: OOM - // inner (matches first), Validation outer (catches the rest). - wgpuDevicePushErrorScope(device_, WGPUErrorFilter_Validation); - wgpuDevicePushErrorScope(device_, WGPUErrorFilter_OutOfMemory); - - // Test allocation. If it survives both scopes, this size works - // and becomes the pool's per-sub-buffer capacity. - WGPUBufferDescriptor desc = {}; - desc.usage = pool_usage; - desc.size = try_size; - desc.label.data = "ifcviewer-wgpu.pool_probe"; - desc.label.length = std::strlen("ifcviewer-wgpu.pool_probe"); - WGPUBuffer probe_buf = wgpuDeviceCreateBuffer(device_, &desc); - - struct PopResult { bool done = false; bool error = false; }; - auto pop = [&](PopResult& pr) { - WGPUPopErrorScopeCallbackInfo pcb = {}; - pcb.mode = WGPUCallbackMode_AllowProcessEvents; - pcb.callback = [](WGPUPopErrorScopeStatus, WGPUErrorType type, - WGPUStringView, void* ud1, void* /*ud2*/) { - auto* p = static_cast(ud1); - p->done = true; - p->error = (type != WGPUErrorType_NoError); - }; - pcb.userdata1 = ≺ - wgpuDevicePopErrorScope(device_, pcb); - while (!pr.done) wgpuInstanceProcessEvents(instance_); - }; - PopResult oom_pop, validation_pop; - pop(oom_pop); - pop(validation_pop); - - if (probe_buf) wgpuBufferRelease(probe_buf); - if (probe_buf && !oom_pop.error && !validation_pop.error) { - // Per-sub-buffer capacity locked in; the pool can grow - // beyond this by allocating more sub-buffers of the same - // size on demand (up to whatever the driver lets us total). - pool_.configure(instance_, device_, pool_usage, try_size, - "ifcviewer-wgpu.pool"); - Log::info().noquote() - << "wgpu: pool per-sub-buffer capacity =" - << (try_size / (1024 * 1024)) << "MB" - << "(device maxBufferSize =" - << (device_limits.maxBufferSize / (1024 * 1024)) - << "MB); pool will grow on demand"; - return true; - } - try_size /= 2; - } - - Log::warn() << "wgpu: pool probe found no allocatable size >=" - << (MIN_POOL_CAPACITY / (1024 * 1024)) << "MB"; - return false; -} - // ----------------------------------------------------------------------------- // Surface creation — platform-specific native handle plumbing. // ----------------------------------------------------------------------------- @@ -1442,86 +1267,8 @@ bool ViewportWindow::probeAndCreatePool() { # include "MetalSurface_mac.h" #endif -bool ViewportWindow::createSurface() { - WGPUSurfaceDescriptor surface_desc = {}; - -#if defined(Q_OS_LINUX) - const QString platform = QGuiApplication::platformName(); - if (platform == "xcb") { -# if __has_include() - auto* x11 = qApp->nativeInterface(); - if (!x11 || !x11->display()) { - Log::warn() << "Could not get X11 Display* from Qt"; - return false; - } - WGPUSurfaceSourceXlibWindow xlib = {}; - xlib.chain.sType = WGPUSType_SurfaceSourceXlibWindow; - xlib.display = x11->display(); - xlib.window = static_cast(winId()); - surface_desc.nextInChain = &xlib.chain; - surface_ = wgpuInstanceCreateSurface(instance_, &surface_desc); -# else - Log::warn() << "Built without Xlib headers; cannot create X11 surface"; - return false; -# endif - } else if (platform == "wayland") { -# if __has_include() - auto* wl = qApp->nativeInterface(); - if (!wl || !wl->display()) { - Log::warn() << "Could not get Wayland wl_display* from Qt"; - return false; - } - // The wl_surface for a window is exposed via the QPA window-handle - // accessor on the native interface (not the application-wide one). - // For stage 1 we fail loud; stage-1.5 fills this in. - Log::warn() << "Wayland wgpu surface creation not yet wired (stage 1.5)"; - return false; -# else - Log::warn() << "Built without Wayland headers; cannot create Wayland surface"; - return false; -# endif - } else { - Log::warn().noquote() << "Unsupported Qt platform for wgpu surface:" << platform; - return false; - } -#elif defined(Q_OS_WIN) - // wgpu-native maps WGPUSurfaceSourceWindowsHWND.hwnd to a Win32 HWND - // it never dereferences directly (it only hands the handle to D3D12 / - // Vulkan WSI). winId() is the HWND for top-level Qt windows on the - // Windows platform plugin, returned as WId (== quintptr). - WGPUSurfaceSourceWindowsHWND hwndsrc = {}; - hwndsrc.chain.sType = WGPUSType_SurfaceSourceWindowsHWND; - hwndsrc.hinstance = ::GetModuleHandleW(nullptr); - hwndsrc.hwnd = reinterpret_cast(static_cast(winId())); - surface_desc.nextInChain = &hwndsrc.chain; - surface_ = wgpuInstanceCreateSurface(instance_, &surface_desc); -#elif defined(Q_OS_MAC) - // QWindow::winId() returns the backing NSView* on macOS (as WId, - // which is quintptr — same width as void* on all macOS arches we - // care about). Hand it to the Cocoa bridge to attach a - // CAMetalLayer, then wrap that layer in WGPUSurfaceSourceMetalLayer. - void* nsview = reinterpret_cast(static_cast(winId())); - void* layer = wgpu_macos_attach_metal_layer(nsview); - if (!layer) { - Log::warn() << "Could not attach CAMetalLayer to the Qt NSView"; - return false; - } - WGPUSurfaceSourceMetalLayer metalsrc = {}; - metalsrc.chain.sType = WGPUSType_SurfaceSourceMetalLayer; - metalsrc.layer = layer; - surface_desc.nextInChain = &metalsrc.chain; - surface_ = wgpuInstanceCreateSurface(instance_, &surface_desc); -#else - Log::warn() << "wgpu surface creation not yet wired for this platform"; - return false; -#endif - - if (!surface_) { - Log::warn() << "wgpuInstanceCreateSurface returned null"; - return false; - } - return true; -} +// Private bool createSurface() removed in #84-l — its body is now +// inside the public ViewportHost override createSurface(WGPUInstance). // ----------------------------------------------------------------------------- // Surface (re)configure + render @@ -6919,15 +6666,8 @@ void ViewportWindow::wheelEvent(QWheelEvent* event) { } void ViewportWindow::shutdown() { - // Stop the streaming worker first so no late results land in the - // pool after we've torn down the model state. Pending in-flight - // reads are completed (worker drains its queue) then thread joins. - streaming_thread_.stop(); - - // Release per-model buffers before the device they were created from. - for (auto& [mid, m] : models_gpu_) releaseWgpuModelGpuData(m, pool_); - models_gpu_.clear(); - + // VW-only resources first — these depend on core_'s device_ being + // alive, so they must be released before core_.shutdown() releases it. releaseDepthTexture(); releaseMsaaColorTexture(); releaseHizResources(); @@ -6935,28 +6675,8 @@ void ViewportWindow::shutdown() { overlays_.destroy(); releasePickResources(); - if (frame_bind_group_) { wgpuBindGroupRelease(frame_bind_group_); frame_bind_group_ = nullptr; } - if (frame_uniform_buffer_) { wgpuBufferRelease(frame_uniform_buffer_); frame_uniform_buffer_ = nullptr; } - if (selection_flags_buffer_) { wgpuBufferRelease(selection_flags_buffer_); selection_flags_buffer_ = nullptr; } - selection_flags_capacity_ = 0; - if (main_pipeline_) { wgpuRenderPipelineRelease(main_pipeline_); main_pipeline_ = nullptr; } - if (main_pipeline_transparent_) { wgpuRenderPipelineRelease(main_pipeline_transparent_); main_pipeline_transparent_ = nullptr; } - if (main_shader_module_) { wgpuShaderModuleRelease(main_shader_module_); main_shader_module_ = nullptr; } - if (pipeline_layout_) { wgpuPipelineLayoutRelease(pipeline_layout_); pipeline_layout_ = nullptr; } - if (model_bgl_) { wgpuBindGroupLayoutRelease(model_bgl_); model_bgl_ = nullptr; } - if (frame_bgl_) { wgpuBindGroupLayoutRelease(frame_bgl_); frame_bgl_ = nullptr; } - - // Destroy the streaming pool while device_ is still alive (it owns - // the underlying WGPUBuffer). All chunks have already returned their - // ranges via releaseWgpuModelGpuData above; pool's free-list count - // should equal capacity at this point. - pool_.destroy(); - - if (queue_) { wgpuQueueRelease(queue_); queue_ = nullptr; } - if (device_) { wgpuDeviceRelease(device_); device_ = nullptr; } - if (adapter_) { wgpuAdapterRelease(adapter_); adapter_ = nullptr; } - if (surface_) { wgpuSurfaceRelease(surface_); surface_ = nullptr; } - if (instance_) { wgpuInstanceRelease(instance_); instance_ = nullptr; } - wgpu_initialized_ = false; - surface_configured_ = false; + // Core owns the rest: streaming thread, models, pool, frame/selection + // buffers, pipelines/shaders/layouts, queue/device/adapter/surface/ + // instance. + core_.shutdown(); } diff --git a/src/ifcviewer/ViewportWindow.h b/src/ifcviewer/ViewportWindow.h index 0a3f08f6d7..efbb3223cb 100644 --- a/src/ifcviewer/ViewportWindow.h +++ b/src/ifcviewer/ViewportWindow.h @@ -297,11 +297,6 @@ private: // candidate needs the space. Triggers requestUpdate() if more remain. void driveStreamingLoads(); - // Discover the largest single buffer wgpu will give us by descending - // from the device's limits.maxBufferSize through OOM error scopes. - // Allocates pool_ at the discovered size. Returns false only when - // even a tiny pool can't be created (i.e. the device is unusable). - bool probeAndCreatePool(); void ensureDepthTexture(int w, int h); void releaseDepthTexture(); void ensureMsaaColorTexture(int w, int h); diff --git a/src/ifcviewer/tests/test_federation.cpp b/src/ifcviewer/tests/test_federation.cpp index b95f866dde..9d78a50bde 100644 --- a/src/ifcviewer/tests/test_federation.cpp +++ b/src/ifcviewer/tests/test_federation.cpp @@ -31,7 +31,7 @@ #include #include #include -#include +#include #include @@ -121,7 +121,7 @@ TEST_CASE("setHomeView / clearHomeView toggle dirty + has_home_view", "[federati QSignalSpy spy(&fed, &Federation::dirtyChanged); Federation::HomeView hv; - hv.target = QVector3D(1, 2, 3); + hv.target = Eigen::Vector3f(1, 2, 3); hv.distance = 12.5f; hv.yaw = 33.0f; hv.pitch = 22.0f; @@ -207,7 +207,7 @@ TEST_CASE("save then load round-trips models, transform, visibility, home view", src.setModelVisible(id2, false); Federation::HomeView hv; - hv.target = QVector3D(10, 20, 30); + hv.target = Eigen::Vector3f(10, 20, 30); hv.distance = 77.0f; hv.yaw = 11.0f; hv.pitch = 7.0f; @@ -236,7 +236,7 @@ TEST_CASE("save then load round-trips models, transform, visibility, home view", REQUIRE_FALSE(dst.models()[1].visible); REQUIRE(dst.hasHomeView()); - REQUIRE(dst.homeView().target == QVector3D(10, 20, 30)); + REQUIRE(dst.homeView().target == Eigen::Vector3f(10, 20, 30)); REQUIRE(dst.homeView().distance == 77.0f); REQUIRE(dst.homeView().yaw == 11.0f); REQUIRE(dst.homeView().pitch == 7.0f);