From c314dd3ca82f381d6e1b40336bffd4ad7e319248 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Thu, 4 Jun 2026 19:34:51 +1000 Subject: [PATCH] ifcviewer: scaffold ViewportHost + ViewportCore (Path A step 1) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Define the boundary the Path-A web-bring-up refactor will move things across: - ViewportHost.h is the embedder interface — surface creation, framebuffer geometry, frame scheduling, quit, and notification callbacks (onObjectPicked, onToolModeChanged, …). Desktop hosts forward notifications to Q_SIGNALS; the future web host pushes them to JS callbacks. - ViewportCore.{h,cpp} is the platform-agnostic render-core target. Empty today — the body fills in across the #78-#86 sequence as each Qt subsystem (matrices, vectors, strings, timers, render path, input) gets de-Qt'd and moved over. - ViewportWindow now multiply-inherits ViewportHost alongside QWindow and implements the host overrides as thin forwarders: createSurface returns the cached surface_, requestFrame -> requestUpdate, quit -> QCoreApplication::quit, onObjectPicked -> emit objectPicked. Renamed the DPR accessor `dpr()` (vs `devicePixelRatio`) to avoid the inherited-virtual clash with QWindow's qreal-returning version. No method movement yet — this is purely the architectural scaffold so subsequent commits have a destination. --- src/ifcviewer/CMakeLists.txt | 3 + src/ifcviewer/ViewportCore.cpp | 23 +++++++ src/ifcviewer/ViewportCore.h | 51 ++++++++++++++++ src/ifcviewer/ViewportHost.h | 101 +++++++++++++++++++++++++++++++ src/ifcviewer/ViewportWindow.cpp | 52 ++++++++++++++++ src/ifcviewer/ViewportWindow.h | 29 ++++++++- 6 files changed, 258 insertions(+), 1 deletion(-) create mode 100644 src/ifcviewer/ViewportCore.cpp create mode 100644 src/ifcviewer/ViewportCore.h create mode 100644 src/ifcviewer/ViewportHost.h diff --git a/src/ifcviewer/CMakeLists.txt b/src/ifcviewer/CMakeLists.txt index ffc87e436a..fe84002203 100644 --- a/src/ifcviewer/CMakeLists.txt +++ b/src/ifcviewer/CMakeLists.txt @@ -145,6 +145,7 @@ set(IFCVIEWER_CORE_SOURCES SidecarCache.cpp StreamingLoader.cpp StreamingThread.cpp + ViewportCore.cpp ) set(IFCVIEWER_CORE_HEADERS BufferPool.h @@ -158,6 +159,8 @@ set(IFCVIEWER_CORE_HEADERS StreamingLoader.h StreamingThread.h VertexQuantization.h + ViewportCore.h + ViewportHost.h VisibilityState.h ) list(TRANSFORM IFCVIEWER_CORE_SOURCES PREPEND "${CMAKE_CURRENT_SOURCE_DIR}/") diff --git a/src/ifcviewer/ViewportCore.cpp b/src/ifcviewer/ViewportCore.cpp new file mode 100644 index 0000000000..19f06f6a33 --- /dev/null +++ b/src/ifcviewer/ViewportCore.cpp @@ -0,0 +1,23 @@ +/******************************************************************************** + * * + * 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 "ViewportCore.h" + +ViewportCore::ViewportCore(ViewportHost* host) : host_(host) {} +ViewportCore::~ViewportCore() = default; diff --git a/src/ifcviewer/ViewportCore.h b/src/ifcviewer/ViewportCore.h new file mode 100644 index 0000000000..c526512149 --- /dev/null +++ b/src/ifcviewer/ViewportCore.h @@ -0,0 +1,51 @@ +/******************************************************************************** + * * + * 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 VIEWPORTCORE_H +#define VIEWPORTCORE_H + +// Platform-agnostic render core. Owns the wgpu state + scene state + +// per-frame render path. Talks to its embedder through ViewportHost +// (window/canvas surface, scheduling, notifications) — has no Qt or +// browser dependencies of its own. +// +// Empty for now: this header / TU is the placeholder for the +// incremental Path-A refactor (#77-#86). Each subsequent commit moves +// one subsystem out of ViewportWindow.cpp into here and updates the +// public surface accordingly. Until that work lands, ViewportWindow +// retains its render body; this class is just the target for the +// move. + +#include "ViewportHost.h" + +class ViewportCore { +public: + explicit ViewportCore(ViewportHost* host); + ~ViewportCore(); + + ViewportCore(const ViewportCore&) = delete; + ViewportCore& operator=(const ViewportCore&) = delete; + + ViewportHost* host() const { return host_; } + +private: + ViewportHost* host_; +}; + +#endif // VIEWPORTCORE_H diff --git a/src/ifcviewer/ViewportHost.h b/src/ifcviewer/ViewportHost.h new file mode 100644 index 0000000000..8c33515936 --- /dev/null +++ b/src/ifcviewer/ViewportHost.h @@ -0,0 +1,101 @@ +/******************************************************************************** + * * + * 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 VIEWPORTHOST_H +#define VIEWPORTHOST_H + +// Abstraction layer between the platform-agnostic ViewportCore (wgpu +// state, scene state, render path, cull, input) and whichever host owns +// the native window / canvas / event loop. On desktop the host is a +// QWindow-derived ViewportWindow; on web it'll be a small canvas-bound +// shell in src/ifcviewer-web. The host implements this interface so +// ViewportCore can call back into it for: +// +// - surface creation (only the host knows its native handle) +// - framebuffer geometry (size, DPR) +// - scheduling the next render tick (QWindow::requestUpdate vs RAF) +// - quitting the process +// - bubbling user-facing notifications (selection changed, picked +// object, tool mode, frame stats) up to whatever signal/callback +// mechanism the host uses +// +// All host-side notifications have default empty implementations so +// hosts only override the ones they care about — minimal-boilerplate +// hosts (a screenshot CI runner, the web spike before UI lands) can +// ignore the rest. Inverting control this way keeps ViewportCore from +// having to know whether it's running inside Qt's event loop or +// Emscripten's main-loop. + +#include + +#include + +class ViewportHost { +public: + virtual ~ViewportHost() = default; + + // Surface creation. The host wraps its native window/canvas in the + // appropriate WGPUSurfaceDescriptor extension struct (Xlib, HWND, + // CAMetalLayer, EmscriptenCanvas) and returns the resulting surface. + // Returns nullptr if the surface can't be created — caller logs and + // aborts init. + virtual WGPUSurface createSurface(WGPUInstance instance) = 0; + + // Framebuffer geometry. width/height are in device pixels (post- + // multiplied by devicePixelRatio); the depth attachment and surface + // configuration both consume these. + virtual void framebufferSize(int& width_px, int& height_px) const = 0; + // Device pixel ratio (CSS-px → device-px). Renamed from + // `devicePixelRatio` because QWindow already exposes that name; an + // override conflict between qreal (QWindow) and float (a hypothetical + // ViewportHost::devicePixelRatio) would surprise readers. `dpr` is + // unambiguous. + virtual float dpr() const = 0; + + // Ask the host to schedule another render tick. On Qt this is + // QWindow::requestUpdate (coalesced + DPR-aware); on web this is a + // requestAnimationFrame schedule. Idempotent within a tick. + virtual void requestFrame() = 0; + + // Ask the host to terminate the process / close the tab. Used by + // the --screenshot / --benchmark exits, and by the future + // window-close path. + virtual void quit() = 0; + + // ---- Notifications ---------------------------------------------------- + // + // Each callback corresponds to something ViewportWindow currently + // exposes as a Q_SIGNAL. Default empty body so non-Qt hosts can + // selectively implement; the desktop ViewportWindow override + // forwards each to emit objectPicked() / emit toolModeChanged() etc. + + virtual void onObjectPicked(uint32_t /*object_id*/) {} + virtual void onSurfacePickedInTool(int /*x_px*/, int /*y_px*/, + int /*modifiers*/) {} + virtual void onToolModeChanged(int /*tool_mode*/) {} + virtual void onToolBackspacePressed() {} + // FrameStats is a Qt-using struct declared inside ViewportWindow + // today; once the move to ViewportCore happens it'll come back here + // as a plain POD. For B4 the desktop ViewportWindow keeps the + // existing typed signal and this is just the placeholder. + virtual void onFrameStats(double /*frame_ms*/, double /*cull_ms*/, + uint32_t /*draws*/, uint32_t /*tris*/) {} +}; + +#endif // VIEWPORTHOST_H diff --git a/src/ifcviewer/ViewportWindow.cpp b/src/ifcviewer/ViewportWindow.cpp index 0b0e19f759..333eae2034 100644 --- a/src/ifcviewer/ViewportWindow.cpp +++ b/src/ifcviewer/ViewportWindow.cpp @@ -25,6 +25,7 @@ #include "StreamingLoader.h" #include "VertexQuantization.h" +#include #include #include #include @@ -628,6 +629,57 @@ ViewportWindow::~ViewportWindow() { shutdown(); } +// ---- ViewportHost overrides ------------------------------------------------ +// +// Scaffolding for Path-A. ViewportCore is empty today, so these don't +// yet have callers; the abstract methods exist only to define the +// boundary that subsequent commits will rely on. Each notification +// forwards to the existing Q_SIGNAL so bonsai-side consumers see no +// change. + +WGPUSurface ViewportWindow::createSurface(WGPUInstance /*instance*/) { + // initWgpu() still drives surface creation through the private + // no-arg createSurface() helper that populates surface_. Once + // ViewportCore takes over the wgpu init flow it'll call this + // override instead and the private helper goes away; for now the + // override is a getter. + return surface_; +} + +void ViewportWindow::framebufferSize(int& width_px, int& height_px) const { + const float r = float(QWindow::devicePixelRatio()); + width_px = int(QWindow::width() * r); + height_px = int(QWindow::height() * r); +} + +float ViewportWindow::dpr() const { + return float(QWindow::devicePixelRatio()); +} + +void ViewportWindow::requestFrame() { + requestUpdate(); +} + +void ViewportWindow::quit() { + QCoreApplication::quit(); +} + +void ViewportWindow::onObjectPicked(uint32_t object_id) { + emit objectPicked(object_id); +} + +void ViewportWindow::onSurfacePickedInTool(int x_px, int y_px, int modifiers) { + emit surfacePickedInTool(x_px, y_px, modifiers); +} + +void ViewportWindow::onToolModeChanged(int tool_mode) { + emit toolModeChanged(static_cast(tool_mode)); +} + +void ViewportWindow::onToolBackspacePressed() { + emit toolBackspacePressed(); +} + void ViewportWindow::setBackgroundColor(const QColor& color) { background_color_ = color; if (isExposed()) requestUpdate(); diff --git a/src/ifcviewer/ViewportWindow.h b/src/ifcviewer/ViewportWindow.h index 85589e623c..0bb1d11b74 100644 --- a/src/ifcviewer/ViewportWindow.h +++ b/src/ifcviewer/ViewportWindow.h @@ -47,6 +47,8 @@ #include "OverlayRenderer.h" #include "SelectionState.h" #include "StreamingThread.h" +#include "ViewportCore.h" +#include "ViewportHost.h" #include "VisibilityState.h" // Stage-2 wgpu viewport: opens a native QWindow, brings up a wgpu instance/ @@ -57,12 +59,37 @@ // // Mirrors the lifecycle shape of the GL ViewportWindow so subsequent stages // can grow this into a full IFC renderer without restructuring the host. -class ViewportWindow : public QWindow { +// +// Also implements ViewportHost: as the Path-A refactor moves rendering +// state out into ViewportCore, this class plays the embedder role +// (provides the wgpu surface, schedules frames, forwards notifications +// to Q_SIGNALS). The web target's host is the analog on the Emscripten +// side. Today most state still lives here; the override implementations +// at the bottom of the class are the bridge for whatever has already +// moved. +class ViewportWindow : public QWindow, public ViewportHost { Q_OBJECT public: explicit ViewportWindow(QWindow* parent = nullptr); ~ViewportWindow(); + // --- ViewportHost ---------------------------------------------------- + // + // Implementations live in ViewportWindow.cpp alongside the + // platform-specific surface code so the Qt + native window-handle + // bits stay co-located. Notification overrides (onObjectPicked + // etc.) forward to the existing Q_SIGNALS so bonsai-side consumers + // see no change. + WGPUSurface createSurface(WGPUInstance instance) override; + void framebufferSize(int& width_px, int& height_px) const override; + float dpr() const override; + void requestFrame() override; + void quit() override; + void onObjectPicked(uint32_t object_id) override; + void onSurfacePickedInTool(int x_px, int y_px, int modifiers) override; + void onToolModeChanged(int tool_mode) override; + void onToolBackspacePressed() override; + void setBackgroundColor(const QColor& color); // Queue a sidecar path to be loaded after wgpu init completes. Safe to