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