// Copyright 2025 International Digital Economy Academy
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

///|
extern "js" fn set_viewport_height(
  elem : @dom.HTMLCanvasElement,
  height : Double,
) -> Unit = "(x, y) => { x.height = y; }"

///|
extern "js" fn set_viewport_width(
  elem : @dom.HTMLCanvasElement,
  width : Double,
) -> Unit = "(x, y) => { x.width = y; }"

///|
extern "js" fn request_pointer_lock_if_enabled(
  elem : @dom.HTMLCanvasElement,
  enabled : Bool,
) -> Unit = "(elem, enabled) => { if (enabled && document.pointerLockElement !== elem) { elem.requestPointerLock(); } }"

///|
extern "js" fn is_pointer_locked(canvas : @dom.HTMLCanvasElement) -> Bool = "(canvas) => document.pointerLockElement === canvas"

///|
extern "js" fn exit_pointer_lock() -> Unit = "() => { if (document.exitPointerLock) { document.exitPointerLock(); } }"

///|
extern "js" fn document_add_event_listener(
  event : String,
  callback : (@dom.Event) -> Unit,
) -> Unit = "(event, callback) => { document.addEventListener(event, callback); }"

///|
extern "js" fn document_hidden() -> Bool = "() => document.hidden"

///|
extern "js" fn prevent_default_event(event : @dom.Event) -> Unit = "(event) => event.preventDefault()"

///|
extern "js" fn load_font_async(font : String, path : String) -> Unit =
  #| (font, path) => {
  #|   const ff = new FontFace(font, 'url(' + path + ')');
  #|   ff.load().then(loadedFont => {
  #|     document.fonts.add(loadedFont);
  #|   });
  #| }

///|
extern "js" fn webgpu_preload_font_bytes(font : String, path : String) -> Unit =
  #| (font, path) => {
  #|   const g = globalThis;
  #|   if (!g.__selene_font_bytes_states) {
  #|     g.__selene_font_bytes_states = new Map();
  #|   }
  #|   const states = g.__selene_font_bytes_states;
  #|   const rec = states.get(font);
  #|   if (rec && (rec.status === 0 || rec.status === 1)) {
  #|     return;
  #|   }
  #|   states.set(font, { status: 0, bytes: [] });
  #|   fetch(path)
  #|     .then((res) => {
  #|       if (!res.ok) throw new Error('failed to fetch font');
  #|       return res.arrayBuffer();
  #|     })
  #|     .then((buf) => {
  #|       states.set(font, { status: 1, bytes: Array.from(new Uint8Array(buf)) });
  #|     })
  #|     .catch(() => {
  #|       states.set(font, { status: 2, bytes: [] });
  #|     });
  #| }

///|
extern "js" fn webgpu_font_bytes_status(font : String) -> Int =
  #| (font) => {
  #|   const states = globalThis.__selene_font_bytes_states;
  #|   if (!states) return -1;
  #|   const rec = states.get(font);
  #|   if (!rec) return -1;
  #|   return rec.status | 0;
  #| }

///|
extern "js" fn webgpu_take_font_bytes(font : String) -> Array[Int] =
  #| (font) => {
  #|   const states = globalThis.__selene_font_bytes_states;
  #|   if (!states) return [];
  #|   const rec = states.get(font);
  #|   if (!rec || rec.status !== 1) return [];
  #|   states.set(font, { status: 3, bytes: [] });
  #|   return rec.bytes || [];
  #| }

///|
extern "js" fn resolve_asset_path(path : String) -> String =
  #| (path) => {
  #|   if (
  #|     path.startsWith("http://") ||
  #|     path.startsWith("https://") ||
  #|     path.startsWith("data:") ||
  #|     path.startsWith("blob:")
  #|   ) {
  #|     return path;
  #|   }
  #|   if (!path.startsWith("assets/")) {
  #|     return path;
  #|   }
  #|   try {
  #|     const href = globalThis.location?.href;
  #|     if (!href) return path;
  #|     return new URL(path, href).pathname;
  #|   } catch (_err) {
  #|     return path;
  #|   }
  #| }

///|
extern "js" fn webgpu_load_file_bytes_sync(path : String) -> Array[Int] =
  #| (path) => {
  #|   try {
  #|     const xhr = new XMLHttpRequest();
  #|     xhr.open('GET', path, false);
  #|     // Browsers reject `responseType = "arraybuffer"` for synchronous XHR
  #|     // on document contexts. Use x-user-defined text decoding to preserve
  #|     // raw byte values for sync file reads.
  #|     xhr.overrideMimeType('text/plain; charset=x-user-defined');
  #|     xhr.send(null);
  #|     if (xhr.status >= 200 && xhr.status < 300) {
  #|       const text = xhr.responseText || '';
  #|       const out = new Array(text.length);
  #|       for (let i = 0; i < text.length; i += 1) {
  #|         out[i] = text.charCodeAt(i) & 0xff;
  #|       }
  #|       return out;
  #|     }
  #|   } catch (_err) {
  #|     return [];
  #|   }
  #|   return [];
  #| }

///|
extern "js" fn webgpu_gamepad_count() -> Int =
  #| () => {
  #|   const pads = globalThis.navigator?.getGamepads?.();
  #|   return pads ? (pads.length | 0) : 0;
  #| }

///|
extern "js" fn webgpu_gamepad_connected(index : Int) -> Bool =
  #| (index) => {
  #|   const pads = globalThis.navigator?.getGamepads?.();
  #|   if (!pads || index < 0 || index >= pads.length) return false;
  #|   const pad = pads[index];
  #|   return !!(pad && pad.connected);
  #| }

///|
extern "js" fn webgpu_gamepad_button_pressed(index : Int, button : Int) -> Bool =
  #| (index, button) => {
  #|   const pads = globalThis.navigator?.getGamepads?.();
  #|   const pad = pads?.[index];
  #|   const b = pad?.buttons?.[button];
  #|   return !!(b && b.pressed);
  #| }

///|
extern "js" fn webgpu_gamepad_button_value(index : Int, button : Int) -> Double =
  #| (index, button) => {
  #|   const pads = globalThis.navigator?.getGamepads?.();
  #|   const pad = pads?.[index];
  #|   const b = pad?.buttons?.[button];
  #|   if (!b) return 0;
  #|   const v = Number(b.value);
  #|   if (!Number.isFinite(v)) return 0;
  #|   if (v < 0) return 0;
  #|   if (v > 1) return 1;
  #|   return v;
  #| }

///|
extern "js" fn webgpu_gamepad_axis_value(index : Int, axis : Int) -> Double =
  #| (index, axis) => {
  #|   const pads = globalThis.navigator?.getGamepads?.();
  #|   const pad = pads?.[index];
  #|   const a = pad?.axes?.[axis];
  #|   if (a == null) return 0;
  #|   const v = Number(a);
  #|   if (!Number.isFinite(v)) return 0;
  #|   if (v < -1) return -1;
  #|   if (v > 1) return 1;
  #|   return v;
  #| }

///|
extern "js" fn webgpu_measure_text(
  text : String,
  family : String,
  size : Double,
) -> Array[Double] =
  #| (text, family, size) => {
  #|   const cvs = document.createElement('canvas');
  #|   const ctx = cvs.getContext('2d');
  #|   const fontSize = Number.isFinite(size) && size > 0 ? size : 16;
  #|   const fontFamily = family && family.length > 0 ? family : 'Arial';
  #|   ctx.font = `${fontSize}px ${fontFamily}`;
  #|   const m = ctx.measureText(text ?? '');
  #|   const width = Math.max(0, Number(m.width) || 0);
  #|   const ascent = Math.max(0, Number(m.actualBoundingBoxAscent) || fontSize * 0.8);
  #|   const descent = Math.max(0, Number(m.actualBoundingBoxDescent) || fontSize * 0.2);
  #|   const height = Math.max(1, Math.ceil(ascent + descent));
  #|   return [width, height];
  #| }

