wasm improvements
This commit is contained in:
parent
42a479572d
commit
6aa0338c0f
9 changed files with 603 additions and 70 deletions
|
|
@ -109,10 +109,60 @@ function setStyle(cookie, stylePtr, styleLen) {
|
|||
const el = __jsmemory.get(cookie);
|
||||
if (el) el.style.cssText = __readUtf8(stylePtr, styleLen);
|
||||
}
|
||||
// NOTE: setProperty is a CSS custom-property setter (el.style.setProperty),
|
||||
// NOT setAttribute. The name is historical and misleading; the attribute
|
||||
// functions below are the ones you want for href/src/disabled/aria-*.
|
||||
function setProperty(cookie, propPtr, propLen, valPtr, valLen) {
|
||||
const el = __jsmemory.get(cookie);
|
||||
if (el) el.style.setProperty(__readUtf8(propPtr, propLen), __readUtf8(valPtr, valLen));
|
||||
}
|
||||
// Real attribute access. Without these the only way to change an href, src,
|
||||
// disabled or aria-* was to re-render the parent's innerHTML, which destroys
|
||||
// every descendant (and their event listeners) to change one string.
|
||||
function setAttribute(cookie, namePtr, nameLen, valPtr, valLen) {
|
||||
const el = __jsmemory.get(cookie);
|
||||
if (el) el.setAttribute(__readUtf8(namePtr, nameLen), __readUtf8(valPtr, valLen));
|
||||
}
|
||||
function removeAttribute(cookie, namePtr, nameLen) {
|
||||
const el = __jsmemory.get(cookie);
|
||||
if (el) el.removeAttribute(__readUtf8(namePtr, nameLen));
|
||||
}
|
||||
// Returns 0 for "absent" so the caller can tell it apart from an attribute
|
||||
// that is present with an empty value (`disabled=""`, `alt=""`) — which
|
||||
// matters because for boolean attributes presence alone is the signal.
|
||||
function getAttribute(cookie, namePtr, nameLen) {
|
||||
const el = __jsmemory.get(cookie);
|
||||
if (!el) return 0;
|
||||
const v = el.getAttribute(__readUtf8(namePtr, nameLen));
|
||||
if (v === null) return 0;
|
||||
return __writeUtf8(v);
|
||||
}
|
||||
function hasAttribute(cookie, namePtr, nameLen) {
|
||||
const el = __jsmemory.get(cookie);
|
||||
if (!el) return false;
|
||||
return el.hasAttribute(__readUtf8(namePtr, nameLen));
|
||||
}
|
||||
// Checkbox / radio state. el.value on a checkbox returns "on" regardless of
|
||||
// whether it is ticked, so GetValue cannot express this.
|
||||
function getChecked(cookie) {
|
||||
const el = __jsmemory.get(cookie);
|
||||
return !!(el && el.checked);
|
||||
}
|
||||
function setChecked(cookie, checked) {
|
||||
const el = __jsmemory.get(cookie);
|
||||
if (el) el.checked = !!checked;
|
||||
}
|
||||
// Move keyboard focus. AddFocusListener could observe focus but nothing
|
||||
// could set it, which makes an error summary ("jump to the first invalid
|
||||
// field") impossible to implement accessibly.
|
||||
function focusElement(cookie) {
|
||||
const el = __jsmemory.get(cookie);
|
||||
if (el && el.focus) el.focus();
|
||||
}
|
||||
function blurElement(cookie) {
|
||||
const el = __jsmemory.get(cookie);
|
||||
if (el && el.blur) el.blur();
|
||||
}
|
||||
function addClass(cookie, namePtr, nameLen) {
|
||||
const el = __jsmemory.get(cookie);
|
||||
if (el) el.classList.add(__readUtf8(namePtr, nameLen));
|
||||
|
|
@ -161,12 +211,25 @@ function __dpr() {
|
|||
return window.crafter_dpr || window.devicePixelRatio || 1;
|
||||
}
|
||||
|
||||
// `preventDefault` is opt-in per listener (the C++ Add*Listener overloads
|
||||
// take it as a trailing argument). Two reasons it has to exist here rather
|
||||
// than being something C++ can do after the fact:
|
||||
// * the wasm callback runs synchronously inside the dispatch, but the
|
||||
// decision has to be made before the browser acts on the event, and
|
||||
// there is no handle to the event object on the C++ side;
|
||||
// * intercepting a click on a real <a href> — the whole basis of
|
||||
// client-side routing over crawlable links — is impossible without it.
|
||||
// Cancelling happens BEFORE the wasm call so a trap or exception inside the
|
||||
// handler can't leave the default action to fire anyway.
|
||||
// `{ passive: false }` is required or the browser silently ignores
|
||||
// preventDefault on wheel (and warns); harmless on the other kinds.
|
||||
function __makeMouseListenerPair(kind, eventName, exportName) {
|
||||
return {
|
||||
add(cookie, id) {
|
||||
add(cookie, id, preventDefault) {
|
||||
const el = __jsmemory.get(cookie);
|
||||
if (!el) return;
|
||||
const handler = (event) => {
|
||||
if (preventDefault) event.preventDefault();
|
||||
const s = __dpr();
|
||||
__wasm()[exportName](id,
|
||||
event.clientX * s, event.clientY * s,
|
||||
|
|
@ -175,7 +238,7 @@ function __makeMouseListenerPair(kind, eventName, exportName) {
|
|||
event.altKey, event.ctrlKey, event.shiftKey, event.metaKey);
|
||||
};
|
||||
__listenerHandlers.set(`${cookie}-${id}-${kind}`, handler);
|
||||
el.addEventListener(eventName, handler);
|
||||
el.addEventListener(eventName, handler, preventDefault ? { passive: false } : undefined);
|
||||
},
|
||||
remove(cookie, id) {
|
||||
const el = __jsmemory.get(cookie);
|
||||
|
|
@ -188,17 +251,18 @@ function __makeMouseListenerPair(kind, eventName, exportName) {
|
|||
}
|
||||
function __makeKeyListenerPair(kind, eventName, exportName) {
|
||||
return {
|
||||
add(cookie, id) {
|
||||
add(cookie, id, preventDefault) {
|
||||
const el = __jsmemory.get(cookie);
|
||||
if (!el) return;
|
||||
const handler = (event) => {
|
||||
if (preventDefault) event.preventDefault();
|
||||
const keyPtr = __writeUtf8(event.key || "");
|
||||
__wasm()[exportName](id, keyPtr, event.keyCode,
|
||||
event.altKey, event.ctrlKey, event.shiftKey, event.metaKey);
|
||||
__wasm().WasmFree(keyPtr);
|
||||
};
|
||||
__listenerHandlers.set(`${cookie}-${id}-${kind}`, handler);
|
||||
el.addEventListener(eventName, handler);
|
||||
el.addEventListener(eventName, handler, preventDefault ? { passive: false } : undefined);
|
||||
},
|
||||
remove(cookie, id) {
|
||||
const el = __jsmemory.get(cookie);
|
||||
|
|
@ -339,9 +403,10 @@ const __scrollPair = {
|
|||
}
|
||||
};
|
||||
const __wheelPair = {
|
||||
add(cookie, id) {
|
||||
add(cookie, id, preventDefault) {
|
||||
const el = __jsmemory.get(cookie); if (!el) return;
|
||||
const handler = (event) => {
|
||||
if (preventDefault) event.preventDefault();
|
||||
const s = __dpr();
|
||||
__wasm().ExecuteWheelHandler(id,
|
||||
event.deltaX, event.deltaY, event.deltaZ, event.deltaMode,
|
||||
|
|
@ -350,7 +415,10 @@ const __wheelPair = {
|
|||
event.altKey, event.ctrlKey, event.shiftKey, event.metaKey);
|
||||
};
|
||||
__listenerHandlers.set(`${cookie}-${id}-wheel`, handler);
|
||||
el.addEventListener("wheel", handler);
|
||||
// passive:false is mandatory here — Chrome treats wheel as passive by
|
||||
// default and would otherwise drop the preventDefault with a console
|
||||
// warning rather than an error.
|
||||
el.addEventListener("wheel", handler, preventDefault ? { passive: false } : undefined);
|
||||
},
|
||||
remove(cookie, id) {
|
||||
const el = __jsmemory.get(cookie);
|
||||
|
|
@ -619,9 +687,45 @@ function removePopStateListener(id) {
|
|||
if (h) window.removeEventListener("popstate", h);
|
||||
__listenerHandlers.delete(`popstate-${id}`);
|
||||
}
|
||||
// replaceState mirrors pushState but rewrites the current history entry
|
||||
// instead of adding one. Needed for anything that changes the URL without
|
||||
// being a distinct "back" destination — filter/sort state, canonicalising a
|
||||
// sloppy incoming URL, or recording scroll position on the entry you are
|
||||
// about to leave.
|
||||
function replaceState(dataPtr, dataLen, titlePtr, titleLen, urlPtr, urlLen) {
|
||||
const dataStr = __readUtf8(dataPtr, dataLen);
|
||||
const titleStr = __readUtf8(titlePtr, titleLen);
|
||||
const urlStr = __readUtf8(urlPtr, urlLen);
|
||||
let parsed;
|
||||
try { parsed = JSON.parse(dataStr); } catch { parsed = null; }
|
||||
window.history.replaceState(parsed, titleStr, urlStr);
|
||||
}
|
||||
function getPathName() {
|
||||
return __writeUtf8(window.location.pathname);
|
||||
}
|
||||
// location.search / .hash, including their leading "?" / "#" so an empty
|
||||
// query and "?" are distinguishable, and so the result can be concatenated
|
||||
// straight back onto a path. pathname alone left query strings completely
|
||||
// unreachable from wasm, which made ?page= / ?sort= impossible to read.
|
||||
function getSearch() {
|
||||
return __writeUtf8(window.location.search);
|
||||
}
|
||||
function getHash() {
|
||||
return __writeUtf8(window.location.hash);
|
||||
}
|
||||
function getHref() {
|
||||
return __writeUtf8(window.location.href);
|
||||
}
|
||||
// Full-page navigation, including cross-origin. pushState deliberately
|
||||
// cannot leave the origin, so this is the only way for a wasm app to hand
|
||||
// the user off to an external URL (an OAuth consent screen, a hosted
|
||||
// payment page). `replace` omits the current page from history so Back
|
||||
// doesn't return to a stale checkout.
|
||||
function navigate(urlPtr, urlLen, replace) {
|
||||
const url = __readUtf8(urlPtr, urlLen);
|
||||
if (replace) window.location.replace(url);
|
||||
else window.location.assign(url);
|
||||
}
|
||||
|
||||
// ─── Gamepad polling helper ───────────────────────────────────────────
|
||||
//
|
||||
|
|
@ -659,6 +763,8 @@ Object.assign(window.crafter_webbuild_env, {
|
|||
// DOM lookup / creation / mutation
|
||||
freeJs, getElementById, createElement, getBody,
|
||||
setInnerHTML, setStyle, setProperty,
|
||||
setAttribute, removeAttribute, getAttribute, hasAttribute,
|
||||
getChecked, setChecked, focusElement, blurElement,
|
||||
addClass, removeClass, toggleClass, hasClass,
|
||||
deleteElement, getValue, setValue,
|
||||
|
||||
|
|
@ -697,7 +803,8 @@ Object.assign(window.crafter_webbuild_env, {
|
|||
clipboardSetText, clipboardGetText,
|
||||
|
||||
// History
|
||||
pushState, addPopStateListener, removePopStateListener, getPathName,
|
||||
pushState, replaceState, addPopStateListener, removePopStateListener,
|
||||
getPathName, getSearch, getHash, getHref, navigate,
|
||||
|
||||
// Gamepad
|
||||
gamepadPollConnected, gamepadPollDisconnected,
|
||||
|
|
|
|||
|
|
@ -94,6 +94,16 @@ document.body.appendChild(canvas);
|
|||
// when set, the canvas is reparented into that element and sized to it.
|
||||
let mountEl = null;
|
||||
|
||||
// Upper bound on the canvas backing store, in pixels. Infinity until an RT
|
||||
// dispatch derives the real budget from the device's buffer limits (see
|
||||
// wgpuDispatchRT): the wavefront ray buffers scale with W·H·raysPerPixel,
|
||||
// and a 256 MiB maxBufferSize (Firefox's baseline) overflows at ~0.5 Mpx
|
||||
// for 4 rays/pixel — far below a HiDPI 16:9 mount — failing not at the
|
||||
// createBuffer call but as an uncapturable device OOM at submit. The
|
||||
// mounted canvas is CSS-pinned to width:100%/height:100%, so a clamped
|
||||
// backing store upscales on screen instead of shrinking.
|
||||
let maxCanvasPixels = Infinity;
|
||||
|
||||
function syncCanvasSize() {
|
||||
// Canvas pixel size = CSS size × devicePixelRatio so the GPU draws
|
||||
// at physical pixel resolution on HiDPI displays — otherwise the
|
||||
|
|
@ -117,8 +127,13 @@ function syncCanvasSize() {
|
|||
cssW = window.innerWidth;
|
||||
cssH = window.innerHeight;
|
||||
}
|
||||
const w = Math.max(1, Math.round(cssW * dpr));
|
||||
const h = Math.max(1, Math.round(cssH * dpr));
|
||||
let w = Math.max(1, Math.round(cssW * dpr));
|
||||
let h = Math.max(1, Math.round(cssH * dpr));
|
||||
if (w * h > maxCanvasPixels) {
|
||||
const s = Math.sqrt(maxCanvasPixels / (w * h));
|
||||
w = Math.max(1, Math.floor(w * s));
|
||||
h = Math.max(1, Math.floor(h * s));
|
||||
}
|
||||
if (canvas.width !== w) canvas.width = w;
|
||||
if (canvas.height !== h) canvas.height = h;
|
||||
return { w, h };
|
||||
|
|
@ -3511,6 +3526,28 @@ env.wgpuDispatchRT = (pipelineHandle, pushPtr, pushBytes,
|
|||
}
|
||||
const W = state.width, H = state.height;
|
||||
const depth = Math.max(1, maxDepth | 0);
|
||||
// Clamp the render resolution to what the wavefront buffers can hold.
|
||||
// The payload store is the largest per-ray cost (2 regions ×
|
||||
// WF_PAYLOAD_BYTES) and must fit both maxBufferSize and one storage
|
||||
// binding; the indirect TRACE/SHADE dispatch additionally bounds rays
|
||||
// at maxComputeWorkgroupsPerDimension×64. When the canvas is over
|
||||
// budget, tighten maxCanvasPixels and sit this frame out — the next
|
||||
// wgpuFrameBegin's ensureSized() shrinks the backing store (CSS keeps
|
||||
// the on-screen size) and rendering resumes within budget.
|
||||
const rpp = Math.max(1, raysPerPixel | 0);
|
||||
const rayBudget = Math.min(
|
||||
Math.floor(Math.min(device.limits.maxBufferSize,
|
||||
device.limits.maxStorageBufferBindingSize)
|
||||
/ (2 * WF_PAYLOAD_BYTES)),
|
||||
device.limits.maxComputeWorkgroupsPerDimension * 64);
|
||||
const pixelBudget = Math.max(1, Math.floor(rayBudget / rpp));
|
||||
if (pixelBudget < maxCanvasPixels) maxCanvasPixels = pixelBudget;
|
||||
if (W * H > maxCanvasPixels) {
|
||||
console.warn(`[crafter-wgpu] ${W}x${H} at ${rpp} rays/px overflows the `
|
||||
+ `wavefront budget (maxBufferSize ${device.limits.maxBufferSize}); `
|
||||
+ `reducing render resolution to ~${maxCanvasPixels}px`);
|
||||
return;
|
||||
}
|
||||
const wf = ensureWavefrontBuffers(W, H, raysPerPixel);
|
||||
const cap = wf.cap; // per-bounce ray capacity = raysPerPixel·W·H
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue