From 78931357906357bd4f28267dd7d33ab15ade2cbf Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Wed, 27 May 2026 14:58:18 +1000 Subject: [PATCH] wgpu backend: --camera flag + request adapter's max buffer limits MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two pieces that block proper side-by-side parity with the GL minimal: 1. --camera tx,ty,tz,dist,yaw,pitch. Same format string as the GL minimal so a pasted camera arg lands the same view on both backends. setCamera() also flips initial_view_applied_ = true so the auto- viewAll-on-first-load doesn't snap away from the script-set position when the model finishes uploading. 2. Real BIM models exceed the conservative WebGPU defaults at device create time. A 114k-instance / 19M-index sidecar's vertex storage is 139 MB, which trips wgpu's default 128 MB max_storage_buffer_binding_ size and bind-group creation fails. Now wgpuAdapterGetLimits is called first and the device is requested at the adapter's full ceiling — every desktop driver supports multi-GB. Trade-off worth flagging: web parity will fail here because browsers cap at the defaults. The eventual fix is to split a model's vertex/ instance storage into ≤128 MB chunks with a small per-frame routing table, which is a real chunk of work. For now this unblocks all the native benchmarking the user is actually doing. Co-Authored-By: Claude Opus 4.7 --- src/ifcviewer-wgpu-minimal/main.cpp | 20 +++++++++++++++++ src/ifcviewer-wgpu/WgpuViewportWindow.cpp | 26 +++++++++++++++++++++++ src/ifcviewer-wgpu/WgpuViewportWindow.h | 11 ++++++++-- 3 files changed, 55 insertions(+), 2 deletions(-) diff --git a/src/ifcviewer-wgpu-minimal/main.cpp b/src/ifcviewer-wgpu-minimal/main.cpp index 2f4a4f2d6b..d26c192e68 100644 --- a/src/ifcviewer-wgpu-minimal/main.cpp +++ b/src/ifcviewer-wgpu-minimal/main.cpp @@ -45,6 +45,9 @@ int main(int argc, char* argv[]) { "Render one frame, save to PATH as PNG, exit.", "path"}); parser.addOption({{"b", "benchmark"}, "Render N frames (yaw-sweeping the camera), print stats, exit.", "frames"}); + parser.addOption({{"c", "camera"}, + "Set camera as tx,ty,tz,dist,yaw,pitch (same format as IfcViewerMinimal).", + "params"}); parser.process(app); auto* viewport = new WgpuViewportWindow; @@ -65,6 +68,23 @@ int main(int argc, char* argv[]) { viewport->queueLoadSidecar(path); } + if (parser.isSet("camera")) { + const QStringList parts = parser.value("camera").split(','); + if (parts.size() == 6) { + bool ok = true; + float v[6]; + for (int i = 0; i < 6 && ok; ++i) v[i] = parts[i].toFloat(&ok); + if (ok) { + viewport->setCamera(v[0], v[1], v[2], v[3], v[4], v[5]); + } else { + qWarning() << "--camera: failed to parse" << parser.value("camera"); + } + } else { + qWarning() << "--camera: expected 6 comma-separated floats, got" + << parts.size(); + } + } + if (parser.isSet("screenshot")) { viewport->captureNextFrameToPng(parser.value("screenshot"), /*quit_after=*/true); diff --git a/src/ifcviewer-wgpu/WgpuViewportWindow.cpp b/src/ifcviewer-wgpu/WgpuViewportWindow.cpp index c9feea589b..eb80557072 100644 --- a/src/ifcviewer-wgpu/WgpuViewportWindow.cpp +++ b/src/ifcviewer-wgpu/WgpuViewportWindow.cpp @@ -631,7 +631,19 @@ bool WgpuViewportWindow::initWgpu() { struct DeviceReq { WGPUDevice device = nullptr; bool done = false; bool ok = false; }; DeviceReq dreq; + // Query what the adapter can do, and request the same headroom on the + // device so a single large model's vertex/instance storage buffer doesn't + // hit the conservative defaults (128 MB binding, 256 MB buffer). Real + // BIM models routinely cross 100 MB of vertex bytes; without this the + // first applyCachedModel fails with a bind-group validation error. + // + // For eventual web parity this needs revisiting — browsers cap at the + // defaults — but native targets always support the requested ceilings. + WGPULimits adapter_limits = {}; + wgpuAdapterGetLimits(adapter_, &adapter_limits); + WGPUDeviceDescriptor dev_desc = {}; + dev_desc.requiredLimits = &adapter_limits; // Surface uncaptured errors (validation failures etc.) into qWarning so // they're attributable rather than silently swallowed. dev_desc.uncapturedErrorCallbackInfo.callback = onUncapturedError; @@ -1543,6 +1555,20 @@ bool WgpuViewportWindow::computeSceneAabb(float mn[3], float mx[3]) const { return any; } +void WgpuViewportWindow::setCamera(float tx, float ty, float tz, + float dist, float yaw_deg, float pitch_deg) { + camera_target_[0] = tx; + camera_target_[1] = ty; + camera_target_[2] = tz; + camera_distance_ = std::max(0.01f, dist); + camera_yaw_deg_ = yaw_deg; + camera_pitch_deg_ = std::clamp(pitch_deg, -89.9f, 89.9f); + // Suppress the auto-viewAll on the first model load so the script-set + // camera survives. Manual viewAll() calls after this still work. + initial_view_applied_ = true; + if (isExposed()) requestUpdate(); +} + void WgpuViewportWindow::viewAll() { float mn[3], mx[3]; if (!computeSceneAabb(mn, mx)) return; diff --git a/src/ifcviewer-wgpu/WgpuViewportWindow.h b/src/ifcviewer-wgpu/WgpuViewportWindow.h index fc0e31cb56..348307eb43 100644 --- a/src/ifcviewer-wgpu/WgpuViewportWindow.h +++ b/src/ifcviewer-wgpu/WgpuViewportWindow.h @@ -72,10 +72,17 @@ public: size_t modelCount() const { return models_gpu_.size(); } // Frame the union of all loaded models' world AABBs. No-op on empty - // scenes. Called automatically after the first model loads; clients - // can re-invoke to re-frame. + // scenes. Called automatically after the first model loads (unless + // setCamera was already invoked); clients can re-invoke to re-frame. void viewAll(); + // Explicit camera state, mirroring the GL ViewportWindow API. Suppresses + // the auto-viewAll on first load so a script-driven camera survives + // model loading. Parameters match the GL --camera tx,ty,tz,dist,yaw,pitch + // order so a pasted camera string lands the same view in both backends. + void setCamera(float tx, float ty, float tz, + float dist, float yaw_deg, float pitch_deg); + // Queue a one-shot framebuffer capture: the next rendered frame is // copied back to host memory and saved to `path` as PNG. If // `quit_after` is true, QCoreApplication::quit() is called once the