///|
extern "js" fn webgpu_initialize(
  canvas : @dom.HTMLCanvasElement,
  width : Double,
  height : Double,
  image_smooth : Bool,
) -> Unit =
  #| (canvas, width, height, imageSmooth) => {
  #|   const g = globalThis;
  #|   if (!g.__selene_webgpu_runtime) {
  #|     const rt = {
  #|       canvas: null,
  #|       context: null,
  #|       adapter: null,
  #|       device: null,
  #|       format: null,
  #|       ready: false,
  #|       initPromise: null,
  #|       imageSmooth: true,
  #|       clearColor: [0, 0, 0, 1],
  #|       draw2dCommands: [],
  #|       sections3d: [],
  #|       current3dSection: null,
  #|       imageCache: new Map(),
  #|       textCache: new Map(),
  #|       light3d: {
  #|         directionalLights: [],
  #|         pointLights: [],
  #|         spotLights: [],
  #|         ambient: [1, 1, 1],
  #|         directionalShadowMapSize: 2048,
  #|         pointShadowMapSize: 1024,
  #|       },
  #|       samplers: null,
  #|       pipelines: null,
  #|       bindGroupLayouts: null,
  #|       uniformBuffers: null,
  #|       bindGroups: null,
  #|       depthTexture: null,
  #|       depthView: null,
  #|       depthWidth: 0,
  #|       depthHeight: 0,
  #|       shadowTextures: {
  #|         directional: [null, null, null, null],
  #|         spot: null,
  #|         point: null,
  #|         dummy: null,
  #|       },
  #|       shadowCameraBuffers: [],
  #|       dynamicBuffers: new Map(),
  #|     };
  #|
  #|     const clamp01 = (x) => Math.max(0, Math.min(1, x));
  #|     const colorNorm = (r, g, b, a) => [
  #|       clamp01((r || 0) / 255.0),
  #|       clamp01((g || 0) / 255.0),
  #|       clamp01((b || 0) / 255.0),
  #|       clamp01(a == null ? 1.0 : a),
  #|     ];
  #|     const toCanvasSize = () => [
  #|       Math.max(1, Math.floor(rt.canvas?.width || 1)),
  #|       Math.max(1, Math.floor(rt.canvas?.height || 1)),
  #|     ];
  #|     const nextPow2 = (v) => {
  #|       let n = 1;
  #|       while (n < v) n <<= 1;
  #|       return n;
  #|     };
  #|     const mat4Multiply = (a, b) => {
  #|       const out = new Float32Array(16);
  #|       for (let c = 0; c < 4; c += 1) {
  #|         for (let r = 0; r < 4; r += 1) {
  #|           out[c * 4 + r] =
  #|             a[0 * 4 + r] * b[c * 4 + 0] +
  #|             a[1 * 4 + r] * b[c * 4 + 1] +
  #|             a[2 * 4 + r] * b[c * 4 + 2] +
  #|             a[3 * 4 + r] * b[c * 4 + 3];
  #|         }
  #|       }
  #|       return out;
  #|     };
  #|     const vec3Sub = (a, b) => [a[0] - b[0], a[1] - b[1], a[2] - b[2]];
  #|     const vec3Dot = (a, b) => a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
  #|     const vec3Cross = (a, b) => [
  #|       a[1] * b[2] - a[2] * b[1],
  #|       a[2] * b[0] - a[0] * b[2],
  #|       a[0] * b[1] - a[1] * b[0],
  #|     ];
  #|     const vec3Norm = (v) => {
  #|       const len = Math.hypot(v[0], v[1], v[2]) || 1;
  #|       return [v[0] / len, v[1] / len, v[2] / len];
  #|     };
  #|     const mat4LookAt = (eye, target, up) => {
  #|       const z = vec3Norm(vec3Sub(eye, target));
  #|       const x = vec3Norm(vec3Cross(up, z));
  #|       const y = vec3Cross(z, x);
  #|       return new Float32Array([
  #|         x[0], y[0], z[0], 0,
  #|         x[1], y[1], z[1], 0,
  #|         x[2], y[2], z[2], 0,
  #|         -vec3Dot(x, eye), -vec3Dot(y, eye), -vec3Dot(z, eye), 1,
  #|       ]);
  #|     };
  #|     const mat4Perspective = (fovyDeg, aspect, near, far) => {
  #|       const fovy = (fovyDeg * Math.PI) / 180.0;
  #|       const f = 1.0 / Math.tan(fovy / 2.0);
  #|       return new Float32Array([
  #|         f / aspect, 0, 0, 0,
  #|         0, f, 0, 0,
  #|         0, 0, far / (near - far), -1,
  #|         0, 0, (near * far) / (near - far), 0,
  #|       ]);
  #|     };
  #|     const mat4Orthographic = (width, height, near, far) => {
  #|       const halfW = Math.max(Math.abs(width), 0.0001) / 2.0;
  #|       const halfH = Math.max(Math.abs(height), 0.0001) / 2.0;
  #|       const left = -halfW;
  #|       const right = halfW;
  #|       const bottom = -halfH;
  #|       const top = halfH;
  #|       const dz = Math.max(far - near, 0.0001);
  #|       return new Float32Array([
  #|         2.0 / (right - left), 0, 0, 0,
  #|         0, 2.0 / (top - bottom), 0, 0,
  #|         0, 0, 1.0 / (near - far), 0,
  #|         -(right + left) / (right - left),
  #|         -(top + bottom) / (top - bottom),
  #|         near / (near - far),
  #|         1,
  #|       ]);
  #|     };
  #|     const mat4Translation = (x, y, z) =>
  #|       new Float32Array([
  #|         1, 0, 0, 0,
  #|         0, 1, 0, 0,
  #|         0, 0, 1, 0,
  #|         x, y, z, 1,
  #|       ]);
  #|     ;
  #|     const vec3Add = (a, b) => [a[0] + b[0], a[1] + b[1], a[2] + b[2]];
  #|     const vec3Scale = (v, s) => [v[0] * s, v[1] * s, v[2] * s];
  #|     const vec3LengthSq = (v) => vec3Dot(v, v);
  #|     const vec3Min = (a, b) => [
  #|       Math.min(a[0], b[0]),
  #|       Math.min(a[1], b[1]),
  #|       Math.min(a[2], b[2]),
  #|     ];
  #|     const vec3Max = (a, b) => [
  #|       Math.max(a[0], b[0]),
  #|       Math.max(a[1], b[1]),
  #|       Math.max(a[2], b[2]),
  #|     ];
  #|     const chooseShadowUp = (direction) => {
  #|       const normalized = vec3LengthSq(direction) <= 1e-8
  #|         ? [0, -1, 0]
  #|         : vec3Norm(direction);
  #|       const worldUp = [0, 1, 0];
  #|       return vec3LengthSq(vec3Cross(normalized, worldUp)) <= 1e-4
  #|         ? [0, 0, 1]
  #|         : worldUp;
  #|     };
  #|     const cameraBasis = (camera) => {
  #|       const forward = vec3LengthSq(vec3Sub(camera.target, camera.position)) <= 1e-8
  #|         ? [0, 0, -1]
  #|         : vec3Norm(vec3Sub(camera.target, camera.position));
  #|       const up = vec3LengthSq(camera.up) <= 1e-8
  #|         ? [0, 1, 0]
  #|         : vec3Norm(camera.up);
  #|       const right = vec3LengthSq(vec3Cross(forward, up)) <= 1e-8
  #|         ? [1, 0, 0]
  #|         : vec3Norm(vec3Cross(forward, up));
  #|       const correctedUp = vec3LengthSq(vec3Cross(right, forward)) <= 1e-8
  #|         ? [0, 1, 0]
  #|         : vec3Norm(vec3Cross(right, forward));
  #|       return { forward, right, up: correctedUp };
  #|     };
  #|     const cameraFrustumSliceCorners = (camera, aspect, nearBound, farBound) => {
  #|       const { forward, right, up } = cameraBasis(camera);
  #|       const corners = [];
  #|       if (camera.orthographic) {
  #|         const halfWidth = Math.max(Math.abs(camera.orthoWidth || 20), 0.0001) * 0.5;
  #|         const halfHeight = Math.max(Math.abs(camera.orthoHeight || 20), 0.0001) * 0.5;
  #|         const nearCenter = vec3Add(camera.position, vec3Scale(forward, nearBound));
  #|         const farCenter = vec3Add(camera.position, vec3Scale(forward, farBound));
  #|         corners.push(
  #|           vec3Sub(vec3Sub(nearCenter, vec3Scale(right, halfWidth)), vec3Scale(up, halfHeight)),
  #|           vec3Add(vec3Sub(nearCenter, vec3Scale(up, halfHeight)), vec3Scale(right, halfWidth)),
  #|           vec3Add(vec3Add(nearCenter, vec3Scale(right, halfWidth)), vec3Scale(up, halfHeight)),
  #|           vec3Add(vec3Sub(nearCenter, vec3Scale(right, halfWidth)), vec3Scale(up, halfHeight)),
  #|           vec3Sub(vec3Sub(farCenter, vec3Scale(right, halfWidth)), vec3Scale(up, halfHeight)),
  #|           vec3Add(vec3Sub(farCenter, vec3Scale(up, halfHeight)), vec3Scale(right, halfWidth)),
  #|           vec3Add(vec3Add(farCenter, vec3Scale(right, halfWidth)), vec3Scale(up, halfHeight)),
  #|           vec3Add(vec3Sub(farCenter, vec3Scale(right, halfWidth)), vec3Scale(up, halfHeight)),
  #|         );
  #|         return corners;
  #|       }
  #|       const fovy = (Math.max(0.0001, Number(camera.fovy) || 0.0001) * Math.PI) / 180.0;
  #|       const tanHalfY = Math.tan(fovy * 0.5);
  #|       const nearHeight = nearBound * tanHalfY;
  #|       const nearWidth = nearHeight * aspect;
  #|       const farHeight = farBound * tanHalfY;
  #|       const farWidth = farHeight * aspect;
  #|       const nearCenter = vec3Add(camera.position, vec3Scale(forward, nearBound));
  #|       const farCenter = vec3Add(camera.position, vec3Scale(forward, farBound));
  #|       corners.push(
  #|         vec3Sub(vec3Sub(nearCenter, vec3Scale(right, nearWidth)), vec3Scale(up, nearHeight)),
  #|         vec3Add(vec3Sub(nearCenter, vec3Scale(up, nearHeight)), vec3Scale(right, nearWidth)),
  #|         vec3Add(vec3Add(nearCenter, vec3Scale(right, nearWidth)), vec3Scale(up, nearHeight)),
  #|         vec3Add(vec3Sub(nearCenter, vec3Scale(right, nearWidth)), vec3Scale(up, nearHeight)),
  #|         vec3Sub(vec3Sub(farCenter, vec3Scale(right, farWidth)), vec3Scale(up, farHeight)),
  #|         vec3Add(vec3Sub(farCenter, vec3Scale(up, farHeight)), vec3Scale(right, farWidth)),
  #|         vec3Add(vec3Add(farCenter, vec3Scale(right, farWidth)), vec3Scale(up, farHeight)),
  #|         vec3Add(vec3Sub(farCenter, vec3Scale(right, farWidth)), vec3Scale(up, farHeight)),
  #|       );
  #|       return corners;
  #|     };
  #|     const directionalShadowBounds = (light, camera) => {
  #|       const config = light.cascadeConfig || {};
  #|       const minimumDistance = Math.max(
  #|         Math.max(0, Number(camera.near) || 0.1),
  #|         Math.max(0, Number(config.minimumDistance) || 0),
  #|       );
  #|       const far = Number(camera.far) || (minimumDistance + 200.0);
  #|       if (far <= minimumDistance) return [];
  #|       const overlap = Math.max(0, Math.min(1, Number(config.overlapProportion) || 0));
  #|       const authoredBounds = Array.isArray(config.bounds) ? config.bounds : [];
  #|       const cascadeCount = Math.max(1, Math.min(4, authoredBounds.length || 1));
  #|       const bounds = [];
  #|       let previousFar = minimumDistance;
  #|       for (let index = 0; index < cascadeCount; index += 1) {
  #|         const authoredFar = Number(authoredBounds[index]) || far;
  #|         const farBound = Math.min(far, authoredFar);
  #|         const nearBound = index === 0
  #|           ? minimumDistance
  #|           : Math.max(minimumDistance, previousFar * (1.0 - overlap));
  #|         previousFar = farBound;
  #|         if (farBound <= nearBound) continue;
  #|         bounds.push([nearBound, farBound]);
  #|         if (farBound >= far) break;
  #|       }
  #|       return bounds;
  #|     };
  #|     const directionalAtlasLayout = (cascadeCount) => {
  #|       if (cascadeCount <= 1) return [1, 1];
  #|       if (cascadeCount === 2) return [2, 1];
  #|       return [2, 2];
  #|     };
  #|     const makeShadowAtlasRect = (slot, columns, tileSize, atlasWidth, atlasHeight) => {
  #|       const column = slot % columns;
  #|       const row = Math.floor(slot / columns);
  #|       const tileWidth = tileSize / Math.max(1, atlasWidth);
  #|       const tileHeight = tileSize / Math.max(1, atlasHeight);
  #|       return {
  #|         offset: [column * tileWidth, row * tileHeight],
  #|         scale: [tileWidth, tileHeight],
  #|       };
  #|     };
  #|     const transformPoint = (matrix, point) => {
  #|       const x = point[0];
  #|       const y = point[1];
  #|       const z = point[2];
  #|       return [
  #|         matrix[0] * x + matrix[4] * y + matrix[8] * z + matrix[12],
  #|         matrix[1] * x + matrix[5] * y + matrix[9] * z + matrix[13],
  #|         matrix[2] * x + matrix[6] * y + matrix[10] * z + matrix[14],
  #|       ];
  #|     };
  #|     const buildDirectionalShadowCascadesForLight = (light, camera, aspect, tileSize) => {
  #|       const bounds = directionalShadowBounds(light, camera);
  #|       if (bounds.length === 0) return [];
  #|       const direction = vec3LengthSq(light.direction || [0, -1, 0]) <= 1e-8
  #|         ? [0, -1, 0]
  #|         : vec3Norm(light.direction);
  #|       const lightView = mat4LookAt([0, 0, 0], direction, chooseShadowUp(direction));
  #|       const [atlasColumns, atlasRows] = directionalAtlasLayout(bounds.length);
  #|       const atlasWidth = tileSize * atlasColumns;
  #|       const atlasHeight = tileSize * atlasRows;
  #|       const cascades = [];
  #|       for (let cascadeIndex = 0; cascadeIndex < bounds.length; cascadeIndex += 1) {
  #|         const nearBound = bounds[cascadeIndex][0];
  #|         const farBound = bounds[cascadeIndex][1];
  #|         const corners = cameraFrustumSliceCorners(camera, aspect, nearBound, farBound);
  #|         if (corners.length < 8) continue;
  #|         let minCorner = transformPoint(lightView, corners[0]);
  #|         let maxCorner = minCorner;
  #|         for (let index = 1; index < corners.length; index += 1) {
  #|           const point = transformPoint(lightView, corners[index]);
  #|           minCorner = vec3Min(minCorner, point);
  #|           maxCorner = vec3Max(maxCorner, point);
  #|         }
  #|         const bodyDiagonal = vec3LengthSq(vec3Sub(corners[0], corners[6]));
  #|         const farPlaneDiagonal = vec3LengthSq(vec3Sub(corners[4], corners[6]));
  #|         const diameter = Math.max(1.0, Math.ceil(Math.sqrt(Math.max(bodyDiagonal, farPlaneDiagonal))));
  #|         const texelSize = diameter / Math.max(1, tileSize);
  #|         const centerX = Math.floor(((minCorner[0] + maxCorner[0]) * 0.5) / texelSize) * texelSize;
  #|         const centerY = Math.floor(((minCorner[1] + maxCorner[1]) * 0.5) / texelSize) * texelSize;
  #|         const halfExtent = diameter * 0.5;
  #|         const nearPlane = 0.01;
  #|         const farPlane = Math.max(nearPlane + 1.0, (maxCorner[2] - minCorner[2]) + nearPlane);
  #|         const cascadeView = mat4Multiply(
  #|           lightView,
  #|           mat4Translation(-centerX, -centerY, -(maxCorner[2] + nearPlane)),
  #|         );
  #|         const cascadeProjection = mat4Orthographic(halfExtent * 2.0, halfExtent * 2.0, nearPlane, farPlane);
  #|         const atlasRect = makeShadowAtlasRect(
  #|           cascadeIndex,
  #|           atlasColumns,
  #|           tileSize,
  #|           atlasWidth,
  #|           atlasHeight,
  #|         );
  #|         cascades.push({
  #|           lightViewProjection: mat4Multiply(cascadeProjection, cascadeView),
  #|           atlasRect,
  #|           nearBound,
  #|           farBound,
  #|         });
  #|       }
  #|       return cascades;
  #|     };
  #|     const buildSpotShadowSetup = (light) => {
  #|       const direction = vec3LengthSq(light.direction || [0, -1, 0]) <= 1e-8
  #|         ? [0, -1, 0]
  #|         : vec3Norm(light.direction);
  #|       const target = vec3Add(light.position, direction);
  #|       const nearPlane = Math.max(0.01, Number(light.nearZ) || 0.01);
  #|       const farPlane = Math.max(nearPlane + 1.0, Number(light.range) || (nearPlane + 1.0));
  #|       const lightView = mat4LookAt(light.position, target, chooseShadowUp(direction));
  #|       const outerAngle = Math.max(Number(light.outerAngle) || 0.017453292519943295, 0.008726646259971648);
  #|       const fovyDeg = (outerAngle * 2.0 * 180.0) / Math.PI;
  #|       return {
  #|         lightViewProjection: mat4Multiply(
  #|           mat4Perspective(fovyDeg, 1.0, nearPlane, farPlane),
  #|           lightView,
  #|         ),
  #|       };
  #|     };
  #|     const pointShadowFace = (index) => {
  #|       switch (index) {
  #|         case 0: return [[1, 0, 0], [0, -1, 0]];
  #|         case 1: return [[-1, 0, 0], [0, -1, 0]];
  #|         case 2: return [[0, 1, 0], [0, 0, 1]];
  #|         case 3: return [[0, -1, 0], [0, 0, -1]];
  #|         case 4: return [[0, 0, 1], [0, -1, 0]];
  #|         default: return [[0, 0, -1], [0, -1, 0]];
  #|       }
  #|     };
  #|     const buildPointShadowFaceSetup = (light, faceIndex) => {
  #|       const [direction, up] = pointShadowFace(faceIndex);
  #|       const nearPlane = Math.max(0.01, Number(light.nearZ) || 0.01);
  #|       const farPlane = Math.max(nearPlane + 1.0, Number(light.range) || (nearPlane + 1.0));
  #|       const lightView = mat4LookAt(light.position, vec3Add(light.position, direction), up);
  #|       return {
  #|         lightViewProjection: mat4Multiply(
  #|           mat4Perspective(90.0, 1.0, nearPlane, farPlane),
  #|           lightView,
  #|         ),
  #|       };
  #|     };
  #|     const createShadowTexture = (width, height) => {
  #|       const texture = rt.device.createTexture({
  #|         size: [Math.max(1, width), Math.max(1, height), 1],
  #|         format: 'depth32float',
  #|         usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING,
  #|       });
  #|       return {
  #|         texture,
  #|         view: texture.createView(),
  #|         width: Math.max(1, width),
  #|         height: Math.max(1, height),
  #|       };
  #|     };
  #|     const destroyShadowTexture = (record) => {
  #|       if (!record?.texture) return;
  #|       try { record.texture.destroy(); } catch (_err) {}
  #|     };
  #|     rt.ensureDirectionalShadowTexture = (index, tileSize, cascadeCount) => {
  #|       if (!rt.device || index < 0 || index >= 4) return null;
  #|       const safeTileSize = Math.max(1, tileSize | 0);
  #|       const safeCascadeCount = Math.max(1, Math.min(4, cascadeCount | 0));
  #|       const [columns, rows] = directionalAtlasLayout(safeCascadeCount);
  #|       const width = safeTileSize * columns;
  #|       const height = safeTileSize * rows;
  #|       const existing = rt.shadowTextures.directional[index];
  #|       if (existing &&
  #|         existing.tileSize === safeTileSize &&
  #|         existing.cascadeCount === safeCascadeCount &&
  #|         existing.width === width &&
  #|         existing.height === height) {
  #|         return existing;
  #|       }
  #|       destroyShadowTexture(existing);
  #|       const record = {
  #|         ...createShadowTexture(width, height),
  #|         tileSize: safeTileSize,
  #|         cascadeCount: safeCascadeCount,
  #|         columns,
  #|         rows,
  #|       };
  #|       rt.shadowTextures.directional[index] = record;
  #|       return record;
  #|     };
  #|     rt.ensureSpotShadowTexture = (tileSize) => {
  #|       if (!rt.device) return null;
  #|       const safeTileSize = Math.max(1, tileSize | 0);
  #|       const width = safeTileSize * 2;
  #|       const height = safeTileSize * 2;
  #|       const existing = rt.shadowTextures.spot;
  #|       if (existing && existing.tileSize === safeTileSize && existing.width === width && existing.height === height) {
  #|         return existing;
  #|       }
  #|       destroyShadowTexture(existing);
  #|       const record = { ...createShadowTexture(width, height), tileSize: safeTileSize };
  #|       rt.shadowTextures.spot = record;
  #|       return record;
  #|     };
  #|     rt.ensurePointShadowTexture = (faceSize) => {
  #|       if (!rt.device) return null;
  #|       const safeFaceSize = Math.max(1, faceSize | 0);
  #|       const width = safeFaceSize * 8;
  #|       const height = safeFaceSize * 6;
  #|       const existing = rt.shadowTextures.point;
  #|       if (existing && existing.faceSize === safeFaceSize && existing.width === width && existing.height === height) {
  #|         return existing;
  #|       }
  #|       destroyShadowTexture(existing);
  #|       const record = { ...createShadowTexture(width, height), faceSize: safeFaceSize };
  #|       rt.shadowTextures.point = record;
  #|       return record;
  #|     };
  #|     rt.ensureDummyShadowTexture = () => {
  #|       if (rt.shadowTextures.dummy) return rt.shadowTextures.dummy;
  #|       const record = createShadowTexture(1, 1);
  #|       const encoder = rt.device.createCommandEncoder();
  #|       const pass = encoder.beginRenderPass({
  #|         colorAttachments: [],
  #|         depthStencilAttachment: {
  #|           view: record.view,
  #|           depthClearValue: 1.0,
  #|           depthLoadOp: 'clear',
  #|           depthStoreOp: 'store',
  #|         },
  #|       });
  #|       pass.end();
  #|       rt.device.queue.submit([encoder.finish()]);
  #|       rt.shadowTextures.dummy = record;
  #|       return record;
  #|     };
  #|
  #|     rt.ensureDepth = () => {
  #|       if (!rt.device || !rt.canvas) return;
  #|       const [w, h] = toCanvasSize();
  #|       if (rt.depthTexture && rt.depthWidth === w && rt.depthHeight === h) {
  #|         return;
  #|       }
  #|       if (rt.depthTexture) {
  #|         try { rt.depthTexture.destroy(); } catch (_err) {}
  #|       }
  #|       rt.depthTexture = rt.device.createTexture({
  #|         size: [w, h, 1],
  #|         format: 'depth24plus',
  #|         usage: GPUTextureUsage.RENDER_ATTACHMENT,
  #|       });
  #|       rt.depthWidth = w;
  #|       rt.depthHeight = h;
  #|       rt.depthView = rt.depthTexture.createView();
  #|     };
  #|
  #|     rt.ensureDynamicBuffer = (name, byteSize, usage) => {
  #|       const existed = rt.dynamicBuffers.get(name);
  #|       if (!existed || existed.size < byteSize) {
  #|         if (existed?.buffer) {
  #|           try { existed.buffer.destroy(); } catch (_err) {}
  #|         }
  #|         const size = nextPow2(Math.max(256, byteSize));
  #|         const buffer = rt.device.createBuffer({ size, usage: usage | GPUBufferUsage.COPY_DST });
  #|         rt.dynamicBuffers.set(name, { buffer, size });
  #|       }
  #|       return rt.dynamicBuffers.get(name).buffer;
  #|     };
  #|
  #|     rt.createImageTexture = (source, width, height) => {
  #|       const texture = rt.device.createTexture({
  #|         size: [Math.max(1, width), Math.max(1, height), 1],
  #|         format: 'rgba8unorm',
  #|         usage: GPUTextureUsage.COPY_DST | GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.RENDER_ATTACHMENT,
  #|       });
  #|       rt.device.queue.copyExternalImageToTexture(
  #|         { source },
  #|         { texture },
  #|         [Math.max(1, width), Math.max(1, height)],
  #|       );
  #|       return texture;
  #|     };
  #|
  #|     rt.ensureImage = (path) => {
  #|       let rec = rt.imageCache.get(path);
  #|       if (rec) return rec;
  #|       rec = { state: 'loading', texture: null, view: null, width: 1, height: 1, promise: null };
  #|       rec.promise = (async () => {
  #|         if (!rt.ready && rt.initPromise) {
  #|           await rt.initPromise;
  #|         }
  #|         const img = new Image();
  #|         img.src = path;
  #|         await img.decode();
  #|         const bitmap = await createImageBitmap(img);
  #|         rec.width = Math.max(1, bitmap.width);
  #|         rec.height = Math.max(1, bitmap.height);
  #|         rec.texture = rt.createImageTexture(bitmap, rec.width, rec.height);
  #|         rec.view = rec.texture.createView();
  #|         rec.state = 'ready';
  #|       })().catch(() => {
  #|         rec.state = 'error';
  #|       });
  #|       rt.imageCache.set(path, rec);
  #|       return rec;
  #|     };
  #|
  #|     rt.ensureSolidTexture = (key, r, g, b, a) => {
  #|       let rec = rt.imageCache.get(key);
  #|       if (rec) return rec;
  #|       if (!rt.ready) return null;
  #|       const cvs = document.createElement('canvas');
  #|       cvs.width = 1;
  #|       cvs.height = 1;
  #|       const ctx = cvs.getContext('2d');
  #|       ctx.fillStyle = `rgba(${r}, ${g}, ${b}, ${a})`;
  #|       ctx.fillRect(0, 0, 1, 1);
  #|       const texture = rt.createImageTexture(cvs, 1, 1);
  #|       rec = { state: 'ready', texture, view: texture.createView(), width: 1, height: 1, promise: null };
  #|       rt.imageCache.set(key, rec);
  #|       return rec;
  #|     };
  #|
  #|     rt.ensureTextTexture = (text, family, size, r, g, b, a) => {
  #|       const key = `${text}\u0000${family}\u0000${size}\u0000${r}\u0000${g}\u0000${b}\u0000${a}`;
  #|       const existed = rt.textCache.get(key);
  #|       if (existed) return existed;
  #|       if (!rt.ready) return null;
  #|       const cvs = document.createElement('canvas');
  #|       const ctx = cvs.getContext('2d');
  #|       const font = `${size}px ${family}`;
  #|       ctx.font = font;
  #|       const m = ctx.measureText(text);
  #|       const ascent = Math.max(1, Math.ceil(m.actualBoundingBoxAscent || size * 0.8));
  #|       const descent = Math.max(1, Math.ceil(m.actualBoundingBoxDescent || size * 0.2));
  #|       const width = Math.max(1, Math.ceil(m.width + 4));
  #|       const height = Math.max(1, ascent + descent + 4);
  #|       cvs.width = width;
  #|       cvs.height = height;
  #|       const draw = cvs.getContext('2d');
  #|       draw.font = font;
  #|       draw.textBaseline = 'alphabetic';
  #|       draw.fillStyle = `rgba(${r}, ${g}, ${b}, ${a})`;
  #|       draw.fillText(text, 2, 2 + ascent);
  #|       const texture = rt.createImageTexture(cvs, width, height);
  #|       const rec = { texture, view: texture.createView(), width, height };
  #|       rt.textCache.set(key, rec);
  #|       return rec;
  #|     };
  #|
  #|     rt.uploadTextTexture = (key, width, height, pixels) => {
  #|       if (!rt.ready || !rt.device) return false;
  #|       try {
  #|         const w = Math.max(1, width | 0);
  #|         const h = Math.max(1, height | 0);
  #|         const src = Array.isArray(pixels) ? pixels : [];
  #|         const tightBytesPerRow = w * 4;
  #|         const bytesPerRow = Math.ceil(tightBytesPerRow / 256) * 256;
  #|         const data = new Uint8Array(bytesPerRow * h);
  #|         for (let y = 0; y < h; y += 1) {
  #|           const srcBase = y * tightBytesPerRow;
  #|           const dstBase = y * bytesPerRow;
  #|           for (let x = 0; x < tightBytesPerRow; x += 1) {
  #|             const idx = srcBase + x;
  #|             if (idx >= src.length) break;
  #|             data[dstBase + x] = src[idx] & 0xFF;
  #|           }
  #|         }
  #|         const prev = rt.textCache.get(key);
  #|         if (prev?.texture) {
  #|           try { prev.texture.destroy(); } catch (_err) {}
  #|         }
  #|         const texture = rt.device.createTexture({
  #|           size: [w, h, 1],
  #|           format: 'rgba8unorm',
  #|           usage: GPUTextureUsage.COPY_DST | GPUTextureUsage.TEXTURE_BINDING,
  #|         });
  #|         rt.device.queue.writeTexture(
  #|           { texture },
  #|           data,
  #|           { offset: 0, bytesPerRow, rowsPerImage: h },
  #|           [w, h, 1],
  #|         );
  #|         const rec = { texture, view: texture.createView(), width: w, height: h };
  #|         rt.textCache.set(key, rec);
  #|         return true;
  #|       } catch (_err) {
  #|         return false;
  #|       }
  #|     };
  #|
  #|     rt.pushCachedText = (key, x, y, ma, mb, mc, md, tx, ty, align, baseline) => {
  #|       const rec = rt.textCache.get(key);
  #|       if (!rec) return false;
  #|       let ox = x;
  #|       let oy = y;
  #|       if (align === 1) ox -= rec.width / 2;
  #|       else if (align === 2) ox -= rec.width;
  #|       if (baseline === 1) oy -= rec.height / 2;
  #|       else if (baseline === 2) oy -= rec.height;
  #|       const p0x = ma * 0 + mc * 0 + tx + ox;
  #|       const p0y = mb * 0 + md * 0 + ty + oy;
  #|       const p1x = ma * rec.width + mc * 0 + tx + ox;
  #|       const p1y = mb * rec.width + md * 0 + ty + oy;
  #|       const p2x = ma * rec.width + mc * rec.height + tx + ox;
  #|       const p2y = mb * rec.width + md * rec.height + ty + oy;
  #|       const p3x = ma * 0 + mc * rec.height + tx + ox;
  #|       const p3y = mb * 0 + md * rec.height + ty + oy;
  #|       const verts = new Float32Array([
  #|         p0x, p0y, 0, 0, 1, 1, 1, 1,
  #|         p1x, p1y, 1, 0, 1, 1, 1, 1,
  #|         p2x, p2y, 1, 1, 1, 1, 1, 1,
  #|         p0x, p0y, 0, 0, 1, 1, 1, 1,
  #|         p2x, p2y, 1, 1, 1, 1, 1, 1,
  #|         p3x, p3y, 0, 1, 1, 1, 1, 1,
  #|       ]);
  #|       rt.pushTex2d(0, rec, verts);
  #|       return true;
  #|     };
  #|
  #|     rt.pushColor2d = (topology, verts) => {
  #|       rt.draw2dCommands.push({ kind: 'color', topology, verts });
  #|     };
  #|     rt.pushTex2d = (samplerCode, textureRec, verts) => {
  #|       rt.draw2dCommands.push({
  #|         kind: 'tex',
  #|         samplerCode,
  #|         textureRec,
  #|         verts,
  #|       });
  #|     };
  #|
  #|     rt.pushRect = (x, y, w, h, ma, mb, mc, md, tx, ty, fr, fg, fb, fa, hasStroke, sr, sg, sb, sa) => {
  #|       const fill = colorNorm(fr, fg, fb, fa);
  #|       const p0x = ma * 0 + mc * 0 + tx + x;
  #|       const p0y = mb * 0 + md * 0 + ty + y;
  #|       const p1x = ma * w + mc * 0 + tx + x;
  #|       const p1y = mb * w + md * 0 + ty + y;
  #|       const p2x = ma * w + mc * h + tx + x;
  #|       const p2y = mb * w + md * h + ty + y;
  #|       const p3x = ma * 0 + mc * h + tx + x;
  #|       const p3y = mb * 0 + md * h + ty + y;
  #|       const verts = new Float32Array([
  #|         p0x, p0y, fill[0], fill[1], fill[2], fill[3],
  #|         p1x, p1y, fill[0], fill[1], fill[2], fill[3],
  #|         p2x, p2y, fill[0], fill[1], fill[2], fill[3],
  #|         p0x, p0y, fill[0], fill[1], fill[2], fill[3],
  #|         p2x, p2y, fill[0], fill[1], fill[2], fill[3],
  #|         p3x, p3y, fill[0], fill[1], fill[2], fill[3],
  #|       ]);
  #|       rt.pushColor2d('triangle', verts);
  #|       if (hasStroke) {
  #|         const s = colorNorm(sr, sg, sb, sa);
  #|         const line = new Float32Array([
  #|           p0x, p0y, s[0], s[1], s[2], s[3], p1x, p1y, s[0], s[1], s[2], s[3],
  #|           p1x, p1y, s[0], s[1], s[2], s[3], p2x, p2y, s[0], s[1], s[2], s[3],
  #|           p2x, p2y, s[0], s[1], s[2], s[3], p3x, p3y, s[0], s[1], s[2], s[3],
  #|           p3x, p3y, s[0], s[1], s[2], s[3], p0x, p0y, s[0], s[1], s[2], s[3],
  #|         ]);
  #|         rt.pushColor2d('line', line);
  #|       }
  #|     };
  #|
  #|     rt.pushGradientRect = (x, y, w, h, ma, mb, mc, md, tx, ty, sr, sg, sb, sa, er, eg, eb, ea) => {
  #|       const c0 = colorNorm(sr, sg, sb, sa);
  #|       const c1 = colorNorm(er, eg, eb, ea);
  #|       const cm = [
  #|         (c0[0] + c1[0]) * 0.5,
  #|         (c0[1] + c1[1]) * 0.5,
  #|         (c0[2] + c1[2]) * 0.5,
  #|         (c0[3] + c1[3]) * 0.5,
  #|       ];
  #|       const p0x = ma * 0 + mc * 0 + tx + x;
  #|       const p0y = mb * 0 + md * 0 + ty + y;
  #|       const p1x = ma * w + mc * 0 + tx + x;
  #|       const p1y = mb * w + md * 0 + ty + y;
  #|       const p2x = ma * w + mc * h + tx + x;
  #|       const p2y = mb * w + md * h + ty + y;
  #|       const p3x = ma * 0 + mc * h + tx + x;
  #|       const p3y = mb * 0 + md * h + ty + y;
  #|       const verts = new Float32Array([
  #|         p0x, p0y, c0[0], c0[1], c0[2], c0[3],
  #|         p1x, p1y, cm[0], cm[1], cm[2], cm[3],
  #|         p2x, p2y, c1[0], c1[1], c1[2], c1[3],
  #|         p0x, p0y, c0[0], c0[1], c0[2], c0[3],
  #|         p2x, p2y, c1[0], c1[1], c1[2], c1[3],
  #|         p3x, p3y, cm[0], cm[1], cm[2], cm[3],
  #|       ]);
  #|       rt.pushColor2d('triangle', verts);
  #|     };
  #|
  #|     rt.pushCircle = (cx, cy, radius, ma, mb, mc, md, tx, ty, fr, fg, fb, fa, hasStroke, sr, sg, sb, sa) => {
  #|       const n = 40;
  #|       const fill = colorNorm(fr, fg, fb, fa);
  #|       const tri = [];
  #|       for (let i = 0; i < n; i += 1) {
  #|         const a0 = (i / n) * Math.PI * 2.0;
  #|         const a1 = ((i + 1) / n) * Math.PI * 2.0;
  #|         const lx0 = Math.cos(a0) * radius;
  #|         const ly0 = Math.sin(a0) * radius;
  #|         const lx1 = Math.cos(a1) * radius;
  #|         const ly1 = Math.sin(a1) * radius;
  #|         const x0 = ma * lx0 + mc * ly0 + tx + cx;
  #|         const y0 = mb * lx0 + md * ly0 + ty + cy;
  #|         const x1 = ma * lx1 + mc * ly1 + tx + cx;
  #|         const y1 = mb * lx1 + md * ly1 + ty + cy;
  #|         tri.push(
  #|           tx + cx, ty + cy, fill[0], fill[1], fill[2], fill[3],
  #|           x0, y0, fill[0], fill[1], fill[2], fill[3],
  #|           x1, y1, fill[0], fill[1], fill[2], fill[3],
  #|         );
  #|       }
  #|       rt.pushColor2d('triangle', new Float32Array(tri));
  #|       if (hasStroke) {
  #|         const stroke = colorNorm(sr, sg, sb, sa);
  #|         const lines = [];
  #|         for (let i = 0; i < n; i += 1) {
  #|           const a0 = (i / n) * Math.PI * 2.0;
  #|           const a1 = ((i + 1) / n) * Math.PI * 2.0;
  #|           const lx0 = Math.cos(a0) * radius;
  #|           const ly0 = Math.sin(a0) * radius;
  #|           const lx1 = Math.cos(a1) * radius;
  #|           const ly1 = Math.sin(a1) * radius;
  #|           const x0 = ma * lx0 + mc * ly0 + tx + cx;
  #|           const y0 = mb * lx0 + md * ly0 + ty + cy;
  #|           const x1 = ma * lx1 + mc * ly1 + tx + cx;
  #|           const y1 = mb * lx1 + md * ly1 + ty + cy;
  #|           lines.push(
  #|             x0, y0, stroke[0], stroke[1], stroke[2], stroke[3],
  #|             x1, y1, stroke[0], stroke[1], stroke[2], stroke[3],
  #|           );
  #|         }
  #|         rt.pushColor2d('line', new Float32Array(lines));
  #|       }
  #|     };
  #|
  #|     rt.pushImage = (path, dx, dy, dw, dh, hasSource, sx, sy, sw, sh, a, b, c, d, tx, ty, repeatMode, tr, tg, tb, ta) => {
  #|       const rec = rt.ensureImage(path);
  #|       if (!rec || rec.state !== 'ready') return;
  #|       const p0x = a * 0 + c * 0 + tx + dx;
  #|       const p0y = b * 0 + d * 0 + ty + dy;
  #|       const p1x = a * dw + c * 0 + tx + dx;
  #|       const p1y = b * dw + d * 0 + ty + dy;
  #|       const p2x = a * dw + c * dh + tx + dx;
  #|       const p2y = b * dw + d * dh + ty + dy;
  #|       const p3x = a * 0 + c * dh + tx + dx;
  #|       const p3y = b * 0 + d * dh + ty + dy;
  #|
  #|       let u0 = 0;
  #|       let v0 = 0;
  #|       let u1 = 1;
  #|       let v1 = 1;
  #|       let samplerCode = 0;
  #|       if (hasSource) {
  #|         u0 = sx / rec.width;
  #|         v0 = sy / rec.height;
  #|         u1 = (sx + sw) / rec.width;
  #|         v1 = (sy + sh) / rec.height;
  #|         samplerCode = 0;
  #|       } else {
  #|         const scaleU = rec.width > 0 ? dw / rec.width : 1;
  #|         const scaleV = rec.height > 0 ? dh / rec.height : 1;
  #|         if (repeatMode === 3) {
  #|           u1 = scaleU;
  #|           v1 = scaleV;
  #|           samplerCode = 3;
  #|         } else if (repeatMode === 0) {
  #|           u1 = scaleU;
  #|           v1 = 1;
  #|           samplerCode = 1;
  #|         } else if (repeatMode === 1) {
  #|           u1 = 1;
  #|           v1 = scaleV;
  #|           samplerCode = 2;
  #|         } else {
  #|           u1 = 1;
  #|           v1 = 1;
  #|           samplerCode = 0;
  #|         }
  #|       }
  #|
  #|       const tint = colorNorm(tr, tg, tb, ta);
  #|       const verts = new Float32Array([
  #|         p0x, p0y, u0, v0, tint[0], tint[1], tint[2], tint[3],
  #|         p1x, p1y, u1, v0, tint[0], tint[1], tint[2], tint[3],
  #|         p2x, p2y, u1, v1, tint[0], tint[1], tint[2], tint[3],
  #|         p0x, p0y, u0, v0, tint[0], tint[1], tint[2], tint[3],
  #|         p2x, p2y, u1, v1, tint[0], tint[1], tint[2], tint[3],
  #|         p3x, p3y, u0, v1, tint[0], tint[1], tint[2], tint[3],
  #|       ]);
  #|       rt.pushTex2d(samplerCode, rec, verts);
  #|     };
  #|
  #|     rt.pushText = (text, x, y, ma, mb, mc, md, tx, ty, family, size, align, baseline, r, g, b, alpha) => {
  #|       const rec = rt.ensureTextTexture(text, family, size, r, g, b, alpha);
  #|       if (!rec) return;
  #|       let ox = x;
  #|       let oy = y;
  #|       if (align === 1) ox -= rec.width / 2;
  #|       else if (align === 2) ox -= rec.width;
  #|       if (baseline === 1) oy -= rec.height / 2;
  #|       else if (baseline === 2) oy -= rec.height;
  #|       const p0x = ma * 0 + mc * 0 + tx + ox;
  #|       const p0y = mb * 0 + md * 0 + ty + oy;
  #|       const p1x = ma * rec.width + mc * 0 + tx + ox;
  #|       const p1y = mb * rec.width + md * 0 + ty + oy;
  #|       const p2x = ma * rec.width + mc * rec.height + tx + ox;
  #|       const p2y = mb * rec.width + md * rec.height + ty + oy;
  #|       const p3x = ma * 0 + mc * rec.height + tx + ox;
  #|       const p3y = mb * 0 + md * rec.height + ty + oy;
  #|       const verts = new Float32Array([
  #|         p0x, p0y, 0, 0, 1, 1, 1, 1,
  #|         p1x, p1y, 1, 0, 1, 1, 1, 1,
  #|         p2x, p2y, 1, 1, 1, 1, 1, 1,
  #|         p0x, p0y, 0, 0, 1, 1, 1, 1,
  #|         p2x, p2y, 1, 1, 1, 1, 1, 1,
  #|         p3x, p3y, 0, 1, 1, 1, 1, 1,
  #|       ]);
  #|       rt.pushTex2d(0, rec, verts);
  #|     };
  #|
  #|     rt.transformVerts3dWithStride = (verts, stride, tx, ty, tz, qx, qy, qz, qw, normalOffset = -1, tangentOffset = -1) => {
  #|       let qnx = qx;
  #|       let qny = qy;
  #|       let qnz = qz;
  #|       let qnw = qw;
  #|       const qLen = Math.hypot(qnx, qny, qnz, qnw);
  #|       if (qLen <= 1e-8) {
  #|         qnx = 0;
  #|         qny = 0;
  #|         qnz = 0;
  #|         qnw = 1;
  #|       } else {
  #|         qnx /= qLen;
  #|         qny /= qLen;
  #|         qnz /= qLen;
  #|         qnw /= qLen;
  #|       }
  #|       const rotate = (x, y, z) => {
  #|         const dotUV = qnx * x + qny * y + qnz * z;
  #|         const dotUU = qnx * qnx + qny * qny + qnz * qnz;
  #|         const cx = qny * z - qnz * y;
  #|         const cy = qnz * x - qnx * z;
  #|         const cz = qnx * y - qny * x;
  #|         const rx = 2 * dotUV * qnx + (qnw * qnw - dotUU) * x + 2 * qnw * cx;
  #|         const ry = 2 * dotUV * qny + (qnw * qnw - dotUU) * y + 2 * qnw * cy;
  #|         const rz = 2 * dotUV * qnz + (qnw * qnw - dotUU) * z + 2 * qnw * cz;
  #|         return [rx, ry, rz];
  #|       };
  #|       for (let i = 0; i < verts.length; i += stride) {
  #|         const [rx, ry, rz] = rotate(verts[i], verts[i + 1], verts[i + 2]);
  #|         verts[i] = rx + tx;
  #|         verts[i + 1] = ry + ty;
  #|         verts[i + 2] = rz + tz;
  #|         if (normalOffset >= 0) {
  #|           const ni = i + normalOffset;
  #|           if (ni + 2 < verts.length) {
  #|             const [nx, ny, nz] = rotate(verts[ni], verts[ni + 1], verts[ni + 2]);
  #|             const len = Math.hypot(nx, ny, nz);
  #|             if (len > 1e-8) {
  #|               verts[ni] = nx / len;
  #|               verts[ni + 1] = ny / len;
  #|               verts[ni + 2] = nz / len;
  #|             } else {
  #|               verts[ni] = 0.0;
  #|               verts[ni + 1] = 1.0;
  #|               verts[ni + 2] = 0.0;
  #|             }
  #|           }
  #|         }
  #|         if (tangentOffset >= 0) {
  #|           const ti = i + tangentOffset;
  #|           if (ti + 2 < verts.length) {
  #|             const [txr, tyr, tzr] = rotate(verts[ti], verts[ti + 1], verts[ti + 2]);
  #|             const len = Math.hypot(txr, tyr, tzr);
  #|             if (len > 1e-8) {
  #|               verts[ti] = txr / len;
  #|               verts[ti + 1] = tyr / len;
  #|               verts[ti + 2] = tzr / len;
  #|             } else {
  #|               verts[ti] = 1.0;
  #|               verts[ti + 1] = 0.0;
  #|               verts[ti + 2] = 0.0;
  #|             }
  #|           }
  #|         }
  #|       }
  #|       return verts;
  #|     };
  #|     rt.transformVerts3d = (verts, tx, ty, tz, qx, qy, qz, qw) =>
  #|       rt.transformVerts3dWithStride(verts, 7, tx, ty, tz, qx, qy, qz, qw);
  #|     rt.transformTexVerts3d = (verts, tx, ty, tz, qx, qy, qz, qw) =>
  #|       rt.transformVerts3dWithStride(verts, 9, tx, ty, tz, qx, qy, qz, qw);
  #|     rt.transformLitVerts3d = (verts, tx, ty, tz, qx, qy, qz, qw) =>
  #|       rt.transformVerts3dWithStride(verts, 17, tx, ty, tz, qx, qy, qz, qw, 3);
  #|     rt.transformLitTexVerts3d = (verts, tx, ty, tz, qx, qy, qz, qw) =>
  #|       rt.transformVerts3dWithStride(verts, 35, tx, ty, tz, qx, qy, qz, qw, 3, 23);
  #|
  #|     rt.makeCube = (sx, sy, sz, color) => {
  #|       const hx = sx / 2;
  #|       const hy = sy / 2;
  #|       const hz = sz / 2;
  #|       const p = [
  #|         [-hx, -hy, -hz],
  #|         [hx, -hy, -hz],
  #|         [hx, hy, -hz],
  #|         [-hx, hy, -hz],
  #|         [-hx, -hy, hz],
  #|         [hx, -hy, hz],
  #|         [hx, hy, hz],
  #|         [-hx, hy, hz],
  #|       ];
  #|       const idx = [
  #|         0, 1, 2, 0, 2, 3,
  #|         5, 4, 7, 5, 7, 6,
  #|         4, 0, 3, 4, 3, 7,
  #|         1, 5, 6, 1, 6, 2,
  #|         3, 2, 6, 3, 6, 7,
  #|         4, 5, 1, 4, 1, 0,
  #|       ];
  #|       const out = [];
  #|       for (let i = 0; i < idx.length; i += 1) {
  #|         const v = p[idx[i]];
  #|         out.push(v[0], v[1], v[2], color[0], color[1], color[2], color[3]);
  #|       }
  #|       return new Float32Array(out);
  #|     };
  #|
  #|     rt.pushLitVertex = (out, position, normal, color, emissive, alphaMode, alphaCutoff, unlit) => {
  #|       out.push(
  #|         position[0], position[1], position[2],
  #|         normal[0], normal[1], normal[2],
  #|         color[0], color[1], color[2], color[3],
  #|         emissive[0], emissive[1], emissive[2],
  #|         alphaMode,
  #|         alphaCutoff,
  #|         unlit ? 1.0 : 0.0,
  #|         0.0,
  #|       );
  #|     };
  #|
  #|     rt.makeLitCube = (sx, sy, sz, color, emissive, alphaMode, alphaCutoff, unlit) => {
  #|       const hx = sx / 2;
  #|       const hy = sy / 2;
  #|       const hz = sz / 2;
  #|       const faces = [
  #|         { normal: [0, 0, -1], corners: [[-hx, -hy, -hz], [-hx, hy, -hz], [hx, hy, -hz], [hx, -hy, -hz]] },
  #|         { normal: [0, 0, 1], corners: [[-hx, -hy, hz], [hx, -hy, hz], [hx, hy, hz], [-hx, hy, hz]] },
  #|         { normal: [-1, 0, 0], corners: [[-hx, -hy, hz], [-hx, hy, hz], [-hx, hy, -hz], [-hx, -hy, -hz]] },
  #|         { normal: [1, 0, 0], corners: [[hx, -hy, -hz], [hx, hy, -hz], [hx, hy, hz], [hx, -hy, hz]] },
  #|         { normal: [0, 1, 0], corners: [[-hx, hy, -hz], [-hx, hy, hz], [hx, hy, hz], [hx, hy, -hz]] },
  #|         { normal: [0, -1, 0], corners: [[-hx, -hy, hz], [-hx, -hy, -hz], [hx, -hy, -hz], [hx, -hy, hz]] },
  #|       ];
  #|       const out = [];
  #|       for (let index = 0; index < faces.length; index += 1) {
  #|         const face = faces[index];
  #|         rt.pushLitVertex(out, face.corners[0], face.normal, color, emissive, alphaMode, alphaCutoff, unlit);
  #|         rt.pushLitVertex(out, face.corners[1], face.normal, color, emissive, alphaMode, alphaCutoff, unlit);
  #|         rt.pushLitVertex(out, face.corners[2], face.normal, color, emissive, alphaMode, alphaCutoff, unlit);
  #|         rt.pushLitVertex(out, face.corners[0], face.normal, color, emissive, alphaMode, alphaCutoff, unlit);
  #|         rt.pushLitVertex(out, face.corners[2], face.normal, color, emissive, alphaMode, alphaCutoff, unlit);
  #|         rt.pushLitVertex(out, face.corners[3], face.normal, color, emissive, alphaMode, alphaCutoff, unlit);
  #|       }
  #|       return new Float32Array(out);
  #|     };
  #|
  #|     rt.makeCubeEdges = (sx, sy, sz, color) => {
  #|       const hx = sx / 2;
  #|       const hy = sy / 2;
  #|       const hz = sz / 2;
  #|       const p = [
  #|         [-hx, -hy, -hz],
  #|         [hx, -hy, -hz],
  #|         [hx, hy, -hz],
  #|         [-hx, hy, -hz],
  #|         [-hx, -hy, hz],
  #|         [hx, -hy, hz],
  #|         [hx, hy, hz],
  #|         [-hx, hy, hz],
  #|       ];
  #|       const e = [
  #|         0,1, 1,2, 2,3, 3,0,
  #|         4,5, 5,6, 6,7, 7,4,
  #|         0,4, 1,5, 2,6, 3,7,
  #|       ];
  #|       const out = [];
  #|       for (let i = 0; i < e.length; i += 1) {
  #|         const v = p[e[i]];
  #|         out.push(v[0], v[1], v[2], color[0], color[1], color[2], color[3]);
  #|       }
  #|       return new Float32Array(out);
  #|     };
  #|
  #|     rt.makeSphere = (radius, color) => {
  #|       const stacks = 12;
  #|       const slices = 24;
  #|       const out = [];
  #|       for (let i = 0; i < stacks; i += 1) {
  #|         const v0 = i / stacks;
  #|         const v1 = (i + 1) / stacks;
  #|         const phi0 = v0 * Math.PI;
  #|         const phi1 = v1 * Math.PI;
  #|         for (let j = 0; j < slices; j += 1) {
  #|           const u0 = j / slices;
  #|           const u1 = (j + 1) / slices;
  #|           const th0 = u0 * Math.PI * 2;
  #|           const th1 = u1 * Math.PI * 2;
  #|           const p0 = [Math.cos(th0) * Math.sin(phi0) * radius, Math.cos(phi0) * radius, Math.sin(th0) * Math.sin(phi0) * radius];
  #|           const p1 = [Math.cos(th1) * Math.sin(phi0) * radius, Math.cos(phi0) * radius, Math.sin(th1) * Math.sin(phi0) * radius];
  #|           const p2 = [Math.cos(th1) * Math.sin(phi1) * radius, Math.cos(phi1) * radius, Math.sin(th1) * Math.sin(phi1) * radius];
  #|           const p3 = [Math.cos(th0) * Math.sin(phi1) * radius, Math.cos(phi1) * radius, Math.sin(th0) * Math.sin(phi1) * radius];
  #|           out.push(
  #|             p0[0], p0[1], p0[2], color[0], color[1], color[2], color[3],
  #|             p1[0], p1[1], p1[2], color[0], color[1], color[2], color[3],
  #|             p2[0], p2[1], p2[2], color[0], color[1], color[2], color[3],
  #|             p0[0], p0[1], p0[2], color[0], color[1], color[2], color[3],
  #|             p2[0], p2[1], p2[2], color[0], color[1], color[2], color[3],
  #|             p3[0], p3[1], p3[2], color[0], color[1], color[2], color[3],
  #|           );
  #|         }
  #|       }
  #|       return new Float32Array(out);
  #|     };
  #|
  #|     rt.makeLitSphere = (radius, color, emissive, alphaMode, alphaCutoff, unlit) => {
  #|       const stacks = 12;
  #|       const slices = 24;
  #|       const out = [];
  #|       const safeRadius = Math.max(Math.abs(radius), 0.0001);
  #|       const pushSphereVertex = (point) => {
  #|         const normal = [point[0] / safeRadius, point[1] / safeRadius, point[2] / safeRadius];
  #|         rt.pushLitVertex(out, point, normal, color, emissive, alphaMode, alphaCutoff, unlit);
  #|       };
  #|       for (let i = 0; i < stacks; i += 1) {
  #|         const v0 = i / stacks;
  #|         const v1 = (i + 1) / stacks;
  #|         const phi0 = v0 * Math.PI;
  #|         const phi1 = v1 * Math.PI;
  #|         for (let j = 0; j < slices; j += 1) {
  #|           const u0 = j / slices;
  #|           const u1 = (j + 1) / slices;
  #|           const th0 = u0 * Math.PI * 2;
  #|           const th1 = u1 * Math.PI * 2;
  #|           const p0 = [Math.cos(th0) * Math.sin(phi0) * radius, Math.cos(phi0) * radius, Math.sin(th0) * Math.sin(phi0) * radius];
  #|           const p1 = [Math.cos(th1) * Math.sin(phi0) * radius, Math.cos(phi0) * radius, Math.sin(th1) * Math.sin(phi0) * radius];
  #|           const p2 = [Math.cos(th1) * Math.sin(phi1) * radius, Math.cos(phi1) * radius, Math.sin(th1) * Math.sin(phi1) * radius];
  #|           const p3 = [Math.cos(th0) * Math.sin(phi1) * radius, Math.cos(phi1) * radius, Math.sin(th0) * Math.sin(phi1) * radius];
  #|           pushSphereVertex(p0);
  #|           pushSphereVertex(p1);
  #|           pushSphereVertex(p2);
  #|           pushSphereVertex(p0);
  #|           pushSphereVertex(p2);
  #|           pushSphereVertex(p3);
  #|         }
  #|       }
  #|       return new Float32Array(out);
  #|     };
  #|
  #|     rt.makeCylinder = (rtTop, rtBottom, height, slices, color) => {
  #|       const out = [];
  #|       const h2 = height / 2;
  #|       const n = Math.max(3, slices | 0);
  #|       for (let i = 0; i < n; i += 1) {
  #|         const a0 = (i / n) * Math.PI * 2;
  #|         const a1 = ((i + 1) / n) * Math.PI * 2;
  #|         const t0 = [Math.cos(a0) * rtTop, h2, Math.sin(a0) * rtTop];
  #|         const t1 = [Math.cos(a1) * rtTop, h2, Math.sin(a1) * rtTop];
  #|         const b0 = [Math.cos(a0) * rtBottom, -h2, Math.sin(a0) * rtBottom];
  #|         const b1 = [Math.cos(a1) * rtBottom, -h2, Math.sin(a1) * rtBottom];
  #|         out.push(
  #|           t0[0], t0[1], t0[2], color[0], color[1], color[2], color[3],
  #|           b0[0], b0[1], b0[2], color[0], color[1], color[2], color[3],
  #|           b1[0], b1[1], b1[2], color[0], color[1], color[2], color[3],
  #|           t0[0], t0[1], t0[2], color[0], color[1], color[2], color[3],
  #|           b1[0], b1[1], b1[2], color[0], color[1], color[2], color[3],
  #|           t1[0], t1[1], t1[2], color[0], color[1], color[2], color[3],
  #|         );
  #|         const top = [0, h2, 0];
  #|         out.push(
  #|           top[0], top[1], top[2], color[0], color[1], color[2], color[3],
  #|           t1[0], t1[1], t1[2], color[0], color[1], color[2], color[3],
  #|           t0[0], t0[1], t0[2], color[0], color[1], color[2], color[3],
  #|         );
  #|         const bottom = [0, -h2, 0];
  #|         out.push(
  #|           bottom[0], bottom[1], bottom[2], color[0], color[1], color[2], color[3],
  #|           b0[0], b0[1], b0[2], color[0], color[1], color[2], color[3],
  #|           b1[0], b1[1], b1[2], color[0], color[1], color[2], color[3],
  #|         );
  #|       }
  #|       return new Float32Array(out);
  #|     };
  #|
  #|     rt.makeLitCylinder = (rtTop, rtBottom, height, slices, color, emissive, alphaMode, alphaCutoff, unlit) => {
  #|       const out = [];
  #|       const h2 = height / 2;
  #|       const n = Math.max(3, slices | 0);
  #|       const safeHeight = Math.max(Math.abs(height), 0.0001);
  #|       const slope = (rtBottom - rtTop) / safeHeight;
  #|       const sideNormal = (angle) => {
  #|         const normal = [Math.cos(angle), slope, Math.sin(angle)];
  #|         const len = Math.hypot(normal[0], normal[1], normal[2]);
  #|         if (len <= 1e-8) return [1, 0, 0];
  #|         return [normal[0] / len, normal[1] / len, normal[2] / len];
  #|       };
  #|       for (let i = 0; i < n; i += 1) {
  #|         const a0 = (i / n) * Math.PI * 2;
  #|         const a1 = ((i + 1) / n) * Math.PI * 2;
  #|         const t0 = [Math.cos(a0) * rtTop, h2, Math.sin(a0) * rtTop];
  #|         const t1 = [Math.cos(a1) * rtTop, h2, Math.sin(a1) * rtTop];
  #|         const b0 = [Math.cos(a0) * rtBottom, -h2, Math.sin(a0) * rtBottom];
  #|         const b1 = [Math.cos(a1) * rtBottom, -h2, Math.sin(a1) * rtBottom];
  #|         const n0 = sideNormal(a0);
  #|         const n1 = sideNormal(a1);
  #|         rt.pushLitVertex(out, t0, n0, color, emissive, alphaMode, alphaCutoff, unlit);
  #|         rt.pushLitVertex(out, b0, n0, color, emissive, alphaMode, alphaCutoff, unlit);
  #|         rt.pushLitVertex(out, b1, n1, color, emissive, alphaMode, alphaCutoff, unlit);
  #|         rt.pushLitVertex(out, t0, n0, color, emissive, alphaMode, alphaCutoff, unlit);
  #|         rt.pushLitVertex(out, b1, n1, color, emissive, alphaMode, alphaCutoff, unlit);
  #|         rt.pushLitVertex(out, t1, n1, color, emissive, alphaMode, alphaCutoff, unlit);
  #|         const top = [0, h2, 0];
  #|         const topNormal = [0, 1, 0];
  #|         rt.pushLitVertex(out, top, topNormal, color, emissive, alphaMode, alphaCutoff, unlit);
  #|         rt.pushLitVertex(out, t1, topNormal, color, emissive, alphaMode, alphaCutoff, unlit);
  #|         rt.pushLitVertex(out, t0, topNormal, color, emissive, alphaMode, alphaCutoff, unlit);
  #|         const bottom = [0, -h2, 0];
  #|         const bottomNormal = [0, -1, 0];
  #|         rt.pushLitVertex(out, bottom, bottomNormal, color, emissive, alphaMode, alphaCutoff, unlit);
  #|         rt.pushLitVertex(out, b0, bottomNormal, color, emissive, alphaMode, alphaCutoff, unlit);
  #|         rt.pushLitVertex(out, b1, bottomNormal, color, emissive, alphaMode, alphaCutoff, unlit);
  #|       }
  #|       return new Float32Array(out);
  #|     };
  #|
  #|     rt.begin3d = (px, py, pz, tx, ty, tz, ux, uy, uz, fovy, near, far, orthographic, orthoWidth, orthoHeight) => {
  #|       const section = {
  #|         camera: {
  #|           position: [px, py, pz],
  #|           target: [tx, ty, tz],
  #|           up: [ux, uy, uz],
  #|           fovy,
  #|           near,
  #|           far,
  #|           orthographic: orthographic !== 0,
  #|           orthoWidth,
  #|           orthoHeight,
  #|         },
  #|         triCommands: [],
  #|         litTriCommands: [],
  #|         texTriCommands: [],
  #|         lineCommands: [],
  #|       };
  #|       rt.sections3d.push(section);
  #|       rt.current3dSection = section;
  #|     };
  #|     rt.end3d = () => {
  #|       rt.current3dSection = null;
  #|     };
  #|     rt.pushCube3d = (cx, cy, cz, sx, sy, sz, qx, qy, qz, qw, r, g, b, a, emissiveR, emissiveG, emissiveB, alphaMode, alphaCutoff, unlit, castShadows, receiveShadows) => {
  #|       if (!rt.current3dSection) return;
  #|       const verts = rt.makeLitCube(
  #|         sx,
  #|         sy,
  #|         sz,
  #|         colorNorm(r, g, b, a),
  #|         [clamp01(emissiveR), clamp01(emissiveG), clamp01(emissiveB)],
  #|         Number(alphaMode) || 0.0,
  #|         Math.max(0.0, Number(alphaCutoff) || 0.0),
  #|         !!unlit,
  #|       );
  #|       rt.pushColoredTriangles3d(verts, cx, cy, cz, qx, qy, qz, qw, false, castShadows, receiveShadows);
  #|     };
  #|     rt.pushCubeWires3d = (cx, cy, cz, sx, sy, sz, qx, qy, qz, qw, r, g, b, a) => {
  #|       if (!rt.current3dSection) return;
  #|       const verts = rt.makeCubeEdges(sx, sy, sz, colorNorm(r, g, b, a));
  #|       rt.transformVerts3d(verts, cx, cy, cz, qx, qy, qz, qw);
  #|       rt.current3dSection.lineCommands.push(verts);
  #|     };
  #|     rt.pushSphere3d = (cx, cy, cz, radius, qx, qy, qz, qw, r, g, b, a, emissiveR, emissiveG, emissiveB, alphaMode, alphaCutoff, unlit, castShadows, receiveShadows) => {
  #|       if (!rt.current3dSection) return;
  #|       const verts = rt.makeLitSphere(
  #|         radius,
  #|         colorNorm(r, g, b, a),
  #|         [clamp01(emissiveR), clamp01(emissiveG), clamp01(emissiveB)],
  #|         Number(alphaMode) || 0.0,
  #|         Math.max(0.0, Number(alphaCutoff) || 0.0),
  #|         !!unlit,
  #|       );
  #|       rt.pushColoredTriangles3d(verts, cx, cy, cz, qx, qy, qz, qw, false, castShadows, receiveShadows);
  #|     };
  #|     rt.pushCylinder3d = (cx, cy, cz, rtTop, rtBottom, h, slices, qx, qy, qz, qw, r, g, b, a, emissiveR, emissiveG, emissiveB, alphaMode, alphaCutoff, unlit, castShadows, receiveShadows) => {
  #|       if (!rt.current3dSection) return;
  #|       const verts = rt.makeLitCylinder(
  #|         rtTop,
  #|         rtBottom,
  #|         h,
  #|         slices,
  #|         colorNorm(r, g, b, a),
  #|         [clamp01(emissiveR), clamp01(emissiveG), clamp01(emissiveB)],
  #|         Number(alphaMode) || 0.0,
  #|         Math.max(0.0, Number(alphaCutoff) || 0.0),
  #|         !!unlit,
  #|       );
  #|       rt.pushColoredTriangles3d(verts, cx, cy, cz, qx, qy, qz, qw, false, castShadows, receiveShadows);
  #|     };
  #|     rt.pushTriangles3d = (verts, tx, ty, tz, qx, qy, qz, qw, r, g, b, a) => {
  #|       if (!rt.current3dSection) return;
  #|       if (!verts || verts.length < 9) return;
  #|       const triVertexCount = Math.floor(verts.length / 3);
  #|       if (triVertexCount < 3) return;
  #|       const out = new Float32Array(triVertexCount * 7);
  #|       const cr = clamp01((r || 0) / 255.0);
  #|       const cg = clamp01((g || 0) / 255.0);
  #|       const cb = clamp01((b || 0) / 255.0);
  #|       const ca = clamp01(a ?? 1.0);
  #|       for (let i = 0; i < triVertexCount; i += 1) {
  #|         const src = i * 3;
  #|         const dst = i * 7;
  #|         out[dst] = Number(verts[src]) || 0.0;
  #|         out[dst + 1] = Number(verts[src + 1]) || 0.0;
  #|         out[dst + 2] = Number(verts[src + 2]) || 0.0;
  #|         out[dst + 3] = cr;
  #|         out[dst + 4] = cg;
  #|         out[dst + 5] = cb;
  #|         out[dst + 6] = ca;
  #|       }
  #|       rt.transformVerts3d(out, tx, ty, tz, qx, qy, qz, qw);
  #|       rt.current3dSection.triCommands.push({ verts: out, castShadows: true });
  #|     };
  #|     rt.pushColoredTriangles3d = (verts, tx, ty, tz, qx, qy, qz, qw, doubleSided, castShadows, receiveShadows) => {
  #|       if (!rt.current3dSection) return;
  #|       if (!verts || verts.length < 51) return;
  #|       const triVertexCount = Math.floor(verts.length / 17);
  #|       if (triVertexCount < 3) return;
  #|       const out = new Float32Array(triVertexCount * 17);
  #|       for (let i = 0; i < out.length; i += 1) {
  #|         out[i] = Number(verts[i]) || 0.0;
  #|       }
  #|       rt.transformLitVerts3d(out, tx, ty, tz, qx, qy, qz, qw);
  #|       if (out.length >= 17) {
  #|         const receiveValue = receiveShadows ? 1.0 : 0.0;
  #|         for (let index = 16; index < out.length; index += 17) {
  #|           out[index] = receiveValue;
  #|         }
  #|       }
  #|       rt.current3dSection.litTriCommands.push({
  #|         verts: out,
  #|         doubleSided: !!doubleSided,
  #|         castShadows: !!castShadows,
  #|         receiveShadows: !!receiveShadows,
  #|       });
  #|     };
  #|     rt.pushTexturedTriangles3d = (basePath, emissivePath, metallicRoughnessPath, occlusionPath, normalPath, baseSamplerCode, emissiveSamplerCode, metallicRoughnessSamplerCode, occlusionSamplerCode, normalSamplerCode, verts, tx, ty, tz, qx, qy, qz, qw, doubleSided, castShadows, receiveShadows) => {
  #|       if (!rt.current3dSection) return;
  #|       if (!verts || verts.length < 105) return;
  #|       const baseRec = (basePath && basePath.length > 0)
  #|         ? rt.ensureImage(basePath)
  #|         : rt.ensureSolidTexture('__solid:white', 255, 255, 255, 1.0);
  #|       const emissiveRec = (emissivePath && emissivePath.length > 0)
  #|         ? rt.ensureImage(emissivePath)
  #|         : rt.ensureSolidTexture('__solid:white', 255, 255, 255, 1.0);
  #|       const metallicRoughnessRec = (metallicRoughnessPath && metallicRoughnessPath.length > 0)
  #|         ? rt.ensureImage(metallicRoughnessPath)
  #|         : rt.ensureSolidTexture('__solid:white', 255, 255, 255, 1.0);
  #|       const occlusionRec = (occlusionPath && occlusionPath.length > 0)
  #|         ? rt.ensureImage(occlusionPath)
  #|         : rt.ensureSolidTexture('__solid:white', 255, 255, 255, 1.0);
  #|       const normalRec = (normalPath && normalPath.length > 0)
  #|         ? rt.ensureImage(normalPath)
  #|         : rt.ensureSolidTexture('__solid:normal', 128, 128, 255, 1.0);
  #|       if (!baseRec || !emissiveRec || !metallicRoughnessRec || !occlusionRec || !normalRec) return;
  #|       const triVertexCount = Math.floor(verts.length / 35);
  #|       if (triVertexCount < 3) return;
  #|       const out = new Float32Array(triVertexCount * 35);
  #|       for (let i = 0; i < out.length; i += 1) {
  #|         out[i] = Number(verts[i]) || 0.0;
  #|       }
  #|       rt.transformLitTexVerts3d(out, tx, ty, tz, qx, qy, qz, qw);
  #|       if (out.length >= 35) {
  #|         const receiveValue = receiveShadows ? 1.0 : 0.0;
  #|         for (let index = 34; index < out.length; index += 35) {
  #|           out[index] = receiveValue;
  #|         }
  #|       }
  #|       rt.current3dSection.texTriCommands.push({
  #|         textureRec: baseRec,
  #|         emissiveTextureRec: emissiveRec,
  #|         metallicRoughnessTextureRec: metallicRoughnessRec,
  #|         occlusionTextureRec: occlusionRec,
  #|         normalTextureRec: normalRec,
  #|         baseSamplerCode,
  #|         emissiveSamplerCode,
  #|         metallicRoughnessSamplerCode,
  #|         occlusionSamplerCode,
  #|         normalSamplerCode,
  #|         verts: out,
  #|         doubleSided: !!doubleSided,
  #|         castShadows: !!castShadows,
  #|         receiveShadows: !!receiveShadows,
  #|       });
  #|     };
  #|
  #|     rt.beginFrame = (r, g, b, a) => {
  #|       rt.clearColor = [clamp01(r), clamp01(g), clamp01(b), clamp01(a)];
  #|       rt.draw2dCommands.length = 0;
  #|       rt.sections3d.length = 0;
  #|       rt.current3dSection = null;
  #|     };
  #|
  #|     rt.resolveSampler = (samplerCode) =>
  #|       samplerCode === 3
  #|         ? rt.samplers.repeat
  #|         : (samplerCode === 1
  #|           ? rt.samplers.repeatX
  #|           : (samplerCode === 2 ? rt.samplers.repeatY : rt.samplers.clamp));
  #|
  #|     rt.uploadCommandBuffer3d = (name, commands, getVerts) => {
  #|       if (!commands || commands.length === 0) {
  #|         return { buffer: null, offsets: [] };
  #|       }
  #|       let totalBytes = 0;
  #|       for (const command of commands) {
  #|         const verts = getVerts(command);
  #|         totalBytes += verts?.byteLength || 0;
  #|       }
  #|       if (totalBytes <= 0) {
  #|         return { buffer: null, offsets: [] };
  #|       }
  #|       const buffer = rt.ensureDynamicBuffer(name, totalBytes, GPUBufferUsage.VERTEX);
  #|       const offsets = [];
  #|       let offset = 0;
  #|       for (const command of commands) {
  #|         const verts = getVerts(command);
  #|         offsets.push(offset);
  #|         if (verts?.byteLength > 0) {
  #|           rt.device.queue.writeBuffer(buffer, offset, verts.buffer, verts.byteOffset, verts.byteLength);
  #|           offset += verts.byteLength;
  #|         }
  #|       }
  #|       return { buffer, offsets };
  #|     };
  #|
  #|     rt.prepareSectionBuffers3d = (section) => ({
  #|       tri: rt.uploadCommandBuffer3d('dyn3dTri', section.triCommands, (cmd) => cmd.verts),
  #|       line: rt.uploadCommandBuffer3d('dyn3dLine', section.lineCommands, (verts) => verts),
  #|       lit: rt.uploadCommandBuffer3d('dyn3dLitTri', section.litTriCommands, (cmd) => cmd.verts),
  #|       tex: rt.uploadCommandBuffer3d('dyn3dLitTexTri', section.texTriCommands, (cmd) => cmd.verts),
  #|     });
  #|
  #|     rt.ensureShadowCameraBuffer = (index) => {
  #|       while (rt.shadowCameraBuffers.length <= index) {
  #|         rt.shadowCameraBuffers.push(
  #|           rt.device.createBuffer({ size: 64, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST }),
  #|         );
  #|       }
  #|       return rt.shadowCameraBuffers[index];
  #|     };
  #|
  #|     rt.makeShadowTexBindGroup = (cmd, shadowCameraBuffer) => rt.device.createBindGroup({
  #|       layout: rt.bindGroupLayouts.shadow3dTex,
  #|       entries: [
  #|         { binding: 0, resource: { buffer: shadowCameraBuffer } },
  #|         { binding: 1, resource: rt.resolveSampler(cmd.baseSamplerCode) },
  #|         { binding: 2, resource: cmd.textureRec.view },
  #|       ],
  #|     });
  #|
  #|     rt.renderShadowCastersForSetup = (pass, section, buffers, lightViewProjection, shadowCameraBufferIndex) => {
  #|       const shadowCameraBuffer = rt.ensureShadowCameraBuffer(shadowCameraBufferIndex);
  #|       rt.device.queue.writeBuffer(
  #|         shadowCameraBuffer,
  #|         0,
  #|         lightViewProjection.buffer,
  #|         lightViewProjection.byteOffset,
  #|         lightViewProjection.byteLength,
  #|       );
  #|       const solidBindGroup = rt.device.createBindGroup({
  #|         layout: rt.bindGroupLayouts.shadow3d,
  #|         entries: [{ binding: 0, resource: { buffer: shadowCameraBuffer } }],
  #|       });
  #|       pass.setBindGroup(0, solidBindGroup);
  #|       if (buffers.tri.buffer && section.triCommands.length > 0) {
  #|         pass.setPipeline(rt.pipelines.shadow3dTri);
  #|         for (let index = 0; index < section.triCommands.length; index += 1) {
  #|           const cmd = section.triCommands[index];
  #|           if (!cmd.castShadows) continue;
  #|           pass.setVertexBuffer(0, buffers.tri.buffer, buffers.tri.offsets[index], cmd.verts.byteLength);
  #|           pass.draw(cmd.verts.length / 7, 1, 0, 0);
  #|         }
  #|       }
  #|       if (buffers.lit.buffer && section.litTriCommands.length > 0) {
  #|         pass.setPipeline(rt.pipelines.shadow3dLitTri);
  #|         for (let index = 0; index < section.litTriCommands.length; index += 1) {
  #|           const cmd = section.litTriCommands[index];
  #|           if (!cmd.castShadows) continue;
  #|           pass.setBindGroup(0, solidBindGroup);
  #|           pass.setVertexBuffer(0, buffers.lit.buffer, buffers.lit.offsets[index], cmd.verts.byteLength);
  #|           pass.draw(cmd.verts.length / 17, 1, 0, 0);
  #|         }
  #|       }
  #|       if (buffers.tex.buffer && section.texTriCommands.length > 0) {
  #|         pass.setPipeline(rt.pipelines.shadow3dTexTri);
  #|         for (let index = 0; index < section.texTriCommands.length; index += 1) {
  #|           const cmd = section.texTriCommands[index];
  #|           if (!cmd.castShadows || !cmd.textureRec?.view) continue;
  #|           pass.setBindGroup(0, rt.makeShadowTexBindGroup(cmd, shadowCameraBuffer));
  #|           pass.setVertexBuffer(0, buffers.tex.buffer, buffers.tex.offsets[index], cmd.verts.byteLength);
  #|           pass.draw(cmd.verts.length / 35, 1, 0, 0);
  #|         }
  #|       }
  #|     };
  #|
  #|     rt.createShadowState3d = () => ({
  #|       directionalRecords: [rt.ensureDummyShadowTexture(), rt.ensureDummyShadowTexture(), rt.ensureDummyShadowTexture(), rt.ensureDummyShadowTexture()],
  #|       directionalEnabled: [0, 0, 0, 0],
  #|       directionalCascadeCounts: [0, 0, 0, 0],
  #|       directionalDepthBiases: [0, 0, 0, 0],
  #|       directionalNormalBiases: [0, 0, 0, 0],
  #|       directionalMatrices: Array.from({ length: 16 }, () => new Float32Array(16)),
  #|       directionalRects: Array.from({ length: 16 }, () => [0, 0, 0, 0]),
  #|       directionalBounds: Array.from({ length: 16 }, () => [0, 0, 0, 0]),
  #|       directionalTexel: [0, 0],
  #|       spotRecord: rt.ensureDummyShadowTexture(),
  #|       spotEnabled: [0, 0, 0, 0],
  #|       spotDepthBiases: [0, 0, 0, 0],
  #|       spotNormalBiases: [0, 0, 0, 0],
  #|       spotMatrices: Array.from({ length: 4 }, () => new Float32Array(16)),
  #|       spotRects: Array.from({ length: 4 }, () => [0, 0, 0, 0]),
  #|       spotTexel: [0, 0],
  #|       pointRecord: rt.ensureDummyShadowTexture(),
  #|       pointEnabled: [0, 0, 0, 0, 0, 0, 0, 0],
  #|       pointDepthBiases: [0, 0, 0, 0, 0, 0, 0, 0],
  #|       pointNormalBiases: [0, 0, 0, 0, 0, 0, 0, 0],
  #|       pointMatrices: Array.from({ length: 48 }, () => new Float32Array(16)),
  #|       pointRects: Array.from({ length: 48 }, () => [0, 0, 0, 0]),
  #|       pointTexel: [0, 0],
  #|       shadowCameraBufferCursor: 0,
  #|     });
  #|
  #|     rt.renderDirectionalShadowMaps3d = (encoder, section, buffers, camera, aspect, light, shadowState) => {
  #|       const directionalLights = Array.isArray(light.directionalLights) ? light.directionalLights : [];
  #|       const tileSize = Math.max(1, Number(light.directionalShadowMapSize) || 2048);
  #|       if (directionalLights.length <= 0) return;
  #|       for (let lightIndex = 0; lightIndex < Math.min(4, directionalLights.length); lightIndex += 1) {
  #|         const directionalLight = directionalLights[lightIndex];
  #|         if (!directionalLight?.shadows) continue;
  #|         const cascades = buildDirectionalShadowCascadesForLight(directionalLight, camera, aspect, tileSize);
  #|         if (cascades.length <= 0) continue;
  #|         const record = rt.ensureDirectionalShadowTexture(lightIndex, tileSize, cascades.length);
  #|         if (!record?.view) continue;
  #|         shadowState.directionalRecords[lightIndex] = record;
  #|         shadowState.directionalEnabled[lightIndex] = 1;
  #|         shadowState.directionalCascadeCounts[lightIndex] = cascades.length;
  #|         shadowState.directionalDepthBiases[lightIndex] = Math.max(0, Number(directionalLight.depthBias) || 0);
  #|         shadowState.directionalNormalBiases[lightIndex] = Math.max(0, Number(directionalLight.normalBias) || 0);
  #|         shadowState.directionalTexel = [1 / tileSize, 1 / tileSize];
  #|         const pass = encoder.beginRenderPass({
  #|           colorAttachments: [],
  #|           depthStencilAttachment: {
  #|             view: record.view,
  #|             depthClearValue: 1.0,
  #|             depthLoadOp: 'clear',
  #|             depthStoreOp: 'store',
  #|           },
  #|         });
  #|         for (let cascadeIndex = 0; cascadeIndex < cascades.length; cascadeIndex += 1) {
  #|           const cascade = cascades[cascadeIndex];
  #|           const viewportX = cascade.atlasRect.offset[0] * record.width;
  #|           const viewportY = cascade.atlasRect.offset[1] * record.height;
  #|           const scissorX = Math.max(0, Math.floor(viewportX));
  #|           const scissorY = Math.max(0, Math.floor(viewportY));
  #|           pass.setViewport(viewportX, viewportY, tileSize, tileSize, 0.0, 1.0);
  #|           pass.setScissorRect(scissorX, scissorY, tileSize | 0, tileSize | 0);
  #|           rt.renderShadowCastersForSetup(
  #|             pass,
  #|             section,
  #|             buffers,
  #|             cascade.lightViewProjection,
  #|             shadowState.shadowCameraBufferCursor,
  #|           );
  #|           shadowState.shadowCameraBufferCursor += 1;
  #|           const stateIndex = lightIndex * 4 + cascadeIndex;
  #|           shadowState.directionalMatrices[stateIndex] = cascade.lightViewProjection;
  #|           shadowState.directionalRects[stateIndex] = [
  #|             cascade.atlasRect.offset[0],
  #|             cascade.atlasRect.offset[1],
  #|             cascade.atlasRect.scale[0],
  #|             cascade.atlasRect.scale[1],
  #|           ];
  #|           shadowState.directionalBounds[stateIndex] = [cascade.nearBound, cascade.farBound, 0, 0];
  #|         }
  #|         pass.end();
  #|       }
  #|     };
  #|
  #|     rt.renderSpotShadowMaps3d = (encoder, section, buffers, light, shadowState) => {
  #|       const spotLights = Array.isArray(light.spotLights) ? light.spotLights : [];
  #|       const tileSize = Math.max(1, Number(light.directionalShadowMapSize) || 2048);
  #|       if (spotLights.length <= 0) return;
  #|       const record = rt.ensureSpotShadowTexture(tileSize);
  #|       if (!record?.view) return;
  #|       shadowState.spotRecord = record;
  #|       shadowState.spotTexel = [1 / tileSize, 1 / tileSize];
  #|       const pass = encoder.beginRenderPass({
  #|         colorAttachments: [],
  #|         depthStencilAttachment: {
  #|           view: record.view,
  #|           depthClearValue: 1.0,
  #|           depthLoadOp: 'clear',
  #|           depthStoreOp: 'store',
  #|         },
  #|       });
  #|       for (let lightIndex = 0; lightIndex < Math.min(4, spotLights.length); lightIndex += 1) {
  #|         const spotLight = spotLights[lightIndex];
  #|         if (!spotLight?.shadows) continue;
  #|         const rect = makeShadowAtlasRect(lightIndex, 2, tileSize, record.width, record.height);
  #|         const viewportX = rect.offset[0] * record.width;
  #|         const viewportY = rect.offset[1] * record.height;
  #|         const scissorX = Math.max(0, Math.floor(viewportX));
  #|         const scissorY = Math.max(0, Math.floor(viewportY));
  #|         pass.setViewport(viewportX, viewportY, tileSize, tileSize, 0.0, 1.0);
  #|         pass.setScissorRect(scissorX, scissorY, tileSize | 0, tileSize | 0);
  #|         const setup = buildSpotShadowSetup(spotLight);
  #|         rt.renderShadowCastersForSetup(
  #|           pass,
  #|           section,
  #|           buffers,
  #|           setup.lightViewProjection,
  #|           shadowState.shadowCameraBufferCursor,
  #|         );
  #|         shadowState.shadowCameraBufferCursor += 1;
  #|         shadowState.spotEnabled[lightIndex] = 1;
  #|         shadowState.spotDepthBiases[lightIndex] = Math.max(0, Number(spotLight.depthBias) || 0);
  #|         shadowState.spotNormalBiases[lightIndex] = Math.max(0, Number(spotLight.normalBias) || 0);
  #|         shadowState.spotMatrices[lightIndex] = setup.lightViewProjection;
  #|         shadowState.spotRects[lightIndex] = [rect.offset[0], rect.offset[1], rect.scale[0], rect.scale[1]];
  #|       }
  #|       pass.end();
  #|     };
  #|
  #|     rt.renderPointShadowMaps3d = (encoder, section, buffers, light, shadowState) => {
  #|       const pointLights = Array.isArray(light.pointLights) ? light.pointLights : [];
  #|       const faceSize = Math.max(1, Number(light.pointShadowMapSize) || 1024);
  #|       if (pointLights.length <= 0) return;
  #|       const record = rt.ensurePointShadowTexture(faceSize);
  #|       if (!record?.view) return;
  #|       shadowState.pointRecord = record;
  #|       shadowState.pointTexel = [1 / faceSize, 1 / faceSize];
  #|       const pass = encoder.beginRenderPass({
  #|         colorAttachments: [],
  #|         depthStencilAttachment: {
  #|           view: record.view,
  #|           depthClearValue: 1.0,
  #|           depthLoadOp: 'clear',
  #|           depthStoreOp: 'store',
  #|         },
  #|       });
  #|       for (let lightIndex = 0; lightIndex < Math.min(8, pointLights.length); lightIndex += 1) {
  #|         const pointLight = pointLights[lightIndex];
  #|         if (!pointLight?.shadows) continue;
  #|         shadowState.pointEnabled[lightIndex] = 1;
  #|         shadowState.pointDepthBiases[lightIndex] = Math.max(0, Number(pointLight.depthBias) || 0);
  #|         shadowState.pointNormalBiases[lightIndex] = Math.max(0, Number(pointLight.normalBias) || 0);
  #|         for (let faceIndex = 0; faceIndex < 6; faceIndex += 1) {
  #|           const atlasSlot = lightIndex * 6 + faceIndex;
  #|           const rect = makeShadowAtlasRect(atlasSlot, 8, faceSize, record.width, record.height);
  #|           const viewportX = rect.offset[0] * record.width;
  #|           const viewportY = rect.offset[1] * record.height;
  #|           const scissorX = Math.max(0, Math.floor(viewportX));
  #|           const scissorY = Math.max(0, Math.floor(viewportY));
  #|           pass.setViewport(viewportX, viewportY, faceSize, faceSize, 0.0, 1.0);
  #|           pass.setScissorRect(scissorX, scissorY, faceSize | 0, faceSize | 0);
  #|           const setup = buildPointShadowFaceSetup(pointLight, faceIndex);
  #|           rt.renderShadowCastersForSetup(
  #|             pass,
  #|             section,
  #|             buffers,
  #|             setup.lightViewProjection,
  #|             shadowState.shadowCameraBufferCursor,
  #|           );
  #|           shadowState.shadowCameraBufferCursor += 1;
  #|           const stateIndex = lightIndex * 6 + faceIndex;
  #|           shadowState.pointMatrices[stateIndex] = setup.lightViewProjection;
  #|           shadowState.pointRects[stateIndex] = [rect.offset[0], rect.offset[1], rect.scale[0], rect.scale[1]];
  #|         }
  #|       }
  #|       pass.end();
  #|     };
  #|
  #|     rt.buildShadowUniform3d = (shadowState) => {
  #|       const out = new Float32Array(1496);
  #|       out[0] = Number(shadowState.directionalTexel?.[0]) || 0;
  #|       out[1] = Number(shadowState.directionalTexel?.[1]) || 0;
  #|       out[2] = Number(shadowState.spotTexel?.[0]) || 0;
  #|       out[3] = Number(shadowState.spotTexel?.[1]) || 0;
  #|       out[4] = Number(shadowState.pointTexel?.[0]) || 0;
  #|       out[5] = Number(shadowState.pointTexel?.[1]) || 0;
  #|       let offset = 8;
  #|       for (let index = 0; index < 4; index += 1) {
  #|         out[offset] = shadowState.directionalEnabled[index];
  #|         out[offset + 1] = shadowState.directionalCascadeCounts[index];
  #|         out[offset + 2] = shadowState.directionalDepthBiases[index];
  #|         out[offset + 3] = shadowState.directionalNormalBiases[index];
  #|         offset += 4;
  #|       }
  #|       for (let index = 0; index < 16; index += 1) {
  #|         out[offset] = shadowState.directionalBounds[index][0];
  #|         out[offset + 1] = shadowState.directionalBounds[index][1];
  #|         offset += 4;
  #|       }
  #|       for (let index = 0; index < 16; index += 1) {
  #|         out[offset] = shadowState.directionalRects[index][0];
  #|         out[offset + 1] = shadowState.directionalRects[index][1];
  #|         out[offset + 2] = shadowState.directionalRects[index][2];
  #|         out[offset + 3] = shadowState.directionalRects[index][3];
  #|         offset += 4;
  #|       }
  #|       for (let index = 0; index < 16; index += 1) {
  #|         out.set(shadowState.directionalMatrices[index], offset);
  #|         offset += 16;
  #|       }
  #|       for (let index = 0; index < 4; index += 1) {
  #|         out[offset] = shadowState.spotEnabled[index];
  #|         out[offset + 1] = shadowState.spotDepthBiases[index];
  #|         out[offset + 2] = shadowState.spotNormalBiases[index];
  #|         offset += 4;
  #|       }
  #|       for (let index = 0; index < 4; index += 1) {
  #|         out[offset] = shadowState.spotRects[index][0];
  #|         out[offset + 1] = shadowState.spotRects[index][1];
  #|         out[offset + 2] = shadowState.spotRects[index][2];
  #|         out[offset + 3] = shadowState.spotRects[index][3];
  #|         offset += 4;
  #|       }
  #|       for (let index = 0; index < 4; index += 1) {
  #|         out.set(shadowState.spotMatrices[index], offset);
  #|         offset += 16;
  #|       }
  #|       for (let index = 0; index < 8; index += 1) {
  #|         out[offset] = shadowState.pointEnabled[index];
  #|         out[offset + 1] = shadowState.pointDepthBiases[index];
  #|         out[offset + 2] = shadowState.pointNormalBiases[index];
  #|         offset += 4;
  #|       }
  #|       for (let index = 0; index < 48; index += 1) {
  #|         out[offset] = shadowState.pointRects[index][0];
  #|         out[offset + 1] = shadowState.pointRects[index][1];
  #|         out[offset + 2] = shadowState.pointRects[index][2];
  #|         out[offset + 3] = shadowState.pointRects[index][3];
  #|         offset += 4;
  #|       }
  #|       for (let index = 0; index < 48; index += 1) {
  #|         out.set(shadowState.pointMatrices[index], offset);
  #|         offset += 16;
  #|       }
  #|       return out;
  #|     };
  #|
  #|     rt.prepareSection3d = (encoder, section) => {
  #|       const [w, h] = toCanvasSize();
  #|       const aspect = w / h;
  #|       const camera = section.camera;
  #|       const proj = camera.orthographic
  #|         ? mat4Orthographic(
  #|             camera.orthoWidth > 0 ? camera.orthoWidth : 20,
  #|             camera.orthoHeight > 0 ? camera.orthoHeight : 20,
  #|             camera.near > 0 ? camera.near : 0.1,
  #|             camera.far > camera.near ? camera.far : (camera.near + 200.0),
  #|           )
  #|         : mat4Perspective(
  #|             camera.fovy,
  #|             aspect,
  #|             camera.near > 0 ? camera.near : 0.1,
  #|             camera.far > camera.near ? camera.far : (camera.near + 200.0),
  #|           );
  #|       const view = mat4LookAt(camera.position, camera.target, camera.up);
  #|       const viewProj = mat4Multiply(proj, view);
  #|       const light = rt.light3d || {
  #|         directionalLights: [],
  #|         pointLights: [],
  #|         spotLights: [],
  #|         ambient: [1, 1, 1],
  #|         directionalShadowMapSize: 2048,
  #|         pointShadowMapSize: 1024,
  #|       };
  #|       const directionalLights = Array.isArray(light.directionalLights) ? light.directionalLights : [];
  #|       const pointLights = Array.isArray(light.pointLights) ? light.pointLights : [];
  #|       const spotLights = Array.isArray(light.spotLights) ? light.spotLights : [];
  #|       const directionalCount = Math.min(4, directionalLights.length);
  #|       const pointCount = Math.min(8, pointLights.length);
  #|       const spotCount = Math.min(4, spotLights.length);
  #|       const basis = cameraBasis(camera);
  #|       const lightUniform = new Float32Array(44 * 4);
  #|       lightUniform[0] = clamp01(Number(light.ambient?.[0]) || 0);
  #|       lightUniform[1] = clamp01(Number(light.ambient?.[1]) || 0);
  #|       lightUniform[2] = clamp01(Number(light.ambient?.[2]) || 0);
  #|       lightUniform[3] = directionalCount;
  #|       lightUniform[4] = pointCount;
  #|       lightUniform[5] = spotCount;
  #|       lightUniform[8] = Number(camera.position?.[0]) || 0;
  #|       lightUniform[9] = Number(camera.position?.[1]) || 0;
  #|       lightUniform[10] = Number(camera.position?.[2]) || 0;
  #|       lightUniform[12] = Number(basis.forward?.[0]) || 0;
  #|       lightUniform[13] = Number(basis.forward?.[1]) || 0;
  #|       lightUniform[14] = Number(basis.forward?.[2]) || -1;
  #|       for (let index = 0; index < directionalCount; index += 1) {
  #|         const src = directionalLights[index];
  #|         const base = 16 + index * 8;
  #|         lightUniform[base] = Number(src.direction?.[0]) || 0;
  #|         lightUniform[base + 1] = Number(src.direction?.[1]) || -1;
  #|         lightUniform[base + 2] = Number(src.direction?.[2]) || 0;
  #|         lightUniform[base + 3] = Math.max(0, Number(src.intensity) || 0);
  #|         lightUniform[base + 4] = clamp01(Number(src.color?.[0]) || 0);
  #|         lightUniform[base + 5] = clamp01(Number(src.color?.[1]) || 0);
  #|         lightUniform[base + 6] = clamp01(Number(src.color?.[2]) || 0);
  #|       }
  #|       for (let index = 0; index < pointCount; index += 1) {
  #|         const src = pointLights[index];
  #|         const base = 48 + index * 8;
  #|         lightUniform[base] = Number(src.position?.[0]) || 0;
  #|         lightUniform[base + 1] = Number(src.position?.[1]) || 0;
  #|         lightUniform[base + 2] = Number(src.position?.[2]) || 0;
  #|         lightUniform[base + 3] = Math.max(0, Number(src.intensity) || 0);
  #|         lightUniform[base + 4] = clamp01(Number(src.color?.[0]) || 0);
  #|         lightUniform[base + 5] = clamp01(Number(src.color?.[1]) || 0);
  #|         lightUniform[base + 6] = clamp01(Number(src.color?.[2]) || 0);
  #|         lightUniform[base + 7] = Math.max(0.0001, Number(src.range) || 0.0001);
  #|       }
  #|       for (let index = 0; index < spotCount; index += 1) {
  #|         const src = spotLights[index];
  #|         const base = 112 + index * 16;
  #|         lightUniform[base] = Number(src.position?.[0]) || 0;
  #|         lightUniform[base + 1] = Number(src.position?.[1]) || 0;
  #|         lightUniform[base + 2] = Number(src.position?.[2]) || 0;
  #|         lightUniform[base + 3] = Math.max(0, Number(src.intensity) || 0);
  #|         lightUniform[base + 4] = clamp01(Number(src.color?.[0]) || 0);
  #|         lightUniform[base + 5] = clamp01(Number(src.color?.[1]) || 0);
  #|         lightUniform[base + 6] = clamp01(Number(src.color?.[2]) || 0);
  #|         lightUniform[base + 7] = Math.max(0.0001, Number(src.range) || 0.0001);
  #|         lightUniform[base + 8] = Number(src.direction?.[0]) || 0;
  #|         lightUniform[base + 9] = Number(src.direction?.[1]) || -1;
  #|         lightUniform[base + 10] = Number(src.direction?.[2]) || 0;
  #|         lightUniform[base + 11] = Number(src.innerCos) || 0;
  #|         lightUniform[base + 12] = Number(src.outerCos) || 0;
  #|       }
  #|       const buffers = rt.prepareSectionBuffers3d(section);
  #|       const shadowState = rt.createShadowState3d();
  #|       rt.renderDirectionalShadowMaps3d(encoder, section, buffers, camera, aspect, light, shadowState);
  #|       rt.renderSpotShadowMaps3d(encoder, section, buffers, light, shadowState);
  #|       rt.renderPointShadowMaps3d(encoder, section, buffers, light, shadowState);
  #|       const shadowUniform = rt.buildShadowUniform3d(shadowState);
  #|       rt.device.queue.writeBuffer(rt.uniformBuffers.camera3d, 0, viewProj.buffer, viewProj.byteOffset, viewProj.byteLength);
  #|       rt.device.queue.writeBuffer(rt.uniformBuffers.light3d, 0, lightUniform.buffer, lightUniform.byteOffset, lightUniform.byteLength);
  #|       rt.device.queue.writeBuffer(rt.uniformBuffers.shadow3d, 0, shadowUniform.buffer, shadowUniform.byteOffset, shadowUniform.byteLength);
  #|       return { buffers, shadowState };
  #|     };
  #|
  #|     rt.renderSection3d = (pass, section, state) => {
  #|       pass.setBindGroup(0, rt.bindGroups.camera3d);
  #|       if (state.buffers.tri.buffer && section.triCommands.length > 0) {
  #|         pass.setPipeline(rt.pipelines.color3dTri);
  #|         for (let index = 0; index < section.triCommands.length; index += 1) {
  #|           const cmd = section.triCommands[index];
  #|           pass.setVertexBuffer(0, state.buffers.tri.buffer, state.buffers.tri.offsets[index], cmd.verts.byteLength);
  #|           pass.draw(cmd.verts.length / 7, 1, 0, 0);
  #|         }
  #|       }
  #|       if (state.buffers.line.buffer && section.lineCommands.length > 0) {
  #|         pass.setPipeline(rt.pipelines.color3dLine);
  #|         for (let index = 0; index < section.lineCommands.length; index += 1) {
  #|           const verts = section.lineCommands[index];
  #|           pass.setVertexBuffer(0, state.buffers.line.buffer, state.buffers.line.offsets[index], verts.byteLength);
  #|           pass.draw(verts.length / 7, 1, 0, 0);
  #|         }
  #|       }
  #|       if (state.buffers.lit.buffer && section.litTriCommands.length > 0) {
  #|         const bindGroup = rt.device.createBindGroup({
  #|           layout: rt.bindGroupLayouts.lit3d,
  #|           entries: [
  #|             { binding: 0, resource: { buffer: rt.uniformBuffers.camera3d } },
  #|             { binding: 1, resource: { buffer: rt.uniformBuffers.light3d } },
  #|             { binding: 2, resource: { buffer: rt.uniformBuffers.shadow3d } },
  #|             { binding: 3, resource: rt.samplers.shadowCompare },
  #|             { binding: 4, resource: state.shadowState.directionalRecords[0].view },
  #|             { binding: 5, resource: state.shadowState.directionalRecords[1].view },
  #|             { binding: 6, resource: state.shadowState.directionalRecords[2].view },
  #|             { binding: 7, resource: state.shadowState.directionalRecords[3].view },
  #|             { binding: 8, resource: state.shadowState.spotRecord.view },
  #|             { binding: 9, resource: state.shadowState.pointRecord.view },
  #|           ],
  #|         });
  #|         pass.setBindGroup(0, bindGroup);
  #|         for (let index = 0; index < section.litTriCommands.length; index += 1) {
  #|           const cmd = section.litTriCommands[index];
  #|           pass.setPipeline(cmd.doubleSided ? rt.pipelines.lit3dTriDouble : rt.pipelines.lit3dTriSingle);
  #|           pass.setVertexBuffer(0, state.buffers.lit.buffer, state.buffers.lit.offsets[index], cmd.verts.byteLength);
  #|           pass.draw(cmd.verts.length / 17, 1, 0, 0);
  #|         }
  #|       }
  #|       if (state.buffers.tex.buffer && section.texTriCommands.length > 0) {
  #|         for (let index = 0; index < section.texTriCommands.length; index += 1) {
  #|           const cmd = section.texTriCommands[index];
  #|           const rec = cmd.textureRec;
  #|           const emissiveRec = cmd.emissiveTextureRec;
  #|           const metallicRoughnessRec = cmd.metallicRoughnessTextureRec;
  #|           const occlusionRec = cmd.occlusionTextureRec;
  #|           const normalRec = cmd.normalTextureRec;
  #|           if (!rec?.view || !emissiveRec?.view || !metallicRoughnessRec?.view || !occlusionRec?.view || !normalRec?.view) continue;
  #|           const bindGroup = rt.device.createBindGroup({
  #|             layout: rt.bindGroupLayouts.lit3dTex,
  #|             entries: [
  #|               { binding: 0, resource: { buffer: rt.uniformBuffers.camera3d } },
  #|               { binding: 1, resource: { buffer: rt.uniformBuffers.light3d } },
  #|               { binding: 2, resource: { buffer: rt.uniformBuffers.shadow3d } },
  #|               { binding: 3, resource: rt.samplers.shadowCompare },
  #|               { binding: 4, resource: state.shadowState.directionalRecords[0].view },
  #|               { binding: 5, resource: state.shadowState.directionalRecords[1].view },
  #|               { binding: 6, resource: state.shadowState.directionalRecords[2].view },
  #|               { binding: 7, resource: state.shadowState.directionalRecords[3].view },
  #|               { binding: 8, resource: state.shadowState.spotRecord.view },
  #|               { binding: 9, resource: state.shadowState.pointRecord.view },
  #|               { binding: 10, resource: rt.resolveSampler(cmd.baseSamplerCode) },
  #|               { binding: 11, resource: rec.view },
  #|               { binding: 12, resource: rt.resolveSampler(cmd.emissiveSamplerCode) },
  #|               { binding: 13, resource: emissiveRec.view },
  #|               { binding: 14, resource: rt.resolveSampler(cmd.metallicRoughnessSamplerCode) },
  #|               { binding: 15, resource: metallicRoughnessRec.view },
  #|               { binding: 16, resource: rt.resolveSampler(cmd.occlusionSamplerCode) },
  #|               { binding: 17, resource: occlusionRec.view },
  #|               { binding: 18, resource: rt.resolveSampler(cmd.normalSamplerCode) },
  #|               { binding: 19, resource: normalRec.view },
  #|             ],
  #|           });
  #|           pass.setPipeline(cmd.doubleSided ? rt.pipelines.lit3dTexTriDouble : rt.pipelines.lit3dTexTriSingle);
  #|           pass.setBindGroup(0, bindGroup);
  #|           pass.setVertexBuffer(0, state.buffers.tex.buffer, state.buffers.tex.offsets[index], cmd.verts.byteLength);
  #|           pass.draw(cmd.verts.length / 35, 1, 0, 0);
  #|         }
  #|       }
  #|     };
  #|
  #|     rt.render2d = (pass) => {
  #|       const [w, h] = toCanvasSize();
  #|       const canvasUniform = new Float32Array([w, h, 0, 0]);
  #|       rt.device.queue.writeBuffer(rt.uniformBuffers.canvas2d, 0, canvasUniform.buffer, canvasUniform.byteOffset, canvasUniform.byteLength);
  #|       let colorBytes = 0;
  #|       let texBytes = 0;
  #|       for (const cmd of rt.draw2dCommands) {
  #|         if (cmd.kind === 'color') colorBytes += cmd.verts.byteLength;
  #|         else texBytes += cmd.verts.byteLength;
  #|       }
  #|       const colorVb = colorBytes > 0
  #|         ? rt.ensureDynamicBuffer('dyn2dColor', colorBytes, GPUBufferUsage.VERTEX)
  #|         : null;
  #|       const texVb = texBytes > 0
  #|         ? rt.ensureDynamicBuffer('dyn2dTex', texBytes, GPUBufferUsage.VERTEX)
  #|         : null;
  #|       let colorOffset = 0;
  #|       let texOffset = 0;
  #|       for (const cmd of rt.draw2dCommands) {
  #|         if (cmd.kind === 'color') {
  #|           if (cmd.topology === 'line') pass.setPipeline(rt.pipelines.color2dLine);
  #|           else pass.setPipeline(rt.pipelines.color2dTri);
  #|           pass.setBindGroup(0, rt.bindGroups.canvas2d);
  #|           rt.device.queue.writeBuffer(colorVb, colorOffset, cmd.verts.buffer, cmd.verts.byteOffset, cmd.verts.byteLength);
  #|           pass.setVertexBuffer(0, colorVb, colorOffset, cmd.verts.byteLength);
  #|           pass.draw(cmd.verts.length / 6, 1, 0, 0);
  #|           colorOffset += cmd.verts.byteLength;
  #|           continue;
  #|         }
  #|         const rec = cmd.textureRec;
  #|         if (!rec?.view) continue;
  #|         pass.setPipeline(rt.pipelines.tex2dTri);
  #|         rt.device.queue.writeBuffer(texVb, texOffset, cmd.verts.buffer, cmd.verts.byteOffset, cmd.verts.byteLength);
  #|         const sampler = cmd.samplerCode === 3
  #|           ? rt.samplers.repeat
  #|           : (cmd.samplerCode === 1
  #|             ? rt.samplers.repeatX
  #|             : (cmd.samplerCode === 2 ? rt.samplers.repeatY : rt.samplers.clamp));
  #|         const bindGroup = rt.device.createBindGroup({
  #|           layout: rt.bindGroupLayouts.tex2d,
  #|           entries: [
  #|             { binding: 0, resource: { buffer: rt.uniformBuffers.canvas2d } },
  #|             { binding: 1, resource: sampler },
  #|             { binding: 2, resource: rec.view },
  #|           ],
  #|         });
  #|         pass.setBindGroup(0, bindGroup);
  #|         pass.setVertexBuffer(0, texVb, texOffset, cmd.verts.byteLength);
  #|         pass.draw(cmd.verts.length / 8, 1, 0, 0);
  #|         texOffset += cmd.verts.byteLength;
  #|       }
  #|     };
  #|
  #|     rt.endFrame = () => {
  #|       if (!rt.ready || !rt.device || !rt.context || !rt.canvas) return;
  #|       const view = rt.context.getCurrentTexture().createView();
  #|       const encoder = rt.device.createCommandEncoder();
  #|       let rendered = false;
  #|       if (rt.sections3d.length > 0) {
  #|         rt.ensureDepth();
  #|         let first = true;
  #|         for (const section of rt.sections3d) {
  #|           const sectionState = rt.prepareSection3d(encoder, section);
  #|           const pass = encoder.beginRenderPass({
  #|             colorAttachments: [{
  #|               view,
  #|               clearValue: { r: rt.clearColor[0], g: rt.clearColor[1], b: rt.clearColor[2], a: rt.clearColor[3] },
  #|               loadOp: first ? 'clear' : 'load',
  #|               storeOp: 'store',
  #|             }],
  #|             depthStencilAttachment: {
  #|               view: rt.depthView,
  #|               depthClearValue: 1.0,
  #|               depthLoadOp: first ? 'clear' : 'load',
  #|               depthStoreOp: 'store',
  #|             },
  #|           });
  #|           rt.renderSection3d(pass, section, sectionState);
  #|           pass.end();
  #|           first = false;
  #|           rendered = true;
  #|         }
  #|       }
  #|       if (rt.draw2dCommands.length > 0 || !rendered) {
  #|         const pass = encoder.beginRenderPass({
  #|           colorAttachments: [{
  #|             view,
  #|             clearValue: { r: rt.clearColor[0], g: rt.clearColor[1], b: rt.clearColor[2], a: rt.clearColor[3] },
  #|             loadOp: rendered ? 'load' : 'clear',
  #|             storeOp: 'store',
  #|           }],
  #|         });
  #|         rt.render2d(pass);
  #|         pass.end();
  #|       }
  #|       rt.device.queue.submit([encoder.finish()]);
  #|     };
  #|
  #|     rt.init = async () => {
  #|       if (!navigator.gpu) {
  #|         throw new Error('WebGPU is unavailable in this browser');
  #|       }
  #|       rt.adapter = await navigator.gpu.requestAdapter();
  #|       if (!rt.adapter) {
  #|         throw new Error('Failed to acquire WebGPU adapter');
  #|       }
  #|       rt.device = await rt.adapter.requestDevice();
  #|       rt.context = rt.canvas.getContext('webgpu');
  #|       rt.format = navigator.gpu.getPreferredCanvasFormat();
  #|       rt.context.configure({
  #|         device: rt.device,
  #|         format: rt.format,
  #|         alphaMode: 'premultiplied',
  #|       });
  #|       const magFilter = rt.imageSmooth ? 'linear' : 'nearest';
  #|       const minFilter = rt.imageSmooth ? 'linear' : 'nearest';
  #|       rt.samplers = {
  #|         clamp: rt.device.createSampler({ magFilter, minFilter, addressModeU: 'clamp-to-edge', addressModeV: 'clamp-to-edge' }),
  #|         repeatX: rt.device.createSampler({ magFilter, minFilter, addressModeU: 'repeat', addressModeV: 'clamp-to-edge' }),
  #|         repeatY: rt.device.createSampler({ magFilter, minFilter, addressModeU: 'clamp-to-edge', addressModeV: 'repeat' }),
  #|         repeat: rt.device.createSampler({ magFilter, minFilter, addressModeU: 'repeat', addressModeV: 'repeat' }),
  #|         shadowCompare: rt.device.createSampler({
  #|           compare: 'less',
  #|           magFilter: 'linear',
  #|           minFilter: 'linear',
  #|           mipmapFilter: 'nearest',
  #|           addressModeU: 'clamp-to-edge',
  #|           addressModeV: 'clamp-to-edge',
  #|         }),
  #|       };
  #|
  #|       const shader2dColor = rt.device.createShaderModule({
  #|         code: `
  #| struct CanvasUniform { size: vec2, _pad: vec2 };
  #| @group(0) @binding(0) var u_canvas: CanvasUniform;
  #| struct VSIn {
  #|   @location(0) pos: vec2,
  #|   @location(1) color: vec4,
  #| };
  #| struct VSOut {
  #|   @builtin(position) pos: vec4,
  #|   @location(0) color: vec4,
  #| };
  #| @vertex fn vs_main(v: VSIn) -> VSOut {
  #|   var o: VSOut;
  #|   let nx = (v.pos.x / u_canvas.size.x) * 2.0 - 1.0;
  #|   let ny = 1.0 - (v.pos.y / u_canvas.size.y) * 2.0;
  #|   o.pos = vec4(nx, ny, 0.0, 1.0);
  #|   o.color = v.color;
  #|   return o;
  #| }
  #| @fragment fn fs_main(v: VSOut) -> @location(0) vec4 {
  #|   return v.color;
  #| }
  #| `,
  #|       });
  #|       const shader2dTex = rt.device.createShaderModule({
  #|         code: `
  #| struct CanvasUniform { size: vec2, _pad: vec2 };
  #| @group(0) @binding(0) var u_canvas: CanvasUniform;
  #| @group(0) @binding(1) var u_sampler: sampler;
  #| @group(0) @binding(2) var u_texture: texture_2d;
  #| struct VSIn {
  #|   @location(0) pos: vec2,
  #|   @location(1) uv: vec2,
  #|   @location(2) color: vec4,
  #| };
  #| struct VSOut {
  #|   @builtin(position) pos: vec4,
  #|   @location(0) uv: vec2,
  #|   @location(1) color: vec4,
  #| };
  #| @vertex fn vs_main(v: VSIn) -> VSOut {
  #|   var o: VSOut;
  #|   let nx = (v.pos.x / u_canvas.size.x) * 2.0 - 1.0;
  #|   let ny = 1.0 - (v.pos.y / u_canvas.size.y) * 2.0;
  #|   o.pos = vec4(nx, ny, 0.0, 1.0);
  #|   o.uv = v.uv;
  #|   o.color = v.color;
  #|   return o;
  #| }
  #| @fragment fn fs_main(v: VSOut) -> @location(0) vec4 {
  #|   return textureSample(u_texture, u_sampler, v.uv) * v.color;
  #| }
  #| `,
  #|       });
  #|       const shader3dColor = rt.device.createShaderModule({
  #|         code: `
  #| struct CameraUniform {
  #|   view_proj: mat4x4,
  #| };
  #| @group(0) @binding(0) var u_camera: CameraUniform;
  #| struct VSIn {
  #|   @location(0) pos: vec3,
  #|   @location(1) color: vec4,
  #| };
  #| struct VSOut {
  #|   @builtin(position) pos: vec4,
  #|   @location(0) color: vec4,
  #| };
  #| @vertex fn vs_main(v: VSIn) -> VSOut {
  #|   var o: VSOut;
  #|   o.pos = u_camera.view_proj * vec4(v.pos, 1.0);
  #|   o.color = v.color;
  #|   return o;
  #| }
  #| @fragment fn fs_main(v: VSOut) -> @location(0) vec4 {
  #|   return v.color;
  #| }
  #| `,
  #|       });
  #|       const lightShadowStructsWgsl = `
  #| struct LightUniform {
  #|   ambient_directional_count: vec4,
  #|   point_spot_count: vec4,
  #|   view_position: vec4,
  #|   view_forward: vec4,
  #|   directional_data: array, 8>,
  #|   point_data: array, 16>,
  #|   spot_data: array, 16>,
  #| };
  #| struct ShadowUniform {
  #|   directional_spot_texel: vec4,
  #|   point_texel: vec4,
  #|   directional_meta: array, 4>,
  #|   directional_bounds: array, 16>,
  #|   directional_rects: array, 16>,
  #|   directional_matrices: array, 16>,
  #|   spot_meta: array, 4>,
  #|   spot_rects: array, 4>,
  #|   spot_matrices: array, 4>,
  #|   point_meta: array, 8>,
  #|   point_rects: array, 48>,
  #|   point_matrices: array, 48>,
  #| };
  #| `;
  #|       const shadowSamplingWgsl = `
  #| fn saturate(value: f32) -> f32 { return clamp(value, 0.0, 1.0); }
  #| fn point_light_attenuation(distance: f32, range: f32) -> f32 {
  #|   let safe_range = max(range, 0.0001);
  #|   return saturate(1.0 - distance / safe_range);
  #| }
  #| fn safe_normalize(value: vec3, fallback: vec3) -> vec3 {
  #|   let len = length(value);
  #|   if (len <= 0.0001) { return fallback; }
  #|   return value / len;
  #| }
  #| fn shadow_local_uv(ndc: vec3) -> vec2 {
  #|   return vec2(ndc.x * 0.5 + 0.5, 0.5 - ndc.y * 0.5);
  #| }
  #| fn sample_directional_shadow_compare(index: i32, uv: vec2, depth_ref: f32) -> f32 {
  #|   if (index == 0) { return textureSampleCompareLevel(u_directional_shadow0, u_shadow_sampler, uv, depth_ref); }
  #|   if (index == 1) { return textureSampleCompareLevel(u_directional_shadow1, u_shadow_sampler, uv, depth_ref); }
  #|   if (index == 2) { return textureSampleCompareLevel(u_directional_shadow2, u_shadow_sampler, uv, depth_ref); }
  #|   if (index == 3) { return textureSampleCompareLevel(u_directional_shadow3, u_shadow_sampler, uv, depth_ref); }
  #|   return 1.0;
  #| }
  #| fn sample_directional_shadow(light_index: i32, world_pos: vec3, normal: vec3, light_dir: vec3, receive_shadows: bool) -> f32 {
  #|   if (!receive_shadows || light_index < 0 || light_index >= 4) { return 1.0; }
  #|   let shadow_meta = u_shadow.directional_meta[light_index];
  #|   if (shadow_meta.x < 0.5) { return 1.0; }
  #|   let cascade_count = clamp(i32(round(shadow_meta.y)), 0, 4);
  #|   if (cascade_count <= 0) { return 1.0; }
  #|   let view_depth = dot(world_pos - u_light.view_position.xyz, u_light.view_forward.xyz);
  #|   let base_index = light_index * 4;
  #|   let first_near = u_shadow.directional_bounds[base_index].x;
  #|   let last_far = u_shadow.directional_bounds[base_index + cascade_count - 1].y;
  #|   if (view_depth < first_near || view_depth > last_far) { return 1.0; }
  #|   var selected = cascade_count - 1;
  #|   for (var cascade_index = 0; cascade_index < 4; cascade_index = cascade_index + 1) {
  #|     if (cascade_index >= cascade_count) { continue; }
  #|     if (view_depth <= u_shadow.directional_bounds[base_index + cascade_index].y) {
  #|       selected = cascade_index;
  #|       break;
  #|     }
  #|   }
  #|   let texel = u_shadow.directional_spot_texel.xy;
  #|   let ndotl = max(dot(normal, light_dir), 0.0);
  #|   let bias = max(0.0, shadow_meta.z) + max(0.0, shadow_meta.w) * max(texel.x, texel.y) * (1.0 - ndotl);
  #|   let filter_radius = 1.25;
  #|   let matrix_index = base_index + selected;
  #|   let clip = u_shadow.directional_matrices[matrix_index] * vec4(world_pos, 1.0);
  #|   if (abs(clip.w) <= 0.00001) { return 1.0; }
  #|   let ndc = clip.xyz / clip.w;
  #|   let local_uv = shadow_local_uv(ndc);
  #|   let depth = ndc.z;
  #|   if (depth <= 0.0 || depth >= 1.0 || any(local_uv < vec2(0.0)) || any(local_uv > vec2(1.0))) { return 1.0; }
  #|   let rect = u_shadow.directional_rects[matrix_index];
  #|   var lit = 0.0;
  #|   var total = 0.0;
  #|   for (var x = -1; x <= 1; x = x + 1) {
  #|     for (var y = -1; y <= 1; y = y + 1) {
  #|       let weight = 1.0 / (1.0 + abs(f32(x)) + abs(f32(y)));
  #|       let sample_uv = clamp(local_uv + vec2(f32(x), f32(y)) * texel * filter_radius, vec2(0.0), vec2(1.0));
  #|       let atlas_uv = rect.xy + sample_uv * rect.zw;
  #|       lit = lit + sample_directional_shadow_compare(light_index, atlas_uv, depth - bias) * weight;
  #|       total = total + weight;
  #|     }
  #|   }
  #|   var shadow = select(1.0, lit / total, total > 0.00001);
  #|   if (selected > 0) {
  #|     let previous = selected - 1;
  #|     let overlap_near = u_shadow.directional_bounds[base_index + selected].x;
  #|     let overlap_far = u_shadow.directional_bounds[base_index + previous].y;
  #|     if (overlap_near < overlap_far && view_depth >= overlap_near && view_depth <= overlap_far) {
  #|       let prev_index = base_index + previous;
  #|       let prev_clip = u_shadow.directional_matrices[prev_index] * vec4(world_pos, 1.0);
  #|       if (abs(prev_clip.w) > 0.00001) {
  #|         let prev_ndc = prev_clip.xyz / prev_clip.w;
  #|         let prev_local_uv = shadow_local_uv(prev_ndc);
  #|         let prev_depth = prev_ndc.z;
  #|         if (!(prev_depth <= 0.0 || prev_depth >= 1.0 || any(prev_local_uv < vec2(0.0)) || any(prev_local_uv > vec2(1.0)))) {
  #|           let prev_rect = u_shadow.directional_rects[prev_index];
  #|           var prev_lit = 0.0;
  #|           var prev_total = 0.0;
  #|           for (var px = -1; px <= 1; px = px + 1) {
  #|             for (var py = -1; py <= 1; py = py + 1) {
  #|               let prev_weight = 1.0 / (1.0 + abs(f32(px)) + abs(f32(py)));
  #|               let prev_sample_uv = clamp(prev_local_uv + vec2(f32(px), f32(py)) * texel * filter_radius, vec2(0.0), vec2(1.0));
  #|               let prev_atlas_uv = prev_rect.xy + prev_sample_uv * prev_rect.zw;
  #|               prev_lit = prev_lit + sample_directional_shadow_compare(light_index, prev_atlas_uv, prev_depth - bias) * prev_weight;
  #|               prev_total = prev_total + prev_weight;
  #|             }
  #|           }
  #|           let previous_shadow = select(1.0, prev_lit / prev_total, prev_total > 0.00001);
  #|           let blend = clamp((view_depth - overlap_near) / (overlap_far - overlap_near), 0.0, 1.0);
  #|           shadow = mix(previous_shadow, shadow, blend);
  #|         }
  #|       }
  #|     }
  #|   }
  #|   return shadow;
  #| }
  #| fn sample_spot_shadow(index: i32, world_pos: vec3, normal: vec3, light_dir: vec3, receive_shadows: bool) -> f32 {
  #|   if (!receive_shadows || index < 0 || index >= 4) { return 1.0; }
  #|   let shadow_meta = u_shadow.spot_meta[index];
  #|   if (shadow_meta.x < 0.5) { return 1.0; }
  #|   let clip = u_shadow.spot_matrices[index] * vec4(world_pos, 1.0);
  #|   if (abs(clip.w) <= 0.00001) { return 1.0; }
  #|   let ndc = clip.xyz / clip.w;
  #|   let local_uv = shadow_local_uv(ndc);
  #|   let depth = ndc.z;
  #|   if (depth <= 0.0 || depth >= 1.0 || any(local_uv < vec2(0.0)) || any(local_uv > vec2(1.0))) { return 1.0; }
  #|   let rect = u_shadow.spot_rects[index];
  #|   let texel = u_shadow.directional_spot_texel.zw;
  #|   let ndotl = max(dot(normal, light_dir), 0.0);
  #|   let bias = max(0.0, shadow_meta.y) + max(0.0, shadow_meta.z) * max(texel.x, texel.y) * (1.0 - ndotl);
  #|   var lit = 0.0;
  #|   var total = 0.0;
  #|   for (var x = -1; x <= 1; x = x + 1) {
  #|     for (var y = -1; y <= 1; y = y + 1) {
  #|       let weight = 1.0 / (1.0 + abs(f32(x)) + abs(f32(y)));
  #|       let sample_uv = clamp(local_uv + vec2(f32(x), f32(y)) * texel * 1.25, vec2(0.0), vec2(1.0));
  #|       let atlas_uv = rect.xy + sample_uv * rect.zw;
  #|       lit = lit + textureSampleCompareLevel(u_spot_shadow_atlas, u_shadow_sampler, atlas_uv, depth - bias) * weight;
  #|       total = total + weight;
  #|     }
  #|   }
  #|   return select(1.0, lit / total, total > 0.00001);
  #| }
  #| fn select_point_shadow_face(dir: vec3) -> i32 {
  #|   let abs_dir = abs(dir);
  #|   if (abs_dir.x >= abs_dir.y && abs_dir.x >= abs_dir.z) { return select(1, 0, dir.x >= 0.0); }
  #|   if (abs_dir.y >= abs_dir.x && abs_dir.y >= abs_dir.z) { return select(3, 2, dir.y >= 0.0); }
  #|   return select(5, 4, dir.z >= 0.0);
  #| }
  #| fn sample_point_shadow(index: i32, world_pos: vec3, normal: vec3, light_dir: vec3, light_to_fragment: vec3, receive_shadows: bool) -> f32 {
  #|   if (!receive_shadows || index < 0 || index >= 8) { return 1.0; }
  #|   let shadow_meta = u_shadow.point_meta[index];
  #|   if (shadow_meta.x < 0.5) { return 1.0; }
  #|   let face = select_point_shadow_face(light_to_fragment);
  #|   let matrix_index = index * 6 + face;
  #|   let clip = u_shadow.point_matrices[matrix_index] * vec4(world_pos, 1.0);
  #|   if (abs(clip.w) <= 0.00001) { return 1.0; }
  #|   let ndc = clip.xyz / clip.w;
  #|   let local_uv = shadow_local_uv(ndc);
  #|   let depth = ndc.z;
  #|   if (depth <= 0.0 || depth >= 1.0 || any(local_uv < vec2(0.0)) || any(local_uv > vec2(1.0))) { return 1.0; }
  #|   let rect = u_shadow.point_rects[matrix_index];
  #|   let texel = u_shadow.point_texel.xy;
  #|   let ndotl = max(dot(normal, light_dir), 0.0);
  #|   let bias = max(0.0, shadow_meta.y) + max(0.0, shadow_meta.z) * max(texel.x, texel.y) * (1.0 - ndotl);
  #|   var lit = 0.0;
  #|   var total = 0.0;
  #|   for (var x = -1; x <= 1; x = x + 1) {
  #|     for (var y = -1; y <= 1; y = y + 1) {
  #|       let weight = 1.0 / (1.0 + abs(f32(x)) + abs(f32(y)));
  #|       let sample_uv = clamp(local_uv + vec2(f32(x), f32(y)) * texel, vec2(0.0), vec2(1.0));
  #|       let atlas_uv = rect.xy + sample_uv * rect.zw;
  #|       lit = lit + textureSampleCompareLevel(u_point_shadow_atlas, u_shadow_sampler, atlas_uv, depth - bias) * weight;
  #|       total = total + weight;
  #|     }
  #|   }
  #|   return select(1.0, lit / total, total > 0.00001);
  #| }
  #| `;
  #|       const shader3dShadowSolid = rt.device.createShaderModule({
  #|         code: `
  #| struct ShadowCameraUniform { light_view_proj: mat4x4, };
  #| @group(0) @binding(0) var u_shadow_camera: ShadowCameraUniform;
  #| struct VSIn { @location(0) pos: vec3, @location(1) color: vec4, };
  #| struct VSOut { @builtin(position) pos: vec4, };
  #| @vertex fn vs_main(v: VSIn) -> VSOut {
  #|   var o: VSOut;
  #|   o.pos = u_shadow_camera.light_view_proj * vec4(v.pos, 1.0);
  #|   return o;
  #| }
  #| `,
  #|       });
  #|       const shader3dShadowLit = rt.device.createShaderModule({
  #|         code: `
  #| struct ShadowCameraUniform { light_view_proj: mat4x4, };
  #| @group(0) @binding(0) var u_shadow_camera: ShadowCameraUniform;
  #| struct VSIn {
  #|   @location(0) pos: vec3,
  #|   @location(2) color: vec4,
  #|   @location(4) material: vec4,
  #| };
  #| struct VSOut {
  #|   @builtin(position) pos: vec4,
  #|   @location(0) color: vec4,
  #|   @location(1) material: vec4,
  #| };
  #| @vertex fn vs_main(v: VSIn) -> VSOut {
  #|   var o: VSOut;
  #|   o.pos = u_shadow_camera.light_view_proj * vec4(v.pos, 1.0);
  #|   o.color = v.color;
  #|   o.material = v.material;
  #|   return o;
  #| }
  #| @fragment fn fs_main(v: VSOut) {
  #|   let alpha_mode = i32(round(v.material.x));
  #|   let alpha_cutoff = v.material.y;
  #|   if (alpha_mode == 2) { discard; }
  #|   if (alpha_mode == 1 && v.color.a < alpha_cutoff) { discard; }
  #| }
  #| `,
  #|       });
  #|       const shader3dShadowTex = rt.device.createShaderModule({
  #|         code: `
  #| struct ShadowCameraUniform { light_view_proj: mat4x4, };
  #| @group(0) @binding(0) var u_shadow_camera: ShadowCameraUniform;
  #| @group(0) @binding(1) var u_shadow_base_sampler: sampler;
  #| @group(0) @binding(2) var u_shadow_base_texture: texture_2d;
  #| struct VSIn {
  #|   @location(0) pos: vec3,
  #|   @location(2) base_uv: vec2,
  #|   @location(7) color: vec4,
  #|   @location(10) material: vec4,
  #| };
  #| struct VSOut {
  #|   @builtin(position) pos: vec4,
  #|   @location(0) base_uv: vec2,
  #|   @location(1) color: vec4,
  #|   @location(2) material: vec4,
  #| };
  #| @vertex fn vs_main(v: VSIn) -> VSOut {
  #|   var o: VSOut;
  #|   o.pos = u_shadow_camera.light_view_proj * vec4(v.pos, 1.0);
  #|   o.base_uv = v.base_uv;
  #|   o.color = v.color;
  #|   o.material = v.material;
  #|   return o;
  #| }
  #| @fragment fn fs_main(v: VSOut) {
  #|   let alpha_mode = i32(round(v.material.x));
  #|   let alpha_cutoff = v.material.y;
  #|   if (alpha_mode == 2) { discard; }
  #|   let albedo = textureSample(u_shadow_base_texture, u_shadow_base_sampler, v.base_uv) * v.color;
  #|   if (alpha_mode == 1 && albedo.a < alpha_cutoff) { discard; }
  #| }
  #| `,
  #|       });
  #|       const shader3dLitColor = rt.device.createShaderModule({
  #|         code: `
  #| struct CameraUniform { view_proj: mat4x4, };
  #| ${lightShadowStructsWgsl}
  #| @group(0) @binding(0) var u_camera: CameraUniform;
  #| @group(0) @binding(1) var u_light: LightUniform;
  #| @group(0) @binding(2) var u_shadow: ShadowUniform;
  #| @group(0) @binding(3) var u_shadow_sampler: sampler_comparison;
  #| @group(0) @binding(4) var u_directional_shadow0: texture_depth_2d;
  #| @group(0) @binding(5) var u_directional_shadow1: texture_depth_2d;
  #| @group(0) @binding(6) var u_directional_shadow2: texture_depth_2d;
  #| @group(0) @binding(7) var u_directional_shadow3: texture_depth_2d;
  #| @group(0) @binding(8) var u_spot_shadow_atlas: texture_depth_2d;
  #| @group(0) @binding(9) var u_point_shadow_atlas: texture_depth_2d;
  #| struct VSIn {
  #|   @location(0) pos: vec3,
  #|   @location(1) normal: vec3,
  #|   @location(2) color: vec4,
  #|   @location(3) emissive: vec3,
  #|   @location(4) material: vec4,
  #| };
  #| struct VSOut {
  #|   @builtin(position) pos: vec4,
  #|   @location(0) world_pos: vec3,
  #|   @location(1) normal: vec3,
  #|   @location(2) color: vec4,
  #|   @location(3) emissive: vec3,
  #|   @location(4) material: vec4,
  #| };
  #| @vertex fn vs_main(v: VSIn) -> VSOut {
  #|   var o: VSOut;
  #|   o.pos = u_camera.view_proj * vec4(v.pos, 1.0);
  #|   o.world_pos = v.pos;
  #|   o.normal = normalize(v.normal);
  #|   o.color = v.color;
  #|   o.emissive = v.emissive;
  #|   o.material = v.material;
  #|   return o;
  #| }
  #| ${shadowSamplingWgsl}
  #| fn apply_lighting(world_pos: vec3, normal: vec3, receive_shadows: bool) -> vec3 {
  #|   let n = normalize(normal);
  #|   var lit = u_light.ambient_directional_count.xyz;
  #|   let directional_count = clamp(i32(round(u_light.ambient_directional_count.w)), 0, 4);
  #|   for (var idx = 0; idx < 4; idx = idx + 1) {
  #|     if (idx >= directional_count) { continue; }
  #|     let base = idx * 2;
  #|     let direction_intensity = u_light.directional_data[base];
  #|     let color_data = u_light.directional_data[base + 1];
  #|     let light_dir = normalize(-direction_intensity.xyz);
  #|     let lambert = max(dot(n, light_dir), 0.0);
  #|     let shadow = sample_directional_shadow(idx, world_pos, normal, light_dir, receive_shadows);
  #|     lit = lit + color_data.xyz * direction_intensity.w * lambert * shadow;
  #|   }
  #|   let point_count = clamp(i32(round(u_light.point_spot_count.x)), 0, 8);
  #|   for (var idx = 0; idx < 8; idx = idx + 1) {
  #|     if (idx >= point_count) { continue; }
  #|     let base = idx * 2;
  #|     let position_intensity = u_light.point_data[base];
  #|     let color_range = u_light.point_data[base + 1];
  #|     let offset = position_intensity.xyz - world_pos;
  #|     let distance = length(offset);
  #|     if (distance <= 0.0001) { continue; }
  #|     let light_dir = offset / distance;
  #|     let lambert = max(dot(n, light_dir), 0.0);
  #|     let attenuation = point_light_attenuation(distance, color_range.w) *
  #|       sample_point_shadow(idx, world_pos, normal, light_dir, world_pos - position_intensity.xyz, receive_shadows);
  #|     lit = lit + color_range.xyz * position_intensity.w * lambert * attenuation;
  #|   }
  #|   let spot_count = clamp(i32(round(u_light.point_spot_count.y)), 0, 4);
  #|   for (var idx = 0; idx < 4; idx = idx + 1) {
  #|     if (idx >= spot_count) { continue; }
  #|     let base = idx * 4;
  #|     let position_intensity = u_light.spot_data[base];
  #|     let color_range = u_light.spot_data[base + 1];
  #|     let direction_inner = u_light.spot_data[base + 2];
  #|     let outer_cos = u_light.spot_data[base + 3].x;
  #|     let offset = position_intensity.xyz - world_pos;
  #|     let distance = length(offset);
  #|     if (distance <= 0.0001) { continue; }
  #|     let light_dir = offset / distance;
  #|     let to_fragment = -light_dir;
  #|     let spot_direction = normalize(direction_inner.xyz);
  #|     let spot_cos = dot(spot_direction, to_fragment);
  #|     let inner_cos = direction_inner.w;
  #|     let spot_denom = max(inner_cos - outer_cos, 0.0001);
  #|     let spot_factor = saturate((spot_cos - outer_cos) / spot_denom);
  #|     if (spot_factor <= 0.0) { continue; }
  #|     let lambert = max(dot(n, light_dir), 0.0);
  #|     let attenuation = point_light_attenuation(distance, color_range.w) *
  #|       spot_factor * sample_spot_shadow(idx, world_pos, normal, light_dir, receive_shadows);
  #|     lit = lit + color_range.xyz * position_intensity.w * lambert * attenuation;
  #|   }
  #|   return lit;
  #| }
  #| @fragment fn fs_main(v: VSOut) -> @location(0) vec4 {
  #|   var albedo = v.color;
  #|   let alpha_mode = i32(round(v.material.x));
  #|   let alpha_cutoff = v.material.y;
  #|   let unlit = v.material.z >= 0.5;
  #|   let receive_shadows = v.material.w >= 0.5;
  #|   if (alpha_mode == 0) {
  #|     albedo.a = 1.0;
  #|   } else if (alpha_mode == 1) {
  #|     if (albedo.a < alpha_cutoff) { discard; }
  #|     albedo.a = 1.0;
  #|   } else if (albedo.a <= 0.0001) {
  #|     discard;
  #|   }
  #|   if (unlit) { return albedo; }
  #|   let lit = apply_lighting(v.world_pos, v.normal, receive_shadows);
  #|   return vec4(albedo.rgb * lit + v.emissive, albedo.a);
  #| }
  #| `,
  #|       });
  #|       const shader3dLitTex = rt.device.createShaderModule({
  #|         code: `
  #| struct CameraUniform { view_proj: mat4x4, };
  #| ${lightShadowStructsWgsl}
  #| @group(0) @binding(0) var u_camera: CameraUniform;
  #| @group(0) @binding(1) var u_light: LightUniform;
  #| @group(0) @binding(2) var u_shadow: ShadowUniform;
  #| @group(0) @binding(3) var u_shadow_sampler: sampler_comparison;
  #| @group(0) @binding(4) var u_directional_shadow0: texture_depth_2d;
  #| @group(0) @binding(5) var u_directional_shadow1: texture_depth_2d;
  #| @group(0) @binding(6) var u_directional_shadow2: texture_depth_2d;
  #| @group(0) @binding(7) var u_directional_shadow3: texture_depth_2d;
  #| @group(0) @binding(8) var u_spot_shadow_atlas: texture_depth_2d;
  #| @group(0) @binding(9) var u_point_shadow_atlas: texture_depth_2d;
  #| @group(0) @binding(10) var u_base_sampler: sampler;
  #| @group(0) @binding(11) var u_texture: texture_2d;
  #| @group(0) @binding(12) var u_emissive_sampler: sampler;
  #| @group(0) @binding(13) var u_emissive_texture: texture_2d;
  #| @group(0) @binding(14) var u_metallic_roughness_sampler: sampler;
  #| @group(0) @binding(15) var u_metallic_roughness_texture: texture_2d;
  #| @group(0) @binding(16) var u_occlusion_sampler: sampler;
  #| @group(0) @binding(17) var u_occlusion_texture: texture_2d;
  #| @group(0) @binding(18) var u_normal_sampler: sampler;
  #| @group(0) @binding(19) var u_normal_texture: texture_2d;
  #| struct VSIn {
  #|   @location(0) pos: vec3,
  #|   @location(1) normal: vec3,
  #|   @location(2) base_uv: vec2,
  #|   @location(3) emissive_uv: vec2,
  #|   @location(4) metallic_roughness_uv: vec2,
  #|   @location(5) occlusion_uv: vec2,
  #|   @location(6) normal_uv: vec2,
  #|   @location(7) color: vec4,
  #|   @location(8) emissive: vec3,
  #|   @location(9) tangent: vec4,
  #|   @location(10) material: vec4,
  #|   @location(11) surface: vec4,
  #| };
  #| struct VSOut {
  #|   @builtin(position) pos: vec4,
  #|   @location(0) world_pos: vec3,
  #|   @location(1) normal: vec3,
  #|   @location(2) base_uv: vec2,
  #|   @location(3) emissive_uv: vec2,
  #|   @location(4) metallic_roughness_uv: vec2,
  #|   @location(5) occlusion_uv: vec2,
  #|   @location(6) normal_uv: vec2,
  #|   @location(7) color: vec4,
  #|   @location(8) emissive: vec3,
  #|   @location(9) tangent: vec4,
  #|   @location(10) material: vec4,
  #|   @location(11) surface: vec4,
  #| };
  #| @vertex fn vs_main(v: VSIn) -> VSOut {
  #|   var o: VSOut;
  #|   o.pos = u_camera.view_proj * vec4(v.pos, 1.0);
  #|   o.world_pos = v.pos;
  #|   o.normal = normalize(v.normal);
  #|   o.base_uv = v.base_uv;
  #|   o.emissive_uv = v.emissive_uv;
  #|   o.metallic_roughness_uv = v.metallic_roughness_uv;
  #|   o.occlusion_uv = v.occlusion_uv;
  #|   o.normal_uv = v.normal_uv;
  #|   o.color = v.color;
  #|   o.emissive = v.emissive;
  #|   o.tangent = v.tangent;
  #|   o.material = v.material;
  #|   o.surface = v.surface;
  #|   return o;
  #| }
  #| ${shadowSamplingWgsl}
  #| fn apply_lighting(world_pos: vec3, shading_normal: vec3, geometric_normal: vec3, view_dir: vec3, roughness: f32, metallic: f32, receive_shadows: bool) -> vec3 {
  #|   let n = normalize(shading_normal);
  #|   var lit = u_light.ambient_directional_count.xyz;
  #|   var specular = vec3(0.0);
  #|   let shininess = mix(96.0, 8.0, clamp(roughness, 0.0, 1.0));
  #|   let specular_strength = mix(0.04, 1.0, clamp(metallic, 0.0, 1.0));
  #|   let directional_count = clamp(i32(round(u_light.ambient_directional_count.w)), 0, 4);
  #|   for (var idx = 0; idx < 4; idx = idx + 1) {
  #|     if (idx >= directional_count) { continue; }
  #|     let base = idx * 2;
  #|     let direction_intensity = u_light.directional_data[base];
  #|     let color_data = u_light.directional_data[base + 1];
  #|     let light_dir = normalize(-direction_intensity.xyz);
  #|     let lambert = max(dot(n, light_dir), 0.0);
  #|     let shadow = sample_directional_shadow(idx, world_pos, geometric_normal, light_dir, receive_shadows);
  #|     lit = lit + color_data.xyz * direction_intensity.w * lambert * shadow;
  #|     if (lambert > 0.0) {
  #|       let halfway = safe_normalize(light_dir + view_dir, view_dir);
  #|       let spec = pow(max(dot(n, halfway), 0.0), shininess) * specular_strength;
  #|       specular = specular + color_data.xyz * direction_intensity.w * spec * shadow;
  #|     }
  #|   }
  #|   let point_count = clamp(i32(round(u_light.point_spot_count.x)), 0, 8);
  #|   for (var idx = 0; idx < 8; idx = idx + 1) {
  #|     if (idx >= point_count) { continue; }
  #|     let base = idx * 2;
  #|     let position_intensity = u_light.point_data[base];
  #|     let color_range = u_light.point_data[base + 1];
  #|     let offset = position_intensity.xyz - world_pos;
  #|     let distance = length(offset);
  #|     if (distance <= 0.0001) { continue; }
  #|     let light_dir = offset / distance;
  #|     let lambert = max(dot(n, light_dir), 0.0);
  #|     let shadow = sample_point_shadow(idx, world_pos, geometric_normal, light_dir, world_pos - position_intensity.xyz, receive_shadows);
  #|     let attenuation = point_light_attenuation(distance, color_range.w) * shadow;
  #|     lit = lit + color_range.xyz * position_intensity.w * lambert * attenuation;
  #|     if (lambert > 0.0 && attenuation > 0.0) {
  #|       let halfway = safe_normalize(light_dir + view_dir, view_dir);
  #|       let spec = pow(max(dot(n, halfway), 0.0), shininess) * specular_strength;
  #|       specular = specular + color_range.xyz * position_intensity.w * spec * attenuation;
  #|     }
  #|   }
  #|   let spot_count = clamp(i32(round(u_light.point_spot_count.y)), 0, 4);
  #|   for (var idx = 0; idx < 4; idx = idx + 1) {
  #|     if (idx >= spot_count) { continue; }
  #|     let base = idx * 4;
  #|     let position_intensity = u_light.spot_data[base];
  #|     let color_range = u_light.spot_data[base + 1];
  #|     let direction_inner = u_light.spot_data[base + 2];
  #|     let outer_cos = u_light.spot_data[base + 3].x;
  #|     let offset = position_intensity.xyz - world_pos;
  #|     let distance = length(offset);
  #|     if (distance <= 0.0001) { continue; }
  #|     let light_dir = offset / distance;
  #|     let to_fragment = -light_dir;
  #|     let spot_direction = normalize(direction_inner.xyz);
  #|     let spot_cos = dot(spot_direction, to_fragment);
  #|     let inner_cos = direction_inner.w;
  #|     let spot_denom = max(inner_cos - outer_cos, 0.0001);
  #|     let spot_factor = saturate((spot_cos - outer_cos) / spot_denom);
  #|     if (spot_factor <= 0.0) { continue; }
  #|     let lambert = max(dot(n, light_dir), 0.0);
  #|     let shadow = sample_spot_shadow(idx, world_pos, geometric_normal, light_dir, receive_shadows);
  #|     let attenuation = point_light_attenuation(distance, color_range.w) * spot_factor * shadow;
  #|     lit = lit + color_range.xyz * position_intensity.w * lambert * attenuation;
  #|     if (lambert > 0.0 && attenuation > 0.0) {
  #|       let halfway = safe_normalize(light_dir + view_dir, view_dir);
  #|       let spec = pow(max(dot(n, halfway), 0.0), shininess) * specular_strength;
  #|       specular = specular + color_range.xyz * position_intensity.w * spec * attenuation;
  #|     }
  #|   }
  #|   return lit + specular;
  #| }
  #| @fragment fn fs_main(v: VSOut) -> @location(0) vec4 {
  #|   var albedo = textureSample(u_texture, u_base_sampler, v.base_uv) * v.color;
  #|   let emissive_sample = textureSample(u_emissive_texture, u_emissive_sampler, v.emissive_uv).rgb;
  #|   let mr_sample = textureSample(u_metallic_roughness_texture, u_metallic_roughness_sampler, v.metallic_roughness_uv);
  #|   let occlusion_sample = textureSample(u_occlusion_texture, u_occlusion_sampler, v.occlusion_uv);
  #|   let normal_sample = textureSample(u_normal_texture, u_normal_sampler, v.normal_uv).xyz * 2.0 - vec3(1.0, 1.0, 1.0);
  #|   let alpha_mode = i32(round(v.material.x));
  #|   let alpha_cutoff = v.material.y;
  #|   let unlit = v.surface.z >= 0.5;
  #|   let receive_shadows = v.surface.w >= 0.5;
  #|   if (alpha_mode == 0) {
  #|     albedo.a = 1.0;
  #|   } else if (alpha_mode == 1) {
  #|     if (albedo.a < alpha_cutoff) { discard; }
  #|     albedo.a = 1.0;
  #|   } else if (albedo.a <= 0.0001) {
  #|     discard;
  #|   }
  #|   if (unlit) { return albedo; }
  #|   let tangent = safe_normalize(v.tangent.xyz - v.normal * dot(v.tangent.xyz, v.normal), vec3(1.0, 0.0, 0.0));
  #|   let bitangent = safe_normalize(cross(v.normal, tangent) * v.tangent.w, vec3(0.0, 1.0, 0.0));
  #|   let tangent_normal = normalize(vec3(normal_sample.xy * v.surface.y, normal_sample.z));
  #|   let geometric_normal = normalize(v.normal);
  #|   let mapped_normal = safe_normalize(
  #|     tangent * tangent_normal.x + bitangent * tangent_normal.y + v.normal * tangent_normal.z,
  #|     geometric_normal,
  #|   );
  #|   let metallic = clamp(v.material.z * mr_sample.b, 0.0, 1.0);
  #|   let roughness = clamp(v.material.w * mr_sample.g, 0.0, 1.0);
  #|   let view_dir = safe_normalize(u_light.view_position.xyz - v.world_pos, vec3(0.0, 0.0, 1.0));
  #|   let lit = apply_lighting(v.world_pos, mapped_normal, geometric_normal, view_dir, roughness, metallic, receive_shadows);
  #|   let occlusion = mix(1.0, occlusion_sample.r, clamp(v.surface.x, 0.0, 1.0));
  #|   let emissive = emissive_sample * v.emissive;
  #|   return vec4(albedo.rgb * lit * occlusion + emissive, albedo.a);
  #| }
  #| `,
  #|       });
  #|
  #|       rt.uniformBuffers = {
  #|         canvas2d: rt.device.createBuffer({ size: 16, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST }),
  #|         camera3d: rt.device.createBuffer({ size: 64, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST }),
  #|         light3d: rt.device.createBuffer({ size: 704, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST }),
  #|         shadowCamera3d: rt.device.createBuffer({ size: 64, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST }),
  #|         shadow3d: rt.device.createBuffer({ size: 5984, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST }),
  #|       };
  #|       rt.bindGroupLayouts = {
  #|         canvas2d: rt.device.createBindGroupLayout({
  #|           entries: [{ binding: 0, visibility: GPUShaderStage.VERTEX, buffer: {} }],
  #|         }),
  #|         tex2d: rt.device.createBindGroupLayout({
  #|           entries: [
  #|             { binding: 0, visibility: GPUShaderStage.VERTEX, buffer: {} },
  #|             { binding: 1, visibility: GPUShaderStage.FRAGMENT, sampler: { type: 'filtering' } },
  #|             { binding: 2, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: 'float' } },
  #|           ],
  #|         }),
  #|         camera3d: rt.device.createBindGroupLayout({
  #|           entries: [{ binding: 0, visibility: GPUShaderStage.VERTEX, buffer: {} }],
  #|         }),
  #|         shadow3d: rt.device.createBindGroupLayout({
  #|           entries: [{ binding: 0, visibility: GPUShaderStage.VERTEX, buffer: {} }],
  #|         }),
  #|         shadow3dTex: rt.device.createBindGroupLayout({
  #|           entries: [
  #|             { binding: 0, visibility: GPUShaderStage.VERTEX, buffer: {} },
  #|             { binding: 1, visibility: GPUShaderStage.FRAGMENT, sampler: { type: 'filtering' } },
  #|             { binding: 2, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: 'float' } },
  #|           ],
  #|         }),
  #|         lit3d: rt.device.createBindGroupLayout({
  #|           entries: [
  #|             { binding: 0, visibility: GPUShaderStage.VERTEX, buffer: {} },
  #|             { binding: 1, visibility: GPUShaderStage.FRAGMENT, buffer: {} },
  #|             { binding: 2, visibility: GPUShaderStage.FRAGMENT, buffer: {} },
  #|             { binding: 3, visibility: GPUShaderStage.FRAGMENT, sampler: { type: 'comparison' } },
  #|             { binding: 4, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: 'depth' } },
  #|             { binding: 5, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: 'depth' } },
  #|             { binding: 6, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: 'depth' } },
  #|             { binding: 7, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: 'depth' } },
  #|             { binding: 8, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: 'depth' } },
  #|             { binding: 9, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: 'depth' } },
  #|           ],
  #|         }),
  #|         lit3dTex: rt.device.createBindGroupLayout({
  #|           entries: [
  #|             { binding: 0, visibility: GPUShaderStage.VERTEX, buffer: {} },
  #|             { binding: 1, visibility: GPUShaderStage.FRAGMENT, buffer: {} },
  #|             { binding: 2, visibility: GPUShaderStage.FRAGMENT, buffer: {} },
  #|             { binding: 3, visibility: GPUShaderStage.FRAGMENT, sampler: { type: 'comparison' } },
  #|             { binding: 4, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: 'depth' } },
  #|             { binding: 5, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: 'depth' } },
  #|             { binding: 6, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: 'depth' } },
  #|             { binding: 7, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: 'depth' } },
  #|             { binding: 8, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: 'depth' } },
  #|             { binding: 9, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: 'depth' } },
  #|             { binding: 10, visibility: GPUShaderStage.FRAGMENT, sampler: { type: 'filtering' } },
  #|             { binding: 11, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: 'float' } },
  #|             { binding: 12, visibility: GPUShaderStage.FRAGMENT, sampler: { type: 'filtering' } },
  #|             { binding: 13, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: 'float' } },
  #|             { binding: 14, visibility: GPUShaderStage.FRAGMENT, sampler: { type: 'filtering' } },
  #|             { binding: 15, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: 'float' } },
  #|             { binding: 16, visibility: GPUShaderStage.FRAGMENT, sampler: { type: 'filtering' } },
  #|             { binding: 17, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: 'float' } },
  #|             { binding: 18, visibility: GPUShaderStage.FRAGMENT, sampler: { type: 'filtering' } },
  #|             { binding: 19, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: 'float' } },
  #|           ],
  #|         }),
  #|       };
  #|       rt.bindGroups = {
  #|         canvas2d: rt.device.createBindGroup({
  #|           layout: rt.bindGroupLayouts.canvas2d,
  #|           entries: [{ binding: 0, resource: { buffer: rt.uniformBuffers.canvas2d } }],
  #|         }),
  #|         camera3d: rt.device.createBindGroup({
  #|           layout: rt.bindGroupLayouts.camera3d,
  #|           entries: [{ binding: 0, resource: { buffer: rt.uniformBuffers.camera3d } }],
  #|         }),
  #|         shadow3d: rt.device.createBindGroup({
  #|           layout: rt.bindGroupLayouts.shadow3d,
  #|           entries: [{ binding: 0, resource: { buffer: rt.uniformBuffers.shadowCamera3d } }],
  #|         }),
  #|       };
  #|       const blendState = {
  #|         color: { srcFactor: 'src-alpha', dstFactor: 'one-minus-src-alpha', operation: 'add' },
  #|         alpha: { srcFactor: 'one', dstFactor: 'one-minus-src-alpha', operation: 'add' },
  #|       };
  #|
  #|       const layout2dColor = rt.device.createPipelineLayout({ bindGroupLayouts: [rt.bindGroupLayouts.canvas2d] });
  #|       const layout2dTex = rt.device.createPipelineLayout({ bindGroupLayouts: [rt.bindGroupLayouts.tex2d] });
  #|       const layout3d = rt.device.createPipelineLayout({ bindGroupLayouts: [rt.bindGroupLayouts.camera3d] });
  #|       const layoutShadow3d = rt.device.createPipelineLayout({ bindGroupLayouts: [rt.bindGroupLayouts.shadow3d] });
  #|       const layoutShadow3dTex = rt.device.createPipelineLayout({ bindGroupLayouts: [rt.bindGroupLayouts.shadow3dTex] });
  #|       const layoutLit3d = rt.device.createPipelineLayout({ bindGroupLayouts: [rt.bindGroupLayouts.lit3d] });
  #|       const layoutLit3dTex = rt.device.createPipelineLayout({ bindGroupLayouts: [rt.bindGroupLayouts.lit3dTex] });
  #|
  #|       const color2dVertex = {
  #|         arrayStride: 24,
  #|         attributes: [
  #|           { shaderLocation: 0, offset: 0, format: 'float32x2' },
  #|           { shaderLocation: 1, offset: 8, format: 'float32x4' },
  #|         ],
  #|       };
  #|       const tex2dVertex = {
  #|         arrayStride: 32,
  #|         attributes: [
  #|           { shaderLocation: 0, offset: 0, format: 'float32x2' },
  #|           { shaderLocation: 1, offset: 8, format: 'float32x2' },
  #|           { shaderLocation: 2, offset: 16, format: 'float32x4' },
  #|         ],
  #|       };
  #|       const color3dVertex = {
  #|         arrayStride: 28,
  #|         attributes: [
  #|           { shaderLocation: 0, offset: 0, format: 'float32x3' },
  #|           { shaderLocation: 1, offset: 12, format: 'float32x4' },
  #|         ],
  #|       };
  #|       const lit3dVertex = {
  #|         arrayStride: 68,
  #|         attributes: [
  #|           { shaderLocation: 0, offset: 0, format: 'float32x3' },
  #|           { shaderLocation: 1, offset: 12, format: 'float32x3' },
  #|           { shaderLocation: 2, offset: 24, format: 'float32x4' },
  #|           { shaderLocation: 3, offset: 40, format: 'float32x3' },
  #|           { shaderLocation: 4, offset: 52, format: 'float32x4' },
  #|         ],
  #|       };
  #|       const litTex3dVertex = {
  #|         arrayStride: 140,
  #|         attributes: [
  #|           { shaderLocation: 0, offset: 0, format: 'float32x3' },
  #|           { shaderLocation: 1, offset: 12, format: 'float32x3' },
  #|           { shaderLocation: 2, offset: 24, format: 'float32x2' },
  #|           { shaderLocation: 3, offset: 32, format: 'float32x2' },
  #|           { shaderLocation: 4, offset: 40, format: 'float32x2' },
  #|           { shaderLocation: 5, offset: 48, format: 'float32x2' },
  #|           { shaderLocation: 6, offset: 56, format: 'float32x2' },
  #|           { shaderLocation: 7, offset: 64, format: 'float32x4' },
  #|           { shaderLocation: 8, offset: 80, format: 'float32x3' },
  #|           { shaderLocation: 9, offset: 92, format: 'float32x4' },
  #|           { shaderLocation: 10, offset: 108, format: 'float32x4' },
  #|           { shaderLocation: 11, offset: 124, format: 'float32x4' },
  #|         ],
  #|       };
  #|
  #|       rt.pipelines = {
  #|         color2dTri: rt.device.createRenderPipeline({
  #|           layout: layout2dColor,
  #|           vertex: { module: shader2dColor, entryPoint: 'vs_main', buffers: [color2dVertex] },
  #|           fragment: { module: shader2dColor, entryPoint: 'fs_main', targets: [{ format: rt.format, blend: blendState }] },
  #|           primitive: { topology: 'triangle-list', cullMode: 'none' },
  #|         }),
  #|         color2dLine: rt.device.createRenderPipeline({
  #|           layout: layout2dColor,
  #|           vertex: { module: shader2dColor, entryPoint: 'vs_main', buffers: [color2dVertex] },
  #|           fragment: { module: shader2dColor, entryPoint: 'fs_main', targets: [{ format: rt.format, blend: blendState }] },
  #|           primitive: { topology: 'line-list', cullMode: 'none' },
  #|         }),
  #|         tex2dTri: rt.device.createRenderPipeline({
  #|           layout: layout2dTex,
  #|           vertex: { module: shader2dTex, entryPoint: 'vs_main', buffers: [tex2dVertex] },
  #|           fragment: { module: shader2dTex, entryPoint: 'fs_main', targets: [{ format: rt.format, blend: blendState }] },
  #|           primitive: { topology: 'triangle-list', cullMode: 'none' },
  #|         }),
  #|         color3dTri: rt.device.createRenderPipeline({
  #|           layout: layout3d,
  #|           vertex: { module: shader3dColor, entryPoint: 'vs_main', buffers: [color3dVertex] },
  #|           fragment: { module: shader3dColor, entryPoint: 'fs_main', targets: [{ format: rt.format, blend: blendState }] },
  #|           primitive: { topology: 'triangle-list', cullMode: 'none' },
  #|           depthStencil: { format: 'depth24plus', depthWriteEnabled: true, depthCompare: 'less' },
  #|         }),
  #|         color3dLine: rt.device.createRenderPipeline({
  #|           layout: layout3d,
  #|           vertex: { module: shader3dColor, entryPoint: 'vs_main', buffers: [color3dVertex] },
  #|           fragment: { module: shader3dColor, entryPoint: 'fs_main', targets: [{ format: rt.format, blend: blendState }] },
  #|           primitive: { topology: 'line-list', cullMode: 'none' },
  #|           depthStencil: { format: 'depth24plus', depthWriteEnabled: false, depthCompare: 'less' },
  #|         }),
  #|         shadow3dTri: rt.device.createRenderPipeline({
  #|           layout: layoutShadow3d,
  #|           vertex: { module: shader3dShadowSolid, entryPoint: 'vs_main', buffers: [color3dVertex] },
  #|           primitive: { topology: 'triangle-list', cullMode: 'none' },
  #|           depthStencil: { format: 'depth32float', depthWriteEnabled: true, depthCompare: 'less' },
  #|         }),
  #|         shadow3dLitTri: rt.device.createRenderPipeline({
  #|           layout: layoutShadow3d,
  #|           vertex: { module: shader3dShadowLit, entryPoint: 'vs_main', buffers: [lit3dVertex] },
  #|           fragment: { module: shader3dShadowLit, entryPoint: 'fs_main', targets: [] },
  #|           primitive: { topology: 'triangle-list', cullMode: 'none' },
  #|           depthStencil: { format: 'depth32float', depthWriteEnabled: true, depthCompare: 'less' },
  #|         }),
  #|         shadow3dTexTri: rt.device.createRenderPipeline({
  #|           layout: layoutShadow3dTex,
  #|           vertex: { module: shader3dShadowTex, entryPoint: 'vs_main', buffers: [litTex3dVertex] },
  #|           fragment: { module: shader3dShadowTex, entryPoint: 'fs_main', targets: [] },
  #|           primitive: { topology: 'triangle-list', cullMode: 'none' },
  #|           depthStencil: { format: 'depth32float', depthWriteEnabled: true, depthCompare: 'less' },
  #|         }),
  #|         lit3dTriSingle: rt.device.createRenderPipeline({
  #|           layout: layoutLit3d,
  #|           vertex: { module: shader3dLitColor, entryPoint: 'vs_main', buffers: [lit3dVertex] },
  #|           fragment: { module: shader3dLitColor, entryPoint: 'fs_main', targets: [{ format: rt.format, blend: blendState }] },
  #|           primitive: { topology: 'triangle-list', cullMode: 'back' },
  #|           depthStencil: { format: 'depth24plus', depthWriteEnabled: true, depthCompare: 'less' },
  #|         }),
  #|         lit3dTriDouble: rt.device.createRenderPipeline({
  #|           layout: layoutLit3d,
  #|           vertex: { module: shader3dLitColor, entryPoint: 'vs_main', buffers: [lit3dVertex] },
  #|           fragment: { module: shader3dLitColor, entryPoint: 'fs_main', targets: [{ format: rt.format, blend: blendState }] },
  #|           primitive: { topology: 'triangle-list', cullMode: 'none' },
  #|           depthStencil: { format: 'depth24plus', depthWriteEnabled: true, depthCompare: 'less' },
  #|         }),
  #|         lit3dTexTriSingle: rt.device.createRenderPipeline({
  #|           layout: layoutLit3dTex,
  #|           vertex: { module: shader3dLitTex, entryPoint: 'vs_main', buffers: [litTex3dVertex] },
  #|           fragment: { module: shader3dLitTex, entryPoint: 'fs_main', targets: [{ format: rt.format, blend: blendState }] },
  #|           primitive: { topology: 'triangle-list', cullMode: 'back' },
  #|           depthStencil: { format: 'depth24plus', depthWriteEnabled: true, depthCompare: 'less' },
  #|         }),
  #|         lit3dTexTriDouble: rt.device.createRenderPipeline({
  #|           layout: layoutLit3dTex,
  #|           vertex: { module: shader3dLitTex, entryPoint: 'vs_main', buffers: [litTex3dVertex] },
  #|           fragment: { module: shader3dLitTex, entryPoint: 'fs_main', targets: [{ format: rt.format, blend: blendState }] },
  #|           primitive: { topology: 'triangle-list', cullMode: 'none' },
  #|           depthStencil: { format: 'depth24plus', depthWriteEnabled: true, depthCompare: 'less' },
  #|         }),
  #|       };
  #|       rt.ensureDepth();
  #|       rt.ready = true;
  #|     };
  #|
  #|     g.__selene_webgpu_runtime = rt;
  #|   }
  #|   const rt = g.__selene_webgpu_runtime;
  #|   rt.canvas = canvas;
  #|   rt.imageSmooth = !!imageSmooth;
  #|   canvas.width = Math.max(1, Math.floor(width));
  #|   canvas.height = Math.max(1, Math.floor(height));
  #|   if (rt.ready && rt.context && rt.device && rt.format) {
  #|     rt.context.configure({ device: rt.device, format: rt.format, alphaMode: 'premultiplied' });
  #|     rt.ensureDepth();
  #|   }
  #|   if (!rt.initPromise) {
  #|     rt.initPromise = rt.init().catch((err) => {
  #|       console.error('[selene-webgpu] init failed:', err);
  #|     });
  #|   }
  #| }

