mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-09-19 22:50:21 +00:00
ifcviewer-web: wire WebViewportHost + ViewportCore (#87)
Replaces the standalone wgpu-only clear-color spike in main_web.cpp with a real WebViewportHost implementation: surface creation via the emdawnwebgpu canvas-selector source, framebufferSize through emscripten_get_element_css_size + dpr, requestFrame as a deferred flag the RAF main_loop consumes, quit through emscripten_force_exit. main_web.cpp now does the same lifecycle the desktop initWgpu shell does: core_.initWgpu(web_limits=true) → buildPipelines → buildHiz/ Edge/Pick. The render loop runs core_.render() once per RAF tick when the host has flagged a frame pending, with a surface reconfigure on size changes. Builds clean under emcc 6.0 + emdawnwebgpu (1.4 MB wasm, 278 KB JS glue). Renders an empty scene with the configured background — the plumbing is end-to-end through the same ViewportCore code path the desktop build uses. No sidecar load yet: that lands with the emscripten_fetch streaming backend (#88).
This commit is contained in:
@@ -74,7 +74,11 @@ add_subdirectory(${IFCVIEWER_DIR} ifcviewer EXCLUDE_FROM_ALL)
|
|||||||
|
|
||||||
# --- The web executable ------------------------------------------------------
|
# --- The web executable ------------------------------------------------------
|
||||||
|
|
||||||
add_executable(IfcViewerWeb main_web.cpp)
|
add_executable(IfcViewerWeb
|
||||||
|
main_web.cpp
|
||||||
|
WebViewportHost.cpp
|
||||||
|
WebViewportHost.h
|
||||||
|
)
|
||||||
target_link_libraries(IfcViewerWeb PRIVATE IfcViewerCore)
|
target_link_libraries(IfcViewerWeb PRIVATE IfcViewerCore)
|
||||||
target_link_options(IfcViewerWeb PRIVATE
|
target_link_options(IfcViewerWeb PRIVATE
|
||||||
# Asyncify lets us await wgpu's RequestAdapter/RequestDevice/MapAsync
|
# Asyncify lets us await wgpu's RequestAdapter/RequestDevice/MapAsync
|
||||||
|
|||||||
@@ -0,0 +1,75 @@
|
|||||||
|
/********************************************************************************
|
||||||
|
* *
|
||||||
|
* 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 "WebViewportHost.h"
|
||||||
|
|
||||||
|
#include <emscripten/emscripten.h>
|
||||||
|
#include <emscripten/html5.h>
|
||||||
|
|
||||||
|
#include <cstring>
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
|
WebViewportHost::WebViewportHost(std::string canvas_selector)
|
||||||
|
: canvas_selector_(std::move(canvas_selector)) {}
|
||||||
|
|
||||||
|
WGPUSurface WebViewportHost::createSurface(WGPUInstance instance) {
|
||||||
|
// Emdawnwebgpu canvas-selector surface source. Same shape as
|
||||||
|
// WGPUSurfaceSourceCanvasHTMLSelector_Emscripten in Dawn's headers.
|
||||||
|
WGPUEmscriptenSurfaceSourceCanvasHTMLSelector canvas_desc = {};
|
||||||
|
canvas_desc.chain.sType =
|
||||||
|
WGPUSType_EmscriptenSurfaceSourceCanvasHTMLSelector;
|
||||||
|
canvas_desc.selector.data = canvas_selector_.c_str();
|
||||||
|
canvas_desc.selector.length = canvas_selector_.size();
|
||||||
|
|
||||||
|
WGPUSurfaceDescriptor surface_desc = {};
|
||||||
|
surface_desc.nextInChain = &canvas_desc.chain;
|
||||||
|
return wgpuInstanceCreateSurface(instance, &surface_desc);
|
||||||
|
}
|
||||||
|
|
||||||
|
void WebViewportHost::framebufferSize(int& width_px, int& height_px) const {
|
||||||
|
double w_css = 0.0, h_css = 0.0;
|
||||||
|
// Pull the element's logical (CSS-pixel) size, then multiply by DPR
|
||||||
|
// — matches the QWindow desktop host's framebufferSize semantics.
|
||||||
|
emscripten_get_element_css_size(canvas_selector_.c_str(), &w_css, &h_css);
|
||||||
|
const float ratio = dpr();
|
||||||
|
width_px = int(w_css * double(ratio));
|
||||||
|
height_px = int(h_css * double(ratio));
|
||||||
|
if (width_px < 1) width_px = 1;
|
||||||
|
if (height_px < 1) height_px = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
float WebViewportHost::dpr() const {
|
||||||
|
return float(emscripten_get_device_pixel_ratio());
|
||||||
|
}
|
||||||
|
|
||||||
|
void WebViewportHost::requestFrame() {
|
||||||
|
request_frame_pending_ = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void WebViewportHost::quit() {
|
||||||
|
// emscripten_force_exit honours -sEXIT_RUNTIME=1; without that flag
|
||||||
|
// the runtime swallows the call and keeps the page interactive.
|
||||||
|
emscripten_force_exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool WebViewportHost::consumeFrameRequest() {
|
||||||
|
const bool pending = request_frame_pending_;
|
||||||
|
request_frame_pending_ = false;
|
||||||
|
return pending;
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
/********************************************************************************
|
||||||
|
* *
|
||||||
|
* 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 WEBVIEWPORTHOST_H
|
||||||
|
#define WEBVIEWPORTHOST_H
|
||||||
|
|
||||||
|
// Web implementation of ViewportHost. ViewportCore owns the wgpu state
|
||||||
|
// + render path; this class adapts the embedder hooks to a browser
|
||||||
|
// canvas (surface creation via emdawnwebgpu's canvas-selector extension,
|
||||||
|
// framebuffer size from the canvas element, requestFrame via
|
||||||
|
// requestAnimationFrame, quit via emscripten_force_exit). All
|
||||||
|
// notifications fall through to the base class no-ops for now —
|
||||||
|
// future iterations route them to DOM events / a status bar.
|
||||||
|
|
||||||
|
#include "ViewportHost.h"
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
class WebViewportHost final : public ViewportHost {
|
||||||
|
public:
|
||||||
|
// `canvas_selector` is the CSS selector for the host <canvas> (e.g.
|
||||||
|
// "#viewer-canvas" — matches shell.html). The string is stored;
|
||||||
|
// it must outlive the host.
|
||||||
|
explicit WebViewportHost(std::string canvas_selector);
|
||||||
|
|
||||||
|
WGPUSurface createSurface(WGPUInstance instance) override;
|
||||||
|
void framebufferSize(int& width_px, int& height_px) const override;
|
||||||
|
float dpr() const override;
|
||||||
|
// Sets request_frame_pending_ so the RAF callback knows to render
|
||||||
|
// on the next browser tick. The actual frame scheduling is done by
|
||||||
|
// the main_web.cpp loop; this avoids spamming RAF callbacks when
|
||||||
|
// multiple sources request a frame in the same tick.
|
||||||
|
void requestFrame() override;
|
||||||
|
void quit() override;
|
||||||
|
|
||||||
|
// True when ViewportCore has asked for a frame since the last one
|
||||||
|
// was rendered. The main loop clears this before invoking
|
||||||
|
// core.render().
|
||||||
|
bool consumeFrameRequest();
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::string canvas_selector_;
|
||||||
|
bool request_frame_pending_ = true; // arm an initial frame
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // WEBVIEWPORTHOST_H
|
||||||
+71
-156
@@ -17,181 +17,96 @@
|
|||||||
* *
|
* *
|
||||||
********************************************************************************/
|
********************************************************************************/
|
||||||
|
|
||||||
// Phase-B-step-3 scaffold for the web target. Brings up a wgpu instance
|
// Web entry point. Wires a WebViewportHost to a ViewportCore, brings up
|
||||||
// against a #canvas via the emdawnwebgpu port, requests adapter+device
|
// wgpu through emdawnwebgpu (the spec-compatible WebGPU header set that
|
||||||
// asynchronously, configures the surface, and renders a clear color on
|
// shipped with Dawn), then drives a render() per requestAnimationFrame
|
||||||
// each requestAnimationFrame tick. No sidecar load, no pipelines, no
|
// tick. No sidecar load yet — that lands with the emscripten_fetch
|
||||||
// scene state yet — the goal of this commit is "something renders in a
|
// streaming backend (#88). For this scaffold we render an empty scene
|
||||||
// browser tab" so the build + canvas + wgpu plumbing is end-to-end
|
// with the configured background so init + present is verified
|
||||||
// verified before we wire in IfcViewerCore.
|
// end-to-end through the same ViewportCore code path the desktop build
|
||||||
|
// uses.
|
||||||
|
|
||||||
|
#include "ViewportCore.h"
|
||||||
|
#include "WebViewportHost.h"
|
||||||
|
#include "Log.h"
|
||||||
|
|
||||||
#include <emscripten/emscripten.h>
|
#include <emscripten/emscripten.h>
|
||||||
#include <emscripten/html5.h>
|
#include <emscripten/html5.h>
|
||||||
|
|
||||||
#include <webgpu/webgpu.h>
|
|
||||||
|
|
||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
#include <cstdlib>
|
|
||||||
#include <cstring>
|
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
// Async-arrived handles. Populated by the requestAdapter/requestDevice
|
// Shared by the main_loop trampoline + the cleanup path. Allocated on
|
||||||
// callback chain in startup(); render() short-circuits until they're
|
// the heap so Emscripten's set_main_loop callback (which is C-style)
|
||||||
// all set. Keeps the path single-threaded — the JS event loop drives
|
// can recover state through a void*.
|
||||||
// progress between callbacks.
|
struct AppState {
|
||||||
WGPUInstance g_instance = nullptr;
|
WebViewportHost host{ "#viewer-canvas" };
|
||||||
WGPUAdapter g_adapter = nullptr;
|
ViewportCore core{ &host };
|
||||||
WGPUDevice g_device = nullptr;
|
int last_w = 0;
|
||||||
WGPUQueue g_queue = nullptr;
|
int last_h = 0;
|
||||||
WGPUSurface g_surface = nullptr;
|
};
|
||||||
WGPUTextureFormat g_surface_format = WGPUTextureFormat_Undefined;
|
|
||||||
bool g_main_loop_started = false;
|
|
||||||
|
|
||||||
// Canvas dimensions in CSS pixels. emscripten reports the canvas size in
|
// The main_loop is a free function (Emscripten signature em_callback_func)
|
||||||
// CSS pixels but the GPU surface wants device pixels; we keep things at
|
// so we can hand it directly to emscripten_set_main_loop_arg.
|
||||||
// 1× DPR for the scaffold and re-derive on resize once we wire input.
|
void main_loop(void* user) {
|
||||||
int g_width = 1280;
|
auto* app = static_cast<AppState*>(user);
|
||||||
int g_height = 800;
|
|
||||||
|
|
||||||
void frame() {
|
// Reconfigure when the canvas resizes. The first tick also lands
|
||||||
if (!g_surface || !g_device) return;
|
// here because last_w / last_h start at 0.
|
||||||
|
int w = 0, h = 0;
|
||||||
WGPUSurfaceTexture st = {};
|
app->host.framebufferSize(w, h);
|
||||||
wgpuSurfaceGetCurrentTexture(g_surface, &st);
|
if (w != app->last_w || h != app->last_h) {
|
||||||
if (st.status != WGPUSurfaceGetCurrentTextureStatus_SuccessOptimal &&
|
app->core.configureSurface(w, h);
|
||||||
st.status != WGPUSurfaceGetCurrentTextureStatus_SuccessSuboptimal) {
|
app->last_w = w;
|
||||||
// Lost / outdated / OOM / device-lost — log once and bail out
|
app->last_h = h;
|
||||||
// of this frame so we don't queue work against a torn surface.
|
|
||||||
static int s_complained = 0;
|
|
||||||
if (s_complained++ < 4) {
|
|
||||||
std::fprintf(stderr,
|
|
||||||
"[viewer-web] surface texture status %d; skipping frame\n",
|
|
||||||
int(st.status));
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
WGPUTextureView view = wgpuTextureCreateView(st.texture, nullptr);
|
// Only render when something has requested a frame — saves battery
|
||||||
|
// on the still-camera case. The initial frame request is armed by
|
||||||
WGPURenderPassColorAttachment ca = {};
|
// WebViewportHost's ctor so the canvas always paints once at startup.
|
||||||
ca.view = view;
|
if (app->host.consumeFrameRequest()) {
|
||||||
ca.loadOp = WGPULoadOp_Clear;
|
app->core.render();
|
||||||
ca.storeOp = WGPUStoreOp_Store;
|
|
||||||
ca.clearValue = {0.18, 0.21, 0.28, 1.0}; // BonsaiViewer slate background
|
|
||||||
ca.depthSlice = WGPU_DEPTH_SLICE_UNDEFINED;
|
|
||||||
|
|
||||||
WGPURenderPassDescriptor rp_desc = {};
|
|
||||||
rp_desc.colorAttachmentCount = 1;
|
|
||||||
rp_desc.colorAttachments = &ca;
|
|
||||||
|
|
||||||
WGPUCommandEncoderDescriptor enc_desc = {};
|
|
||||||
WGPUCommandEncoder enc = wgpuDeviceCreateCommandEncoder(g_device, &enc_desc);
|
|
||||||
WGPURenderPassEncoder rp = wgpuCommandEncoderBeginRenderPass(enc, &rp_desc);
|
|
||||||
wgpuRenderPassEncoderEnd(rp);
|
|
||||||
wgpuRenderPassEncoderRelease(rp);
|
|
||||||
|
|
||||||
WGPUCommandBufferDescriptor cb_desc = {};
|
|
||||||
WGPUCommandBuffer cb = wgpuCommandEncoderFinish(enc, &cb_desc);
|
|
||||||
wgpuQueueSubmit(g_queue, 1, &cb);
|
|
||||||
|
|
||||||
wgpuCommandBufferRelease(cb);
|
|
||||||
wgpuCommandEncoderRelease(enc);
|
|
||||||
wgpuTextureViewRelease(view);
|
|
||||||
wgpuTextureRelease(st.texture);
|
|
||||||
}
|
|
||||||
|
|
||||||
void configure_surface() {
|
|
||||||
WGPUSurfaceCapabilities caps = {};
|
|
||||||
if (wgpuSurfaceGetCapabilities(g_surface, g_adapter, &caps) != WGPUStatus_Success
|
|
||||||
|| caps.formatCount == 0) {
|
|
||||||
std::fprintf(stderr, "[viewer-web] surface has no formats\n");
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
g_surface_format = caps.formats[0];
|
|
||||||
wgpuSurfaceCapabilitiesFreeMembers(caps);
|
|
||||||
|
|
||||||
WGPUSurfaceConfiguration cfg = {};
|
|
||||||
cfg.device = g_device;
|
|
||||||
cfg.format = g_surface_format;
|
|
||||||
cfg.usage = WGPUTextureUsage_RenderAttachment;
|
|
||||||
cfg.width = uint32_t(g_width);
|
|
||||||
cfg.height = uint32_t(g_height);
|
|
||||||
cfg.alphaMode = WGPUCompositeAlphaMode_Auto;
|
|
||||||
cfg.presentMode = WGPUPresentMode_Fifo;
|
|
||||||
wgpuSurfaceConfigure(g_surface, &cfg);
|
|
||||||
|
|
||||||
if (!g_main_loop_started) {
|
|
||||||
emscripten_set_main_loop(frame, 0, /*simulate_infinite=*/0);
|
|
||||||
g_main_loop_started = true;
|
|
||||||
std::fprintf(stderr,
|
|
||||||
"[viewer-web] surface configured (%dx%d format=%d); RAF loop started\n",
|
|
||||||
g_width, g_height, int(g_surface_format));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void on_device_ready(WGPURequestDeviceStatus status, WGPUDevice device,
|
|
||||||
WGPUStringView message, void* /*ud1*/, void* /*ud2*/) {
|
|
||||||
if (status != WGPURequestDeviceStatus_Success || !device) {
|
|
||||||
std::fprintf(stderr, "[viewer-web] requestDevice failed: %.*s\n",
|
|
||||||
int(message.length), message.data ? message.data : "");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
g_device = device;
|
|
||||||
g_queue = wgpuDeviceGetQueue(device);
|
|
||||||
|
|
||||||
WGPUEmscriptenSurfaceSourceCanvasHTMLSelector canvas = {};
|
|
||||||
canvas.chain.sType = WGPUSType_EmscriptenSurfaceSourceCanvasHTMLSelector;
|
|
||||||
static const char* kSelector = "#viewer-canvas";
|
|
||||||
canvas.selector.data = kSelector;
|
|
||||||
canvas.selector.length = std::strlen(kSelector);
|
|
||||||
|
|
||||||
WGPUSurfaceDescriptor sd = {};
|
|
||||||
sd.nextInChain = &canvas.chain;
|
|
||||||
g_surface = wgpuInstanceCreateSurface(g_instance, &sd);
|
|
||||||
if (!g_surface) {
|
|
||||||
std::fprintf(stderr,
|
|
||||||
"[viewer-web] wgpuInstanceCreateSurface returned null "
|
|
||||||
"(canvas '#viewer-canvas' missing?)\n");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
configure_surface();
|
|
||||||
}
|
|
||||||
|
|
||||||
void on_adapter_ready(WGPURequestAdapterStatus status, WGPUAdapter adapter,
|
|
||||||
WGPUStringView message, void* /*ud1*/, void* /*ud2*/) {
|
|
||||||
if (status != WGPURequestAdapterStatus_Success || !adapter) {
|
|
||||||
std::fprintf(stderr, "[viewer-web] requestAdapter failed: %.*s\n",
|
|
||||||
int(message.length), message.data ? message.data : "");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
g_adapter = adapter;
|
|
||||||
|
|
||||||
WGPUDeviceDescriptor dd = {};
|
|
||||||
WGPURequestDeviceCallbackInfo cb = {};
|
|
||||||
cb.mode = WGPUCallbackMode_AllowSpontaneous;
|
|
||||||
cb.callback = on_device_ready;
|
|
||||||
wgpuAdapterRequestDevice(adapter, &dd, cb);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
int main() {
|
int main(int /*argc*/, char** /*argv*/) {
|
||||||
WGPUInstanceDescriptor desc = {};
|
Log::info() << "ifcviewer-web: starting";
|
||||||
g_instance = wgpuCreateInstance(&desc);
|
|
||||||
if (!g_instance) {
|
auto* app = new AppState();
|
||||||
std::fprintf(stderr, "[viewer-web] wgpuCreateInstance returned null\n");
|
|
||||||
|
// Web limits floor: requestDevice the WebGPU spec's mandatory floor
|
||||||
|
// (maxStorageBufferBindingSize=128MB, maxBufferSize=256MB) so the
|
||||||
|
// chunking + pool probe see the same constraints they would hit in
|
||||||
|
// any browser. Desktop --web-limits did this opt-in; on web it's
|
||||||
|
// the only sensible default.
|
||||||
|
if (!app->core.initWgpu(/*web_limits=*/true)) {
|
||||||
|
std::fprintf(stderr, "[viewer-web] initWgpu failed\n");
|
||||||
|
delete app;
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
if (!app->core.buildPipelines()) {
|
||||||
|
std::fprintf(stderr, "[viewer-web] buildPipelines failed\n");
|
||||||
|
delete app;
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
// HiZ + edge + pick pipelines: built up-front to match the desktop
|
||||||
|
// path's lifetime. shutdown() in ViewportCore expects each
|
||||||
|
// resource to be either constructed or null, so building them all
|
||||||
|
// here keeps teardown symmetric.
|
||||||
|
app->core.buildHizPipeline();
|
||||||
|
app->core.buildEdgePipeline();
|
||||||
|
app->core.buildPickPipeline();
|
||||||
|
|
||||||
WGPURequestAdapterOptions opts = {};
|
// fps = 0 means "use the browser's natural rate (RAF)" — Emscripten
|
||||||
WGPURequestAdapterCallbackInfo cb = {};
|
// schedules the callback once per requestAnimationFrame tick.
|
||||||
cb.mode = WGPUCallbackMode_AllowSpontaneous;
|
// simulate_infinite_loop = false because we want main() to return
|
||||||
cb.callback = on_adapter_ready;
|
// so the runtime + JS event loop keep ticking. The AppState leaks
|
||||||
wgpuInstanceRequestAdapter(g_instance, &opts, cb);
|
// on tab close, which is fine — emscripten_force_exit (called
|
||||||
|
// from host_->quit()) is the only formal shutdown path on web.
|
||||||
// main() returns; the browser keeps the JS event loop running so
|
emscripten_set_main_loop_arg(main_loop, app, /*fps=*/0,
|
||||||
// the async adapter/device callbacks above land naturally and the
|
/*simulate_infinite_loop=*/0);
|
||||||
// main loop kicks off from configure_surface().
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user