ifcviewer: scaffold ViewportHost + ViewportCore (Path A step 1)

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.
This commit is contained in:
Dion Moult
2026-06-04 19:34:51 +10:00
parent e55a360aa2
commit c314dd3ca8
6 changed files with 258 additions and 1 deletions
+3
View File
@@ -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}/")
+23
View File
@@ -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 <http://www.gnu.org/licenses/>. *
* *
********************************************************************************/
#include "ViewportCore.h"
ViewportCore::ViewportCore(ViewportHost* host) : host_(host) {}
ViewportCore::~ViewportCore() = default;
+51
View File
@@ -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 <http://www.gnu.org/licenses/>. *
* *
********************************************************************************/
#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
+101
View File
@@ -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 <http://www.gnu.org/licenses/>. *
* *
********************************************************************************/
#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 <webgpu/webgpu.h>
#include <cstdint>
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
+52
View File
@@ -25,6 +25,7 @@
#include "StreamingLoader.h"
#include "VertexQuantization.h"
#include <QCoreApplication>
#include <QGuiApplication>
#include <QResizeEvent>
#include <QDebug>
@@ -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<ToolMode>(tool_mode));
}
void ViewportWindow::onToolBackspacePressed() {
emit toolBackspacePressed();
}
void ViewportWindow::setBackgroundColor(const QColor& color) {
background_color_ = color;
if (isExposed()) requestUpdate();
+28 -1
View File
@@ -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