///|
extern "js" fn webgpu_begin_frame(
  clear_r : Double,
  clear_g : Double,
  clear_b : Double,
  clear_a : Double,
) -> Unit =
  #| (r, g, b, a) => {
  #|   const rt = globalThis.__selene_webgpu_runtime;
  #|   if (rt) {
  #|     rt.beginFrame(r, g, b, a);
  #|   }
  #| }

///|
extern "js" fn webgpu_end_frame() -> Unit =
  #| () => {
  #|   const rt = globalThis.__selene_webgpu_runtime;
  #|   if (rt) {
  #|     rt.endFrame();
  #|   }
  #| }

///|
extern "js" fn webgpu_preload_img(path : String) -> Unit =
  #| (path) => {
  #|   const rt = globalThis.__selene_webgpu_runtime;
  #|   if (rt) {
  #|     rt.ensureImage(path);
  #|   }
  #| }

///|
extern "js" fn webgpu_draw_image(
  path : String,
  destination_x : Double,
  destination_y : Double,
  destination_width : Double,
  destination_height : Double,
  has_source : Bool,
  source_x : Double,
  source_y : Double,
  source_width : Double,
  source_height : Double,
  transform_a : Double,
  transform_b : Double,
  transform_c : Double,
  transform_d : Double,
  transform_tx : Double,
  transform_ty : Double,
  repeat_mode : Int,
  tint_r : Double,
  tint_g : Double,
  tint_b : Double,
  tint_a : Double,
) -> Unit =
  #| (path, dx, dy, dw, dh, hasSource, sx, sy, sw, sh, a, b, c, d, tx, ty, repeatMode, tr, tg, tb, ta) => {
  #|   const rt = globalThis.__selene_webgpu_runtime;
  #|   if (rt) {
  #|     rt.pushImage(path, dx, dy, dw, dh, hasSource, sx, sy, sw, sh, a, b, c, d, tx, ty, repeatMode, tr, tg, tb, ta);
  #|   }
  #| }

