From a879c834c78c007724920c00abb6af5a5e672062 Mon Sep 17 00:00:00 2001 From: Jorijn van der Graaf Date: Sun, 19 Jul 2026 01:38:25 +0200 Subject: [PATCH] webgpu embedding --- additional/dom-webgpu.js | 54 ++++++++++++++++++++- implementations/Crafter.Graphics-Device.cpp | 10 +++- interfaces/Crafter.Graphics-WebGPU.cppm | 17 +++++++ project.cpp | 9 ++++ 4 files changed, 86 insertions(+), 4 deletions(-) diff --git a/additional/dom-webgpu.js b/additional/dom-webgpu.js index 77f5211..fe7b7bd 100644 --- a/additional/dom-webgpu.js +++ b/additional/dom-webgpu.js @@ -54,6 +54,7 @@ function stub(name) { "wgpuRegisterMeshBLAS", "wgpuRegisterMeshBLASDeviceAabbs", "wgpuRefitMeshBLASDeviceAabbs", "wgpuLoadRTPipeline", "wgpuDispatchRT", "wgpuBuildTLAS", "wgpuLoadComputePipeline", "wgpuDispatchCompute", + "wgpuSetCanvasMount", ]) { // Read-write ints don't need a stub-throw; return 0 for the size queries. e[n] = n.endsWith("Width") || n.endsWith("Height") @@ -85,6 +86,11 @@ canvas.style.cssText = "position:fixed;inset:0;width:100vw;height:100vh;display: document.body.style.margin = "0"; document.body.appendChild(canvas); +// Optional mount target set via wgpuSetCanvasMount() (below). When null the +// canvas is a full-viewport fixed layer (the original, unchanged default); +// when set, the canvas is reparented into that element and sized to it. +let mountEl = null; + function syncCanvasSize() { // Canvas pixel size = CSS size × devicePixelRatio so the GPU draws // at physical pixel resolution on HiDPI displays — otherwise the @@ -98,8 +104,18 @@ function syncCanvasSize() { // share the physical-pixel coordinate space with window.width/.height. const dpr = window.devicePixelRatio || 1; window.crafter_dpr = dpr; - const w = Math.max(1, Math.round(window.innerWidth * dpr)); - const h = Math.max(1, Math.round(window.innerHeight * dpr)); + // Mounted → track the host element's box; unmounted → the viewport. + let cssW, cssH; + if (mountEl) { + const r = mountEl.getBoundingClientRect(); + cssW = r.width; + cssH = r.height; + } else { + cssW = window.innerWidth; + cssH = window.innerHeight; + } + const w = Math.max(1, Math.round(cssW * dpr)); + const h = Math.max(1, Math.round(cssH * dpr)); if (canvas.width !== w) canvas.width = w; if (canvas.height !== h) canvas.height = h; return { w, h }; @@ -566,6 +582,40 @@ const env = window.crafter_webbuild_env; env.wgpuGetCanvasWidth = () => canvas.width; env.wgpuGetCanvasHeight = () => canvas.height; +// Reparent the render canvas into a host DOM element so a Crafter.Graphics +// scene can render inline inside an app's own layout instead of as a +// full-page layer. `idPtr`/`idLen` is a UTF-8 wasm string: +// - non-empty id of an existing element → move the canvas into it, +// position it to fill it (absolute inset:0), and size the render +// surface to that element's box (syncCanvasSize picks it up per frame). +// - empty string → detach back to and hide it (display:none), so +// the host page is a plain DOM document again. +// The element is made position:relative if it is otherwise static, so the +// absolutely-positioned canvas anchors to it. Pointer events pass through +// (the inline use cases are non-interactive display surfaces). +env.wgpuSetCanvasMount = (idPtr, idLen) => { + const id = idLen > 0 + ? new TextDecoder().decode(memU8().subarray(idPtr, idPtr + idLen)) + : ""; + if (!id) { + mountEl = null; + if (canvas.parentNode !== document.body) document.body.appendChild(canvas); + canvas.style.cssText = "position:fixed;inset:0;width:100vw;height:100vh;display:none;"; + return; + } + const el = document.getElementById(id); + if (!el) { + console.warn(`[crafter-wgpu] wgpuSetCanvasMount: no element #${id}`); + return; + } + mountEl = el; + if (getComputedStyle(el).position === "static") el.style.position = "relative"; + if (canvas.parentNode !== el) el.appendChild(canvas); + canvas.style.cssText = + "position:absolute;inset:0;width:100%;height:100%;display:block;pointer-events:none;"; + ensureSized(); +}; + env.wgpuCreateBuffer = (byteSize) => { const h = newHandle(); const buf = device.createBuffer({ diff --git a/implementations/Crafter.Graphics-Device.cpp b/implementations/Crafter.Graphics-Device.cpp index 8738494..70235f8 100644 --- a/implementations/Crafter.Graphics-Device.cpp +++ b/implementations/Crafter.Graphics-Device.cpp @@ -568,6 +568,12 @@ void Device::Initialize() { // https://github.com/KhronosGroup/Vulkan-ValidationLayers/issues/12103 // Per spencer-lunarg (LunarG) that was fixed in the next SDK release. // The validation layer is now 1.4.350 (> 1.4.341), so re-enable it. + // Validation (incl. GPU-Assisted Validation) is a DEVELOPER tool and is OFF + // by default: GPU-AV instruments shaders and has crashed vendor SPIR-V + // compilers (observed: NVIDIA libnvidia-glvkspirv segfault on the + // descriptor-heap compute pipelines). Opt in by setting the environment + // variable CRAFTER_GRAPHICS_VALIDATION. + bool enableValidation = std::getenv("CRAFTER_GRAPHICS_VALIDATION") != nullptr; VkValidationFeatureEnableEXT enabledValidationFeatures[] = { VK_VALIDATION_FEATURE_ENABLE_GPU_ASSISTED_EXT, }; @@ -579,7 +585,7 @@ void Device::Initialize() { VkInstanceCreateInfo instanceCreateInfo = {}; instanceCreateInfo.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO; - instanceCreateInfo.pNext = &validationFeatures; + instanceCreateInfo.pNext = enableValidation ? (const void*)&validationFeatures : nullptr; instanceCreateInfo.pApplicationInfo = &app; instanceCreateInfo.enabledExtensionCount = sizeof(instanceExtensionNames) / sizeof(const char*); instanceCreateInfo.ppEnabledExtensionNames = instanceExtensionNames; @@ -603,7 +609,7 @@ void Device::Initialize() { } } - if (foundInstanceLayers >= sizeof(layerNames) / sizeof(const char*)) + if (enableValidation && foundInstanceLayers >= sizeof(layerNames) / sizeof(const char*)) { instanceCreateInfo.enabledLayerCount = sizeof(layerNames) / sizeof(const char*); instanceCreateInfo.ppEnabledLayerNames = layerNames; diff --git a/interfaces/Crafter.Graphics-WebGPU.cppm b/interfaces/Crafter.Graphics-WebGPU.cppm index 0cc81c9..0fdf263 100644 --- a/interfaces/Crafter.Graphics-WebGPU.cppm +++ b/interfaces/Crafter.Graphics-WebGPU.cppm @@ -31,6 +31,23 @@ namespace Crafter::WebGPU { __attribute__((import_module("env"), import_name("wgpuInit"))) extern "C" void wgpuInit(); + // Reparent the render canvas into a host DOM element so a scene renders + // inline inside the app's own layout rather than as a full-page layer. + // `idPtr`/`idLen` is a UTF-8 string naming an element id; an empty id + // detaches the canvas back to and hides it. Implemented in + // additional/dom-webgpu.js. Prefer the SetCanvasMount() wrapper below. + __attribute__((import_module("env"), import_name("wgpuSetCanvasMount"))) + extern "C" void wgpuSetCanvasMount(const void* idPtr, std::int32_t idLen); + + // Mount the render canvas into the DOM element with id `elementId` + // (the render surface is sized to that element each frame). Pass an + // empty string to detach and hide the canvas, leaving a plain DOM page. + // Safe to call any time after the module has started — e.g. from a + // route handler once the target element exists in the DOM. + export inline void SetCanvasMount(std::string_view elementId) { + wgpuSetCanvasMount(elementId.data(), static_cast(elementId.size())); + } + __attribute__((import_module("env"), import_name("wgpuCreateBuffer"))) extern "C" std::uint32_t wgpuCreateBuffer(std::int32_t byteSize); __attribute__((import_module("env"), import_name("wgpuWriteBuffer"))) diff --git a/project.cpp b/project.cpp index 97fb4de..3ccdd39 100644 --- a/project.cpp +++ b/project.cpp @@ -6,6 +6,15 @@ using namespace Crafter; extern "C" Configuration CrafterBuildProject(std::span args) { std::vector depArgs(args.begin(), args.end()); + // Crafter.Event/Math/Asset are always resolved from forgejo (GitProject); + // there is no local-sibling branch for them here. A consumer building + // THIS project with --local (to pick up local Crafter.Graphics edits) + // forwards --local down through depArgs, but these git deps can't honour + // it — the flag makes their resolver look for an unsuffixed local + // checkout that doesn't exist. Strip it so the sub-deps resolve normally + // while Crafter.Graphics itself is still built from the local tree. + std::erase(depArgs, "--local"); + Configuration* event = GitProject({ .source = { .url = "https://forgejo.catcrafts.net/Catcrafts/Crafter.Event.git" }, .args = depArgs,