From 9daa5fe19518cb98d14e4b327c081269956edeb9 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Wed, 27 May 2026 12:48:03 +1000 Subject: [PATCH] wgpu backend: load .ifcview sidecars onto GPU buffers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Stage 2 of the wgpu port. WgpuViewportWindow gains a queueLoadSidecar API (called from the minimal driver before init) and an applyCachedModel that runs after init: reads via SidecarCache::readSidecar, allocates four wgpu buffers per model (vertex storage, index, mesh-quant storage, instance storage), uploads via wgpuQueueWriteBuffer, retains a CPU mirror of the MeshInfo/InstanceCpu arrays for the cull and picking paths that arrive in later stages. MeshGpu (the per-mesh quantization basis) is derived from MeshInfo on the fly; InstanceGpu (transform + ids) is derived from InstanceCpu and uses the cached float transform — composing from placement_transformation against federation-stage matrices lands when stage 5 wires those. SidecarCache.cpp is compiled into IfcViewerWgpu directly: it's pure C++ with no Qt/OCCT/IFC-parse deps, so dragging in the IfcViewer static lib for one source file would be wasteful. This duplication goes away once src/ifcviewer-core/ is extracted (task #12). Verified on a synthesised v13 sidecar (4 verts, 6 indices, 1 mesh, 1 instance) and a multi-sidecar load that assigns successive model_ids. Co-Authored-By: Claude Opus 4.7 --- src/ifcviewer-wgpu-minimal/main.cpp | 13 +- src/ifcviewer-wgpu/CMakeLists.txt | 15 +- src/ifcviewer-wgpu/WgpuModelGpuData.h | 71 ++++++++ src/ifcviewer-wgpu/WgpuViewportWindow.cpp | 187 ++++++++++++++++++++++ src/ifcviewer-wgpu/WgpuViewportWindow.h | 44 ++++- 5 files changed, 325 insertions(+), 5 deletions(-) create mode 100644 src/ifcviewer-wgpu/WgpuModelGpuData.h diff --git a/src/ifcviewer-wgpu-minimal/main.cpp b/src/ifcviewer-wgpu-minimal/main.cpp index 770e8ffc55..a4de3b44e1 100644 --- a/src/ifcviewer-wgpu-minimal/main.cpp +++ b/src/ifcviewer-wgpu-minimal/main.cpp @@ -36,8 +36,11 @@ int main(int argc, char* argv[]) { QCommandLineParser parser; parser.setApplicationDescription( - "IfcOpenShell minimal wgpu IFC viewer (stage 1: clear-color smoke test)"); + "IfcOpenShell minimal wgpu IFC viewer (stage 2: sidecar load, no draw)"); parser.addHelpOption(); + parser.addPositionalArgument("files", + "Sidecar (.ifcview) files to load. Stem-based: foo.ifc resolves to foo.ifcview.", + "[files...]"); parser.process(app); auto* viewport = new WgpuViewportWindow; @@ -47,10 +50,16 @@ int main(int argc, char* argv[]) { container->setMinimumSize(320, 240); QMainWindow main_window; - main_window.setWindowTitle("IfcViewer (wgpu) — stage 1"); + main_window.setWindowTitle("IfcViewer (wgpu) — stage 2"); main_window.setCentralWidget(container); main_window.resize(1280, 800); main_window.show(); + // Queue sidecars; they're loaded after wgpu init completes in + // exposeEvent. Ordering matches the command line. + for (const QString& path : parser.positionalArguments()) { + viewport->queueLoadSidecar(path); + } + return app.exec(); } diff --git a/src/ifcviewer-wgpu/CMakeLists.txt b/src/ifcviewer-wgpu/CMakeLists.txt index 85fe074d44..1fab6233e3 100644 --- a/src/ifcviewer-wgpu/CMakeLists.txt +++ b/src/ifcviewer-wgpu/CMakeLists.txt @@ -100,6 +100,16 @@ file(GLOB IFCVIEWER_WGPU_CPP_FILES ${CMAKE_CURRENT_SOURCE_DIR}/*.cpp) file(GLOB IFCVIEWER_WGPU_H_FILES ${CMAKE_CURRENT_SOURCE_DIR}/*.h) set(IFCVIEWER_WGPU_FILES ${IFCVIEWER_WGPU_CPP_FILES} ${IFCVIEWER_WGPU_H_FILES}) +# Intentional source-level borrowing from the GL backend until ifcviewer-core +# is extracted (task #12). SidecarCache + InstancedGeometry have zero Qt / +# OCCT / IFC-parse deps, so compiling them directly into IfcViewerWgpu is +# cheaper than dragging in the IfcViewer static lib (which would pull all +# of IfcGeom + IfcParse + OCCT + Qt OpenGL). +set(IFCVIEWER_SHARED_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../ifcviewer) +list(APPEND IFCVIEWER_WGPU_FILES + ${IFCVIEWER_SHARED_DIR}/SidecarCache.cpp +) + add_library(IfcViewerWgpu STATIC ${IFCVIEWER_WGPU_FILES}) set_target_properties(IfcViewerWgpu PROPERTIES @@ -108,7 +118,10 @@ set_target_properties(IfcViewerWgpu PROPERTIES SOVERSION "${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}" ) -target_include_directories(IfcViewerWgpu PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) +target_include_directories(IfcViewerWgpu + PUBLIC ${CMAKE_CURRENT_SOURCE_DIR} + PUBLIC ${IFCVIEWER_SHARED_DIR} # SidecarCache.h is reachable via WgpuViewportWindow.h +) target_link_libraries(IfcViewerWgpu PUBLIC Qt${QT_VERSION}::Core diff --git a/src/ifcviewer-wgpu/WgpuModelGpuData.h b/src/ifcviewer-wgpu/WgpuModelGpuData.h new file mode 100644 index 0000000000..fa77dbe24f --- /dev/null +++ b/src/ifcviewer-wgpu/WgpuModelGpuData.h @@ -0,0 +1,71 @@ +/******************************************************************************** + * * + * This file is part of IfcOpenShell. * + * * + * IfcOpenShell is free software: you can redistribute it and/or modify * + * it under the terms of the Lesser GNU General Public License as published by * + * the Free Software Foundation, either version 3.0 of the License, or * + * (at your option) any later version. * + * * + * IfcOpenShell is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * Lesser GNU General Public License for more details. * + * * + * You should have received a copy of the Lesser GNU General Public License * + * along with this program. If not, see . * + * * + ********************************************************************************/ + +#ifndef WGPUMODELGPUDATA_H +#define WGPUMODELGPUDATA_H + +#include + +#include +#include +#include + +#include "InstancedGeometry.h" + +// Per-model wgpu state. Mirrors the GL backend's ModelGpuData but with +// wgpu handles. Stage 2 only allocates and uploads the four core buffers; +// bind groups, pipelines, BVH and cull scratch land in later stages. +// +// All vertex/index/mesh/instance bytes are uploaded once at load time via +// wgpuQueueWriteBuffer. The vertex storage buffer is read by the vertex +// shader (vertex pulling), not used as a classic vertex buffer — there is +// no input-assembler vertex layout to match. +struct WgpuModelGpuData { + // Raw VBO bytes at the INSTANCED_VERTEX_STRIDE_BYTES layout (12 B/vertex). + // Bound as a read-only storage buffer in the vertex shader. + WGPUBuffer vertex_storage = nullptr; + // Mesh-local u32 indices. base_vertex applied at draw time so a single + // index buffer per model is shared across meshes. + WGPUBuffer index_buffer = nullptr; + // MeshGpu[] — per-mesh quantization basis (aabb_min/max as vec4 pair). + // Derived from MeshInfo on upload. + WGPUBuffer mesh_storage = nullptr; + // InstanceGpu[] — per-instance transform + ids. Derived from InstanceCpu + // on upload. Stage 2 stores the cached `transform`; later stages will + // recompose from placement_transformation when stage matrices change. + WGPUBuffer instance_storage = nullptr; + + // Size mirrors for stats / range checks. + size_t vertex_bytes = 0; + uint32_t index_count = 0; + uint32_t mesh_count = 0; + uint32_t instance_count = 0; + + // CPU side, kept for cull / picking / federation recompose. + std::vector meshes; + std::vector instances; + + bool hidden = false; +}; + +// Release every wgpu handle in `m` and clear its size mirrors. Safe to call +// repeatedly; idempotent on already-released entries. +void releaseWgpuModelGpuData(WgpuModelGpuData& m); + +#endif // WGPUMODELGPUDATA_H diff --git a/src/ifcviewer-wgpu/WgpuViewportWindow.cpp b/src/ifcviewer-wgpu/WgpuViewportWindow.cpp index 3f7bede2f0..5c07872b07 100644 --- a/src/ifcviewer-wgpu/WgpuViewportWindow.cpp +++ b/src/ifcviewer-wgpu/WgpuViewportWindow.cpp @@ -22,10 +22,12 @@ #include #include #include +#include #include // wgpu-native extensions (logging, MULTI_DRAW_INDIRECT, …) #include +#include // ----------------------------------------------------------------------------- // Small helpers @@ -58,6 +60,43 @@ static void onUncapturedError(WGPUDevice const* /*device*/, qWarning().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 +// in validation messages when something goes wrong. +static WGPUBuffer createBufferWithData(WGPUDevice device, WGPUQueue queue, + const void* data, size_t size_bytes, + WGPUBufferUsage usage, + const char* label) { + if (size_bytes == 0) return nullptr; + + WGPUBufferDescriptor desc = {}; + desc.size = uint64_t(size_bytes); + desc.usage = usage | WGPUBufferUsage_CopyDst; + if (label) { + desc.label.data = label; + desc.label.length = std::strlen(label); + } + WGPUBuffer buf = wgpuDeviceCreateBuffer(device, &desc); + if (buf && data) { + wgpuQueueWriteBuffer(queue, buf, 0, data, size_bytes); + } + return buf; +} + +void releaseWgpuModelGpuData(WgpuModelGpuData& m) { + if (m.vertex_storage) { wgpuBufferRelease(m.vertex_storage); m.vertex_storage = nullptr; } + if (m.index_buffer) { wgpuBufferRelease(m.index_buffer); m.index_buffer = nullptr; } + if (m.mesh_storage) { wgpuBufferRelease(m.mesh_storage); m.mesh_storage = nullptr; } + if (m.instance_storage) { wgpuBufferRelease(m.instance_storage); m.instance_storage = nullptr; } + m.vertex_bytes = 0; + m.index_count = 0; + m.mesh_count = 0; + m.instance_count = 0; + m.meshes.clear(); + m.instances.clear(); +} + // ----------------------------------------------------------------------------- // Construction / destruction // ----------------------------------------------------------------------------- @@ -80,6 +119,147 @@ void WgpuViewportWindow::setBackgroundColor(const QColor& color) { if (isExposed()) requestUpdate(); } +// ----------------------------------------------------------------------------- +// Sidecar load + GPU upload +// ----------------------------------------------------------------------------- + +void WgpuViewportWindow::queueLoadSidecar(const QString& path) { + if (wgpu_initialized_) { + loadSidecar(path); + } else { + pending_sidecars_.push_back(path); + } +} + +uint32_t WgpuViewportWindow::loadSidecar(const QString& path) { + if (!wgpu_initialized_) { + qWarning().noquote() << "loadSidecar called before wgpu init:" << path; + return 0; + } + + auto data_opt = readSidecar(path.toStdString()); + if (!data_opt) { + qWarning().noquote() << "Failed to read sidecar:" << path + << "(file missing, wrong magic, or schema mismatch)"; + return 0; + } + + const uint32_t mid = next_model_id_++; + applyCachedModel(mid, std::move(*data_opt)); + return mid; +} + +void WgpuViewportWindow::applyCachedModel(uint32_t model_id, SidecarData data) { + if (!device_ || !queue_) { + qWarning() << "applyCachedModel without an initialised device"; + return; + } + + // Replace any existing state for this id. + auto it = models_gpu_.find(model_id); + if (it != models_gpu_.end()) { + releaseWgpuModelGpuData(it->second); + models_gpu_.erase(it); + } + + WgpuModelGpuData m; + m.vertex_bytes = data.vertices.size(); + m.index_count = uint32_t(data.indices.size()); + m.mesh_count = uint32_t(data.meshes.size()); + m.instance_count = uint32_t(data.instances.size()); + + // Vertex storage — raw bytes at INSTANCED_VERTEX_STRIDE_BYTES layout. The + // vertex shader will read this as a u8 storage buffer in stage 3. + m.vertex_storage = createBufferWithData( + device_, queue_, + data.vertices.data(), data.vertices.size(), + WGPUBufferUsage_Storage, + "model.vertex_storage"); + + // Index buffer — mesh-local u32 indices; baseVertex applied per-draw. + m.index_buffer = createBufferWithData( + device_, queue_, + data.indices.data(), data.indices.size() * sizeof(uint32_t), + WGPUBufferUsage_Index, + "model.index_buffer"); + + // Derive MeshGpu[] (vec4 aabb_min + vec4 aabb_max) from MeshInfo's + // local_aabb_*. Mirrors the GL backend's mesh_info_ssbo population. + std::vector mesh_gpu; + mesh_gpu.reserve(data.meshes.size()); + for (const auto& mi : data.meshes) { + MeshGpu mg = {}; + mg.aabb_min[0] = mi.local_aabb_min[0]; + mg.aabb_min[1] = mi.local_aabb_min[1]; + mg.aabb_min[2] = mi.local_aabb_min[2]; + mg.aabb_min[3] = 0.0f; + mg.aabb_max[0] = mi.local_aabb_max[0]; + mg.aabb_max[1] = mi.local_aabb_max[1]; + mg.aabb_max[2] = mi.local_aabb_max[2]; + mg.aabb_max[3] = 0.0f; + mesh_gpu.push_back(mg); + } + m.mesh_storage = createBufferWithData( + device_, queue_, + mesh_gpu.data(), mesh_gpu.size() * sizeof(MeshGpu), + WGPUBufferUsage_Storage, + "model.mesh_storage"); + + // Derive InstanceGpu[] from InstanceCpu[]. Stage 2 uses the cached + // `transform` directly (stage matrices are identity until stage 5+ + // adds federation composition). + std::vector inst_gpu; + inst_gpu.reserve(data.instances.size()); + for (const auto& ic : data.instances) { + InstanceGpu ig = {}; + std::memcpy(ig.transform, ic.transform, sizeof(ig.transform)); + ig.object_id = ic.object_id; + ig.color_override_rgba8 = ic.color_override_rgba8; + ig.mesh_id = ic.mesh_id; + inst_gpu.push_back(ig); + } + m.instance_storage = createBufferWithData( + device_, queue_, + inst_gpu.data(), inst_gpu.size() * sizeof(InstanceGpu), + WGPUBufferUsage_Storage, + "model.instance_storage"); + + // Hand off CPU mirrors (cull / picking will need them later). + m.meshes = std::move(data.meshes); + m.instances = std::move(data.instances); + + models_gpu_.emplace(model_id, std::move(m)); + + qInfo().noquote().nospace() + << "[wgpu] applyCachedModel mid=" << model_id + << " verts=" << m.vertex_bytes << "B" + << " idx=" << m.index_count + << " meshes=" << m.mesh_count + << " instances=" << m.instance_count; +} + +void WgpuViewportWindow::removeModel(uint32_t model_id) { + auto it = models_gpu_.find(model_id); + if (it == models_gpu_.end()) return; + releaseWgpuModelGpuData(it->second); + models_gpu_.erase(it); + if (isExposed()) requestUpdate(); +} + +void WgpuViewportWindow::resetScene() { + for (auto& [mid, m] : models_gpu_) releaseWgpuModelGpuData(m); + models_gpu_.clear(); + if (isExposed()) requestUpdate(); +} + +void WgpuViewportWindow::flushPendingSidecarQueue() { + while (!pending_sidecars_.empty()) { + const QString p = pending_sidecars_.front(); + pending_sidecars_.pop_front(); + loadSidecar(p); + } +} + // ----------------------------------------------------------------------------- // Lifecycle // ----------------------------------------------------------------------------- @@ -93,6 +273,9 @@ void WgpuViewportWindow::exposeEvent(QExposeEvent* /*event*/) { return; } wgpu_initialized_ = true; + // Drain any sidecar paths queued before init; uploads run on the + // now-valid device. + flushPendingSidecarQueue(); } const int w = int(width() * devicePixelRatio()); @@ -367,6 +550,10 @@ void WgpuViewportWindow::render() { } void WgpuViewportWindow::shutdown() { + // Release per-model buffers before the device they were created from. + for (auto& [mid, m] : models_gpu_) releaseWgpuModelGpuData(m); + models_gpu_.clear(); + if (queue_) { wgpuQueueRelease(queue_); queue_ = nullptr; } if (device_) { wgpuDeviceRelease(device_); device_ = nullptr; } if (adapter_) { wgpuAdapterRelease(adapter_); adapter_ = nullptr; } diff --git a/src/ifcviewer-wgpu/WgpuViewportWindow.h b/src/ifcviewer-wgpu/WgpuViewportWindow.h index 5d3004fb9c..3bb38f57d3 100644 --- a/src/ifcviewer-wgpu/WgpuViewportWindow.h +++ b/src/ifcviewer-wgpu/WgpuViewportWindow.h @@ -22,12 +22,22 @@ #include #include +#include #include -// Stage-1 wgpu viewport: opens a native QWindow, brings up a wgpu instance/ +#include +#include +#include + +#include "SidecarCache.h" +#include "WgpuModelGpuData.h" + +// Stage-2 wgpu viewport: opens a native QWindow, brings up a wgpu instance/ // adapter/device, configures a surface against the platform-native window -// handle, and clears to background_color_ on every UpdateRequest. +// handle, and clears to background_color_ on every UpdateRequest. Models +// loaded from `.ifcview` sidecars are uploaded as wgpu buffers (no draw +// path yet — that's stage 3). // // Mirrors the lifecycle shape of the GL ViewportWindow so subsequent stages // can grow this into a full IFC renderer without restructuring the host. @@ -39,6 +49,27 @@ public: void setBackgroundColor(const QColor& color); + // Queue a sidecar path to be loaded after wgpu init completes. Safe to + // call before the window is exposed. The path is resolved against the + // working directory and read via SidecarCache::readSidecar (which + // normalises stem → .ifcview). + void queueLoadSidecar(const QString& path); + + // Synchronous load + GPU upload. Requires wgpu init to have completed + // (i.e. the window has been exposed at least once). Returns the + // assigned model_id, or 0 on failure. + uint32_t loadSidecar(const QString& path); + + // Restore a finalised model from a SidecarData struct: allocate wgpu + // buffers, upload vertex/index/mesh/instance bytes, register in + // models_gpu_. Replaces any existing state for model_id. + void applyCachedModel(uint32_t model_id, SidecarData data); + + void removeModel(uint32_t model_id); + void resetScene(); + + size_t modelCount() const { return models_gpu_.size(); } + protected: void exposeEvent(QExposeEvent* event) override; void resizeEvent(QResizeEvent* event) override; @@ -51,6 +82,8 @@ private: void render(); void shutdown(); + void flushPendingSidecarQueue(); + bool wgpu_initialized_ = false; bool surface_configured_ = false; int configured_w_ = 0; @@ -64,6 +97,13 @@ private: WGPUTextureFormat surface_format_ = WGPUTextureFormat_Undefined; QColor background_color_ = QColor("#202329"); + + // Per-model state, keyed by viewport-assigned model_id. + std::unordered_map models_gpu_; + uint32_t next_model_id_ = 1; + + // Sidecar paths queued before init completes. + std::deque pending_sidecars_; }; #endif // WGPUVIEWPORTWINDOW_H