///|
extern "js" fn webgpu_draw_text(
  text : String,
  x : Double,
  y : Double,
  transform_a : Double,
  transform_b : Double,
  transform_c : Double,
  transform_d : Double,
  transform_tx : Double,
  transform_ty : Double,
  family : String,
  size : Double,
  align : Int,
  baseline : Int,
  color_r : Double,
  color_g : Double,
  color_b : Double,
  color_a : Double,
) -> Unit =
  #| (text, x, y, a, b, c, d, tx, ty, family, size, align, baseline, r, g, bcol, alpha) => {
  #|   const rt = globalThis.__selene_webgpu_runtime;
  #|   if (rt) {
  #|     rt.pushText(text, x, y, a, b, c, d, tx, ty, family, size, align, baseline, r, g, bcol, alpha);
  #|   }
  #| }

///|
extern "js" fn webgpu_upload_text_texture(
  key : String,
  width : Int,
  height : Int,
  pixels : Array[Int],
) -> Bool =
  #| (key, width, height, pixels) => {
  #|   const rt = globalThis.__selene_webgpu_runtime;
  #|   if (!rt) return false;
  #|   try {
  #|     return !!rt.uploadTextTexture?.(key, width, height, pixels);
  #|   } catch (_err) {
  #|     return false;
  #|   }
  #| }

