mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-11 02:02:22 +00:00
wgpu backend: load .ifcview sidecars onto GPU buffers
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 <noreply@anthropic.com>
This commit is contained in:
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 <http://www.gnu.org/licenses/>. *
|
||||
* *
|
||||
********************************************************************************/
|
||||
|
||||
#ifndef WGPUMODELGPUDATA_H
|
||||
#define WGPUMODELGPUDATA_H
|
||||
|
||||
#include <webgpu/webgpu.h>
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
|
||||
#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<MeshInfo> meshes;
|
||||
std::vector<InstanceCpu> 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
|
||||
@@ -22,10 +22,12 @@
|
||||
#include <QGuiApplication>
|
||||
#include <QResizeEvent>
|
||||
#include <QDebug>
|
||||
#include <QFileInfo>
|
||||
|
||||
#include <webgpu/wgpu.h> // wgpu-native extensions (logging, MULTI_DRAW_INDIRECT, …)
|
||||
|
||||
#include <cstring>
|
||||
#include <utility>
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// 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<MeshGpu> 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<InstanceGpu> 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; }
|
||||
|
||||
@@ -22,12 +22,22 @@
|
||||
|
||||
#include <QWindow>
|
||||
#include <QColor>
|
||||
#include <QString>
|
||||
|
||||
#include <webgpu/webgpu.h>
|
||||
|
||||
// Stage-1 wgpu viewport: opens a native QWindow, brings up a wgpu instance/
|
||||
#include <cstdint>
|
||||
#include <deque>
|
||||
#include <unordered_map>
|
||||
|
||||
#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<uint32_t, WgpuModelGpuData> models_gpu_;
|
||||
uint32_t next_model_id_ = 1;
|
||||
|
||||
// Sidecar paths queued before init completes.
|
||||
std::deque<QString> pending_sidecars_;
|
||||
};
|
||||
|
||||
#endif // WGPUVIEWPORTWINDOW_H
|
||||
|
||||
Reference in New Issue
Block a user