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