///|
extern "js" fn webgpu_draw_cached_text(
  key : String,
  x : Double,
  y : Double,
  transform_a : Double,
  transform_b : Double,
  transform_c : Double,
  transform_d : Double,
  transform_tx : Double,
  transform_ty : Double,
  align : Int,
  baseline : Int,
) -> Unit =
  #| (key, x, y, a, b, c, d, tx, ty, align, baseline) => {
  #|   const rt = globalThis.__selene_webgpu_runtime;
  #|   if (rt) {
  #|     rt.pushCachedText?.(key, x, y, a, b, c, d, tx, ty, align, baseline);
  #|   }
  #| }

///|
extern "js" fn webgpu_draw_rect(
  x : Double,
  y : Double,
  width : Double,
  height : Double,
  transform_a : Double,
  transform_b : Double,
  transform_c : Double,
  transform_d : Double,
  transform_tx : Double,
  transform_ty : Double,
  fill_r : Double,
  fill_g : Double,
  fill_b : Double,
  fill_a : Double,
  has_stroke : Bool,
  stroke_r : Double,
  stroke_g : Double,
  stroke_b : Double,
  stroke_a : Double,
) -> Unit =
  #| (x, y, w, h, a, b, c, d, tx, ty, fr, fg, fb, fa, hasStroke, sr, sg, sb, sa) => {
  #|   const rt = globalThis.__selene_webgpu_runtime;
  #|   if (rt) {
  #|     rt.pushRect(x, y, w, h, a, b, c, d, tx, ty, fr, fg, fb, fa, hasStroke, sr, sg, sb, sa);
  #|   }
  #| }

