diff --git a/cmake/CMakeLists.txt b/cmake/CMakeLists.txt
index 4521785b35..ef05a66936 100644
--- a/cmake/CMakeLists.txt
+++ b/cmake/CMakeLists.txt
@@ -73,6 +73,7 @@ option(BUILD_IFCMAX "Build IfcMax, a 3ds Max plug-in, Windows-only." OFF)
option(BUILD_QTVIEWER "Build IfcOpenShell Qt GUI Viewer" OFF) # QtViewer requires Qt6
option(BUILD_BONSAIVIEWER "Build Bonsai Viewer" OFF) # Requires Qt6 + OpenGL 4.5
option(BUILD_BONSAIVIEWER_TESTS "Build unit tests for Bonsai Viewer core (fetches Catch2 v3)" OFF)
+option(BUILD_BONSAIVIEWER_WGPU "Build the experimental wgpu backend (fetches wgpu-native binary release)" OFF)
option(BUILD_PACKAGE "" OFF)
option(
@@ -690,6 +691,16 @@ if(BUILD_BONSAIVIEWER)
add_subdirectory(../src/bonsaiviewer bonsaiviewer)
endif()
+# The wgpu backend is gated independently of BUILD_BONSAIVIEWER: it shares
+# Qt but does not depend on the GL viewer's static lib, so a developer can
+# build it on its own to iterate on the port without compiling IfcGeom etc.
+# However most stages of the port will reference Federation/SceneLoader
+# headers from src/ifcviewer, so in practice it ships alongside.
+if(BUILD_BONSAIVIEWER_WGPU)
+ add_subdirectory(../src/ifcviewer-wgpu ifcviewer-wgpu)
+ add_subdirectory(../src/ifcviewer-wgpu-minimal ifcviewer-wgpu-minimal)
+endif()
+
# Cmake uninstall target
if(NOT TARGET uninstall)
configure_file(
diff --git a/src/ifcviewer-wgpu-minimal/CMakeLists.txt b/src/ifcviewer-wgpu-minimal/CMakeLists.txt
new file mode 100644
index 0000000000..b2c0a94cd9
--- /dev/null
+++ b/src/ifcviewer-wgpu-minimal/CMakeLists.txt
@@ -0,0 +1,49 @@
+################################################################################
+# #
+# 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 . #
+# #
+################################################################################
+
+message("Running CMakeLists.txt in /src/ifcviewer-wgpu-minimal")
+
+find_package(Qt${QT_VERSION} COMPONENTS Widgets REQUIRED PATHS ${QT_DIR})
+
+file(GLOB IFCVIEWER_WGPU_MIN_CPP_FILES ${CMAKE_CURRENT_SOURCE_DIR}/*.cpp)
+
+add_executable(IfcViewerWgpuMinimal ${IFCVIEWER_WGPU_MIN_CPP_FILES})
+
+set_target_properties(IfcViewerWgpuMinimal PROPERTIES
+ AUTOMOC ON
+ WIN32_EXECUTABLE ON
+ MACOSX_BUNDLE ON
+)
+
+target_link_libraries(IfcViewerWgpuMinimal PRIVATE
+ IfcViewerWgpu
+ Qt${QT_VERSION}::Widgets
+)
+
+# Pin the build-tree rpath so the executable finds libwgpu_native.so when
+# run directly out of build-viewer-wgpu/. The patched SONAME means DT_NEEDED
+# is just the basename, so a single rpath entry suffices.
+if(UNIX AND NOT APPLE AND WGPU_NATIVE_LIB_DIR)
+ set_target_properties(IfcViewerWgpuMinimal PROPERTIES
+ BUILD_RPATH "${WGPU_NATIVE_LIB_DIR}"
+ )
+endif()
+
+install(TARGETS IfcViewerWgpuMinimal EXPORT ${IFCOPENSHELL_EXPORT_TARGETS})
+ifcopenshell_deploy_qt_runtime(IfcViewerWgpuMinimal)
diff --git a/src/ifcviewer-wgpu-minimal/main.cpp b/src/ifcviewer-wgpu-minimal/main.cpp
new file mode 100644
index 0000000000..770e8ffc55
--- /dev/null
+++ b/src/ifcviewer-wgpu-minimal/main.cpp
@@ -0,0 +1,56 @@
+/********************************************************************************
+ * *
+ * 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 . *
+ * *
+ ********************************************************************************/
+
+#include
+#include
+#include
+#include
+#include
+
+#include "WgpuViewportWindow.h"
+
+// Stage-1 driver: opens a single window with the wgpu viewport embedded,
+// clears to background colour, and exits on close. The shape mirrors
+// ifcviewer-minimal so subsequent stages can grow this into a full
+// benchmark-comparable binary.
+int main(int argc, char* argv[]) {
+ QApplication app(argc, argv);
+ app.setApplicationName("IfcViewerWgpuMinimal");
+ app.setOrganizationName("IfcOpenShell");
+
+ QCommandLineParser parser;
+ parser.setApplicationDescription(
+ "IfcOpenShell minimal wgpu IFC viewer (stage 1: clear-color smoke test)");
+ parser.addHelpOption();
+ parser.process(app);
+
+ auto* viewport = new WgpuViewportWindow;
+ viewport->resize(1280, 800);
+
+ QWidget* container = QWidget::createWindowContainer(viewport);
+ container->setMinimumSize(320, 240);
+
+ QMainWindow main_window;
+ main_window.setWindowTitle("IfcViewer (wgpu) — stage 1");
+ main_window.setCentralWidget(container);
+ main_window.resize(1280, 800);
+ main_window.show();
+
+ return app.exec();
+}
diff --git a/src/ifcviewer-wgpu/CMakeLists.txt b/src/ifcviewer-wgpu/CMakeLists.txt
new file mode 100644
index 0000000000..85fe074d44
--- /dev/null
+++ b/src/ifcviewer-wgpu/CMakeLists.txt
@@ -0,0 +1,140 @@
+################################################################################
+# #
+# 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 . #
+# #
+################################################################################
+
+message("Running CMakeLists.txt in /src/ifcviewer-wgpu")
+
+set(QT_VERSION 6 CACHE STRING "Qt version")
+find_package(Qt${QT_VERSION} COMPONENTS Core Gui REQUIRED PATHS ${QT_DIR})
+
+# wgpu-native — fetched as a pre-built binary release from upstream.
+# Pin the version with WGPU_NATIVE_VERSION; bump to pull a newer release.
+set(WGPU_NATIVE_VERSION "v29.0.0.0" CACHE STRING "wgpu-native release tag")
+
+# Pick the right release archive for the host platform.
+if(CMAKE_SYSTEM_NAME STREQUAL "Windows")
+ set(_wgpu_archive "wgpu-windows-x86_64-msvc-release.zip")
+ set(_wgpu_lib "wgpu_native.dll.lib")
+ set(_wgpu_runtime "wgpu_native.dll")
+elseif(CMAKE_SYSTEM_NAME STREQUAL "Darwin")
+ if(CMAKE_SYSTEM_PROCESSOR MATCHES "arm64|aarch64")
+ set(_wgpu_archive "wgpu-macos-aarch64-release.zip")
+ else()
+ set(_wgpu_archive "wgpu-macos-x86_64-release.zip")
+ endif()
+ set(_wgpu_lib "libwgpu_native.dylib")
+else() # Linux + BSDs
+ if(CMAKE_SYSTEM_PROCESSOR MATCHES "aarch64|arm64")
+ set(_wgpu_archive "wgpu-linux-aarch64-release.zip")
+ else()
+ set(_wgpu_archive "wgpu-linux-x86_64-release.zip")
+ endif()
+ set(_wgpu_lib "libwgpu_native.so")
+endif()
+
+include(FetchContent)
+FetchContent_Declare(
+ wgpu_native
+ URL https://github.com/gfx-rs/wgpu-native/releases/download/${WGPU_NATIVE_VERSION}/${_wgpu_archive}
+ DOWNLOAD_NO_PROGRESS FALSE
+)
+FetchContent_MakeAvailable(wgpu_native)
+
+# Release archive layout: include/webgpu/*.h and lib/.
+#
+# The Linux .so shipped in the v29 release has no DT_SONAME, which causes
+# CMake to bake the relative IMPORTED_LOCATION path into DT_NEEDED. We patch
+# the SONAME in once at configure time so dependents get a clean
+# libwgpu_native.so reference, and pin the executable's rpath to the lib dir.
+if(UNIX AND NOT APPLE)
+ find_program(PATCHELF_EXECUTABLE patchelf)
+ if(PATCHELF_EXECUTABLE)
+ execute_process(
+ COMMAND ${PATCHELF_EXECUTABLE} --set-soname "${_wgpu_lib}"
+ "${wgpu_native_SOURCE_DIR}/lib/${_wgpu_lib}"
+ RESULT_VARIABLE _patchelf_rc
+ )
+ if(NOT _patchelf_rc EQUAL 0)
+ message(WARNING "patchelf --set-soname failed on libwgpu_native.so")
+ endif()
+ else()
+ message(WARNING
+ "patchelf not found; libwgpu_native.so will be linked with a "
+ "relative DT_NEEDED. Install patchelf to fix.")
+ endif()
+endif()
+
+add_library(wgpu_native SHARED IMPORTED GLOBAL)
+set_target_properties(wgpu_native PROPERTIES
+ IMPORTED_LOCATION "${wgpu_native_SOURCE_DIR}/lib/${_wgpu_lib}"
+ INTERFACE_INCLUDE_DIRECTORIES "${wgpu_native_SOURCE_DIR}/include"
+)
+if(WIN32)
+ # On Windows the .lib is the import library; the .dll is the runtime.
+ set_target_properties(wgpu_native PROPERTIES
+ IMPORTED_IMPLIB "${wgpu_native_SOURCE_DIR}/lib/${_wgpu_lib}"
+ IMPORTED_LOCATION "${wgpu_native_SOURCE_DIR}/lib/${_wgpu_runtime}"
+ )
+endif()
+
+# Expose the lib dir so dependents can put it on their rpath.
+set(WGPU_NATIVE_LIB_DIR "${wgpu_native_SOURCE_DIR}/lib" CACHE INTERNAL
+ "Directory containing the wgpu-native shared library")
+
+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})
+
+add_library(IfcViewerWgpu STATIC ${IFCVIEWER_WGPU_FILES})
+
+set_target_properties(IfcViewerWgpu PROPERTIES
+ AUTOMOC ON
+ VERSION "${PROJECT_VERSION}"
+ SOVERSION "${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}"
+)
+
+target_include_directories(IfcViewerWgpu PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
+
+target_link_libraries(IfcViewerWgpu PUBLIC
+ Qt${QT_VERSION}::Core
+ Qt${QT_VERSION}::Gui
+ wgpu_native
+)
+
+# Qt platform-handle access (QNativeInterface::QX11Application etc.) is in
+# the public Gui headers in Qt 6.2+, no PRIVATE_INCLUDE_DIRS needed.
+
+if(UNIX AND NOT APPLE)
+ find_package(Threads REQUIRED)
+ target_link_libraries(IfcViewerWgpu PUBLIC Threads::Threads)
+endif()
+
+install(TARGETS IfcViewerWgpu EXPORT ${IFCOPENSHELL_EXPORT_TARGETS})
+
+install(FILES ${IFCVIEWER_WGPU_H_FILES}
+ DESTINATION ${INCLUDEDIR}/ifcviewer-wgpu
+)
+
+# Install the wgpu_native shared library so the deployed runtime can find it.
+# At build/run-from-build-tree time CMake adds wgpu_native_SOURCE_DIR to the
+# binary's rpath automatically (IMPORTED_LOCATION dirname).
+if(NOT WIN32)
+ install(FILES "${wgpu_native_SOURCE_DIR}/lib/${_wgpu_lib}" DESTINATION lib)
+else()
+ install(FILES "${wgpu_native_SOURCE_DIR}/lib/${_wgpu_runtime}" DESTINATION bin)
+endif()
diff --git a/src/ifcviewer-wgpu/WgpuViewportWindow.cpp b/src/ifcviewer-wgpu/WgpuViewportWindow.cpp
new file mode 100644
index 0000000000..3f7bede2f0
--- /dev/null
+++ b/src/ifcviewer-wgpu/WgpuViewportWindow.cpp
@@ -0,0 +1,377 @@
+/********************************************************************************
+ * *
+ * 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 . *
+ * *
+ ********************************************************************************/
+
+#include "WgpuViewportWindow.h"
+
+#include
+#include
+#include
+
+#include // wgpu-native extensions (logging, MULTI_DRAW_INDIRECT, …)
+
+#include
+
+// -----------------------------------------------------------------------------
+// Small helpers
+// -----------------------------------------------------------------------------
+
+static QString sv(WGPUStringView s) {
+ if (!s.data) return QString();
+ // WGPU_STRLEN sentinel == SIZE_MAX -> nul-terminated.
+ const int len = (s.length == WGPU_STRLEN)
+ ? int(std::strlen(s.data))
+ : int(s.length);
+ 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: qWarning().noquote() << "[wgpu err]" << m; break;
+ case WGPULogLevel_Warn: qWarning().noquote() << "[wgpu warn]" << m; break;
+ case WGPULogLevel_Info: qInfo ().noquote() << "[wgpu info]" << m; break;
+ case WGPULogLevel_Debug: qDebug ().noquote() << "[wgpu dbg]" << m; break;
+ case WGPULogLevel_Trace: qDebug ().noquote() << "[wgpu trace]" << m; break;
+ default: break;
+ }
+}
+
+static void onUncapturedError(WGPUDevice const* /*device*/,
+ WGPUErrorType type, WGPUStringView message,
+ void* /*ud1*/, void* /*ud2*/) {
+ qWarning().noquote() << "[wgpu device error" << int(type) << "]" << sv(message);
+}
+
+// -----------------------------------------------------------------------------
+// Construction / destruction
+// -----------------------------------------------------------------------------
+
+WgpuViewportWindow::WgpuViewportWindow(QWindow* parent)
+ : QWindow(parent) {
+ // wgpu doesn't need a GL context; we just need a real native window that
+ // the platform window manager has actually created. OpenGLSurface is the
+ // most portable way to ask Qt for a hardware-rendering-ready native
+ // window — we never bind a GL context on top of it.
+ setSurfaceType(QSurface::OpenGLSurface);
+}
+
+WgpuViewportWindow::~WgpuViewportWindow() {
+ shutdown();
+}
+
+void WgpuViewportWindow::setBackgroundColor(const QColor& color) {
+ background_color_ = color;
+ if (isExposed()) requestUpdate();
+}
+
+// -----------------------------------------------------------------------------
+// Lifecycle
+// -----------------------------------------------------------------------------
+
+void WgpuViewportWindow::exposeEvent(QExposeEvent* /*event*/) {
+ if (!isExposed()) return;
+
+ if (!wgpu_initialized_) {
+ if (!initWgpu()) {
+ qWarning() << "wgpu init failed; viewport will not render";
+ return;
+ }
+ wgpu_initialized_ = true;
+ }
+
+ const int w = int(width() * devicePixelRatio());
+ const int h = int(height() * devicePixelRatio());
+ if (w > 0 && h > 0 && (w != configured_w_ || h != configured_h_)) {
+ configureSurface(w, h);
+ }
+ requestUpdate();
+}
+
+void WgpuViewportWindow::resizeEvent(QResizeEvent* /*event*/) {
+ if (!wgpu_initialized_ || !isExposed()) return;
+ const int w = int(width() * devicePixelRatio());
+ const int h = int(height() * devicePixelRatio());
+ if (w > 0 && h > 0) {
+ configureSurface(w, h);
+ requestUpdate();
+ }
+}
+
+bool WgpuViewportWindow::event(QEvent* event) {
+ if (event->type() == QEvent::UpdateRequest) {
+ if (wgpu_initialized_ && surface_configured_) {
+ render();
+ }
+ return true;
+ }
+ return QWindow::event(event);
+}
+
+// -----------------------------------------------------------------------------
+// wgpu init: instance, surface, adapter, device, queue
+// -----------------------------------------------------------------------------
+
+bool WgpuViewportWindow::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);
+
+ instance_ = wgpuCreateInstance(nullptr);
+ if (!instance_) {
+ qWarning() << "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 {
+ qWarning().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;
+
+ WGPUDeviceDescriptor dev_desc = {};
+ // Surface uncaptured errors (validation failures etc.) into qWarning 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 {
+ qWarning().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_);
+
+ // ---- Pick a surface format -------------------------------------------
+ WGPUSurfaceCapabilities caps = {};
+ if (wgpuSurfaceGetCapabilities(surface_, adapter_, &caps) != WGPUStatus_Success
+ || caps.formatCount == 0) {
+ qWarning() << "wgpuSurfaceGetCapabilities returned no formats";
+ return false;
+ }
+ surface_format_ = caps.formats[0]; // preferred format per wgpu docs
+ wgpuSurfaceCapabilitiesFreeMembers(caps);
+
+ qInfo() << "wgpu init OK; surface format =" << int(surface_format_);
+ return true;
+}
+
+// -----------------------------------------------------------------------------
+// Surface creation — platform-specific native handle plumbing.
+// -----------------------------------------------------------------------------
+
+#if defined(Q_OS_LINUX)
+// QNativeInterface::QX11Application::display() returns Display*; pulling
+// Xlib.h is fine on any system that has Qt6Gui built with xcb support
+// (which already depends on libX11). We never look inside Display* — we
+// only forward the pointer to wgpu as opaque.
+# if __has_include()
+# include
+# endif
+// QWaylandApplication::display() and ::surface() return wl_display* and
+// wl_surface* (wayland-client-core.h). Same story.
+# if __has_include()
+# include
+# endif
+#endif
+
+bool WgpuViewportWindow::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()) {
+ qWarning() << "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
+ qWarning() << "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()) {
+ qWarning() << "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.
+ qWarning() << "Wayland wgpu surface creation not yet wired (stage 1.5)";
+ return false;
+# else
+ qWarning() << "Built without Wayland headers; cannot create Wayland surface";
+ return false;
+# endif
+ } else {
+ qWarning().noquote() << "Unsupported Qt platform for wgpu surface:" << platform;
+ return false;
+ }
+#else
+ // macOS / Windows native-handle wiring lands when those targets become
+ // active. Stage-1 development happens on Linux.
+ qWarning() << "wgpu surface creation not yet wired for this platform";
+ return false;
+#endif
+
+ if (!surface_) {
+ qWarning() << "wgpuInstanceCreateSurface returned null";
+ return false;
+ }
+ return true;
+}
+
+// -----------------------------------------------------------------------------
+// Surface (re)configure + render
+// -----------------------------------------------------------------------------
+
+void WgpuViewportWindow::configureSurface(int width_px, int height_px) {
+ WGPUSurfaceConfiguration cfg = {};
+ cfg.device = device_;
+ cfg.format = surface_format_;
+ cfg.usage = WGPUTextureUsage_RenderAttachment;
+ cfg.width = uint32_t(width_px);
+ cfg.height = uint32_t(height_px);
+ cfg.presentMode = WGPUPresentMode_Fifo;
+ cfg.alphaMode = WGPUCompositeAlphaMode_Auto;
+
+ wgpuSurfaceConfigure(surface_, &cfg);
+ configured_w_ = width_px;
+ configured_h_ = height_px;
+ surface_configured_ = true;
+}
+
+void WgpuViewportWindow::render() {
+ WGPUSurfaceTexture surf_tex = {};
+ wgpuSurfaceGetCurrentTexture(surface_, &surf_tex);
+
+ switch (surf_tex.status) {
+ case WGPUSurfaceGetCurrentTextureStatus_SuccessOptimal:
+ case WGPUSurfaceGetCurrentTextureStatus_SuccessSuboptimal:
+ break; // proceed
+ case WGPUSurfaceGetCurrentTextureStatus_Timeout:
+ case WGPUSurfaceGetCurrentTextureStatus_Outdated:
+ case WGPUSurfaceGetCurrentTextureStatus_Lost: {
+ // Reconfigure and try again next frame.
+ const int w = int(width() * devicePixelRatio());
+ const int h = int(height() * devicePixelRatio());
+ if (w > 0 && h > 0) configureSurface(w, h);
+ requestUpdate();
+ return;
+ }
+ default:
+ qWarning() << "GetCurrentTexture status" << int(surf_tex.status);
+ return;
+ }
+
+ WGPUTextureView view = wgpuTextureCreateView(surf_tex.texture, nullptr);
+
+ WGPUCommandEncoder enc = wgpuDeviceCreateCommandEncoder(device_, nullptr);
+
+ WGPURenderPassColorAttachment color = {};
+ color.view = view;
+ color.loadOp = WGPULoadOp_Clear;
+ color.storeOp = WGPUStoreOp_Store;
+ color.clearValue = {
+ background_color_.redF(),
+ background_color_.greenF(),
+ background_color_.blueF(),
+ 1.0,
+ };
+ color.depthSlice = WGPU_DEPTH_SLICE_UNDEFINED;
+
+ WGPURenderPassDescriptor pass_desc = {};
+ pass_desc.colorAttachmentCount = 1;
+ pass_desc.colorAttachments = &color;
+
+ WGPURenderPassEncoder pass = wgpuCommandEncoderBeginRenderPass(enc, &pass_desc);
+ wgpuRenderPassEncoderEnd(pass);
+ wgpuRenderPassEncoderRelease(pass);
+
+ WGPUCommandBuffer cmd = wgpuCommandEncoderFinish(enc, nullptr);
+ wgpuQueueSubmit(queue_, 1, &cmd);
+
+ wgpuCommandBufferRelease(cmd);
+ wgpuCommandEncoderRelease(enc);
+ wgpuTextureViewRelease(view);
+
+ wgpuSurfacePresent(surface_);
+ wgpuTextureRelease(surf_tex.texture);
+}
+
+void WgpuViewportWindow::shutdown() {
+ 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-wgpu/WgpuViewportWindow.h b/src/ifcviewer-wgpu/WgpuViewportWindow.h
new file mode 100644
index 0000000000..5d3004fb9c
--- /dev/null
+++ b/src/ifcviewer-wgpu/WgpuViewportWindow.h
@@ -0,0 +1,69 @@
+/********************************************************************************
+ * *
+ * 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 WGPUVIEWPORTWINDOW_H
+#define WGPUVIEWPORTWINDOW_H
+
+#include
+#include
+
+#include
+
+// Stage-1 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.
+//
+// Mirrors the lifecycle shape of the GL ViewportWindow so subsequent stages
+// can grow this into a full IFC renderer without restructuring the host.
+class WgpuViewportWindow : public QWindow {
+ Q_OBJECT
+public:
+ explicit WgpuViewportWindow(QWindow* parent = nullptr);
+ ~WgpuViewportWindow();
+
+ void setBackgroundColor(const QColor& color);
+
+protected:
+ void exposeEvent(QExposeEvent* event) override;
+ void resizeEvent(QResizeEvent* event) override;
+ bool event(QEvent* event) override;
+
+private:
+ bool initWgpu();
+ bool createSurface();
+ void configureSurface(int width_px, int height_px);
+ void render();
+ void shutdown();
+
+ bool wgpu_initialized_ = false;
+ bool surface_configured_ = false;
+ int configured_w_ = 0;
+ int configured_h_ = 0;
+
+ WGPUInstance instance_ = nullptr;
+ WGPUAdapter adapter_ = nullptr;
+ WGPUDevice device_ = nullptr;
+ WGPUQueue queue_ = nullptr;
+ WGPUSurface surface_ = nullptr;
+ WGPUTextureFormat surface_format_ = WGPUTextureFormat_Undefined;
+
+ QColor background_color_ = QColor("#202329");
+};
+
+#endif // WGPUVIEWPORTWINDOW_H