@godot-scene-web/canvas-effects 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
1
+ {"version":3,"file":"webgpu.mjs","names":["BUFFER_USAGE","INSTANCE_STRIDE","TRANSPARENT"],"sources":["../src/webgpu-pipeline.ts","../src/webgpu-readback.ts","../src/particles-webgpu.ts","../src/shader-webgpu.ts","../src/webgpu-pack-uniforms.ts","../src/webgpu-textures.ts","../src/webgpu.ts"],"sourcesContent":["// Shader-module compilation, pipeline creation and the per-cell uniform ring, ported from the S7\n// probe (`packages/perf-harness/src/scenarios/webgpu/renderer.ts`). The probe THREW on a bad\n// shader or a failed validation because an arm that renders nothing at a wonderful frame rate is a\n// lie in a perf table; the product instead returns `null`, latches `pipeline-error` and lets the\n// caller fall back to WebGL — but it keeps the probe's diagnostics, because WebGPU's own default\n// for a bad shader is a module that fails later with a message the console eats.\n\n/** Normative WebGPU flags, stated locally so this module needs no browser host. */\nexport const BUFFER_USAGE = { COPY_DST: 0x0008, UNIFORM: 0x0040 } as const;\nexport const SHADER_STAGE = { VERTEX: 0x1, FRAGMENT: 0x2 } as const;\n\n/**\n * Bytes per per-cell uniform slot. 256 is the maximum `minUniformBufferOffsetAlignment` any WebGPU\n * implementation may report, so a buffer laid out at this pitch is bindable with a dynamic offset\n * everywhere; the real limit is read off the device and only ever rounds DOWN from here.\n */\nexport const UNIFORM_SLOT_BYTES = 256;\n\nlet lastError: string | null = null;\n\n/** The first error message from the most recent failed compile/validation (with WGSL line/col when\n * the failure was a shader), for diagnostics. Null until something fails. */\nexport function lastPipelineError(): string | null {\n return lastError;\n}\n\nfunction fail(message: string, onError?: () => void): null {\n onError?.();\n lastError = message;\n // Surfaced once, like `compileProgram`'s link/compile warnings: the node then renders on WebGL,\n // and without this the only symptom is a binding that quietly took the fallback.\n console.warn(\"[gsw webgpu]\", message);\n return null;\n}\n\n/**\n * Compile a WGSL module, returning null when the source has an error-severity message.\n *\n * Reading `getCompilationInfo()` rather than waiting for pipeline creation is what makes the\n * message available at all: a module built from bad WGSL is a valid object that only fails later,\n * inside `createRenderPipeline`, with a generic validation error.\n */\nexport async function compileModule(\n device: GPUDevice,\n code: string,\n label: string,\n onError?: () => void,\n): Promise<GPUShaderModule | null> {\n let module: GPUShaderModule;\n try {\n module = device.createShaderModule({ code, label });\n } catch (error) {\n return fail(\n `${label} WGSL module creation failed — ${describe(error)}`,\n onError,\n );\n }\n try {\n const info = await module.getCompilationInfo();\n const errors = info.messages.filter((message) => message.type === \"error\");\n if (errors.length > 0) {\n const first = errors[0];\n return fail(\n `${label} WGSL failed to compile — ${first.message} (line ${first.lineNum}, col ${first.linePos}); ${errors.length} error(s) total`,\n onError,\n );\n }\n } catch (error) {\n return fail(\n `${label} WGSL compilation info failed — ${describe(error)}`,\n onError,\n );\n }\n return module;\n}\n\n/**\n * Create a render pipeline inside a validation error scope, so a layout/blend/vertex-buffer\n * mismatch comes back as a message and a null instead of as a surface that draws nothing.\n */\nexport async function createPipeline(\n device: GPUDevice,\n descriptor: GPURenderPipelineDescriptor,\n label: string,\n onError?: () => void,\n): Promise<GPURenderPipeline | null> {\n try {\n device.pushErrorScope(\"validation\");\n const pipeline = device.createRenderPipeline(descriptor);\n const error = await device.popErrorScope();\n if (error) {\n return fail(\n `${label} pipeline failed WebGPU validation — ${error.message}`,\n onError,\n );\n }\n return pipeline;\n } catch (error) {\n return fail(\n `${label} pipeline creation threw — ${describe(error)}`,\n onError,\n );\n }\n}\n\n/** A uniform bind-group layout with binding 0 as a DYNAMIC-offset uniform buffer — the shape every\n * ring in this package binds through. `minBindingSize` must not exceed the size the bind group\n * actually binds (the slot, not the buffer), so callers that build their own bind group pass the\n * same number to both. */\nexport function uniformBindGroupLayout(\n device: GPUDevice,\n visibility: number = SHADER_STAGE.VERTEX | SHADER_STAGE.FRAGMENT,\n minBindingSize?: number,\n label = \"gsw-uniform\",\n): GPUBindGroupLayout {\n return device.createBindGroupLayout({\n label,\n entries: [\n {\n binding: 0,\n visibility,\n buffer:\n minBindingSize === undefined\n ? { type: \"uniform\", hasDynamicOffset: true }\n : { type: \"uniform\", hasDynamicOffset: true, minBindingSize },\n },\n ],\n });\n}\n\n/** The per-cell uniform ring: one buffer holding N slots addressed by dynamic offset, so N cells\n * cost ONE buffer and one `writeBuffer` per frame instead of N of each. */\nexport interface UniformRing {\n buffer: GPUBuffer;\n layout: GPUBindGroupLayout;\n bindGroup: GPUBindGroup;\n /** Slot pitch in BYTES: the dynamic offset for cell i is `i * pitch`. */\n pitch: number;\n /** Slot size in BYTES — what the bind group binds, always ≤ `pitch`. */\n slotBytes: number;\n /** Slot pitch in FLOATS (`pitch / 4`): cell i's staging window starts at `i * stride`. */\n stride: number;\n /** CPU-side mirror of the whole ring, uploaded in one `writeBuffer`. */\n staging: Float32Array;\n destroy(): void;\n}\n\nexport interface UniformRingOptions {\n label?: string;\n /** Reuse a caller-owned layout (e.g. one shared with a pipeline layout) instead of making one. */\n layout?: GPUBindGroupLayout;\n visibility?: number;\n}\n\nexport function createUniformRing(\n device: GPUDevice,\n cells: number,\n slotFloats: number = UNIFORM_SLOT_BYTES / 4,\n options: UniformRingOptions = {},\n): UniformRing {\n const label = options.label ?? \"gsw-uniform-ring\";\n const count = Math.max(1, Math.floor(cells) || 0);\n const slotBytes = Math.max(4, Math.ceil(slotFloats) * 4);\n // The device's alignment is the FLOOR of what a dynamic offset may be; 256 already satisfies\n // every conformant implementation, but read it back rather than assume, and round up if a future\n // device ever reports more. A slot larger than the alignment widens the pitch too — the offset\n // must be aligned AND the slot must fit inside its own cell.\n const alignment = Math.max(\n UNIFORM_SLOT_BYTES,\n device.limits.minUniformBufferOffsetAlignment || UNIFORM_SLOT_BYTES,\n slotBytes,\n );\n const pitch = Math.ceil(alignment / UNIFORM_SLOT_BYTES) * UNIFORM_SLOT_BYTES;\n const buffer = device.createBuffer({\n label,\n size: pitch * count,\n usage: BUFFER_USAGE.UNIFORM | BUFFER_USAGE.COPY_DST,\n });\n const layout =\n options.layout ??\n uniformBindGroupLayout(device, options.visibility, slotBytes, label);\n const bindGroup = device.createBindGroup({\n label,\n layout,\n // `size` is the SLOT, not the buffer: a dynamic offset addresses one slot, and binding the\n // whole buffer would make every cell read cell 0.\n entries: [{ binding: 0, resource: { buffer, offset: 0, size: slotBytes } }],\n });\n return {\n buffer,\n layout,\n bindGroup,\n pitch,\n slotBytes,\n stride: pitch / 4,\n staging: new Float32Array((pitch / 4) * count),\n destroy() {\n buffer.destroy();\n },\n };\n}\n\nfunction describe(error: unknown): string {\n return error instanceof Error ? error.message : String(error);\n}\n\n/** TEST-ONLY: clear the last recorded pipeline/shader error. */\nexport function __resetPipelineErrorForTest(): void {\n lastError = null;\n}\n","// Reading WebGPU pixels back to the CPU.\n//\n// THE ONLY SANCTIONED WAY. The obvious alternatives do not work: `ctx2d.drawImage(webgpuCanvas)`\n// is blank under SwiftShader and pathologically slow on Android Chrome (S7 measured the blit-shaped\n// WebGPU arm at 23 Hz against 87 for direct presentation), and `toDataURL`/`toBlob` on a WebGPU\n// canvas reads the same presentation path. Headless Chrome never composites a WebGPU canvas at all,\n// so a canvas-sourced read there returns nothing regardless of the API used. Copying the TEXTURE —\n// `copyTextureToBuffer` + `mapAsync` — is the one path verified to work fully headless, and it is\n// what the WebGL↔WebGPU image-parity harness and any future surface-image-swap must use.\n\nconst BUFFER_USAGE = { MAP_READ: 0x0001, COPY_DST: 0x0008 } as const;\nconst MAP_MODE = { READ: 0x1 } as const;\n\nexport interface WebgpuReadbackDevice {\n readonly device: GPUDevice;\n}\n\n/** `copyTextureToBuffer` requires every row to start on a 256-byte boundary, which is why the read\n * is padded and then re-packed rather than mapped straight into the caller's hands. */\nconst BYTES_PER_ROW_ALIGNMENT = 256;\n\n/**\n * Read `width`×`height` RGBA bytes out of `texture`, tightly packed (`width * 4` bytes per row,\n * top-down, PREMULTIPLIED alpha as stored — every fragment on this backend emits `vec4f(rgb*a, a)`\n * under `PREMULTIPLIED_BLEND`, so the bytes in the texture are already multiplied through. A\n * consumer that needs straight alpha (an encode into a 2D canvas) converts: see\n * `./still-capture`).\n *\n * `texture` must have been created with COPY_SRC usage — a texture that will be read back has to\n * declare it at creation, and there is no way to add it afterwards.\n */\nexport async function readTexturePixels(\n shared: WebgpuReadbackDevice,\n texture: GPUTexture,\n width: number,\n height: number,\n): Promise<Uint8Array> {\n const { device } = shared;\n const w = Math.max(1, Math.floor(width));\n const h = Math.max(1, Math.floor(height));\n const bytesPerRow =\n Math.ceil((w * 4) / BYTES_PER_ROW_ALIGNMENT) * BYTES_PER_ROW_ALIGNMENT;\n const buffer = device.createBuffer({\n label: \"gsw-readback\",\n size: bytesPerRow * h,\n usage: BUFFER_USAGE.COPY_DST | BUFFER_USAGE.MAP_READ,\n });\n try {\n const encoder = device.createCommandEncoder({ label: \"gsw-readback\" });\n encoder.copyTextureToBuffer(\n { texture },\n { buffer, bytesPerRow, rowsPerImage: h },\n [w, h, 1],\n );\n device.queue.submit([encoder.finish()]);\n await buffer.mapAsync(MAP_MODE.READ);\n const padded = new Uint8Array(buffer.getMappedRange());\n const packed = new Uint8Array(w * h * 4);\n // Strip the row padding. The copy is per-row because only the first `w * 4` bytes of each\n // `bytesPerRow` stride hold pixels; the rest is whatever the alignment left behind.\n for (let row = 0; row < h; row++) {\n packed.set(\n padded.subarray(row * bytesPerRow, row * bytesPerRow + w * 4),\n row * w * 4,\n );\n }\n buffer.unmap();\n return packed;\n } finally {\n // The mapped range is invalidated by `destroy()`, so this runs only after `packed` is copied.\n buffer.destroy();\n }\n}\n","// The WebGPU particle backend: the peer of `./render-webgl`, rendering STRAIGHT into each node's own\n// canvas. There is no shared canvas and no blit — the pixels are produced where the compositor\n// already reads them, which is the whole measured point (docs/perf-harness.md S7: 47 → 87 Hz on the\n// phone, and a blit-shaped WebGPU arm that collapsed to 23).\n//\n// WHAT \"PORT\" MEANS HERE. The WGSL below is `render-webgl.ts`'s `VERTEX_SRC`/`FRAGMENT_SRC`\n// transcribed, not re-derived: same flipbook arithmetic, same polar remap, same PRE-LUT coverage\n// read, same erode-then-mask order, same `1 - smoothstep(0.7, 1.0, r)` dot, same additive\n// accumulate+resolve pair. Those orderings are load-bearing (each one is a bug this pipeline already\n// had once), so `webgpu-particles-wgsl.test.ts` asserts them as TEXT — jsdom has no WebGPU to run\n// them in, exactly as `FRAGMENT_SRC`'s own coverage tests work.\n//\n// PREMULTIPLIED OUTPUT IS NOT OPTIONAL. A `GPUCanvasContext` offers only\n// `alphaMode: \"opaque\" | \"premultiplied\"`, so the only arrangement that composites over the page is:\n// the fragment returns `vec4f(rgb * a, a)` AND the pipeline blends `one / one-minus-src-alpha` on\n// colour and alpha. The pairing is load-bearing in both directions and neither half errors on its\n// own (a premultiplied fragment under a src-alpha blend double-multiplies; a straight fragment under\n// this blend halos), which is why `PREMULTIPLIED_BLEND` sits next to the shader text and one test\n// asserts the two TOGETHER.\n//\n// `./render-webgl.ts` states the SAME contract — its shared canvas declares\n// `premultipliedAlpha: true`, its MIX fragment premultiplies, and `blendFactorsFor(0)` is this blend\n// in GL enum names. That is recent: the GL canvas used to declare itself straight-alpha, which made\n// this file's arrangement a translation rather than a restatement, and left the GL side free to\n// disagree with itself (it did — MIX composited at a²).\n//\n// ONE ENCODER PER TICK. `beginFrame` opens a single `GPUCommandEncoder`, every `draw`/`clear` records\n// passes into it, and `endFrame` submits it ONCE. That is the shape S7 measured; a per-draw submit\n// would make \"WebGPU\" mean \"N submits\" and give back the win.\n\nimport { INSTANCE_STRIDE, type InstanceBuffer } from \"@godot-scene-web/effects\";\nimport { frameGridFor } from \"@godot-scene-web/effects/particles\";\nimport {\n compileModule,\n createPipeline,\n createUniformRing,\n SHADER_STAGE,\n type UniformRing,\n uniformBindGroupLayout,\n} from \"./webgpu-pipeline\";\nimport { readTexturePixels } from \"./webgpu-readback\";\n\nconst PARTICLE_BUFFER_USAGE = { VERTEX: 0x0020, COPY_DST: 0x0008 } as const;\nconst PARTICLE_TEXTURE_USAGE = {\n COPY_SRC: 0x01,\n COPY_DST: 0x02,\n TEXTURE_BINDING: 0x04,\n RENDER_ATTACHMENT: 0x10,\n} as const;\n\n/** Resolved texture resource supplied by the host. This package never loads URLs or owns images. */\nexport interface WebgpuParticleTexture {\n readonly view: GPUTextureView;\n readonly sampler: GPUSampler;\n}\n\nexport interface WebgpuParticleDrawOptions {\n width: number;\n height: number;\n texture: unknown | null;\n textured: boolean;\n lutTexture: unknown | null;\n maskTexture: unknown | null;\n hframes: number;\n vframes: number;\n blendMode: number;\n alphaFromRed?: boolean;\n erode?: { threshold: number; softness: number } | null;\n uvPolar?: boolean;\n}\n\nexport interface WebgpuParticleSurface {\n readonly context: GPUCanvasContext;\n readonly textures: {\n sprite: WebgpuParticleTexture | null;\n lut: WebgpuParticleTexture | null;\n mask: WebgpuParticleTexture | null;\n };\n readonly onTexturesChanged?: (callback: () => void) => () => void;\n}\n\nexport interface WebgpuParticleRendererOptions {\n readonly device: GPUDevice;\n readonly format: GPUTextureFormat;\n readonly readTexturePixels?: (\n texture: GPUTexture,\n width: number,\n height: number,\n ) => Promise<Uint8Array>;\n readonly onPipelineError?: () => void;\n}\n\n/** Bytes per packed instance record: the shipped `INSTANCE_STRIDE` (10 floats) × 4. Stated as a\n * literal because it is what the vertex-buffer layout declares as its `arrayStride` and what every\n * attribute offset below is measured in. */\nexport const INSTANCE_STRIDE_BYTES = 40;\n\n// DRIFT GUARD, at module load. The attribute offsets below are hand-written byte positions into the\n// record `InstanceBuffer.push` writes; a change to `INSTANCE_STRIDE` that did not update them would\n// not fail to compile, it would draw garbage — every instance reading a sliding window of the\n// previous one's floats. Throwing here turns that into an immediate, named failure at import.\nif (INSTANCE_STRIDE * 4 !== INSTANCE_STRIDE_BYTES) {\n throw new Error(\n `gsw particle instance stride drift: INSTANCE_STRIDE (${INSTANCE_STRIDE}) * 4 !== ${INSTANCE_STRIDE_BYTES}. ` +\n \"PARTICLE_VERTEX_BUFFERS in particles/render-webgpu.ts declares byte offsets into the packed record \" +\n \"(center@0, scale@8, rotation@16, color@20, frame@36) and must be updated with it.\",\n );\n}\n\n/** Vertex entry point of `PARTICLE_WGSL`. Referenced by the pipeline descriptors, never spelled twice. */\nexport const PARTICLE_VS_ENTRY = \"vs_particles\";\n/** Fragment entry point for MIX (and every non-additive) blend: premultiplied colour out. */\nexport const PARTICLE_FS_ENTRY = \"fs_particles\";\n/** Fragment entry point for the ADDITIVE accumulate pass: raw light out, alpha 0. */\nexport const PARTICLE_FS_ADDITIVE_ENTRY = \"fs_particles_additive\";\n/** Vertex entry point of `ADDITIVE_RESOLVE_WGSL` (a full-target strip). */\nexport const RESOLVE_VS_ENTRY = \"vs_resolve\";\n/** Fragment entry point of `ADDITIVE_RESOLVE_WGSL`. */\nexport const RESOLVE_FS_ENTRY = \"fs_resolve\";\n\n/**\n * The blend state every non-additive draw uses, and the other half of the premultiply contract in\n * the module header.\n *\n * `one / one-minus-src-alpha` on colour AND alpha: the fragment already carries `rgb * a`, so the\n * source contributes its light unscaled and the destination is attenuated by the coverage the source\n * claims — `dst' = src.rgb*src.a + dst*(1-src.a)`, which is what `SRC_ALPHA / ONE_MINUS_SRC_ALPHA`\n * computes over a straight-alpha source (the GL path's blend). The alpha channel gets the same pair\n * so the canvas's own alpha accumulates the same way.\n */\nexport const PREMULTIPLIED_BLEND: GPUBlendState = {\n color: {\n operation: \"add\",\n srcFactor: \"one\",\n dstFactor: \"one-minus-src-alpha\",\n },\n alpha: {\n operation: \"add\",\n srcFactor: \"one\",\n dstFactor: \"one-minus-src-alpha\",\n },\n};\n\n/** The ACCUMULATE pass's blend: `ONE, ONE` on both channels, i.e. `render-webgl`'s\n * `blendFactorsFor(1)` — overlapping additive particles SUM their raw light (Godot\n * `BLEND_MODE_ADD`) instead of compositing over one another. */\nexport const ADDITIVE_BLEND: GPUBlendState = {\n color: { operation: \"add\", srcFactor: \"one\", dstFactor: \"one\" },\n alpha: { operation: \"add\", srcFactor: \"one\", dstFactor: \"one\" },\n};\n\n/**\n * The vertex buffer layout: slot 0 is the static unit-quad corner (stepped per VERTEX), slot 1 is the\n * packed instance record (stepped per INSTANCE).\n *\n * The attribute offsets are the shipped float offsets × 4 — center@0, scale@8, rotation@16,\n * color@20, frame@36 — i.e. exactly `INSTANCE_ATTRS` in `render-webgl.ts`, in bytes. Exported so a\n * test can read the stride back without building a device.\n */\nexport const PARTICLE_VERTEX_BUFFERS: GPUVertexBufferLayout[] = [\n {\n arrayStride: 8,\n stepMode: \"vertex\",\n attributes: [{ shaderLocation: 0, offset: 0, format: \"float32x2\" }],\n },\n {\n arrayStride: INSTANCE_STRIDE_BYTES,\n stepMode: \"instance\",\n attributes: [\n { shaderLocation: 1, offset: 0, format: \"float32x2\" }, // a_center, device px\n { shaderLocation: 2, offset: 8, format: \"float32x2\" }, // a_scale, device px\n { shaderLocation: 3, offset: 16, format: \"float32\" }, // a_rotation, rad\n { shaderLocation: 4, offset: 20, format: \"float32x4\" }, // a_color, straight\n { shaderLocation: 5, offset: 36, format: \"float32\" }, // a_frame (flipbook index)\n ],\n },\n];\n\n/** Floats in the per-surface uniform slot (`Params` below): 6 f32 then 6 u32, all 4 bytes. */\nconst PARAMS_FLOATS = 12;\nconst PARAMS_BYTES = PARAMS_FLOATS * 4;\n\n/**\n * The instanced particle module — `render-webgl.ts`'s two shader stages, transcribed.\n *\n * PER-SYSTEM VALUES ARE UNIFORMS, NOT PIPELINE VARIANTS. `textured`/`lut`/`mask`/`uvPolar` etc. select\n * branches at runtime out of ONE pipeline, exactly as the GL program's `u_textured`/`u_lut` do — the\n * alternative (a pipeline per feature combination) is 64 pipelines for a fragment whose branches are\n * uniform-valued and therefore free of divergence. Sampling inside those branches is legal for two\n * independent reasons: a branch on a uniform value is UNIFORM CONTROL FLOW (so even\n * derivative-taking `textureSample` would be allowed), and the samples below use\n * `textureSampleLevel(..., 0.0)`, which needs no derivatives at all. Level 0 is not an approximation\n * here: every texture this backend binds is created with a single mip level (see `../webgpu/textures`).\n */\nexport const PARTICLE_WGSL = `struct Params {\n viewport: vec2f, // canvas backing size, device px — REWRITTEN EVERY DRAW (canvases resize)\n grid: vec2f, // hframes, vframes\n erodeFactors: vec2f, // threshold, softness\n textured: u32,\n lut: u32,\n alphaFromRed: u32,\n erode: u32,\n mask: u32,\n uvPolar: u32,\n};\n\n@group(0) @binding(0) var<uniform> params: Params;\n@group(1) @binding(0) var sprite_tex: texture_2d<f32>;\n@group(1) @binding(1) var sprite_smp: sampler;\n@group(1) @binding(2) var lut_tex: texture_2d<f32>;\n@group(1) @binding(3) var lut_smp: sampler;\n@group(1) @binding(4) var mask_tex: texture_2d<f32>;\n@group(1) @binding(5) var mask_smp: sampler;\n\n// GLSL's \\`mod\\` is FLOOR-signed; WGSL's \\`%\\` is TRUNC-signed. The flipbook cell index and the polar\n// wrap are both written against GLSL semantics in the shader this ports, so re-derive them rather\n// than swap in an operator that agrees only for positive operands.\nfn godot_mod(x: f32, y: f32) -> f32 {\n return x - y * floor(x / y);\n}\n\nstruct VsOut {\n @builtin(position) pos: vec4f,\n @location(0) uv: vec2f,\n @location(1) quad: vec2f,\n @location(2) cell: vec2f,\n @location(3) color: vec4f,\n};\n\n@vertex\nfn ${PARTICLE_VS_ENTRY}(\n @location(0) corner: vec2f,\n @location(1) center: vec2f,\n @location(2) scale: vec2f,\n @location(3) rotation: f32,\n @location(4) color: vec4f,\n @location(5) frame: f32,\n) -> VsOut {\n let c = cos(rotation);\n let s = sin(rotation);\n let rotated = vec2f(corner.x * c - corner.y * s, corner.x * s + corner.y * c);\n let px = center + rotated * scale;\n var clip = (px / params.viewport) * 2.0 - 1.0;\n clip.y = -clip.y; // canvas Y-down -> clip Y-up\n var out: VsOut;\n out.pos = vec4f(clip, 0.0, 1.0);\n let uv01 = corner + 0.5; // 0..1 across the SPRITE quad\n let cell = vec2f(godot_mod(frame, params.grid.x), floor(frame / params.grid.x));\n out.uv = (cell + uv01) / params.grid;\n // The flipbook cell INDEX, so a fragment that re-derives its own sprite-local UV (the polar remap)\n // can map it back into the same cell instead of over the whole sheet.\n out.cell = cell;\n // Quad-local 0..1, INDEPENDENT of the flipbook grid: the untextured dot and the coverage mask are\n // both measured from this. Deriving them from the atlas-mapped uv put the dot's centre at the\n // SHEET's centre, so any grid > 1x1 left it off-centre and clipped to a sliver of one cell.\n out.quad = uv01;\n out.color = color;\n return out;\n}\n\nstruct Shaded {\n col: vec4f,\n coverage: f32,\n};\n\n// The whole fragment feature set, shared by both entry points below so the mix and additive paths\n// can never disagree about it (in GL they are one shader and a branch).\nfn shade(in: VsOut) -> Shaded {\n var uv = in.uv;\n if (params.uvPolar == 1u) {\n // Godot polar_coordinates(UV, vec2(0.5), 1, 1) (shaders/vfx/_util/polar_coordinates.gdshaderinc):\n // x = radius from the sprite centre (0..1.41 at the corners), y = the angle mapped to 0..1, both\n // wrapped — then RE-WRAPPED INTO THE SAME CELL through in.cell, so a flipbook stays a flipbook.\n // A radial sheet (common_ring_polar_a) is a RING only through this; sampled flat it is a\n // vertical BAR.\n let dir = in.quad - vec2f(0.5);\n let radius = length(dir) * 2.0;\n let angle = atan2(dir.y, dir.x) * (1.0 / (3.1416 * 2.0));\n uv = (in.cell + vec2f(godot_mod(radius, 1.0), godot_mod(angle, 1.0))) / params.grid;\n }\n var tex: vec4f;\n if (params.textured == 1u) {\n tex = textureSampleLevel(sprite_tex, sprite_smp, uv, 0.0);\n } else {\n // Soft round dot when the system has no texture — measured across the QUAD, not the atlas-mapped\n // uv, so it stays centred whatever the (meaningless, textureless) grid is.\n let r = length(in.quad - vec2f(0.5)) * 2.0;\n tex = vec4f(1.0, 1.0, 1.0, 1.0 - smoothstep(0.7, 1.0, r));\n }\n // COVERAGE — taken PRE-LUT, because the LUT is a colour lookup INDEXED by that same red channel:\n // reading it afterwards would sample the LUT's own (usually white) output instead of the sheet's\n // shape. Grayscale VFX sheets are alpha-less PNGs, so tex.a is 1.0 everywhere and the alpha branch\n // draws a SQUARE.\n var coverage = select(tex.a, tex.r, params.alphaFromRed == 1u);\n if (params.lut == 1u) {\n // Godot's VFX particle-shader family: COLOR = vec4(texture(lut, texture_color.rr).rgb, alpha) *\n // vertex_color. The sheet is a single-channel MASK, so its own RGB is meaningless; the LUT holds\n // the real colours. Sampled AFTER the texture/dot resolve so both branches are recoloured, and\n // the source ALPHA is preserved untouched — only RGB comes from the LUT.\n tex = vec4f(textureSampleLevel(lut_tex, lut_smp, vec2f(tex.r, 0.5), 0.0).rgb, tex.a);\n }\n if (params.erode == 1u) {\n // Godot erosion_from_factors(vec2(threshold, softness), coverage) — a CONSTANT erosion curve,\n // i.e. the threshold does not sweep over the particle's life. AFTER the LUT, BEFORE the mask.\n coverage = smoothstep(params.erodeFactors.x, params.erodeFactors.x + params.erodeFactors.y, coverage);\n }\n if (params.mask == 1u) {\n // Godot's mask sampler reads the sprite's own UV, NOT the flipbook cell — it shapes the whole quad.\n coverage = coverage * textureSampleLevel(mask_tex, mask_smp, in.quad, 0.0).r;\n }\n tex.a = coverage;\n var out: Shaded;\n out.col = tex * in.color;\n out.coverage = coverage;\n return out;\n}\n\n@fragment\nfn ${PARTICLE_FS_ENTRY}(in: VsOut) -> @location(0) vec4f {\n let col = shade(in).col;\n // PREMULTIPLIED — see the module header. Only correct under PREMULTIPLIED_BLEND.\n return vec4f(col.rgb * col.a, col.a);\n}\n\n@fragment\nfn ${PARTICLE_FS_ADDITIVE_ENTRY}(in: VsOut) -> @location(0) vec4f {\n // Additive sprites contribute light = colour x alpha (Godot BLEND_MODE_ADD adds src.rgb * src.a to\n // the framebuffer, so an opaque-black glow background or an alpha-shaped sprite's transparent area\n // both add nothing). Emit that light RAW and SUM it across particles (ADDITIVE_BLEND, into the\n // accumulator); the resolve pass converts the per-pixel TOTAL to coverage ONCE. Normalizing per\n // PARTICLE amplified every faint texel to full brightness and let overlaps clamp to white while\n // stacking coverage — a subtle 5-particle fog rendered as an opaque white haze wall.\n // ALPHA IS 0: the accumulator holds light, not coverage.\n let shaded = shade(in);\n return vec4f(shaded.col.rgb * shaded.coverage * in.color.a, 0.0);\n}\n`;\n\n/**\n * The additive RESOLVE pass: read the summed light out of the accumulator and present it.\n *\n * THE ALGEBRA. A premultiplied canvas wants the accumulated light itself: `(light, cov)` composites\n * to `light + dst*(1-cov)`, with no division at all — and therefore no `cov > 0` guard either, since\n * the division that would have needed one is gone (at cov = 0 the fragment is (0,0,0,0), which\n * composites to `dst` exactly). `RESOLVE_FRAGMENT_SRC` in `./render-webgl.ts` is now the same\n * expression, its canvas being premultiplied too; it used to divide by `cov` for a straight-alpha\n * canvas and rely on the blit into the node canvas to multiply it back.\n *\n * `textureLoad` at integer pixel coordinates, like GL's `texelFetch`: the accumulate pass renders\n * into the top-left w×h rect of a grow-only accumulator, and WebGPU framebuffer coordinates are\n * Y-DOWN in both passes, so texel (x, y) is fragment (x, y) with no flip arithmetic anywhere.\n */\nexport const ADDITIVE_RESOLVE_WGSL = `@group(0) @binding(0) var accum_tex: texture_2d<f32>;\n\n@vertex\nfn ${RESOLVE_VS_ENTRY}(@builtin(vertex_index) index: u32) -> @builtin(position) vec4f {\n // TRIANGLE_STRIP corner order, matching the particle quad's: (-1,-1) (1,-1) (-1,1) (1,1).\n var corners = array<vec2f, 4>(\n vec2f(-1.0, -1.0),\n vec2f(1.0, -1.0),\n vec2f(-1.0, 1.0),\n vec2f(1.0, 1.0)\n );\n return vec4f(corners[index], 0.0, 1.0);\n}\n\n@fragment\nfn ${RESOLVE_FS_ENTRY}(@builtin(position) pos: vec4f) -> @location(0) vec4f {\n let light = textureLoad(accum_tex, vec2i(pos.xy), 0).rgb;\n let cov = max(light.r, max(light.g, light.b));\n return vec4f(light, cov);\n}\n`;\n\n/** The accumulator's format. `rgba8unorm` rather than a float target ON PURPOSE: the GL path\n * accumulates into an RGBA/UNSIGNED_BYTE FBO, so its sums CLAMP at 1.0 per channel, and matching\n * that byte-clamping beats being more faithful than the renderer this must look identical to. */\nconst ACCUM_FORMAT: GPUTextureFormat = \"rgba8unorm\";\n\nconst TRANSPARENT: GPUColor = { r: 0, g: 0, b: 0, a: 0 };\n\n/** The grow-only light accumulator, one per DEVICE (the analogue of `ParticleProgram.accum`): every\n * additive system on the page borrows it inside its own pass pair, so N systems cost one texture. */\ninterface AccumTarget {\n texture: GPUTexture;\n view: GPUTextureView;\n bindGroup: GPUBindGroup;\n width: number;\n height: number;\n}\n\n/** Per-surface GPU state, supplied with a presentation context and resolved handles by the host. */\nexport interface WebgpuParticleSurfaceState extends WebgpuParticleSurface {\n ring: UniformRing;\n words: Uint32Array;\n instances: GPUBuffer | null;\n instanceBytes: number;\n bindGroup: GPUBindGroup | null;\n boundViews: [unknown, unknown, unknown];\n bindGroupDirty: boolean;\n disposeTextureListener: (() => void) | null;\n}\n\n/** Device-scope modules, pipelines, static quad, placeholder and additive accumulator. */\ninterface ParticleProgramGpu {\n device: GPUDevice;\n format: GPUTextureFormat;\n module: GPUShaderModule;\n resolveModule: GPUShaderModule;\n uniformLayout: GPUBindGroupLayout;\n textureLayout: GPUBindGroupLayout;\n accumLayout: GPUBindGroupLayout;\n particleLayout: GPUPipelineLayout;\n resolvePipelineLayout: GPUPipelineLayout;\n normal: GPURenderPipeline;\n accumulate: GPURenderPipeline;\n resolve: GPURenderPipeline;\n corners: GPUBuffer;\n placeholderView: GPUTextureView;\n placeholderSampler: GPUSampler;\n accum: AccumTarget | null;\n capture: { normal: GPURenderPipeline; resolve: GPURenderPipeline } | null;\n}\nlet programMemo: Promise<ParticleProgramGpu | null> | undefined;\nlet programSettled: ParticleProgramGpu | null | undefined;\nlet programDevice: GPUDevice | null = null;\n\nfunction acquireParticleProgram(\n options: WebgpuParticleRendererOptions,\n): Promise<ParticleProgramGpu | null> {\n if (programDevice !== options.device) {\n programMemo = undefined;\n programSettled = undefined;\n programDevice = options.device;\n }\n if (programMemo) return programMemo;\n programMemo = buildParticleProgram(options).then((program) => {\n programSettled = program;\n return program;\n });\n return programMemo;\n}\nfunction peekParticleProgram(\n options: WebgpuParticleRendererOptions,\n): ParticleProgramGpu | null | undefined {\n return programDevice === options.device ? programSettled : undefined;\n}\n\nasync function buildParticleProgram(\n options: WebgpuParticleRendererOptions,\n): Promise<ParticleProgramGpu | null> {\n const { device, format } = options;\n const module = await compileModule(device, PARTICLE_WGSL, \"gsw-particles\");\n if (!module) return null;\n const resolveModule = await compileModule(\n device,\n ADDITIVE_RESOLVE_WGSL,\n \"gsw-particle-resolve\",\n );\n if (!resolveModule) return null;\n const uniformLayout = uniformBindGroupLayout(\n device,\n SHADER_STAGE.VERTEX | SHADER_STAGE.FRAGMENT,\n PARAMS_BYTES,\n \"gsw-particle-params\",\n );\n const textureLayout = device.createBindGroupLayout({\n label: \"gsw-particle-textures\",\n entries: [\n { binding: 0, visibility: SHADER_STAGE.FRAGMENT, texture: {} },\n { binding: 1, visibility: SHADER_STAGE.FRAGMENT, sampler: {} },\n { binding: 2, visibility: SHADER_STAGE.FRAGMENT, texture: {} },\n { binding: 3, visibility: SHADER_STAGE.FRAGMENT, sampler: {} },\n { binding: 4, visibility: SHADER_STAGE.FRAGMENT, texture: {} },\n { binding: 5, visibility: SHADER_STAGE.FRAGMENT, sampler: {} },\n ],\n });\n const accumLayout = device.createBindGroupLayout({\n label: \"gsw-particle-accum\",\n entries: [{ binding: 0, visibility: SHADER_STAGE.FRAGMENT, texture: {} }],\n });\n const particleLayout = device.createPipelineLayout({\n label: \"gsw-particles\",\n bindGroupLayouts: [uniformLayout, textureLayout],\n });\n const resolvePipelineLayout = device.createPipelineLayout({\n label: \"gsw-particle-resolve\",\n bindGroupLayouts: [accumLayout],\n });\n const normal = await createPipeline(\n device,\n particlePipelineDescriptor(module, particleLayout, format, false),\n \"gsw-particles\",\n );\n if (!normal) return null;\n const accumulate = await createPipeline(\n device,\n particlePipelineDescriptor(module, particleLayout, ACCUM_FORMAT, true),\n \"gsw-particles-additive\",\n );\n if (!accumulate) return null;\n const resolve = await createPipeline(\n device,\n resolvePipelineDescriptor(resolveModule, resolvePipelineLayout, format),\n \"gsw-particle-resolve\",\n );\n if (!resolve) return null;\n const corners = device.createBuffer({\n label: \"gsw-particle-corners\",\n size: 32,\n usage: PARTICLE_BUFFER_USAGE.VERTEX | PARTICLE_BUFFER_USAGE.COPY_DST,\n });\n device.queue.writeBuffer(\n corners,\n 0,\n new Float32Array([-0.5, -0.5, 0.5, -0.5, -0.5, 0.5, 0.5, 0.5]),\n );\n const placeholder = device.createTexture({\n label: \"gsw-particle-placeholder\",\n size: [1, 1, 1],\n format: \"rgba8unorm\",\n usage:\n PARTICLE_TEXTURE_USAGE.TEXTURE_BINDING | PARTICLE_TEXTURE_USAGE.COPY_DST,\n });\n device.queue.writeTexture(\n { texture: placeholder },\n new Uint8Array([255, 255, 255, 255]),\n { bytesPerRow: 4, rowsPerImage: 1 },\n [1, 1, 1],\n );\n return {\n device,\n format,\n module,\n resolveModule,\n uniformLayout,\n textureLayout,\n accumLayout,\n particleLayout,\n resolvePipelineLayout,\n normal,\n accumulate,\n resolve,\n corners,\n placeholderView: placeholder.createView(),\n placeholderSampler: device.createSampler({\n label: \"gsw-particle-placeholder\",\n magFilter: \"linear\",\n minFilter: \"linear\",\n }),\n accum: null,\n capture: null,\n };\n}\n\nfunction particlePipelineDescriptor(\n module: GPUShaderModule,\n layout: GPUPipelineLayout,\n format: GPUTextureFormat,\n additive: boolean,\n): GPURenderPipelineDescriptor {\n return {\n label: additive ? \"gsw-particles-additive\" : \"gsw-particles\",\n layout,\n vertex: {\n module,\n entryPoint: PARTICLE_VS_ENTRY,\n buffers: PARTICLE_VERTEX_BUFFERS,\n },\n fragment: {\n module,\n entryPoint: additive ? PARTICLE_FS_ADDITIVE_ENTRY : PARTICLE_FS_ENTRY,\n targets: [\n { format, blend: additive ? ADDITIVE_BLEND : PREMULTIPLIED_BLEND },\n ],\n },\n primitive: { topology: \"triangle-strip\" },\n };\n}\nfunction resolvePipelineDescriptor(\n module: GPUShaderModule,\n layout: GPUPipelineLayout,\n format: GPUTextureFormat,\n): GPURenderPipelineDescriptor {\n return {\n label: \"gsw-particle-resolve\",\n layout,\n vertex: { module, entryPoint: RESOLVE_VS_ENTRY },\n fragment: { module, entryPoint: RESOLVE_FS_ENTRY, targets: [{ format }] },\n primitive: { topology: \"triangle-strip\" },\n };\n}\n\n/** Construct a renderer after its device program is compiled. Presentation contexts and texture handles stay host-owned. */\nexport async function createWebgpuParticleRenderer(\n options: WebgpuParticleRendererOptions,\n): Promise<WebgpuParticleRenderer | null> {\n const program = await acquireParticleProgram(options);\n return program ? rendererOver(options, program) : null;\n}\nexport function peekWebgpuParticleRenderer(\n options: WebgpuParticleRendererOptions,\n): WebgpuParticleRenderer | null | undefined {\n const program = peekParticleProgram(options);\n return program === undefined\n ? undefined\n : program === null\n ? null\n : rendererOver(options, program);\n}\nexport interface WebgpuParticleRenderer {\n createSurface(surface: WebgpuParticleSurface): WebgpuParticleSurfaceState;\n disposeSurface(surface: WebgpuParticleSurfaceState): void;\n beginFrame(): void;\n endFrame(): void;\n clear(surface: WebgpuParticleSurfaceState): void;\n draw(\n surface: WebgpuParticleSurfaceState,\n buffer: InstanceBuffer,\n opts: WebgpuParticleDrawOptions,\n ): void;\n captureSurface(\n surface: WebgpuParticleSurfaceState,\n buffer: InstanceBuffer,\n opts: WebgpuParticleDrawOptions,\n ): Promise<Uint8Array | null>;\n submits(): number;\n}\nfunction rendererOver(\n options: WebgpuParticleRendererOptions,\n program: ParticleProgramGpu,\n): WebgpuParticleRenderer {\n const { device } = options;\n let encoder: GPUCommandEncoder | null = null;\n let recorded = false;\n let implicit = false;\n let submitCount = 0;\n const ensureEncoder = () => {\n if (!encoder) {\n encoder = device.createCommandEncoder({ label: \"gsw-particles\" });\n implicit = true;\n }\n return encoder;\n };\n const flush = () => {\n if (encoder && recorded) {\n device.queue.submit([encoder.finish()]);\n submitCount++;\n }\n encoder = null;\n recorded = false;\n implicit = false;\n };\n const flushImplicit = () => {\n if (implicit) flush();\n };\n return {\n createSurface(surface) {\n const ring = createUniformRing(device, 1, PARAMS_FLOATS, {\n label: \"gsw-particle-params\",\n layout: program.uniformLayout,\n });\n const state = Object.assign(surface, {\n ring,\n words: new Uint32Array(ring.staging.buffer),\n instances: null,\n instanceBytes: 0,\n bindGroup: null,\n boundViews: [null, null, null] as [unknown, unknown, unknown],\n bindGroupDirty: true,\n disposeTextureListener: null,\n }) as WebgpuParticleSurfaceState;\n state.disposeTextureListener =\n surface.onTexturesChanged?.(() => {\n state.bindGroupDirty = true;\n }) ?? null;\n return state;\n },\n disposeSurface(state) {\n state.disposeTextureListener?.();\n state.instances?.destroy();\n state.ring.destroy();\n state.bindGroup = null;\n },\n beginFrame() {\n if (!encoder) {\n encoder = device.createCommandEncoder({ label: \"gsw-particles\" });\n recorded = false;\n implicit = false;\n }\n },\n endFrame: flush,\n clear(state) {\n const view = currentView(state);\n if (view) {\n const pass = ensureEncoder().beginRenderPass({\n label: \"gsw-particle-clear\",\n colorAttachments: [\n {\n view,\n clearValue: TRANSPARENT,\n loadOp: \"clear\",\n storeOp: \"store\",\n },\n ],\n });\n pass.end();\n recorded = true;\n }\n flushImplicit();\n },\n draw(state, buffer, opts) {\n const view = currentView(state);\n if (view) {\n encodeSurface(program, ensureEncoder(), state, view, buffer, opts);\n recorded = true;\n }\n flushImplicit();\n },\n submits: () => submitCount,\n captureSurface: (state, buffer, opts) =>\n captureSurfacePixels(program, options, state, buffer, opts),\n };\n}\n\n/** The canvas's current swap-chain view, or null when it cannot be had (an unconfigured context, a\n * zero-sized canvas, a lost device). A frame that cannot acquire its target is SKIPPED, not thrown\n * out of: the runtime's next tick asks again. */\nfunction currentView(state: WebgpuParticleSurfaceState): GPUTextureView | null {\n try {\n return state.context.getCurrentTexture().createView();\n } catch {\n return null;\n }\n}\n\n/** Record one system's draw into `encoder`, targeting `view` (the canvas's swap-chain image, or a\n * capture texture). Additive systems record TWO passes; passes execute in the order they were\n * recorded, so accumulate→resolve is safe inside the shared one-submit frame. */\nfunction encodeSurface(\n program: ParticleProgramGpu,\n encoder: GPUCommandEncoder,\n state: WebgpuParticleSurfaceState,\n view: GPUTextureView,\n buffer: InstanceBuffer,\n opts: WebgpuParticleDrawOptions,\n capture = false,\n): void {\n const w = Math.max(1, Math.floor(opts.width));\n const h = Math.max(1, Math.floor(opts.height));\n writeParams(program, state, w, h, opts);\n uploadInstances(program, state, buffer);\n const textures = ensureBindGroup(program, state);\n const additive = opts.blendMode === 1;\n // Only the pipelines that write the TARGET are format-dependent; the accumulate pass always\n // renders into the `rgba8unorm` accumulator, capture or not.\n const pipelines = capture ? captureOrThrow(program) : program;\n\n if (additive) {\n const accum = ensureAccum(program, w, h);\n // ACCUMULATE. The load op clears the WHOLE (grow-only) accumulator, exactly as the GL path's\n // `gl.clear` does with no scissor — only the top-left w×h rect is ever read back.\n const accumPass = encoder.beginRenderPass({\n label: \"gsw-particle-accumulate\",\n colorAttachments: [\n {\n view: accum.view,\n clearValue: TRANSPARENT,\n loadOp: \"clear\",\n storeOp: \"store\",\n },\n ],\n });\n // The viewport pins this system's NDC onto the accumulator's top-left w×h rect, so the resolve's\n // `textureLoad(pos.xy)` reads the texel the fragment wrote.\n accumPass.setViewport(0, 0, w, h, 0, 1);\n accumPass.setScissorRect(0, 0, w, h);\n accumPass.setPipeline(program.accumulate);\n accumPass.setBindGroup(0, state.ring.bindGroup, [0]);\n accumPass.setBindGroup(1, textures);\n accumPass.setVertexBuffer(0, program.corners);\n accumPass.setVertexBuffer(1, state.instances);\n accumPass.draw(4, buffer.count);\n accumPass.end();\n\n const resolvePass = encoder.beginRenderPass({\n label: \"gsw-particle-resolve\",\n colorAttachments: [\n { view, clearValue: TRANSPARENT, loadOp: \"clear\", storeOp: \"store\" },\n ],\n });\n resolvePass.setPipeline(pipelines.resolve);\n resolvePass.setBindGroup(0, accum.bindGroup);\n resolvePass.draw(4, 1);\n resolvePass.end();\n return;\n }\n\n const pass = encoder.beginRenderPass({\n label: \"gsw-particle-draw\",\n colorAttachments: [\n { view, clearValue: TRANSPARENT, loadOp: \"clear\", storeOp: \"store\" },\n ],\n });\n pass.setPipeline(pipelines.normal);\n pass.setBindGroup(0, state.ring.bindGroup, [0]);\n pass.setBindGroup(1, textures);\n pass.setVertexBuffer(0, program.corners);\n pass.setVertexBuffer(1, state.instances);\n pass.draw(4, buffer.count);\n pass.end();\n}\n\n/** The per-surface uniform slot. REWRITTEN EVERY DRAW rather than latched at create: a node canvas\n * resizes (renderScale, a pin change, a rotation, a laid-out box moving), and a stale viewport maps\n * every particle to the wrong clip position — silently, since nothing about it is an error. */\nfunction writeParams(\n program: ParticleProgramGpu,\n state: WebgpuParticleSurfaceState,\n w: number,\n h: number,\n opts: WebgpuParticleDrawOptions,\n): void {\n const textured = Boolean(opts.textured && state.textures.sprite);\n const [hframes, vframes] = frameGridFor(textured, opts.hframes, opts.vframes);\n const erode = opts.erode ?? null;\n const floats = state.ring.staging;\n const words = state.words;\n floats[0] = w;\n floats[1] = h;\n floats[2] = hframes;\n floats[3] = vframes;\n floats[4] = erode ? erode.threshold : 0;\n floats[5] = erode ? erode.softness : 0;\n words[6] = textured ? 1 : 0;\n words[7] = opts.lutTexture && state.textures.lut ? 1 : 0;\n words[8] = opts.alphaFromRed ? 1 : 0;\n words[9] = erode ? 1 : 0;\n words[10] = opts.maskTexture && state.textures.mask ? 1 : 0;\n words[11] = opts.uvPolar ? 1 : 0;\n program.device.queue.writeBuffer(\n state.ring.buffer,\n 0,\n floats,\n 0,\n PARAMS_FLOATS,\n );\n}\n\n/** The per-surface instance buffer: GROW-ONLY, like everything else in this pipeline, so a steady\n * fleet allocates nothing per frame. */\nfunction uploadInstances(\n program: ParticleProgramGpu,\n state: WebgpuParticleSurfaceState,\n buffer: InstanceBuffer,\n): void {\n const bytes = Math.max(\n INSTANCE_STRIDE_BYTES,\n buffer.count * INSTANCE_STRIDE_BYTES,\n );\n if (!state.instances || bytes > state.instanceBytes) {\n state.instances?.destroy();\n state.instances = program.device.createBuffer({\n label: \"gsw-particle-instances\",\n size: bytes,\n usage: PARTICLE_BUFFER_USAGE.VERTEX | PARTICLE_BUFFER_USAGE.COPY_DST,\n });\n state.instanceBytes = bytes;\n }\n if (buffer.count <= 0) return;\n // Byte offsets into the ARRAY BUFFER (not element counts into the view): `InstanceBuffer.data` is\n // a grow-doubling `Float32Array` whose tail beyond `count` is stale, and only the used head is\n // uploaded.\n program.device.queue.writeBuffer(\n state.instances,\n 0,\n buffer.data.buffer,\n buffer.data.byteOffset,\n buffer.count * INSTANCE_STRIDE_BYTES,\n );\n}\n\n/** The surface's texture bind group, rebuilt when a decode replaced one of its views (see\n * `WebgpuParticleSurfaceState.boundViews`) and otherwise reused. */\nfunction ensureBindGroup(\n program: ParticleProgramGpu,\n state: WebgpuParticleSurfaceState,\n): GPUBindGroup {\n const spriteView = state.textures.sprite?.view ?? program.placeholderView;\n const lutView = state.textures.lut?.view ?? program.placeholderView;\n const maskView = state.textures.mask?.view ?? program.placeholderView;\n const stale =\n state.boundViews[0] !== spriteView ||\n state.boundViews[1] !== lutView ||\n state.boundViews[2] !== maskView;\n if (state.bindGroup && !state.bindGroupDirty && !stale)\n return state.bindGroup;\n state.bindGroup = program.device.createBindGroup({\n label: \"gsw-particle-textures\",\n layout: program.textureLayout,\n entries: [\n { binding: 0, resource: spriteView },\n {\n binding: 1,\n resource: state.textures.sprite?.sampler ?? program.placeholderSampler,\n },\n { binding: 2, resource: lutView },\n {\n binding: 3,\n resource: state.textures.lut?.sampler ?? program.placeholderSampler,\n },\n { binding: 4, resource: maskView },\n {\n binding: 5,\n resource: state.textures.mask?.sampler ?? program.placeholderSampler,\n },\n ],\n });\n state.boundViews = [spriteView, lutView, maskView];\n state.bindGroupDirty = false;\n return state.bindGroup;\n}\n\n/** Grow (never shrink) the device-scope light accumulator to cover w×h — the same policy, for the\n * same realloc-cost reason, as the GL path's `ensureAccumTarget`. */\nfunction ensureAccum(\n program: ParticleProgramGpu,\n w: number,\n h: number,\n): AccumTarget {\n const current = program.accum;\n if (current && current.width >= w && current.height >= h) return current;\n const width = Math.max(current?.width ?? 0, w);\n const height = Math.max(current?.height ?? 0, h);\n current?.texture.destroy();\n const texture = program.device.createTexture({\n label: \"gsw-particle-accum\",\n size: [width, height, 1],\n format: ACCUM_FORMAT,\n usage:\n PARTICLE_TEXTURE_USAGE.RENDER_ATTACHMENT |\n PARTICLE_TEXTURE_USAGE.TEXTURE_BINDING,\n });\n const view = texture.createView();\n const accum: AccumTarget = {\n texture,\n view,\n bindGroup: program.device.createBindGroup({\n label: \"gsw-particle-accum\",\n layout: program.accumLayout,\n entries: [{ binding: 0, resource: view }],\n }),\n width,\n height,\n };\n program.accum = accum;\n return accum;\n}\n\nfunction captureOrThrow(program: ParticleProgramGpu): {\n normal: GPURenderPipeline;\n resolve: GPURenderPipeline;\n} {\n const capture = program.capture;\n if (!capture) {\n // Unreachable: `captureSurfacePixels` compiles them before it encodes anything.\n throw new Error(\"gsw: capture pipelines were not compiled\");\n }\n return capture;\n}\n\n/**\n * Re-render this surface's CURRENT frame into an offscreen texture and read it back.\n *\n * NEVER `drawImage`/`toDataURL` FROM THE CANVAS. Both read a WebGPU canvas through its presentation\n * path, which is blank under headless SwiftShader and pathological on Android Chrome (S7 measured\n * 23 Hz against 87 for direct presentation). `copyTextureToBuffer` + `mapAsync` — what\n * `../webgpu/readback` does — is the one path verified to work fully headless, which is why this\n * hook exists at all rather than the parity harness simply reading the canvas.\n *\n * The pipelines are the live ones re-created against `rgba8unorm`: a pipeline's fragment target\n * format must match its attachment, and the canvas format is usually `bgra8unorm`. Everything else —\n * module, entry points, blend states, uniforms, bind groups — is shared with the live draw, so what\n * comes back is the frame the canvas is showing, not a second interpretation of it.\n */\nasync function captureSurfacePixels(\n program: ParticleProgramGpu,\n options: WebgpuParticleRendererOptions,\n state: WebgpuParticleSurfaceState,\n buffer: InstanceBuffer,\n opts: WebgpuParticleDrawOptions,\n): Promise<Uint8Array | null> {\n const w = Math.max(1, Math.floor(opts.width));\n const h = Math.max(1, Math.floor(opts.height));\n if (!program.capture) {\n const normal = await createPipeline(\n program.device,\n particlePipelineDescriptor(\n program.module,\n program.particleLayout,\n ACCUM_FORMAT,\n false,\n ),\n \"gsw-particles-capture\",\n );\n const resolve = await createPipeline(\n program.device,\n resolvePipelineDescriptor(\n program.resolveModule,\n program.resolvePipelineLayout,\n ACCUM_FORMAT,\n ),\n \"gsw-particle-resolve-capture\",\n );\n if (!normal || !resolve) return null;\n program.capture = { normal, resolve };\n }\n const target = program.device.createTexture({\n label: \"gsw-particle-capture\",\n size: [w, h, 1],\n format: ACCUM_FORMAT,\n usage:\n PARTICLE_TEXTURE_USAGE.RENDER_ATTACHMENT |\n PARTICLE_TEXTURE_USAGE.COPY_SRC,\n });\n try {\n const encoder = program.device.createCommandEncoder({\n label: \"gsw-particle-capture\",\n });\n encodeSurface(\n program,\n encoder,\n state,\n target.createView(),\n buffer,\n { ...opts, width: w, height: h },\n true,\n );\n program.device.queue.submit([encoder.finish()]);\n return await (\n options.readTexturePixels ??\n ((texture, width, height) =>\n readTexturePixels({ device: options.device }, texture, width, height))\n )(target, w, h);\n } catch {\n // A capture is a diagnostic, never a render: a device that refuses it reports nothing and the\n // live path is untouched.\n options.onPipelineError?.();\n return null;\n } finally {\n target.destroy();\n }\n}\n\n/** TEST-ONLY: drop the device-scope pipelines so a suite can re-probe with a fresh stub device. */\nexport function __resetWebgpuParticleProgramForTest(): void {\n programMemo = undefined;\n programSettled = undefined;\n programDevice = null;\n}\n","/** Adapter-neutral WebGPU command executor for canvas shaders. Hosts own DOM, texture resolution and bind groups. */\nconst TEXTURE_USAGE = { COPY_SRC: 0x01, RENDER_ATTACHMENT: 0x10 } as const;\nconst TRANSPARENT: GPUColor = { r: 0, g: 0, b: 0, a: 0 };\n\nexport function createWebgpuShaderBindGroupLayout(\n device: GPUDevice,\n label: string,\n entries: Iterable<GPUBindGroupLayoutEntry>,\n): GPUBindGroupLayout {\n return device.createBindGroupLayout({ label, entries: Array.from(entries) });\n}\n\nexport function createWebgpuShaderUniformBuffer(\n device: GPUDevice,\n label: string,\n size: number,\n): GPUBuffer {\n return device.createBuffer({ label, size, usage: 0x0040 | 0x0008 });\n}\n\nexport function createWebgpuShaderBindGroup(\n device: GPUDevice,\n label: string,\n layout: GPUBindGroupLayout,\n entries: Iterable<GPUBindGroupEntry>,\n): GPUBindGroup {\n return device.createBindGroup({\n label,\n layout,\n entries: Array.from(entries),\n });\n}\n\nexport interface WebgpuShaderDraw {\n context?: GPUCanvasContext;\n target?: GPUTextureView;\n pipeline: GPURenderPipeline;\n bindGroup: GPUBindGroup;\n uniformBuffer: GPUBuffer;\n uniformBytes: ArrayBuffer | ArrayBufferView;\n width: number;\n height: number;\n}\n\n/** Owns one command encoder per host frame. It never creates contexts or texture handles. */\nexport class WebgpuShaderExecutor {\n private encoder: GPUCommandEncoder | null = null;\n private recorded = false;\n private implicit = false;\n private submitCount = 0;\n constructor(private readonly device: GPUDevice) {}\n beginFrame(): void {\n if (this.encoder) return;\n this.encoder = this.device.createCommandEncoder({ label: \"gsw-shaders\" });\n this.recorded = false;\n this.implicit = false;\n }\n endFrame(): void {\n this.flush();\n }\n submits(): number {\n return this.submitCount;\n }\n draw(draw: WebgpuShaderDraw): boolean {\n const target = draw.target ?? this.currentView(draw.context);\n if (!target) return false;\n const encoder = this.ensureEncoder();\n this.device.queue.writeBuffer(\n draw.uniformBuffer,\n 0,\n draw.uniformBytes,\n 0,\n draw.uniformBytes.byteLength,\n );\n const pass = encoder.beginRenderPass({\n label: \"gsw-shader-draw\",\n colorAttachments: [\n {\n view: target,\n clearValue: TRANSPARENT,\n loadOp: \"clear\",\n storeOp: \"store\",\n },\n ],\n });\n pass.setViewport(0, 0, draw.width, draw.height, 0, 1);\n pass.setPipeline(draw.pipeline);\n pass.setBindGroup(0, draw.bindGroup);\n pass.draw(4, 1);\n pass.end();\n this.recorded = true;\n if (this.implicit) this.flush();\n return true;\n }\n async capture(\n draw: Omit<WebgpuShaderDraw, \"context\" | \"target\" | \"width\" | \"height\"> & {\n width: number;\n height: number;\n read: (\n texture: GPUTexture,\n width: number,\n height: number,\n ) => Promise<Uint8Array>;\n },\n ): Promise<Uint8Array | null> {\n const target = this.device.createTexture({\n label: \"gsw-shader-capture\",\n size: [draw.width, draw.height, 1],\n format: \"rgba8unorm\",\n usage: TEXTURE_USAGE.RENDER_ATTACHMENT | TEXTURE_USAGE.COPY_SRC,\n });\n try {\n const encoder = this.device.createCommandEncoder({\n label: \"gsw-shader-capture\",\n });\n this.device.queue.writeBuffer(\n draw.uniformBuffer,\n 0,\n draw.uniformBytes,\n 0,\n draw.uniformBytes.byteLength,\n );\n const pass = encoder.beginRenderPass({\n label: \"gsw-shader-draw\",\n colorAttachments: [\n {\n view: target.createView(),\n clearValue: TRANSPARENT,\n loadOp: \"clear\",\n storeOp: \"store\",\n },\n ],\n });\n pass.setViewport(0, 0, draw.width, draw.height, 0, 1);\n pass.setPipeline(draw.pipeline);\n pass.setBindGroup(0, draw.bindGroup);\n pass.draw(4, 1);\n pass.end();\n this.device.queue.submit([encoder.finish()]);\n return await draw.read(target, draw.width, draw.height);\n } catch {\n return null;\n } finally {\n target.destroy();\n }\n }\n private ensureEncoder(): GPUCommandEncoder {\n if (!this.encoder) {\n this.encoder = this.device.createCommandEncoder({ label: \"gsw-shaders\" });\n this.implicit = true;\n }\n return this.encoder;\n }\n private currentView(\n context: GPUCanvasContext | undefined,\n ): GPUTextureView | null {\n try {\n return context?.getCurrentTexture().createView() ?? null;\n } catch {\n return null;\n }\n }\n private flush(): void {\n if (this.encoder && this.recorded) {\n this.device.queue.submit([this.encoder.finish()]);\n this.submitCount++;\n }\n this.encoder = null;\n this.recorded = false;\n this.implicit = false;\n }\n}\n","// Writing one shader binding's uniform values into the single `var<uniform>` struct that\n// `./transpile-wgsl` laid out.\n//\n// WHY THIS IS ITS OWN FILE, AND PURE. On WebGL every uniform is addressed BY NAME through a\n// `WebGLUniformLocation`, and a wrong type is a warning the driver prints. On WebGPU the struct is\n// one opaque block of bytes: an offset that is four bytes out, or an `array<vec3f>` packed tightly\n// instead of at its 16-byte stride, produces a shader that runs perfectly and draws the wrong\n// picture — silently, with nothing to log. So the packing is a plain function over plain numbers,\n// testable against hand-computed offsets without a device (`test/webgpu-shader-uniforms.test.ts`),\n// rather than a few lines buried in a render path that only a GPU can execute.\n//\n// THE TWO RULES THAT ARE NOT OBVIOUS:\n// * a Godot `bool` uniform is stored as `f32` (WGSL `bool` is not host-shareable), so the writer\n// has to consult `godotType` — an `f32` member may really be a 1/0 flag — and an `int` uniform\n// is a genuine `i32`, which must be written through an Int32Array view, not as a float that\n// happens to hold an integer.\n// * a uniform-address-space array has an element stride rounded up to 16 bytes, so an\n// `array<vec3f, N>` is written every FOUR floats with a padding lane between elements. Packing\n// it tightly is the classic \"the first element is right and the rest are garbage\" bug.\n//\n// Everything here is numbers and typed arrays — no `GPU*` global is referenced — so it imports\n// cleanly into node (the parity harness bundles it) and needs no device to test.\n\nimport type {\n WgslBuiltinOffsets,\n WgslUniformField,\n} from \"@godot-scene-web/effects/shaders\";\n\n/** A shader parameter's value as the runtime parsed it out of `data-godot-shader-params`: a scalar,\n * a vector/array flattened into one number list, or absent (the declared default is used). Kept\n * structurally identical to the shader runtime's `ShaderParamValue` so the two cannot drift. */\nexport type PackedParamValue = number | number[];\n\n/** The layout half of a `TranspiledWgslShader` — everything `packShaderUniforms` needs and nothing\n * else, so a test can hand-build one. */\nexport interface ShaderUniformLayout {\n /** Size of the whole struct in bytes (already rounded up to its alignment). */\n uniformStructSizeBytes: number;\n builtinOffsets: WgslBuiltinOffsets;\n uniforms: WgslUniformField[];\n}\n\n/** The values one render supplies. The built-ins mirror the WebGL backend's uniform writes one for\n * one (see `renderNodeGl`), so the two renderers are fed the SAME numbers; the optional ones are\n * written only when the shader declared them, which is exactly when the offset exists. */\nexport interface ShaderUniformValues {\n /** `_godot_uv_fit` — the fraction of the canvas the fitted texture covers. */\n uvFit: readonly [number, number];\n /** `_godot_uv_window` — the node-local sub-rect this canvas covers, [u0,v0,du,dv]. */\n uvWindow: readonly number[];\n /** `TIME`, seconds. */\n time?: number;\n /** `TEXTURE_PIXEL_SIZE` — 1/texture width, 1/texture height. */\n texturePixelSize?: readonly [number, number];\n /** `MODULATE` — the node's colour multiply, RGBA. */\n modulate?: readonly number[];\n /** `SCREEN_UV`'s node origin and size within the scene-root viewport. */\n screenOrigin?: readonly [number, number];\n screenSize?: readonly [number, number];\n /** User `shader_parameter/<name>` values, by GODOT name (`WgslUniformField.name`). */\n params?: Record<string, PackedParamValue | undefined>;\n /** Godot's own type name per parameter (`PackedColorArray`, …) — the one case where the wire\n * format needs re-interpreting; see `PACKED_COLOR_ARRAY` below. */\n paramKinds?: Record<string, string>;\n}\n\n/** A `PackedColorArray` arrives as RGBA quads even when the uniform is `vec3[]` — the same\n * correction the WebGL path makes in `normalizeVec3ArrayUniformValues`. */\nconst PACKED_COLOR_ARRAY = \"PackedColorArray\";\n\n/** Uniform-address-space arrays are strided to 16 bytes (4 floats) per element. */\nconst ARRAY_STRIDE_FLOATS = 4;\n\n/** How many scalar components each WGSL member type holds. */\nconst COMPONENTS: Record<string, number> = {\n f32: 1,\n i32: 1,\n u32: 1,\n vec2f: 2,\n vec2i: 2,\n vec3f: 3,\n vec3i: 3,\n vec4f: 4,\n vec4i: 4,\n};\n\n/** WGSL types written as signed INTEGERS rather than floats (an `int` uniform, and the `ivecN`s). */\nconst INTEGER_TYPES = new Set([\"i32\", \"vec2i\", \"vec3i\", \"vec4i\"]);\n\n/** The staging buffer for one binding: the same bytes seen as floats and as i32s, because one\n * uniform struct legitimately holds both. `bytes` is what a `writeBuffer` uploads. */\nexport interface UniformStaging {\n bytes: ArrayBuffer;\n floats: Float32Array;\n ints: Int32Array;\n}\n\n/** Allocate a staging block big enough for `sizeBytes` (rounded up to a whole float). */\nexport function createUniformStaging(sizeBytes: number): UniformStaging {\n const size = Math.max(4, Math.ceil(sizeBytes / 4) * 4);\n const bytes = new ArrayBuffer(size);\n return {\n bytes,\n floats: new Float32Array(bytes),\n ints: new Int32Array(bytes),\n };\n}\n\n/**\n * Write `values` into `staging` at the byte offsets `layout` declares, and return it.\n *\n * ZEROED FIRST, deliberately: a uniform whose value disappeared between frames (a param attribute\n * dropped, a `MODULATE` that stopped applying) must read as 0, not as whatever the previous frame\n * left in that lane. The struct is small (tens of bytes) and this happens once per binding per\n * frame, so the clear is not worth optimising away for the class of bug it removes.\n */\nexport function packShaderUniforms(\n layout: ShaderUniformLayout,\n values: ShaderUniformValues,\n staging: UniformStaging = createUniformStaging(layout.uniformStructSizeBytes),\n): UniformStaging {\n staging.floats.fill(0);\n\n const { builtinOffsets } = layout;\n writeFloats(staging, builtinOffsets.uvFit, values.uvFit, 2);\n writeFloats(staging, builtinOffsets.uvWindow, values.uvWindow, 4);\n if (builtinOffsets.time !== undefined) {\n staging.floats[builtinOffsets.time / 4] = values.time ?? 0;\n }\n writeFloats(\n staging,\n builtinOffsets.texturePixelSize,\n values.texturePixelSize,\n 2,\n );\n writeFloats(staging, builtinOffsets.modulate, values.modulate, 4);\n writeFloats(staging, builtinOffsets.screenOrigin, values.screenOrigin, 2);\n writeFloats(staging, builtinOffsets.screenSize, values.screenSize, 2);\n\n const params = values.params ?? {};\n for (const field of layout.uniforms) {\n // The declared default, exactly as the WebGL path falls back to `uniform.default` when the node\n // carries no value for a parameter.\n const raw = params[field.name] ?? field.default;\n writeUniformField(staging, field, raw, values.paramKinds?.[field.name]);\n }\n return staging;\n}\n\nfunction writeUniformField(\n staging: UniformStaging,\n field: WgslUniformField,\n raw: PackedParamValue | undefined,\n paramKind: string | undefined,\n): void {\n const components = COMPONENTS[field.type];\n // `mat2x2f`/`mat3x3f`/`mat4x4f` have no scalar component count here: no Godot uniform of matrix\n // type reaches a shader parameter attribute (they are computed in-shader), and inventing a\n // column-padded write for a case that cannot occur would be untested code in a silent-failure\n // position. The lane stays zeroed.\n if (components === undefined) return;\n const target = INTEGER_TYPES.has(field.type) ? staging.ints : staging.floats;\n const base = field.offsetBytes / 4;\n\n if (field.arrayLength === undefined) {\n if (components === 1) {\n target[base] = scalarOf(raw, field.godotType);\n return;\n }\n const list = Array.isArray(raw) ? raw : [];\n for (let i = 0; i < components; i++) target[base + i] = list[i] ?? 0;\n return;\n }\n\n // ARRAY: element `i` starts at `base + i * 4` floats, whatever its component count — the uniform\n // address space rounds every array element stride up to 16 bytes, so a `vec3f[]` leaves one\n // padding lane per element and an `f32[]`/`vec2f[]` would need three (which is why\n // `wgslStructLayout` refuses those outright rather than letting a writer guess).\n const list = normalizeArrayValues(raw, field, paramKind);\n for (let element = 0; element < field.arrayLength; element++) {\n const slot = base + element * ARRAY_STRIDE_FLOATS;\n for (let i = 0; i < components; i++) {\n target[slot + i] = list[element * components + i] ?? 0;\n }\n }\n}\n\n// A scalar lane. `bool` is stored as an f32 flag (1/0) and `int` as a rounded i32 — both of which the\n// caller can only know from `godotType`, since both arrive here as plain JS numbers.\nfunction scalarOf(\n raw: PackedParamValue | undefined,\n godotType: string,\n): number {\n const value =\n typeof raw === \"number\" ? raw : Array.isArray(raw) ? (raw[0] ?? 0) : 0;\n if (godotType === \"bool\") return value ? 1 : 0;\n if (godotType === \"int\") return Math.round(value);\n return value;\n}\n\n// The flat component list for an array uniform, with the one wire-format correction the GL path also\n// makes: Godot serialises a `PackedColorArray` as RGBA quads, so a `vec3[]` fed from one arrives with\n// an alpha component per element that has to be dropped (`normalizeVec3ArrayUniformValues`).\nfunction normalizeArrayValues(\n raw: PackedParamValue | undefined,\n field: WgslUniformField,\n paramKind: string | undefined,\n): number[] {\n const list = Array.isArray(raw) ? raw : typeof raw === \"number\" ? [raw] : [];\n if (\n field.type === \"vec3f\" &&\n paramKind === PACKED_COLOR_ARRAY &&\n list.length % 4 === 0\n ) {\n const out: number[] = [];\n for (let i = 0; i < list.length; i += 4) {\n out.push(list[i] ?? 0, list[i + 1] ?? 0, list[i + 2] ?? 0);\n }\n return out;\n }\n return list;\n}\n\nfunction writeFloats(\n staging: UniformStaging,\n offsetBytes: number | undefined,\n values: readonly number[] | undefined,\n count: number,\n): void {\n if (offsetBytes === undefined) return;\n const base = offsetBytes / 4;\n for (let i = 0; i < count; i++) staging.floats[base + i] = values?.[i] ?? 0;\n}\n","/** Device-scoped texture allocation and upload primitives. Hosts own decoding and cache keys. */\nexport const WEBGPU_UPLOAD_TEXTURE_USAGE = 0x01 | 0x02 | 0x04 | 0x10;\n\nexport interface WebgpuTextureUpload {\n readonly texture: GPUTexture;\n readonly width: number;\n readonly height: number;\n}\n\nexport function createWebgpuRgbaTexture(\n device: GPUDevice,\n width: number,\n height: number,\n label?: string,\n): WebgpuTextureUpload {\n const w = Math.max(1, Math.round(width));\n const h = Math.max(1, Math.round(height));\n return {\n texture: device.createTexture({\n label,\n size: [w, h, 1],\n format: \"rgba8unorm\",\n usage: WEBGPU_UPLOAD_TEXTURE_USAGE,\n }),\n width: w,\n height: h,\n };\n}\n\nexport function uploadWebgpuRgba(\n device: GPUDevice,\n target: WebgpuTextureUpload,\n pixels: Uint8Array | Uint8ClampedArray,\n): void {\n device.queue.writeTexture(\n { texture: target.texture },\n pixels,\n { bytesPerRow: target.width * 4, rowsPerImage: target.height },\n [target.width, target.height],\n );\n}\n\n/** Upload a host-decoded external image. The host decides its source and crop. */\nexport function uploadWebgpuExternalImage(\n device: GPUDevice,\n target: WebgpuTextureUpload,\n source: GPUCopyExternalImageSourceInfo,\n): void {\n device.queue.copyExternalImageToTexture(\n source,\n { texture: target.texture, premultipliedAlpha: false },\n [target.width, target.height],\n );\n}\n\nexport function createWebgpuSampler(\n device: GPUDevice,\n opts: { readonly nearest: boolean; readonly repeat: boolean },\n): GPUSampler {\n const filter: GPUFilterMode = opts.nearest ? \"nearest\" : \"linear\";\n const address: GPUAddressMode = opts.repeat ? \"repeat\" : \"clamp-to-edge\";\n return device.createSampler({\n magFilter: filter,\n minFilter: filter,\n addressModeU: address,\n addressModeV: address,\n });\n}\n\nexport function destroyWebgpuTexture(texture: GPUTexture): void {\n try {\n texture.destroy();\n } catch {\n // A lost device may reject destruction; its resources are already dead.\n }\n}\n","/**\n * Explicit WebGPU execution entry point.\n *\n * Callers supply their device and presentation surface; this\n * package never creates a canvas, queries the DOM, fetches images, or schedules\n * animation frames.\n */\n/** Configure a caller-supplied WebGPU presentation context for premultiplied output. */\nexport function configureWebgpuSurface(\n context: GPUCanvasContext,\n device: GPUDevice,\n format: GPUTextureFormat,\n): void {\n context.configure({ device, format, alphaMode: \"premultiplied\" });\n}\n\nexport * from \"./particles-webgpu\";\nexport * from \"./shader-webgpu\";\nexport * from \"./webgpu-pack-uniforms\";\nexport * from \"./webgpu-pipeline\";\nexport * from \"./webgpu-readback\";\nexport * from \"./webgpu-textures\";\n"],"mappings":";;;;AAQA,MAAa,eAAe;CAAE,UAAU;CAAQ,SAAS;AAAO;AAChE,MAAa,eAAe;CAAE,QAAQ;CAAK,UAAU;AAAI;;;;;;AAOzD,MAAa,qBAAqB;AAElC,IAAI,YAA2B;;;AAI/B,SAAgB,oBAAmC;CACjD,OAAO;AACT;AAEA,SAAS,KAAK,SAAiB,SAA4B;CACzD,UAAU;CACV,YAAY;CAGZ,QAAQ,KAAK,gBAAgB,OAAO;CACpC,OAAO;AACT;;;;;;;;AASA,eAAsB,cACpB,QACA,MACA,OACA,SACiC;CACjC,IAAI;CACJ,IAAI;EACF,SAAS,OAAO,mBAAmB;GAAE;GAAM;EAAM,CAAC;CACpD,SAAS,OAAO;EACd,OAAO,KACL,GAAG,MAAM,iCAAiC,SAAS,KAAK,KACxD,OACF;CACF;CACA,IAAI;EAEF,MAAM,UAAS,MADI,OAAO,mBAAmB,GACzB,SAAS,QAAQ,YAAY,QAAQ,SAAS,OAAO;EACzE,IAAI,OAAO,SAAS,GAAG;GACrB,MAAM,QAAQ,OAAO;GACrB,OAAO,KACL,GAAG,MAAM,4BAA4B,MAAM,QAAQ,SAAS,MAAM,QAAQ,QAAQ,MAAM,QAAQ,KAAK,OAAO,OAAO,kBACnH,OACF;EACF;CACF,SAAS,OAAO;EACd,OAAO,KACL,GAAG,MAAM,kCAAkC,SAAS,KAAK,KACzD,OACF;CACF;CACA,OAAO;AACT;;;;;AAMA,eAAsB,eACpB,QACA,YACA,OACA,SACmC;CACnC,IAAI;EACF,OAAO,eAAe,YAAY;EAClC,MAAM,WAAW,OAAO,qBAAqB,UAAU;EACvD,MAAM,QAAQ,MAAM,OAAO,cAAc;EACzC,IAAI,OACF,OAAO,KACL,GAAG,MAAM,uCAAuC,MAAM,WACtD,OACF;EAEF,OAAO;CACT,SAAS,OAAO;EACd,OAAO,KACL,GAAG,MAAM,6BAA6B,SAAS,KAAK,KACpD,OACF;CACF;AACF;;;;;AAMA,SAAgB,uBACd,QACA,aAAqB,aAAa,SAAS,aAAa,UACxD,gBACA,QAAQ,eACY;CACpB,OAAO,OAAO,sBAAsB;EAClC;EACA,SAAS,CACP;GACE,SAAS;GACT;GACA,QACE,mBAAmB,KAAA,IACf;IAAE,MAAM;IAAW,kBAAkB;GAAK,IAC1C;IAAE,MAAM;IAAW,kBAAkB;IAAM;GAAe;EAClE,CACF;CACF,CAAC;AACH;AA0BA,SAAgB,kBACd,QACA,OACA,aAAA,MAA0C,GAC1C,UAA8B,CAAC,GAClB;CACb,MAAM,QAAQ,QAAQ,SAAS;CAC/B,MAAM,QAAQ,KAAK,IAAI,GAAG,KAAK,MAAM,KAAK,KAAK,CAAC;CAChD,MAAM,YAAY,KAAK,IAAI,GAAG,KAAK,KAAK,UAAU,IAAI,CAAC;CAKvD,MAAM,YAAY,KAAK,IAAA,KAErB,OAAO,OAAO,mCAAA,KACd,SACF;CACA,MAAM,QAAQ,KAAK,KAAK,YAAA,GAA8B,IAAA;CACtD,MAAM,SAAS,OAAO,aAAa;EACjC;EACA,MAAM,QAAQ;EACd,OAAO,aAAa,UAAU,aAAa;CAC7C,CAAC;CACD,MAAM,SACJ,QAAQ,UACR,uBAAuB,QAAQ,QAAQ,YAAY,WAAW,KAAK;CAQrE,OAAO;EACL;EACA;EACA,WAVgB,OAAO,gBAAgB;GACvC;GACA;GAGA,SAAS,CAAC;IAAE,SAAS;IAAG,UAAU;KAAE;KAAQ,QAAQ;KAAG,MAAM;IAAU;GAAE,CAAC;EAC5E,CAIU;EACR;EACA;EACA,QAAQ,QAAQ;EAChB,SAAS,IAAI,aAAc,QAAQ,IAAK,KAAK;EAC7C,UAAU;GACR,OAAO,QAAQ;EACjB;CACF;AACF;AAEA,SAAS,SAAS,OAAwB;CACxC,OAAO,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;AAC9D;;AAGA,SAAgB,8BAAoC;CAClD,YAAY;AACd;;;ACvMA,MAAMA,iBAAe;CAAE,UAAU;CAAQ,UAAU;AAAO;AAC1D,MAAM,WAAW,EAAE,MAAM,EAAI;;;AAQ7B,MAAM,0BAA0B;;;;;;;;;;;AAYhC,eAAsB,kBACpB,QACA,SACA,OACA,QACqB;CACrB,MAAM,EAAE,WAAW;CACnB,MAAM,IAAI,KAAK,IAAI,GAAG,KAAK,MAAM,KAAK,CAAC;CACvC,MAAM,IAAI,KAAK,IAAI,GAAG,KAAK,MAAM,MAAM,CAAC;CACxC,MAAM,cACJ,KAAK,KAAM,IAAI,IAAK,uBAAuB,IAAI;CACjD,MAAM,SAAS,OAAO,aAAa;EACjC,OAAO;EACP,MAAM,cAAc;EACpB,OAAOA,eAAa,WAAWA,eAAa;CAC9C,CAAC;CACD,IAAI;EACF,MAAM,UAAU,OAAO,qBAAqB,EAAE,OAAO,eAAe,CAAC;EACrE,QAAQ,oBACN,EAAE,QAAQ,GACV;GAAE;GAAQ;GAAa,cAAc;EAAE,GACvC;GAAC;GAAG;GAAG;EAAC,CACV;EACA,OAAO,MAAM,OAAO,CAAC,QAAQ,OAAO,CAAC,CAAC;EACtC,MAAM,OAAO,SAAS,SAAS,IAAI;EACnC,MAAM,SAAS,IAAI,WAAW,OAAO,eAAe,CAAC;EACrD,MAAM,SAAS,IAAI,WAAW,IAAI,IAAI,CAAC;EAGvC,KAAK,IAAI,MAAM,GAAG,MAAM,GAAG,OACzB,OAAO,IACL,OAAO,SAAS,MAAM,aAAa,MAAM,cAAc,IAAI,CAAC,GAC5D,MAAM,IAAI,CACZ;EAEF,OAAO,MAAM;EACb,OAAO;CACT,UAAU;EAER,OAAO,QAAQ;CACjB;AACF;;;AC9BA,MAAM,wBAAwB;CAAE,QAAQ;CAAQ,UAAU;AAAO;AACjE,MAAM,yBAAyB;CAC7B,UAAU;CACV,UAAU;CACV,iBAAiB;CACjB,mBAAmB;AACrB;;;;AA+CA,MAAa,wBAAwB;AAMrC,IAAIC,oBAAkB,MAAA,IACpB,MAAM,IAAI,MACR,wDAAwDA,kBAAgB,mMAG1E;;AAIF,MAAa,oBAAoB;;AAEjC,MAAa,oBAAoB;;AAEjC,MAAa,6BAA6B;;AAE1C,MAAa,mBAAmB;;AAEhC,MAAa,mBAAmB;;;;;;;;;;;AAYhC,MAAa,sBAAqC;CAChD,OAAO;EACL,WAAW;EACX,WAAW;EACX,WAAW;CACb;CACA,OAAO;EACL,WAAW;EACX,WAAW;EACX,WAAW;CACb;AACF;;;;AAKA,MAAa,iBAAgC;CAC3C,OAAO;EAAE,WAAW;EAAO,WAAW;EAAO,WAAW;CAAM;CAC9D,OAAO;EAAE,WAAW;EAAO,WAAW;EAAO,WAAW;CAAM;AAChE;;;;;;;;;AAUA,MAAa,0BAAmD,CAC9D;CACE,aAAa;CACb,UAAU;CACV,YAAY,CAAC;EAAE,gBAAgB;EAAG,QAAQ;EAAG,QAAQ;CAAY,CAAC;AACpE,GACA;CACE,aAAA;CACA,UAAU;CACV,YAAY;EACV;GAAE,gBAAgB;GAAG,QAAQ;GAAG,QAAQ;EAAY;EACpD;GAAE,gBAAgB;GAAG,QAAQ;GAAG,QAAQ;EAAY;EACpD;GAAE,gBAAgB;GAAG,QAAQ;GAAI,QAAQ;EAAU;EACnD;GAAE,gBAAgB;GAAG,QAAQ;GAAI,QAAQ;EAAY;EACrD;GAAE,gBAAgB;GAAG,QAAQ;GAAI,QAAQ;EAAU;CACrD;AACF,CACF;;AAGA,MAAM,gBAAgB;AACtB,MAAM,eAAe,gBAAgB;;;;;;;;;;;;;AAcrC,MAAa,gBAAgB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KAoCxB,kBAAkB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KAwFlB,kBAAkB;;;;;;;KAOlB,2BAA2B;;;;;;;;;;;;;;;;;;;;;;;;;;AA2BhC,MAAa,wBAAwB;;;KAGhC,iBAAiB;;;;;;;;;;;;KAYjB,iBAAiB;;;;;;;;;AAUtB,MAAM,eAAiC;AAEvC,MAAMC,gBAAwB;CAAE,GAAG;CAAG,GAAG;CAAG,GAAG;CAAG,GAAG;AAAE;AA4CvD,IAAI;AACJ,IAAI;AACJ,IAAI,gBAAkC;AAEtC,SAAS,uBACP,SACoC;CACpC,IAAI,kBAAkB,QAAQ,QAAQ;EACpC,cAAc,KAAA;EACd,iBAAiB,KAAA;EACjB,gBAAgB,QAAQ;CAC1B;CACA,IAAI,aAAa,OAAO;CACxB,cAAc,qBAAqB,OAAO,EAAE,MAAM,YAAY;EAC5D,iBAAiB;EACjB,OAAO;CACT,CAAC;CACD,OAAO;AACT;AACA,SAAS,oBACP,SACuC;CACvC,OAAO,kBAAkB,QAAQ,SAAS,iBAAiB,KAAA;AAC7D;AAEA,eAAe,qBACb,SACoC;CACpC,MAAM,EAAE,QAAQ,WAAW;CAC3B,MAAM,SAAS,MAAM,cAAc,QAAQ,eAAe,eAAe;CACzE,IAAI,CAAC,QAAQ,OAAO;CACpB,MAAM,gBAAgB,MAAM,cAC1B,QACA,uBACA,sBACF;CACA,IAAI,CAAC,eAAe,OAAO;CAC3B,MAAM,gBAAgB,uBACpB,QACA,aAAa,SAAS,aAAa,UACnC,cACA,qBACF;CACA,MAAM,gBAAgB,OAAO,sBAAsB;EACjD,OAAO;EACP,SAAS;GACP;IAAE,SAAS;IAAG,YAAY,aAAa;IAAU,SAAS,CAAC;GAAE;GAC7D;IAAE,SAAS;IAAG,YAAY,aAAa;IAAU,SAAS,CAAC;GAAE;GAC7D;IAAE,SAAS;IAAG,YAAY,aAAa;IAAU,SAAS,CAAC;GAAE;GAC7D;IAAE,SAAS;IAAG,YAAY,aAAa;IAAU,SAAS,CAAC;GAAE;GAC7D;IAAE,SAAS;IAAG,YAAY,aAAa;IAAU,SAAS,CAAC;GAAE;GAC7D;IAAE,SAAS;IAAG,YAAY,aAAa;IAAU,SAAS,CAAC;GAAE;EAC/D;CACF,CAAC;CACD,MAAM,cAAc,OAAO,sBAAsB;EAC/C,OAAO;EACP,SAAS,CAAC;GAAE,SAAS;GAAG,YAAY,aAAa;GAAU,SAAS,CAAC;EAAE,CAAC;CAC1E,CAAC;CACD,MAAM,iBAAiB,OAAO,qBAAqB;EACjD,OAAO;EACP,kBAAkB,CAAC,eAAe,aAAa;CACjD,CAAC;CACD,MAAM,wBAAwB,OAAO,qBAAqB;EACxD,OAAO;EACP,kBAAkB,CAAC,WAAW;CAChC,CAAC;CACD,MAAM,SAAS,MAAM,eACnB,QACA,2BAA2B,QAAQ,gBAAgB,QAAQ,KAAK,GAChE,eACF;CACA,IAAI,CAAC,QAAQ,OAAO;CACpB,MAAM,aAAa,MAAM,eACvB,QACA,2BAA2B,QAAQ,gBAAgB,cAAc,IAAI,GACrE,wBACF;CACA,IAAI,CAAC,YAAY,OAAO;CACxB,MAAM,UAAU,MAAM,eACpB,QACA,0BAA0B,eAAe,uBAAuB,MAAM,GACtE,sBACF;CACA,IAAI,CAAC,SAAS,OAAO;CACrB,MAAM,UAAU,OAAO,aAAa;EAClC,OAAO;EACP,MAAM;EACN,OAAO,sBAAsB,SAAS,sBAAsB;CAC9D,CAAC;CACD,OAAO,MAAM,YACX,SACA,GACA,IAAI,aAAa;EAAC;EAAM;EAAM;EAAK;EAAM;EAAM;EAAK;EAAK;CAAG,CAAC,CAC/D;CACA,MAAM,cAAc,OAAO,cAAc;EACvC,OAAO;EACP,MAAM;GAAC;GAAG;GAAG;EAAC;EACd,QAAQ;EACR,OACE,uBAAuB,kBAAkB,uBAAuB;CACpE,CAAC;CACD,OAAO,MAAM,aACX,EAAE,SAAS,YAAY,GACvB,IAAI,WAAW;EAAC;EAAK;EAAK;EAAK;CAAG,CAAC,GACnC;EAAE,aAAa;EAAG,cAAc;CAAE,GAClC;EAAC;EAAG;EAAG;CAAC,CACV;CACA,OAAO;EACL;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA,iBAAiB,YAAY,WAAW;EACxC,oBAAoB,OAAO,cAAc;GACvC,OAAO;GACP,WAAW;GACX,WAAW;EACb,CAAC;EACD,OAAO;EACP,SAAS;CACX;AACF;AAEA,SAAS,2BACP,QACA,QACA,QACA,UAC6B;CAC7B,OAAO;EACL,OAAO,WAAW,2BAA2B;EAC7C;EACA,QAAQ;GACN;GACA,YAAY;GACZ,SAAS;EACX;EACA,UAAU;GACR;GACA,YAAY,WAAW,6BAA6B;GACpD,SAAS,CACP;IAAE;IAAQ,OAAO,WAAW,iBAAiB;GAAoB,CACnE;EACF;EACA,WAAW,EAAE,UAAU,iBAAiB;CAC1C;AACF;AACA,SAAS,0BACP,QACA,QACA,QAC6B;CAC7B,OAAO;EACL,OAAO;EACP;EACA,QAAQ;GAAE;GAAQ,YAAY;EAAiB;EAC/C,UAAU;GAAE;GAAQ,YAAY;GAAkB,SAAS,CAAC,EAAE,OAAO,CAAC;EAAE;EACxE,WAAW,EAAE,UAAU,iBAAiB;CAC1C;AACF;;AAGA,eAAsB,6BACpB,SACwC;CACxC,MAAM,UAAU,MAAM,uBAAuB,OAAO;CACpD,OAAO,UAAU,aAAa,SAAS,OAAO,IAAI;AACpD;AACA,SAAgB,2BACd,SAC2C;CAC3C,MAAM,UAAU,oBAAoB,OAAO;CAC3C,OAAO,YAAY,KAAA,IACf,KAAA,IACA,YAAY,OACV,OACA,aAAa,SAAS,OAAO;AACrC;AAmBA,SAAS,aACP,SACA,SACwB;CACxB,MAAM,EAAE,WAAW;CACnB,IAAI,UAAoC;CACxC,IAAI,WAAW;CACf,IAAI,WAAW;CACf,IAAI,cAAc;CAClB,MAAM,sBAAsB;EAC1B,IAAI,CAAC,SAAS;GACZ,UAAU,OAAO,qBAAqB,EAAE,OAAO,gBAAgB,CAAC;GAChE,WAAW;EACb;EACA,OAAO;CACT;CACA,MAAM,cAAc;EAClB,IAAI,WAAW,UAAU;GACvB,OAAO,MAAM,OAAO,CAAC,QAAQ,OAAO,CAAC,CAAC;GACtC;EACF;EACA,UAAU;EACV,WAAW;EACX,WAAW;CACb;CACA,MAAM,sBAAsB;EAC1B,IAAI,UAAU,MAAM;CACtB;CACA,OAAO;EACL,cAAc,SAAS;GACrB,MAAM,OAAO,kBAAkB,QAAQ,GAAG,eAAe;IACvD,OAAO;IACP,QAAQ,QAAQ;GAClB,CAAC;GACD,MAAM,QAAQ,OAAO,OAAO,SAAS;IACnC;IACA,OAAO,IAAI,YAAY,KAAK,QAAQ,MAAM;IAC1C,WAAW;IACX,eAAe;IACf,WAAW;IACX,YAAY;KAAC;KAAM;KAAM;IAAI;IAC7B,gBAAgB;IAChB,wBAAwB;GAC1B,CAAC;GACD,MAAM,yBACJ,QAAQ,0BAA0B;IAChC,MAAM,iBAAiB;GACzB,CAAC,KAAK;GACR,OAAO;EACT;EACA,eAAe,OAAO;GACpB,MAAM,yBAAyB;GAC/B,MAAM,WAAW,QAAQ;GACzB,MAAM,KAAK,QAAQ;GACnB,MAAM,YAAY;EACpB;EACA,aAAa;GACX,IAAI,CAAC,SAAS;IACZ,UAAU,OAAO,qBAAqB,EAAE,OAAO,gBAAgB,CAAC;IAChE,WAAW;IACX,WAAW;GACb;EACF;EACA,UAAU;EACV,MAAM,OAAO;GACX,MAAM,OAAO,YAAY,KAAK;GAC9B,IAAI,MAAM;IAYR,cAX2B,EAAE,gBAAgB;KAC3C,OAAO;KACP,kBAAkB,CAChB;MACE;MACA,YAAYA;MACZ,QAAQ;MACR,SAAS;KACX,CACF;IACF,CACG,EAAE,IAAI;IACT,WAAW;GACb;GACA,cAAc;EAChB;EACA,KAAK,OAAO,QAAQ,MAAM;GACxB,MAAM,OAAO,YAAY,KAAK;GAC9B,IAAI,MAAM;IACR,cAAc,SAAS,cAAc,GAAG,OAAO,MAAM,QAAQ,IAAI;IACjE,WAAW;GACb;GACA,cAAc;EAChB;EACA,eAAe;EACf,iBAAiB,OAAO,QAAQ,SAC9B,qBAAqB,SAAS,SAAS,OAAO,QAAQ,IAAI;CAC9D;AACF;;;;AAKA,SAAS,YAAY,OAA0D;CAC7E,IAAI;EACF,OAAO,MAAM,QAAQ,kBAAkB,EAAE,WAAW;CACtD,QAAQ;EACN,OAAO;CACT;AACF;;;;AAKA,SAAS,cACP,SACA,SACA,OACA,MACA,QACA,MACA,UAAU,OACJ;CACN,MAAM,IAAI,KAAK,IAAI,GAAG,KAAK,MAAM,KAAK,KAAK,CAAC;CAC5C,MAAM,IAAI,KAAK,IAAI,GAAG,KAAK,MAAM,KAAK,MAAM,CAAC;CAC7C,YAAY,SAAS,OAAO,GAAG,GAAG,IAAI;CACtC,gBAAgB,SAAS,OAAO,MAAM;CACtC,MAAM,WAAW,gBAAgB,SAAS,KAAK;CAC/C,MAAM,WAAW,KAAK,cAAc;CAGpC,MAAM,YAAY,UAAU,eAAe,OAAO,IAAI;CAEtD,IAAI,UAAU;EACZ,MAAM,QAAQ,YAAY,SAAS,GAAG,CAAC;EAGvC,MAAM,YAAY,QAAQ,gBAAgB;GACxC,OAAO;GACP,kBAAkB,CAChB;IACE,MAAM,MAAM;IACZ,YAAYA;IACZ,QAAQ;IACR,SAAS;GACX,CACF;EACF,CAAC;EAGD,UAAU,YAAY,GAAG,GAAG,GAAG,GAAG,GAAG,CAAC;EACtC,UAAU,eAAe,GAAG,GAAG,GAAG,CAAC;EACnC,UAAU,YAAY,QAAQ,UAAU;EACxC,UAAU,aAAa,GAAG,MAAM,KAAK,WAAW,CAAC,CAAC,CAAC;EACnD,UAAU,aAAa,GAAG,QAAQ;EAClC,UAAU,gBAAgB,GAAG,QAAQ,OAAO;EAC5C,UAAU,gBAAgB,GAAG,MAAM,SAAS;EAC5C,UAAU,KAAK,GAAG,OAAO,KAAK;EAC9B,UAAU,IAAI;EAEd,MAAM,cAAc,QAAQ,gBAAgB;GAC1C,OAAO;GACP,kBAAkB,CAChB;IAAE;IAAM,YAAYA;IAAa,QAAQ;IAAS,SAAS;GAAQ,CACrE;EACF,CAAC;EACD,YAAY,YAAY,UAAU,OAAO;EACzC,YAAY,aAAa,GAAG,MAAM,SAAS;EAC3C,YAAY,KAAK,GAAG,CAAC;EACrB,YAAY,IAAI;EAChB;CACF;CAEA,MAAM,OAAO,QAAQ,gBAAgB;EACnC,OAAO;EACP,kBAAkB,CAChB;GAAE;GAAM,YAAYA;GAAa,QAAQ;GAAS,SAAS;EAAQ,CACrE;CACF,CAAC;CACD,KAAK,YAAY,UAAU,MAAM;CACjC,KAAK,aAAa,GAAG,MAAM,KAAK,WAAW,CAAC,CAAC,CAAC;CAC9C,KAAK,aAAa,GAAG,QAAQ;CAC7B,KAAK,gBAAgB,GAAG,QAAQ,OAAO;CACvC,KAAK,gBAAgB,GAAG,MAAM,SAAS;CACvC,KAAK,KAAK,GAAG,OAAO,KAAK;CACzB,KAAK,IAAI;AACX;;;;AAKA,SAAS,YACP,SACA,OACA,GACA,GACA,MACM;CACN,MAAM,WAAW,QAAQ,KAAK,YAAY,MAAM,SAAS,MAAM;CAC/D,MAAM,CAAC,SAAS,WAAW,aAAa,UAAU,KAAK,SAAS,KAAK,OAAO;CAC5E,MAAM,QAAQ,KAAK,SAAS;CAC5B,MAAM,SAAS,MAAM,KAAK;CAC1B,MAAM,QAAQ,MAAM;CACpB,OAAO,KAAK;CACZ,OAAO,KAAK;CACZ,OAAO,KAAK;CACZ,OAAO,KAAK;CACZ,OAAO,KAAK,QAAQ,MAAM,YAAY;CACtC,OAAO,KAAK,QAAQ,MAAM,WAAW;CACrC,MAAM,KAAK,WAAW,IAAI;CAC1B,MAAM,KAAK,KAAK,cAAc,MAAM,SAAS,MAAM,IAAI;CACvD,MAAM,KAAK,KAAK,eAAe,IAAI;CACnC,MAAM,KAAK,QAAQ,IAAI;CACvB,MAAM,MAAM,KAAK,eAAe,MAAM,SAAS,OAAO,IAAI;CAC1D,MAAM,MAAM,KAAK,UAAU,IAAI;CAC/B,QAAQ,OAAO,MAAM,YACnB,MAAM,KAAK,QACX,GACA,QACA,GACA,aACF;AACF;;;AAIA,SAAS,gBACP,SACA,OACA,QACM;CACN,MAAM,QAAQ,KAAK,IAAA,IAEjB,OAAO,QAAA,EACT;CACA,IAAI,CAAC,MAAM,aAAa,QAAQ,MAAM,eAAe;EACnD,MAAM,WAAW,QAAQ;EACzB,MAAM,YAAY,QAAQ,OAAO,aAAa;GAC5C,OAAO;GACP,MAAM;GACN,OAAO,sBAAsB,SAAS,sBAAsB;EAC9D,CAAC;EACD,MAAM,gBAAgB;CACxB;CACA,IAAI,OAAO,SAAS,GAAG;CAIvB,QAAQ,OAAO,MAAM,YACnB,MAAM,WACN,GACA,OAAO,KAAK,QACZ,OAAO,KAAK,YACZ,OAAO,QAAA,EACT;AACF;;;AAIA,SAAS,gBACP,SACA,OACc;CACd,MAAM,aAAa,MAAM,SAAS,QAAQ,QAAQ,QAAQ;CAC1D,MAAM,UAAU,MAAM,SAAS,KAAK,QAAQ,QAAQ;CACpD,MAAM,WAAW,MAAM,SAAS,MAAM,QAAQ,QAAQ;CACtD,MAAM,QACJ,MAAM,WAAW,OAAO,cACxB,MAAM,WAAW,OAAO,WACxB,MAAM,WAAW,OAAO;CAC1B,IAAI,MAAM,aAAa,CAAC,MAAM,kBAAkB,CAAC,OAC/C,OAAO,MAAM;CACf,MAAM,YAAY,QAAQ,OAAO,gBAAgB;EAC/C,OAAO;EACP,QAAQ,QAAQ;EAChB,SAAS;GACP;IAAE,SAAS;IAAG,UAAU;GAAW;GACnC;IACE,SAAS;IACT,UAAU,MAAM,SAAS,QAAQ,WAAW,QAAQ;GACtD;GACA;IAAE,SAAS;IAAG,UAAU;GAAQ;GAChC;IACE,SAAS;IACT,UAAU,MAAM,SAAS,KAAK,WAAW,QAAQ;GACnD;GACA;IAAE,SAAS;IAAG,UAAU;GAAS;GACjC;IACE,SAAS;IACT,UAAU,MAAM,SAAS,MAAM,WAAW,QAAQ;GACpD;EACF;CACF,CAAC;CACD,MAAM,aAAa;EAAC;EAAY;EAAS;CAAQ;CACjD,MAAM,iBAAiB;CACvB,OAAO,MAAM;AACf;;;AAIA,SAAS,YACP,SACA,GACA,GACa;CACb,MAAM,UAAU,QAAQ;CACxB,IAAI,WAAW,QAAQ,SAAS,KAAK,QAAQ,UAAU,GAAG,OAAO;CACjE,MAAM,QAAQ,KAAK,IAAI,SAAS,SAAS,GAAG,CAAC;CAC7C,MAAM,SAAS,KAAK,IAAI,SAAS,UAAU,GAAG,CAAC;CAC/C,SAAS,QAAQ,QAAQ;CACzB,MAAM,UAAU,QAAQ,OAAO,cAAc;EAC3C,OAAO;EACP,MAAM;GAAC;GAAO;GAAQ;EAAC;EACvB,QAAQ;EACR,OACE,uBAAuB,oBACvB,uBAAuB;CAC3B,CAAC;CACD,MAAM,OAAO,QAAQ,WAAW;CAChC,MAAM,QAAqB;EACzB;EACA;EACA,WAAW,QAAQ,OAAO,gBAAgB;GACxC,OAAO;GACP,QAAQ,QAAQ;GAChB,SAAS,CAAC;IAAE,SAAS;IAAG,UAAU;GAAK,CAAC;EAC1C,CAAC;EACD;EACA;CACF;CACA,QAAQ,QAAQ;CAChB,OAAO;AACT;AAEA,SAAS,eAAe,SAGtB;CACA,MAAM,UAAU,QAAQ;CACxB,IAAI,CAAC,SAEH,MAAM,IAAI,MAAM,0CAA0C;CAE5D,OAAO;AACT;;;;;;;;;;;;;;;AAgBA,eAAe,qBACb,SACA,SACA,OACA,QACA,MAC4B;CAC5B,MAAM,IAAI,KAAK,IAAI,GAAG,KAAK,MAAM,KAAK,KAAK,CAAC;CAC5C,MAAM,IAAI,KAAK,IAAI,GAAG,KAAK,MAAM,KAAK,MAAM,CAAC;CAC7C,IAAI,CAAC,QAAQ,SAAS;EACpB,MAAM,SAAS,MAAM,eACnB,QAAQ,QACR,2BACE,QAAQ,QACR,QAAQ,gBACR,cACA,KACF,GACA,uBACF;EACA,MAAM,UAAU,MAAM,eACpB,QAAQ,QACR,0BACE,QAAQ,eACR,QAAQ,uBACR,YACF,GACA,8BACF;EACA,IAAI,CAAC,UAAU,CAAC,SAAS,OAAO;EAChC,QAAQ,UAAU;GAAE;GAAQ;EAAQ;CACtC;CACA,MAAM,SAAS,QAAQ,OAAO,cAAc;EAC1C,OAAO;EACP,MAAM;GAAC;GAAG;GAAG;EAAC;EACd,QAAQ;EACR,OACE,uBAAuB,oBACvB,uBAAuB;CAC3B,CAAC;CACD,IAAI;EACF,MAAM,UAAU,QAAQ,OAAO,qBAAqB,EAClD,OAAO,uBACT,CAAC;EACD,cACE,SACA,SACA,OACA,OAAO,WAAW,GAClB,QACA;GAAE,GAAG;GAAM,OAAO;GAAG,QAAQ;EAAE,GAC/B,IACF;EACA,QAAQ,OAAO,MAAM,OAAO,CAAC,QAAQ,OAAO,CAAC,CAAC;EAC9C,OAAO,OACL,QAAQ,uBACN,SAAS,OAAO,WAChB,kBAAkB,EAAE,QAAQ,QAAQ,OAAO,GAAG,SAAS,OAAO,MAAM,IACtE,QAAQ,GAAG,CAAC;CAChB,QAAQ;EAGN,QAAQ,kBAAkB;EAC1B,OAAO;CACT,UAAU;EACR,OAAO,QAAQ;CACjB;AACF;;AAGA,SAAgB,sCAA4C;CAC1D,cAAc,KAAA;CACd,iBAAiB,KAAA;CACjB,gBAAgB;AAClB;;;;ACliCA,MAAM,gBAAgB;CAAE,UAAU;CAAM,mBAAmB;AAAK;AAChE,MAAM,cAAwB;CAAE,GAAG;CAAG,GAAG;CAAG,GAAG;CAAG,GAAG;AAAE;AAEvD,SAAgB,kCACd,QACA,OACA,SACoB;CACpB,OAAO,OAAO,sBAAsB;EAAE;EAAO,SAAS,MAAM,KAAK,OAAO;CAAE,CAAC;AAC7E;AAEA,SAAgB,gCACd,QACA,OACA,MACW;CACX,OAAO,OAAO,aAAa;EAAE;EAAO;EAAM,OAAO;CAAgB,CAAC;AACpE;AAEA,SAAgB,4BACd,QACA,OACA,QACA,SACc;CACd,OAAO,OAAO,gBAAgB;EAC5B;EACA;EACA,SAAS,MAAM,KAAK,OAAO;CAC7B,CAAC;AACH;;AAcA,IAAa,uBAAb,MAAkC;CAKH;CAJ7B,UAA4C;CAC5C,WAAmB;CACnB,WAAmB;CACnB,cAAsB;CACtB,YAAY,QAAoC;EAAnB,KAAA,SAAA;CAAoB;CACjD,aAAmB;EACjB,IAAI,KAAK,SAAS;EAClB,KAAK,UAAU,KAAK,OAAO,qBAAqB,EAAE,OAAO,cAAc,CAAC;EACxE,KAAK,WAAW;EAChB,KAAK,WAAW;CAClB;CACA,WAAiB;EACf,KAAK,MAAM;CACb;CACA,UAAkB;EAChB,OAAO,KAAK;CACd;CACA,KAAK,MAAiC;EACpC,MAAM,SAAS,KAAK,UAAU,KAAK,YAAY,KAAK,OAAO;EAC3D,IAAI,CAAC,QAAQ,OAAO;EACpB,MAAM,UAAU,KAAK,cAAc;EACnC,KAAK,OAAO,MAAM,YAChB,KAAK,eACL,GACA,KAAK,cACL,GACA,KAAK,aAAa,UACpB;EACA,MAAM,OAAO,QAAQ,gBAAgB;GACnC,OAAO;GACP,kBAAkB,CAChB;IACE,MAAM;IACN,YAAY;IACZ,QAAQ;IACR,SAAS;GACX,CACF;EACF,CAAC;EACD,KAAK,YAAY,GAAG,GAAG,KAAK,OAAO,KAAK,QAAQ,GAAG,CAAC;EACpD,KAAK,YAAY,KAAK,QAAQ;EAC9B,KAAK,aAAa,GAAG,KAAK,SAAS;EACnC,KAAK,KAAK,GAAG,CAAC;EACd,KAAK,IAAI;EACT,KAAK,WAAW;EAChB,IAAI,KAAK,UAAU,KAAK,MAAM;EAC9B,OAAO;CACT;CACA,MAAM,QACJ,MAS4B;EAC5B,MAAM,SAAS,KAAK,OAAO,cAAc;GACvC,OAAO;GACP,MAAM;IAAC,KAAK;IAAO,KAAK;IAAQ;GAAC;GACjC,QAAQ;GACR,OAAO,cAAc,oBAAoB,cAAc;EACzD,CAAC;EACD,IAAI;GACF,MAAM,UAAU,KAAK,OAAO,qBAAqB,EAC/C,OAAO,qBACT,CAAC;GACD,KAAK,OAAO,MAAM,YAChB,KAAK,eACL,GACA,KAAK,cACL,GACA,KAAK,aAAa,UACpB;GACA,MAAM,OAAO,QAAQ,gBAAgB;IACnC,OAAO;IACP,kBAAkB,CAChB;KACE,MAAM,OAAO,WAAW;KACxB,YAAY;KACZ,QAAQ;KACR,SAAS;IACX,CACF;GACF,CAAC;GACD,KAAK,YAAY,GAAG,GAAG,KAAK,OAAO,KAAK,QAAQ,GAAG,CAAC;GACpD,KAAK,YAAY,KAAK,QAAQ;GAC9B,KAAK,aAAa,GAAG,KAAK,SAAS;GACnC,KAAK,KAAK,GAAG,CAAC;GACd,KAAK,IAAI;GACT,KAAK,OAAO,MAAM,OAAO,CAAC,QAAQ,OAAO,CAAC,CAAC;GAC3C,OAAO,MAAM,KAAK,KAAK,QAAQ,KAAK,OAAO,KAAK,MAAM;EACxD,QAAQ;GACN,OAAO;EACT,UAAU;GACR,OAAO,QAAQ;EACjB;CACF;CACA,gBAA2C;EACzC,IAAI,CAAC,KAAK,SAAS;GACjB,KAAK,UAAU,KAAK,OAAO,qBAAqB,EAAE,OAAO,cAAc,CAAC;GACxE,KAAK,WAAW;EAClB;EACA,OAAO,KAAK;CACd;CACA,YACE,SACuB;EACvB,IAAI;GACF,OAAO,SAAS,kBAAkB,EAAE,WAAW,KAAK;EACtD,QAAQ;GACN,OAAO;EACT;CACF;CACA,QAAsB;EACpB,IAAI,KAAK,WAAW,KAAK,UAAU;GACjC,KAAK,OAAO,MAAM,OAAO,CAAC,KAAK,QAAQ,OAAO,CAAC,CAAC;GAChD,KAAK;EACP;EACA,KAAK,UAAU;EACf,KAAK,WAAW;EAChB,KAAK,WAAW;CAClB;AACF;;;;;ACvGA,MAAM,qBAAqB;;AAG3B,MAAM,sBAAsB;;AAG5B,MAAM,aAAqC;CACzC,KAAK;CACL,KAAK;CACL,KAAK;CACL,OAAO;CACP,OAAO;CACP,OAAO;CACP,OAAO;CACP,OAAO;CACP,OAAO;AACT;;AAGA,MAAM,gBAAgB,IAAI,IAAI;CAAC;CAAO;CAAS;CAAS;AAAO,CAAC;;AAWhE,SAAgB,qBAAqB,WAAmC;CACtE,MAAM,OAAO,KAAK,IAAI,GAAG,KAAK,KAAK,YAAY,CAAC,IAAI,CAAC;CACrD,MAAM,QAAQ,IAAI,YAAY,IAAI;CAClC,OAAO;EACL;EACA,QAAQ,IAAI,aAAa,KAAK;EAC9B,MAAM,IAAI,WAAW,KAAK;CAC5B;AACF;;;;;;;;;AAUA,SAAgB,mBACd,QACA,QACA,UAA0B,qBAAqB,OAAO,sBAAsB,GAC5D;CAChB,QAAQ,OAAO,KAAK,CAAC;CAErB,MAAM,EAAE,mBAAmB;CAC3B,YAAY,SAAS,eAAe,OAAO,OAAO,OAAO,CAAC;CAC1D,YAAY,SAAS,eAAe,UAAU,OAAO,UAAU,CAAC;CAChE,IAAI,eAAe,SAAS,KAAA,GAC1B,QAAQ,OAAO,eAAe,OAAO,KAAK,OAAO,QAAQ;CAE3D,YACE,SACA,eAAe,kBACf,OAAO,kBACP,CACF;CACA,YAAY,SAAS,eAAe,UAAU,OAAO,UAAU,CAAC;CAChE,YAAY,SAAS,eAAe,cAAc,OAAO,cAAc,CAAC;CACxE,YAAY,SAAS,eAAe,YAAY,OAAO,YAAY,CAAC;CAEpE,MAAM,SAAS,OAAO,UAAU,CAAC;CACjC,KAAK,MAAM,SAAS,OAAO,UAIzB,kBAAkB,SAAS,OADf,OAAO,MAAM,SAAS,MAAM,SACD,OAAO,aAAa,MAAM,KAAK;CAExE,OAAO;AACT;AAEA,SAAS,kBACP,SACA,OACA,KACA,WACM;CACN,MAAM,aAAa,WAAW,MAAM;CAKpC,IAAI,eAAe,KAAA,GAAW;CAC9B,MAAM,SAAS,cAAc,IAAI,MAAM,IAAI,IAAI,QAAQ,OAAO,QAAQ;CACtE,MAAM,OAAO,MAAM,cAAc;CAEjC,IAAI,MAAM,gBAAgB,KAAA,GAAW;EACnC,IAAI,eAAe,GAAG;GACpB,OAAO,QAAQ,SAAS,KAAK,MAAM,SAAS;GAC5C;EACF;EACA,MAAM,OAAO,MAAM,QAAQ,GAAG,IAAI,MAAM,CAAC;EACzC,KAAK,IAAI,IAAI,GAAG,IAAI,YAAY,KAAK,OAAO,OAAO,KAAK,KAAK,MAAM;EACnE;CACF;CAMA,MAAM,OAAO,qBAAqB,KAAK,OAAO,SAAS;CACvD,KAAK,IAAI,UAAU,GAAG,UAAU,MAAM,aAAa,WAAW;EAC5D,MAAM,OAAO,OAAO,UAAU;EAC9B,KAAK,IAAI,IAAI,GAAG,IAAI,YAAY,KAC9B,OAAO,OAAO,KAAK,KAAK,UAAU,aAAa,MAAM;CAEzD;AACF;AAIA,SAAS,SACP,KACA,WACQ;CACR,MAAM,QACJ,OAAO,QAAQ,WAAW,MAAM,MAAM,QAAQ,GAAG,IAAK,IAAI,MAAM,IAAK;CACvE,IAAI,cAAc,QAAQ,OAAO,QAAQ,IAAI;CAC7C,IAAI,cAAc,OAAO,OAAO,KAAK,MAAM,KAAK;CAChD,OAAO;AACT;AAKA,SAAS,qBACP,KACA,OACA,WACU;CACV,MAAM,OAAO,MAAM,QAAQ,GAAG,IAAI,MAAM,OAAO,QAAQ,WAAW,CAAC,GAAG,IAAI,CAAC;CAC3E,IACE,MAAM,SAAS,WACf,cAAc,sBACd,KAAK,SAAS,MAAM,GACpB;EACA,MAAM,MAAgB,CAAC;EACvB,KAAK,IAAI,IAAI,GAAG,IAAI,KAAK,QAAQ,KAAK,GACpC,IAAI,KAAK,KAAK,MAAM,GAAG,KAAK,IAAI,MAAM,GAAG,KAAK,IAAI,MAAM,CAAC;EAE3D,OAAO;CACT;CACA,OAAO;AACT;AAEA,SAAS,YACP,SACA,aACA,QACA,OACM;CACN,IAAI,gBAAgB,KAAA,GAAW;CAC/B,MAAM,OAAO,cAAc;CAC3B,KAAK,IAAI,IAAI,GAAG,IAAI,OAAO,KAAK,QAAQ,OAAO,OAAO,KAAK,SAAS,MAAM;AAC5E;;;;ACvOA,MAAa,8BAA8B;AAQ3C,SAAgB,wBACd,QACA,OACA,QACA,OACqB;CACrB,MAAM,IAAI,KAAK,IAAI,GAAG,KAAK,MAAM,KAAK,CAAC;CACvC,MAAM,IAAI,KAAK,IAAI,GAAG,KAAK,MAAM,MAAM,CAAC;CACxC,OAAO;EACL,SAAS,OAAO,cAAc;GAC5B;GACA,MAAM;IAAC;IAAG;IAAG;GAAC;GACd,QAAQ;GACR,OAAA;EACF,CAAC;EACD,OAAO;EACP,QAAQ;CACV;AACF;AAEA,SAAgB,iBACd,QACA,QACA,QACM;CACN,OAAO,MAAM,aACX,EAAE,SAAS,OAAO,QAAQ,GAC1B,QACA;EAAE,aAAa,OAAO,QAAQ;EAAG,cAAc,OAAO;CAAO,GAC7D,CAAC,OAAO,OAAO,OAAO,MAAM,CAC9B;AACF;;AAGA,SAAgB,0BACd,QACA,QACA,QACM;CACN,OAAO,MAAM,2BACX,QACA;EAAE,SAAS,OAAO;EAAS,oBAAoB;CAAM,GACrD,CAAC,OAAO,OAAO,OAAO,MAAM,CAC9B;AACF;AAEA,SAAgB,oBACd,QACA,MACY;CACZ,MAAM,SAAwB,KAAK,UAAU,YAAY;CACzD,MAAM,UAA0B,KAAK,SAAS,WAAW;CACzD,OAAO,OAAO,cAAc;EAC1B,WAAW;EACX,WAAW;EACX,cAAc;EACd,cAAc;CAChB,CAAC;AACH;AAEA,SAAgB,qBAAqB,SAA2B;CAC9D,IAAI;EACF,QAAQ,QAAQ;CAClB,QAAQ,CAER;AACF;;;;;;;;;;;ACnEA,SAAgB,uBACd,SACA,QACA,QACM;CACN,QAAQ,UAAU;EAAE;EAAQ;EAAQ,WAAW;CAAgB,CAAC;AAClE"}
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "@godot-scene-web/canvas-effects",
3
+ "version": "0.1.0",
4
+ "license": "MIT",
5
+ "type": "module",
6
+ "description": "WebGL and WebGPU execution for portable Godot effects.",
7
+ "publishConfig": {
8
+ "access": "public",
9
+ "registry": "https://registry.npmjs.org/"
10
+ },
11
+ "repository": {
12
+ "type": "git",
13
+ "url": "git+https://github.com/tfoxy/godot-scene-web.git"
14
+ },
15
+ "homepage": "https://github.com/tfoxy/godot-scene-web#readme",
16
+ "bugs": {
17
+ "url": "https://github.com/tfoxy/godot-scene-web/issues"
18
+ },
19
+ "sideEffects": false,
20
+ "exports": {
21
+ "./webgl": {
22
+ "development": "./src/webgl.ts",
23
+ "types": "./dist/webgl.d.ts",
24
+ "import": "./dist/webgl.js"
25
+ },
26
+ "./webgpu": {
27
+ "development": "./src/webgpu.ts",
28
+ "types": "./dist/webgpu.d.ts",
29
+ "import": "./dist/webgpu.js"
30
+ }
31
+ },
32
+ "files": [
33
+ "dist",
34
+ "LICENSE"
35
+ ],
36
+ "dependencies": {
37
+ "@godot-scene-web/effects": "0.1.0"
38
+ },
39
+ "scripts": {
40
+ "build": "tsdown",
41
+ "typecheck": "tsc -p tsconfig.json --noEmit",
42
+ "test": "cd ../.. && vitest run --config vitest.config.ts packages/canvas-effects/test"
43
+ }
44
+ }