///|
extern "js" fn webgpu_draw_circle(
  center_x : Double,
  center_y : Double,
  radius : Double,
  transform_a : Double,
  transform_b : Double,
  transform_c : Double,
  transform_d : Double,
  transform_tx : Double,
  transform_ty : Double,
  fill_r : Double,
  fill_g : Double,
  fill_b : Double,
  fill_a : Double,
  has_stroke : Bool,
  stroke_r : Double,
  stroke_g : Double,
  stroke_b : Double,
  stroke_a : Double,
) -> Unit =
  #| (cx, cy, radius, a, b, c, d, tx, ty, fr, fg, fb, fa, hasStroke, sr, sg, sb, sa) => {
  #|   const rt = globalThis.__selene_webgpu_runtime;
  #|   if (rt) {
  #|     rt.pushCircle(cx, cy, radius, a, b, c, d, tx, ty, fr, fg, fb, fa, hasStroke, sr, sg, sb, sa);
  #|   }
  #| }

///|
extern "js" fn webgpu_draw_gradient_rect(
  x : Double,
  y : Double,
  width : Double,
  height : Double,
  transform_a : Double,
  transform_b : Double,
  transform_c : Double,
  transform_d : Double,
  transform_tx : Double,
  transform_ty : Double,
  start_r : Double,
  start_g : Double,
  start_b : Double,
  start_a : Double,
  end_r : Double,
  end_g : Double,
  end_b : Double,
  end_a : Double,
) -> Unit =
  #| (x, y, w, h, a, b, c, d, tx, ty, sr, sg, sb, sa, er, eg, eb, ea) => {
  #|   const rt = globalThis.__selene_webgpu_runtime;
  #|   if (rt) {
  #|     rt.pushGradientRect(x, y, w, h, a, b, c, d, tx, ty, sr, sg, sb, sa, er, eg, eb, ea);
  #|   }
  #| }

