webgpu embedding

This commit is contained in:
Jorijn van der Graaf 2026-07-19 01:38:25 +02:00
commit a879c834c7
4 changed files with 86 additions and 4 deletions

View file

@ -54,6 +54,7 @@ function stub(name) {
"wgpuRegisterMeshBLAS", "wgpuRegisterMeshBLASDeviceAabbs", "wgpuRefitMeshBLASDeviceAabbs", "wgpuRegisterMeshBLAS", "wgpuRegisterMeshBLASDeviceAabbs", "wgpuRefitMeshBLASDeviceAabbs",
"wgpuLoadRTPipeline", "wgpuDispatchRT", "wgpuBuildTLAS", "wgpuLoadRTPipeline", "wgpuDispatchRT", "wgpuBuildTLAS",
"wgpuLoadComputePipeline", "wgpuDispatchCompute", "wgpuLoadComputePipeline", "wgpuDispatchCompute",
"wgpuSetCanvasMount",
]) { ]) {
// Read-write ints don't need a stub-throw; return 0 for the size queries. // Read-write ints don't need a stub-throw; return 0 for the size queries.
e[n] = n.endsWith("Width") || n.endsWith("Height") 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.style.margin = "0";
document.body.appendChild(canvas); 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() { function syncCanvasSize() {
// Canvas pixel size = CSS size × devicePixelRatio so the GPU draws // Canvas pixel size = CSS size × devicePixelRatio so the GPU draws
// at physical pixel resolution on HiDPI displays — otherwise the // 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. // share the physical-pixel coordinate space with window.width/.height.
const dpr = window.devicePixelRatio || 1; const dpr = window.devicePixelRatio || 1;
window.crafter_dpr = dpr; window.crafter_dpr = dpr;
const w = Math.max(1, Math.round(window.innerWidth * dpr)); // Mounted → track the host element's box; unmounted → the viewport.
const h = Math.max(1, Math.round(window.innerHeight * dpr)); 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.width !== w) canvas.width = w;
if (canvas.height !== h) canvas.height = h; if (canvas.height !== h) canvas.height = h;
return { w, h }; return { w, h };
@ -566,6 +582,40 @@ const env = window.crafter_webbuild_env;
env.wgpuGetCanvasWidth = () => canvas.width; env.wgpuGetCanvasWidth = () => canvas.width;
env.wgpuGetCanvasHeight = () => canvas.height; 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 <body> 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) => { env.wgpuCreateBuffer = (byteSize) => {
const h = newHandle(); const h = newHandle();
const buf = device.createBuffer({ const buf = device.createBuffer({

View file

@ -568,6 +568,12 @@ void Device::Initialize() {
// https://github.com/KhronosGroup/Vulkan-ValidationLayers/issues/12103 // https://github.com/KhronosGroup/Vulkan-ValidationLayers/issues/12103
// Per spencer-lunarg (LunarG) that was fixed in the next SDK release. // 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. // 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[] = { VkValidationFeatureEnableEXT enabledValidationFeatures[] = {
VK_VALIDATION_FEATURE_ENABLE_GPU_ASSISTED_EXT, VK_VALIDATION_FEATURE_ENABLE_GPU_ASSISTED_EXT,
}; };
@ -579,7 +585,7 @@ void Device::Initialize() {
VkInstanceCreateInfo instanceCreateInfo = {}; VkInstanceCreateInfo instanceCreateInfo = {};
instanceCreateInfo.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO; instanceCreateInfo.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO;
instanceCreateInfo.pNext = &validationFeatures; instanceCreateInfo.pNext = enableValidation ? (const void*)&validationFeatures : nullptr;
instanceCreateInfo.pApplicationInfo = &app; instanceCreateInfo.pApplicationInfo = &app;
instanceCreateInfo.enabledExtensionCount = sizeof(instanceExtensionNames) / sizeof(const char*); instanceCreateInfo.enabledExtensionCount = sizeof(instanceExtensionNames) / sizeof(const char*);
instanceCreateInfo.ppEnabledExtensionNames = instanceExtensionNames; 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.enabledLayerCount = sizeof(layerNames) / sizeof(const char*);
instanceCreateInfo.ppEnabledLayerNames = layerNames; instanceCreateInfo.ppEnabledLayerNames = layerNames;

View file

@ -31,6 +31,23 @@ namespace Crafter::WebGPU {
__attribute__((import_module("env"), import_name("wgpuInit"))) __attribute__((import_module("env"), import_name("wgpuInit")))
extern "C" void 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 <body> 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<std::int32_t>(elementId.size()));
}
__attribute__((import_module("env"), import_name("wgpuCreateBuffer"))) __attribute__((import_module("env"), import_name("wgpuCreateBuffer")))
extern "C" std::uint32_t wgpuCreateBuffer(std::int32_t byteSize); extern "C" std::uint32_t wgpuCreateBuffer(std::int32_t byteSize);
__attribute__((import_module("env"), import_name("wgpuWriteBuffer"))) __attribute__((import_module("env"), import_name("wgpuWriteBuffer")))

View file

@ -6,6 +6,15 @@ using namespace Crafter;
extern "C" Configuration CrafterBuildProject(std::span<const std::string_view> args) { extern "C" Configuration CrafterBuildProject(std::span<const std::string_view> args) {
std::vector<std::string> depArgs(args.begin(), args.end()); std::vector<std::string> 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({ Configuration* event = GitProject({
.source = { .url = "https://forgejo.catcrafts.net/Catcrafts/Crafter.Event.git" }, .source = { .url = "https://forgejo.catcrafts.net/Catcrafts/Crafter.Event.git" },
.args = depArgs, .args = depArgs,