From 21d394501fc6ad7d8fb19b9b87fba5db36d54163 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Tue, 2 Jun 2026 13:45:42 +1000 Subject: [PATCH] ifcviewer-wgpu: bring up macOS Metal surface (task #32) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Without this, BonsaiViewer.app launched on macOS would show a white viewport for the same reason Windows did before d4d0c934b — the `createSurface()` else branch fell through to "wgpu surface creation not yet wired for this platform", `init()` returned false, and the Qt window background was all the user saw. Pieces: - `WgpuMetalSurface_mac.{h,mm}`: tiny Objective-C++ bridge. Takes the NSView pointer that Qt's `winId()` returns on macOS, attaches a CAMetalLayer (using Qt's existing one when surfaceType is MetalSurface, attaching one ourselves as a defensive fallback), sets `contentsScale` from the window's backing scale factor so retina drawables come out at native resolution, and returns the layer as `void*`. The .mm keeps the Objective-C namespace pollution out of WgpuViewportWindow.cpp. - `WgpuViewportWindow` ctor: `setSurfaceType(QSurface::MetalSurface)` on macOS so Qt backs the NSView with a CAMetalLayer at window creation; OpenGLSurface elsewhere as before. - `WgpuViewportWindow::createSurface()`: new `#elif defined(Q_OS_MAC)` branch that fills a `WGPUSurfaceSourceMetalLayer` with the layer pointer from the bridge and hands it to `wgpuInstanceCreateSurface`. - `CMakeLists.txt`: `enable_language(OBJCXX)` + the .mm file added to the source list on Apple, and links `-framework Cocoa` (NSView) + `-framework QuartzCore` (CAMetalLayer). `winId()` on macOS returns the backing NSView*, not the NSWindow* — that's the layer-bearing host wgpu-native expects. Co-Authored-By: Claude Opus 4.7 --- src/ifcviewer-wgpu/CMakeLists.txt | 18 ++++++++++ src/ifcviewer-wgpu/WgpuMetalSurface_mac.h | 32 +++++++++++++++++ src/ifcviewer-wgpu/WgpuMetalSurface_mac.mm | 42 ++++++++++++++++++++++ src/ifcviewer-wgpu/WgpuViewportWindow.cpp | 37 ++++++++++++++++--- 4 files changed, 124 insertions(+), 5 deletions(-) create mode 100644 src/ifcviewer-wgpu/WgpuMetalSurface_mac.h create mode 100644 src/ifcviewer-wgpu/WgpuMetalSurface_mac.mm diff --git a/src/ifcviewer-wgpu/CMakeLists.txt b/src/ifcviewer-wgpu/CMakeLists.txt index 2382a6cc94..322417bd5e 100644 --- a/src/ifcviewer-wgpu/CMakeLists.txt +++ b/src/ifcviewer-wgpu/CMakeLists.txt @@ -108,6 +108,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}) +# Cocoa bridge for the CAMetalLayer surface attach — Objective-C++. +# Only compiled into the target on Apple platforms; CMake handles `.mm` +# natively once OBJCXX is enabled. +if(APPLE) + enable_language(OBJCXX) + list(APPEND IFCVIEWER_WGPU_FILES + ${CMAKE_CURRENT_SOURCE_DIR}/WgpuMetalSurface_mac.mm + ) +endif() + # 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 @@ -146,6 +156,14 @@ if(UNIX AND NOT APPLE) target_link_libraries(IfcViewerWgpu PUBLIC Threads::Threads) endif() +# Cocoa + QuartzCore for WgpuMetalSurface_mac.mm (NSView, CAMetalLayer). +if(APPLE) + target_link_libraries(IfcViewerWgpu PUBLIC + "-framework Cocoa" + "-framework QuartzCore" + ) +endif() + install(TARGETS IfcViewerWgpu EXPORT ${IFCOPENSHELL_EXPORT_TARGETS}) install(FILES ${IFCVIEWER_WGPU_H_FILES} diff --git a/src/ifcviewer-wgpu/WgpuMetalSurface_mac.h b/src/ifcviewer-wgpu/WgpuMetalSurface_mac.h new file mode 100644 index 0000000000..3c8f75a25b --- /dev/null +++ b/src/ifcviewer-wgpu/WgpuMetalSurface_mac.h @@ -0,0 +1,32 @@ +/** + * Objective-C++ bridge between WgpuViewportWindow (pure C++) and Cocoa / + * QuartzCore (Objective-C). Compiled only on macOS — see CMakeLists.txt. + * + * Qt's QWindow::winId() returns the backing NSView* (as a WId) on macOS; + * we need a CAMetalLayer attached to that view to hand to wgpu-native + * via WGPUSurfaceSourceMetalLayer. Doing that requires Objective-C, so + * the actual layer attach lives in WgpuMetalSurface_mac.mm. + */ + +#ifndef WGPU_METAL_SURFACE_MAC_H +#define WGPU_METAL_SURFACE_MAC_H + +#if defined(__APPLE__) + +#ifdef __cplusplus +extern "C" { +#endif + +/// Ensures the given NSView has a CAMetalLayer as its backing layer. +/// Returns the CAMetalLayer pointer (`void*` so callers don't need to +/// pull QuartzCore into their TU); the layer is owned by the NSView. +/// Returns nullptr if `nsview_ptr` is null. +void* wgpu_macos_attach_metal_layer(void* nsview_ptr); + +#ifdef __cplusplus +} +#endif + +#endif // __APPLE__ + +#endif // WGPU_METAL_SURFACE_MAC_H diff --git a/src/ifcviewer-wgpu/WgpuMetalSurface_mac.mm b/src/ifcviewer-wgpu/WgpuMetalSurface_mac.mm new file mode 100644 index 0000000000..5aa7cd03ea --- /dev/null +++ b/src/ifcviewer-wgpu/WgpuMetalSurface_mac.mm @@ -0,0 +1,42 @@ +/** + * Objective-C++ implementation of the Cocoa bridge declared in + * WgpuMetalSurface_mac.h. Compiled only on macOS. + */ + +#include "WgpuMetalSurface_mac.h" + +#if defined(__APPLE__) + +#import +#import + +void* wgpu_macos_attach_metal_layer(void* nsview_ptr) { + if (!nsview_ptr) { + return nullptr; + } + NSView* view = (__bridge NSView*)nsview_ptr; + + // When QWindow::surfaceType is QSurface::MetalSurface, Qt already + // backs the NSView with a CAMetalLayer — just hand it back. Otherwise + // attach one ourselves (defensive: Qt's behaviour can change between + // major versions and 6.x has occasionally regressed this). + CAMetalLayer* layer = nil; + if ([view.layer isKindOfClass:[CAMetalLayer class]]) { + layer = (CAMetalLayer*)view.layer; + } else { + layer = [CAMetalLayer layer]; + view.wantsLayer = YES; + view.layer = layer; + } + + // Track the screen's backing scale so we get retina-resolution + // drawables. wgpu's surface configure picks the drawable size up + // from layer.drawableSize at present-time. + if (view.window) { + layer.contentsScale = view.window.backingScaleFactor; + } + + return (__bridge void*)layer; +} + +#endif // __APPLE__ diff --git a/src/ifcviewer-wgpu/WgpuViewportWindow.cpp b/src/ifcviewer-wgpu/WgpuViewportWindow.cpp index 5ee5ac7c97..d6570e1a04 100644 --- a/src/ifcviewer-wgpu/WgpuViewportWindow.cpp +++ b/src/ifcviewer-wgpu/WgpuViewportWindow.cpp @@ -686,11 +686,19 @@ static WGPUStringView svFromCStr(const char* s) { 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. + // wgpu doesn't need a GL context; we just need a real native window + // whose backing layer matches the GPU API wgpu will drive. + // + // - Linux/Windows: OpenGLSurface gives us a hardware-rendering-ready + // native window (XCB/HWND); we never bind a GL context on top. + // - macOS: MetalSurface tells Qt to back the NSView with a + // CAMetalLayer, which wgpu-native wraps via + // WGPUSurfaceSourceMetalLayer in createSurface(). +#if defined(Q_OS_MAC) + setSurfaceType(QSurface::MetalSurface); +#else setSurfaceType(QSurface::OpenGLSurface); +#endif } WgpuViewportWindow::~WgpuViewportWindow() { @@ -1818,6 +1826,10 @@ bool WgpuViewportWindow::probeAndCreatePool() { # define WIN32_LEAN_AND_MEAN # endif # include +#elif defined(Q_OS_MAC) +// Cocoa bridge declared in WgpuMetalSurface_mac.h, implemented in the +// adjacent .mm file. Keeps Objective-C out of this pure-C++ TU. +# include "WgpuMetalSurface_mac.h" #endif bool WgpuViewportWindow::createSurface() { @@ -1873,8 +1885,23 @@ bool WgpuViewportWindow::createSurface() { hwndsrc.hwnd = reinterpret_cast(static_cast(winId())); surface_desc.nextInChain = &hwndsrc.chain; surface_ = wgpuInstanceCreateSurface(instance_, &surface_desc); +#elif defined(Q_OS_MAC) + // QWindow::winId() returns the backing NSView* on macOS (as WId, + // which is quintptr — same width as void* on all macOS arches we + // care about). Hand it to the Cocoa bridge to attach a + // CAMetalLayer, then wrap that layer in WGPUSurfaceSourceMetalLayer. + void* nsview = reinterpret_cast(static_cast(winId())); + void* layer = wgpu_macos_attach_metal_layer(nsview); + if (!layer) { + qWarning() << "Could not attach CAMetalLayer to the Qt NSView"; + return false; + } + WGPUSurfaceSourceMetalLayer metalsrc = {}; + metalsrc.chain.sType = WGPUSType_SurfaceSourceMetalLayer; + metalsrc.layer = layer; + surface_desc.nextInChain = &metalsrc.chain; + surface_ = wgpuInstanceCreateSurface(instance_, &surface_desc); #else - // macOS Metal surface wiring lands with task #32. qWarning() << "wgpu surface creation not yet wired for this platform"; return false; #endif