///|
extern "js" fn webgpu_begin_3d(
  position_x : Double,
  position_y : Double,
  position_z : Double,
  target_x : Double,
  target_y : Double,
  target_z : Double,
  up_x : Double,
  up_y : Double,
  up_z : Double,
  fovy : Double,
  near_z : Double,
  far_z : Double,
  orthographic : Double,
  orthographic_width : Double,
  orthographic_height : Double,
) -> Unit =
  #| (px, py, pz, tx, ty, tz, ux, uy, uz, fovy, nearZ, farZ, orthographic, orthoWidth, orthoHeight) => {
  #|   const rt = globalThis.__selene_webgpu_runtime;
  #|   if (rt) {
  #|     rt.begin3d(px, py, pz, tx, ty, tz, ux, uy, uz, fovy, nearZ, farZ, orthographic, orthoWidth, orthoHeight);
  #|   }
  #| }

///|
extern "js" fn webgpu_end_3d() -> Unit =
  #| () => {
  #|   const rt = globalThis.__selene_webgpu_runtime;
  #|   if (rt) {
  #|     rt.end3d();
  #|   }
  #| }

///|
extern "js" fn webgpu_set_lighting_3d(
  directional_data : Array[Double],
  point_data : Array[Double],
  spot_data : Array[Double],
  ambient_r : Double,
  ambient_g : Double,
  ambient_b : Double,
  directional_shadow_map_size : Double,
  point_shadow_map_size : Double,
) -> Unit =
  #| (directionalData, pointData, spotData, ar, ag, ab, directionalShadowMapSize, pointShadowMapSize) => {
  #|   const rt = globalThis.__selene_webgpu_runtime;
  #|   if (!rt) return;
  #|   const clamp01 = (x) => Math.max(0, Math.min(1, Number.isFinite(x) ? x : 0));
  #|   const clampSigned = (x) => Math.max(-1, Math.min(1, Number.isFinite(x) ? x : 0));
  #|   const safeDirectional = Array.isArray(directionalData) ? directionalData : [];
  #|   const safePoint = Array.isArray(pointData) ? pointData : [];
  #|   const safeSpot = Array.isArray(spotData) ? spotData : [];
  #|   const safeDirectionalShadowMapSize = Math.max(1, Math.round(Number(directionalShadowMapSize) || 2048));
  #|   const safePointShadowMapSize = Math.max(1, Math.round(Number(pointShadowMapSize) || 1024));
  #|   const directionalLights = [];
  #|   for (let i = 0; i + 16 < safeDirectional.length && directionalLights.length < 4; i += 17) {
  #|     let dx = Number(safeDirectional[i]) || 0;
  #|     let dy = Number(safeDirectional[i + 1]) || -1;
  #|     let dz = Number(safeDirectional[i + 2]) || 0;
  #|     const len = Math.hypot(dx, dy, dz);
  #|     if (len <= 1e-8) {
  #|       dx = 0;
  #|       dy = -1;
  #|       dz = 0;
  #|     } else {
  #|       dx /= len;
  #|       dy /= len;
  #|       dz /= len;
  #|     }
  #|     directionalLights.push({
  #|       direction: [dx, dy, dz],
  #|       intensity: Math.max(0, Number(safeDirectional[i + 3]) || 0),
  #|       color: [
  #|         clamp01(safeDirectional[i + 4]),
  #|         clamp01(safeDirectional[i + 5]),
  #|         clamp01(safeDirectional[i + 6]),
  #|       ],
  #|       shadows: Number(safeDirectional[i + 7]) >= 0.5,
  #|       depthBias: Math.max(0, Number(safeDirectional[i + 8]) || 0),
  #|       normalBias: Math.max(0, Number(safeDirectional[i + 9]) || 0),
  #|       cascadeConfig: (() => {
  #|         const cascadeCount = Math.max(1, Math.min(4, Math.round(Number(safeDirectional[i + 10]) || 1)));
  #|         const minimumDistance = Math.max(0, Number(safeDirectional[i + 11]) || 0);
  #|         const overlapProportion = clamp01(safeDirectional[i + 12]);
  #|         const bounds = [];
  #|         let fallbackBound = minimumDistance + 1.0;
  #|         for (let boundIndex = 0; boundIndex < 4; boundIndex += 1) {
  #|           const rawBound = Number(safeDirectional[i + 13 + boundIndex]);
  #|           const bound = Number.isFinite(rawBound) ? rawBound : fallbackBound;
  #|           fallbackBound = bound;
  #|           if (boundIndex < cascadeCount) {
  #|             bounds.push(bound);
  #|           }
  #|         }
  #|         return { minimumDistance, overlapProportion, bounds };
  #|       })(),
  #|     });
  #|   }
  #|   const pointLights = [];
  #|   for (let i = 0; i + 11 < safePoint.length && pointLights.length < 8; i += 12) {
  #|     pointLights.push({
  #|       position: [
  #|         Number(safePoint[i]) || 0,
  #|         Number(safePoint[i + 1]) || 0,
  #|         Number(safePoint[i + 2]) || 0,
  #|       ],
  #|       intensity: Math.max(0, Number(safePoint[i + 3]) || 0),
  #|       color: [
  #|         clamp01(safePoint[i + 4]),
  #|         clamp01(safePoint[i + 5]),
  #|         clamp01(safePoint[i + 6]),
  #|       ],
  #|       range: Math.max(0.0001, Number(safePoint[i + 7]) || 0.0001),
  #|       shadows: Number(safePoint[i + 8]) >= 0.5,
  #|       depthBias: Math.max(0, Number(safePoint[i + 9]) || 0),
  #|       normalBias: Math.max(0, Number(safePoint[i + 10]) || 0),
  #|       nearZ: Math.max(0.0001, Number(safePoint[i + 11]) || 0.0001),
  #|     });
  #|   }
  #|   const spotLights = [];
  #|   for (let i = 0; i + 16 < safeSpot.length && spotLights.length < 4; i += 17) {
  #|     let dx = Number(safeSpot[i + 8]) || 0;
  #|     let dy = Number(safeSpot[i + 9]) || -1;
  #|     let dz = Number(safeSpot[i + 10]) || 0;
  #|     const dlen = Math.hypot(dx, dy, dz);
  #|     if (dlen <= 1e-8) {
  #|       dx = 0;
  #|       dy = -1;
  #|       dz = 0;
  #|     } else {
  #|       dx /= dlen;
  #|       dy /= dlen;
  #|       dz /= dlen;
  #|     }
  #|     const innerCos = clampSigned(safeSpot[i + 11]);
  #|     const outerCos = clampSigned(safeSpot[i + 12]);
  #|     spotLights.push({
  #|       position: [
  #|         Number(safeSpot[i]) || 0,
  #|         Number(safeSpot[i + 1]) || 0,
  #|         Number(safeSpot[i + 2]) || 0,
  #|       ],
  #|       intensity: Math.max(0, Number(safeSpot[i + 3]) || 0),
  #|       color: [
  #|         clamp01(safeSpot[i + 4]),
  #|         clamp01(safeSpot[i + 5]),
  #|         clamp01(safeSpot[i + 6]),
  #|       ],
  #|       range: Math.max(0.0001, Number(safeSpot[i + 7]) || 0.0001),
  #|       direction: [dx, dy, dz],
  #|       innerCos,
  #|       outerCos,
  #|       innerAngle: Math.acos(innerCos),
  #|       outerAngle: Math.acos(outerCos),
  #|       shadows: Number(safeSpot[i + 13]) >= 0.5,
  #|       depthBias: Math.max(0, Number(safeSpot[i + 14]) || 0),
  #|       normalBias: Math.max(0, Number(safeSpot[i + 15]) || 0),
  #|       nearZ: Math.max(0.0001, Number(safeSpot[i + 16]) || 0.0001),
  #|     });
  #|   }
  #|   rt.light3d = {
  #|     directionalLights,
  #|     pointLights,
  #|     spotLights,
  #|     ambient: [
  #|       clamp01(ar),
  #|       clamp01(ag),
  #|       clamp01(ab),
  #|     ],
  #|     directionalShadowMapSize: safeDirectionalShadowMapSize,
  #|     pointShadowMapSize: safePointShadowMapSize,
  #|   };
  #| }

///|
extern "js" fn webgpu_draw_cube_3d(
  center_x : Double,
  center_y : Double,
  center_z : Double,
  size_x : Double,
  size_y : Double,
  size_z : Double,
  rotation_x : Double,
  rotation_y : Double,
  rotation_z : Double,
  rotation_w : Double,
  r : Double,
  g : Double,
  b : Double,
  a : Double,
  emissive_r : Double,
  emissive_g : Double,
  emissive_b : Double,
  alpha_mode : Double,
  alpha_cutoff : Double,
  unlit : Bool,
  cast_shadows : Bool,
  receive_shadows : Bool,
) -> Unit =
  #| (cx, cy, cz, sx, sy, sz, qx, qy, qz, qw, r, g, b, a, emissiveR, emissiveG, emissiveB, alphaMode, alphaCutoff, unlit, castShadows, receiveShadows) => {
  #|   const rt = globalThis.__selene_webgpu_runtime;
  #|   if (rt) {
  #|     rt.pushCube3d(cx, cy, cz, sx, sy, sz, qx, qy, qz, qw, r, g, b, a, emissiveR, emissiveG, emissiveB, alphaMode, alphaCutoff, unlit, castShadows, receiveShadows);
  #|   }
  #| }

///|
extern "js" fn webgpu_draw_sphere_3d(
  center_x : Double,
  center_y : Double,
  center_z : Double,
  radius : Double,
  rotation_x : Double,
  rotation_y : Double,
  rotation_z : Double,
  rotation_w : Double,
  r : Double,
  g : Double,
  b : Double,
  a : Double,
  emissive_r : Double,
  emissive_g : Double,
  emissive_b : Double,
  alpha_mode : Double,
  alpha_cutoff : Double,
  unlit : Bool,
  cast_shadows : Bool,
  receive_shadows : Bool,
) -> Unit =
  #| (cx, cy, cz, radius, qx, qy, qz, qw, r, g, b, a, emissiveR, emissiveG, emissiveB, alphaMode, alphaCutoff, unlit, castShadows, receiveShadows) => {
  #|   const rt = globalThis.__selene_webgpu_runtime;
  #|   if (rt) {
  #|     rt.pushSphere3d(cx, cy, cz, radius, qx, qy, qz, qw, r, g, b, a, emissiveR, emissiveG, emissiveB, alphaMode, alphaCutoff, unlit, castShadows, receiveShadows);
  #|   }
  #| }

///|
extern "js" fn webgpu_draw_cylinder_3d(
  center_x : Double,
  center_y : Double,
  center_z : Double,
  radius_top : Double,
  radius_bottom : Double,
  height : Double,
  slices : Int,
  rotation_x : Double,
  rotation_y : Double,
  rotation_z : Double,
  rotation_w : Double,
  r : Double,
  g : Double,
  b : Double,
  a : Double,
  emissive_r : Double,
  emissive_g : Double,
  emissive_b : Double,
  alpha_mode : Double,
  alpha_cutoff : Double,
  unlit : Bool,
  cast_shadows : Bool,
  receive_shadows : Bool,
) -> Unit =
  #| (cx, cy, cz, rtTop, rtBottom, h, slices, qx, qy, qz, qw, r, g, b, a, emissiveR, emissiveG, emissiveB, alphaMode, alphaCutoff, unlit, castShadows, receiveShadows) => {
  #|   const rt = globalThis.__selene_webgpu_runtime;
  #|   if (rt) {
  #|     rt.pushCylinder3d(cx, cy, cz, rtTop, rtBottom, h, slices, qx, qy, qz, qw, r, g, b, a, emissiveR, emissiveG, emissiveB, alphaMode, alphaCutoff, unlit, castShadows, receiveShadows);
  #|   }
  #| }

///|
extern "js" fn webgpu_draw_colored_triangles_3d(
  vertices : Array[Double],
  translation_x : Double,
  translation_y : Double,
  translation_z : Double,
  rotation_x : Double,
  rotation_y : Double,
  rotation_z : Double,
  rotation_w : Double,
  double_sided : Bool,
  cast_shadows : Bool,
  receive_shadows : Bool,
) -> Unit =
  #| (vertices, tx, ty, tz, qx, qy, qz, qw, doubleSided, castShadows, receiveShadows) => {
  #|   const rt = globalThis.__selene_webgpu_runtime;
  #|   if (rt) {
  #|     rt.pushColoredTriangles3d(vertices, tx, ty, tz, qx, qy, qz, qw, doubleSided, castShadows, receiveShadows);
  #|   }
  #| }

///|
extern "js" fn webgpu_draw_textured_triangles_3d(
  base_path : String,
  emissive_path : String,
  metallic_roughness_path : String,
  occlusion_path : String,
  normal_path : String,
  base_sampler_code : Int,
  emissive_sampler_code : Int,
  metallic_roughness_sampler_code : Int,
  occlusion_sampler_code : Int,
  normal_sampler_code : Int,
  vertices : Array[Double],
  translation_x : Double,
  translation_y : Double,
  translation_z : Double,
  rotation_x : Double,
  rotation_y : Double,
  rotation_z : Double,
  rotation_w : Double,
  double_sided : Bool,
  cast_shadows : Bool,
  receive_shadows : Bool,
) -> Unit =
  #| (basePath, emissivePath, metallicRoughnessPath, occlusionPath, normalPath, baseSamplerCode, emissiveSamplerCode, metallicRoughnessSamplerCode, occlusionSamplerCode, normalSamplerCode, vertices, tx, ty, tz, qx, qy, qz, qw, doubleSided, castShadows, receiveShadows) => {
  #|   const rt = globalThis.__selene_webgpu_runtime;
  #|   if (rt) {
  #|     rt.pushTexturedTriangles3d(basePath, emissivePath, metallicRoughnessPath, occlusionPath, normalPath, baseSamplerCode, emissiveSamplerCode, metallicRoughnessSamplerCode, occlusionSamplerCode, normalSamplerCode, vertices, tx, ty, tz, qx, qy, qz, qw, doubleSided, castShadows, receiveShadows);
  #|   }
  #| }

///|
pub extern "js" fn Audio::new(path : String) -> Audio = "(path) => new Audio(path)"

///|
pub extern "js" fn Audio::play(self : Self) -> Unit = "(self) => self.play()"

///|
pub extern "js" fn Audio::pause(self : Self) -> Unit = "(self) => self.pause()"

///|
pub extern "js" fn Audio::set_volume(self : Self, volume : Double) -> Unit = "(self, volume) => self.volume = volume"

///|
pub extern "js" fn Audio::set_loop(self : Self, loop_ : Bool) -> Unit = "(self, loop_) => self.loop = loop_"

///|
pub extern "js" fn Audio::set_playback_rate(
  self : Self,
  playback_rate : Double,
) -> Unit = "(self, playback_rate) => self.playbackRate = playback_rate"

///|
pub extern "js" fn Audio::set_current_time(
  self : Self,
  current_time : Double,
) -> Unit = "(self, current_time) => self.currentTime = current_time"

///|
pub extern "js" fn Audio::is_paused(self : Self) -> Bool = "(self) => self.paused"

///|
pub extern "js" fn Audio::is_ended(self : Self) -> Bool = "(self) => self.ended"

///|
#external
type Audio