@aardworx/wombat.rendering 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.
Files changed (46) hide show
  1. package/package.json +67 -0
  2. package/src/commands/clear.ts +66 -0
  3. package/src/commands/index.ts +11 -0
  4. package/src/commands/render.ts +106 -0
  5. package/src/core/acquire.ts +22 -0
  6. package/src/core/adaptiveResource.ts +88 -0
  7. package/src/core/buffer.ts +45 -0
  8. package/src/core/bufferView.ts +26 -0
  9. package/src/core/clear.ts +16 -0
  10. package/src/core/command.ts +50 -0
  11. package/src/core/drawCall.ts +22 -0
  12. package/src/core/framebuffer.ts +37 -0
  13. package/src/core/framebufferSignature.ts +19 -0
  14. package/src/core/index.ts +116 -0
  15. package/src/core/pipelineState.ts +64 -0
  16. package/src/core/renderContext.ts +48 -0
  17. package/src/core/renderObject.ts +57 -0
  18. package/src/core/renderTask.ts +17 -0
  19. package/src/core/renderTree.ts +29 -0
  20. package/src/core/sampler.ts +12 -0
  21. package/src/core/shader.ts +28 -0
  22. package/src/core/texture.ts +63 -0
  23. package/src/index.ts +18 -0
  24. package/src/resources/adaptiveBuffer.ts +135 -0
  25. package/src/resources/adaptiveSampler.ts +82 -0
  26. package/src/resources/adaptiveTexture.ts +269 -0
  27. package/src/resources/framebuffer.ts +163 -0
  28. package/src/resources/framebufferSignature.ts +36 -0
  29. package/src/resources/index.ts +82 -0
  30. package/src/resources/mipGen.ts +148 -0
  31. package/src/resources/preparedComputeShader.ts +426 -0
  32. package/src/resources/preparedRenderObject.ts +498 -0
  33. package/src/resources/renderPipeline.ts +148 -0
  34. package/src/resources/shaderDiagnostics.ts +52 -0
  35. package/src/resources/sourceMapDecoder.ts +133 -0
  36. package/src/resources/uniformBuffer.ts +170 -0
  37. package/src/resources/webgpuFlags.ts +43 -0
  38. package/src/runtime/copy.ts +13 -0
  39. package/src/runtime/index.ts +24 -0
  40. package/src/runtime/renderTask.ts +137 -0
  41. package/src/runtime/renderTo.ts +154 -0
  42. package/src/runtime/runtime.ts +113 -0
  43. package/src/runtime/scenePass.ts +273 -0
  44. package/src/window/canvas.ts +252 -0
  45. package/src/window/index.ts +14 -0
  46. package/src/window/loop.ts +62 -0
@@ -0,0 +1,116 @@
1
+ // Public API of @aardworx/wombat.rendering-core.
2
+ //
3
+ // Phase 0: types only. Implementations of `compileEffect`,
4
+ // `prepareRenderObject`, `runRenderTask`, etc. live in higher
5
+ // packages (`resources`, `commands`, `runtime`).
6
+
7
+ export {
8
+ IBuffer,
9
+ } from "./buffer.js";
10
+ export type {
11
+ HostBufferSource,
12
+ } from "./buffer.js";
13
+
14
+ export type {
15
+ BufferView,
16
+ } from "./bufferView.js";
17
+
18
+ export type {
19
+ ClearColor,
20
+ ClearValues,
21
+ } from "./clear.js";
22
+
23
+ export type {
24
+ Command,
25
+ CopySpec,
26
+ BufferCopy,
27
+ TextureCopy,
28
+ BufferCopyRange,
29
+ } from "./command.js";
30
+
31
+ export type {
32
+ DrawCall,
33
+ IndexedDrawCall,
34
+ NonIndexedDrawCall,
35
+ } from "./drawCall.js";
36
+
37
+ export type {
38
+ IFramebuffer,
39
+ } from "./framebuffer.js";
40
+
41
+ export type {
42
+ DepthStencilAttachmentSignature,
43
+ FramebufferSignature,
44
+ } from "./framebufferSignature.js";
45
+
46
+ export type {
47
+ BlendComponentState,
48
+ BlendState,
49
+ CullMode,
50
+ DepthState,
51
+ FrontFace,
52
+ PipelineState,
53
+ RasterizerState,
54
+ StencilFaceState,
55
+ StencilState,
56
+ Topology,
57
+ } from "./pipelineState.js";
58
+
59
+ export type {
60
+ RenderObject,
61
+ } from "./renderObject.js";
62
+
63
+ export {
64
+ RenderTree,
65
+ } from "./renderTree.js";
66
+
67
+ export type {
68
+ IRenderTask,
69
+ } from "./renderTask.js";
70
+
71
+ export {
72
+ ISampler,
73
+ } from "./sampler.js";
74
+
75
+ export {
76
+ ITexture,
77
+ } from "./texture.js";
78
+ export type {
79
+ ExternalTextureSource,
80
+ HostTextureSource,
81
+ RawTextureSource,
82
+ } from "./texture.js";
83
+
84
+ export type {
85
+ AttributeInfo,
86
+ CompiledEffect,
87
+ CompiledStage,
88
+ ComputeShader,
89
+ Effect,
90
+ HoleGetter,
91
+ HoleGetters,
92
+ LooseUniformInfo,
93
+ OutputInfo,
94
+ ProgramInterface,
95
+ SamplerInfo,
96
+ Stage,
97
+ StageInfo,
98
+ StorageBufferInfo,
99
+ Target,
100
+ TextureInfo,
101
+ UniformBlockInfo,
102
+ UniformFieldInfo,
103
+ } from "./shader.js";
104
+
105
+ export {
106
+ AdaptiveResource,
107
+ } from "./adaptiveResource.js";
108
+
109
+ export {
110
+ tryAcquire,
111
+ tryRelease,
112
+ } from "./acquire.js";
113
+
114
+ export {
115
+ RenderContext,
116
+ } from "./renderContext.js";
@@ -0,0 +1,64 @@
1
+ // PipelineState — the fixed-function bits the user picks per
2
+ // RenderObject. Vertex layout, blending, depth/stencil, raster.
3
+ // Combined with a CompiledEffect + FramebufferSignature this is
4
+ // enough to bake a GPURenderPipeline.
5
+
6
+ import type { HashMap } from "@aardworx/wombat.adaptive";
7
+
8
+ export type CullMode = "none" | "front" | "back";
9
+ export type FrontFace = "ccw" | "cw";
10
+ export type Topology = GPUPrimitiveTopology;
11
+
12
+ export interface RasterizerState {
13
+ readonly topology: Topology;
14
+ readonly cullMode: CullMode;
15
+ readonly frontFace: FrontFace;
16
+ /** "none" disables polygon offset. */
17
+ readonly depthBias?: { readonly constant: number; readonly slopeScale: number; readonly clamp: number };
18
+ }
19
+
20
+ export interface DepthState {
21
+ readonly write: boolean;
22
+ readonly compare: GPUCompareFunction;
23
+ }
24
+
25
+ export interface StencilFaceState {
26
+ readonly compare: GPUCompareFunction;
27
+ readonly failOp: GPUStencilOperation;
28
+ readonly depthFailOp: GPUStencilOperation;
29
+ readonly passOp: GPUStencilOperation;
30
+ }
31
+
32
+ export interface StencilState {
33
+ readonly readMask: number;
34
+ readonly writeMask: number;
35
+ readonly front: StencilFaceState;
36
+ readonly back: StencilFaceState;
37
+ }
38
+
39
+ export interface BlendComponentState {
40
+ readonly operation: GPUBlendOperation;
41
+ readonly srcFactor: GPUBlendFactor;
42
+ readonly dstFactor: GPUBlendFactor;
43
+ }
44
+
45
+ export interface BlendState {
46
+ readonly color: BlendComponentState;
47
+ readonly alpha: BlendComponentState;
48
+ /** RGBA channel write mask (bitmask: R=1, G=2, B=4, A=8). */
49
+ readonly writeMask: number;
50
+ }
51
+
52
+ export interface PipelineState {
53
+ readonly rasterizer: RasterizerState;
54
+ readonly depth?: DepthState;
55
+ readonly stencil?: StencilState;
56
+ /**
57
+ * Per-color-attachment blend state, keyed by the attachment name
58
+ * from the `FramebufferSignature`. Missing entries default to
59
+ * "no blending, write all channels".
60
+ */
61
+ readonly blends?: HashMap<string, BlendState>;
62
+ /** Multi-sample alpha-to-coverage. Default `false`. */
63
+ readonly alphaToCoverage?: boolean;
64
+ }
@@ -0,0 +1,48 @@
1
+ // RenderContext — ambient state available to AdaptiveResources
2
+ // during evaluation. The runtime sets `encoder` before walking a
3
+ // task's resources so that `compute(token)` calls inside an
4
+ // AdaptiveResource can encode commands (uploads, compute passes,
5
+ // render-to-texture) onto the current frame's encoder.
6
+ //
7
+ // Outside a frame (`encoder === null`) AdaptiveResources should
8
+ // either return their cached handle or throw — they cannot do
9
+ // GPU work without an encoder.
10
+
11
+ interface RenderContextSlot {
12
+ encoder: GPUCommandEncoder | null;
13
+ withEncoder<R>(encoder: GPUCommandEncoder, fn: () => R): R;
14
+ requireEncoder(): GPUCommandEncoder;
15
+ }
16
+
17
+ export const RenderContext: RenderContextSlot = {
18
+ encoder: null,
19
+
20
+ /**
21
+ * Run `fn` with `encoder` exposed as the current frame encoder.
22
+ * Restores the previous value on exit. Nestable.
23
+ */
24
+ withEncoder<R>(encoder: GPUCommandEncoder, fn: () => R): R {
25
+ const prev = RenderContext.encoder;
26
+ RenderContext.encoder = encoder;
27
+ try {
28
+ return fn();
29
+ } finally {
30
+ RenderContext.encoder = prev;
31
+ }
32
+ },
33
+
34
+ /**
35
+ * Convenience: throws if no encoder is active. Use inside
36
+ * `compute(token)` of an AdaptiveResource that genuinely needs
37
+ * to encode work.
38
+ */
39
+ requireEncoder(): GPUCommandEncoder {
40
+ if (RenderContext.encoder === null) {
41
+ throw new Error(
42
+ "RenderContext: no active encoder. AdaptiveResource.compute() " +
43
+ "was called outside RenderContext.withEncoder(...).",
44
+ );
45
+ }
46
+ return RenderContext.encoder;
47
+ },
48
+ };
@@ -0,0 +1,57 @@
1
+ // RenderObject — the user-facing primitive: an effect + a bag of
2
+ // named inputs + a draw call. Names are matched against the
3
+ // shader's ProgramInterface during preparation; extras are
4
+ // silently ignored, missing required bindings are an error.
5
+ //
6
+ // Every input is `aval<T>` over a *source* type (IBuffer, ITexture,
7
+ // ISampler, …). The runtime (resources package) wraps these into
8
+ // internal AdaptiveResources for upload + ref-counting; the user
9
+ // never instantiates an AdaptiveResource directly.
10
+
11
+ import type { HashMap, aval } from "@aardworx/wombat.adaptive";
12
+ import type { IBuffer } from "./buffer.js";
13
+ import type { BufferView } from "./bufferView.js";
14
+ import type { DrawCall } from "./drawCall.js";
15
+ import type { ISampler } from "./sampler.js";
16
+ import type { ITexture } from "./texture.js";
17
+ import type { PipelineState } from "./pipelineState.js";
18
+ import type { Effect } from "./shader.js";
19
+
20
+ export interface RenderObject {
21
+ /**
22
+ * A wombat.shader `Effect` — produced by the `vertex(...) /
23
+ * fragment(...) / effect(...)` markers (transformed at build
24
+ * time by `@aardworx/wombat.shader-vite`) or hand-built via
25
+ * `stage(module, ...)`. The runtime calls
26
+ * `effect.compile({ target: "wgsl" })` (Effect's own
27
+ * hole-value-keyed cache makes this free per frame).
28
+ *
29
+ * `Effect.id` (stable build-time hash) feeds the pipeline cache
30
+ * key, which means renaming a captured value invalidates only
31
+ * its specialization; everything else stays cached.
32
+ *
33
+ * For users with a precompiled `CompiledEffect` (e.g. from
34
+ * `compileShaderSource(...)`), wrap it with the test helper
35
+ * `fakeEffectFromCompiled` or call `stage(module)` to produce
36
+ * a real Effect.
37
+ */
38
+ readonly effect: Effect;
39
+ readonly pipelineState: PipelineState;
40
+
41
+ /** name → vertex buffer view; e.g. "position", "normal", "uv". */
42
+ readonly vertexAttributes: HashMap<string, aval<BufferView>>;
43
+ /** name → instance buffer view; e.g. "modelMatrix", "instanceColor". */
44
+ readonly instanceAttributes?: HashMap<string, aval<BufferView>>;
45
+ /** name → uniform value; runtime packs into UBO based on shader layout. */
46
+ readonly uniforms: HashMap<string, aval<unknown>>;
47
+ /** name → texture source (CPU image or pre-built GPUTexture). */
48
+ readonly textures: HashMap<string, aval<ITexture>>;
49
+ /** name → sampler source (descriptor or pre-built GPUSampler). */
50
+ readonly samplers: HashMap<string, aval<ISampler>>;
51
+ /** name → storage buffer source. */
52
+ readonly storageBuffers?: HashMap<string, aval<IBuffer>>;
53
+
54
+ /** Index buffer for indexed draws. */
55
+ readonly indices?: aval<BufferView>;
56
+ readonly drawCall: aval<DrawCall>;
57
+ }
@@ -0,0 +1,17 @@
1
+ // RenderTask — the compiled, runnable form of an alist<Command>.
2
+ // The runtime returns one of these from `compile`; the user calls
3
+ // `run(token)` to encode + submit a frame.
4
+
5
+ import type { AdaptiveToken } from "@aardworx/wombat.adaptive";
6
+
7
+ export interface IRenderTask {
8
+ /**
9
+ * Encode all commands into the GPU queue using the current
10
+ * adaptive state read via `token`. Returns once submission has
11
+ * been queued (does not wait for GPU completion).
12
+ */
13
+ run(token: AdaptiveToken): void;
14
+
15
+ /** Tear down resources owned by the task. Idempotent. */
16
+ dispose(): void;
17
+ }
@@ -0,0 +1,29 @@
1
+ // RenderTree — intra-pass ordering structure for RenderObjects.
2
+ //
3
+ // `Ordered` children execute left-to-right; `Unordered` children
4
+ // can be reordered by the backend to minimise pipeline / bind-
5
+ // group / vertex-buffer state changes. The Adaptive variants
6
+ // fold reactive containers (alist, aset, aval) directly into the
7
+ // tree.
8
+
9
+ import type { aval, alist, aset } from "@aardworx/wombat.adaptive";
10
+ import type { RenderObject } from "./renderObject.js";
11
+
12
+ export type RenderTree =
13
+ | { readonly kind: "Empty" }
14
+ | { readonly kind: "Leaf"; readonly object: RenderObject }
15
+ | { readonly kind: "Ordered"; readonly children: readonly RenderTree[] }
16
+ | { readonly kind: "Unordered"; readonly children: readonly RenderTree[] }
17
+ | { readonly kind: "Adaptive"; readonly tree: aval<RenderTree> }
18
+ | { readonly kind: "OrderedFromList"; readonly children: alist<RenderTree> }
19
+ | { readonly kind: "UnorderedFromSet"; readonly children: aset<RenderTree> };
20
+
21
+ export const RenderTree = {
22
+ empty: { kind: "Empty" } as const satisfies RenderTree,
23
+ leaf: (object: RenderObject): RenderTree => ({ kind: "Leaf", object }),
24
+ ordered: (...children: RenderTree[]): RenderTree => ({ kind: "Ordered", children }),
25
+ unordered: (...children: RenderTree[]): RenderTree => ({ kind: "Unordered", children }),
26
+ adaptive: (tree: aval<RenderTree>): RenderTree => ({ kind: "Adaptive", tree }),
27
+ orderedFromList: (children: alist<RenderTree>): RenderTree => ({ kind: "OrderedFromList", children }),
28
+ unorderedFromSet: (children: aset<RenderTree>): RenderTree => ({ kind: "UnorderedFromSet", children }),
29
+ } as const;
@@ -0,0 +1,12 @@
1
+ // ISampler — sampler source. Either a pre-built `GPUSampler` or a
2
+ // descriptor for the runtime to materialise (samplers are cheap
3
+ // and dedupable, so descriptors are the common path).
4
+
5
+ export type ISampler =
6
+ | { readonly kind: "gpu"; readonly sampler: GPUSampler }
7
+ | { readonly kind: "desc"; readonly descriptor: GPUSamplerDescriptor };
8
+
9
+ export const ISampler = {
10
+ fromGPU(sampler: GPUSampler): ISampler { return { kind: "gpu", sampler }; },
11
+ fromDescriptor(descriptor: GPUSamplerDescriptor): ISampler { return { kind: "desc", descriptor }; },
12
+ } as const;
@@ -0,0 +1,28 @@
1
+ // Re-exports of the wombat.shader runtime types consumed by the
2
+ // rendering layer. We do NOT duplicate or re-shape these — the
3
+ // rendering layer is just a consumer of wombat.shader's
4
+ // `Effect` / `CompiledEffect` / `ProgramInterface`.
5
+
6
+ export type {
7
+ Effect,
8
+ ComputeShader,
9
+ Stage,
10
+ HoleGetter,
11
+ HoleGetters,
12
+ CompiledEffect,
13
+ CompiledStage,
14
+ Target,
15
+ } from "@aardworx/wombat.shader";
16
+
17
+ export type {
18
+ ProgramInterface,
19
+ StageInfo,
20
+ AttributeInfo,
21
+ OutputInfo,
22
+ LooseUniformInfo,
23
+ UniformBlockInfo,
24
+ UniformFieldInfo,
25
+ SamplerInfo,
26
+ TextureInfo,
27
+ StorageBufferInfo,
28
+ } from "@aardworx/wombat.shader";
@@ -0,0 +1,63 @@
1
+ // ITexture — texture source that can be either a real GPU texture
2
+ // or a host-side image. Mirrors the IBuffer pattern.
3
+ //
4
+ // Host sources cover everything WebGPU's `copyExternalImageToTexture`
5
+ // or `writeTexture` can accept:
6
+ // - `ImageBitmap`, `HTMLImageElement`, `HTMLVideoElement`,
7
+ // `HTMLCanvasElement`, `OffscreenCanvas`, `ImageData` —
8
+ // uploaded via `copyExternalImageToTexture`.
9
+ // - Raw `{ data, width, height, format }` — uploaded via
10
+ // `queue.writeTexture` (one mip; runtime can synthesise the
11
+ // rest if requested).
12
+
13
+ export interface RawTextureSource {
14
+ readonly kind: "raw";
15
+ readonly data: ArrayBuffer | ArrayBufferView;
16
+ readonly width: number;
17
+ readonly height: number;
18
+ readonly depthOrArrayLayers?: number;
19
+ readonly format: GPUTextureFormat;
20
+ readonly mipLevelCount?: number;
21
+ }
22
+
23
+ export interface ExternalTextureSource {
24
+ readonly kind: "external";
25
+ readonly source:
26
+ | ImageBitmap
27
+ | HTMLImageElement
28
+ | HTMLVideoElement
29
+ | HTMLCanvasElement
30
+ | OffscreenCanvas
31
+ | ImageData;
32
+ /** Override the format the runtime should allocate (default: "rgba8unorm"). */
33
+ readonly format?: GPUTextureFormat;
34
+ /** Generate a mip chain on upload. Default `false`. */
35
+ readonly generateMips?: boolean;
36
+ }
37
+
38
+ export type HostTextureSource = RawTextureSource | ExternalTextureSource;
39
+
40
+ export type ITexture =
41
+ | { readonly kind: "gpu"; readonly texture: GPUTexture }
42
+ | { readonly kind: "host"; readonly source: HostTextureSource };
43
+
44
+ export const ITexture = {
45
+ fromGPU(texture: GPUTexture): ITexture {
46
+ return { kind: "gpu", texture };
47
+ },
48
+ fromExternal(
49
+ source: ExternalTextureSource["source"],
50
+ opts: { format?: GPUTextureFormat; generateMips?: boolean } = {},
51
+ ): ITexture {
52
+ const ext: ExternalTextureSource = {
53
+ kind: "external",
54
+ source,
55
+ ...(opts.format !== undefined ? { format: opts.format } : {}),
56
+ ...(opts.generateMips !== undefined ? { generateMips: opts.generateMips } : {}),
57
+ };
58
+ return { kind: "host", source: ext };
59
+ },
60
+ fromRaw(raw: Omit<RawTextureSource, "kind">): ITexture {
61
+ return { kind: "host", source: { kind: "raw", ...raw } };
62
+ },
63
+ } as const;
package/src/index.ts ADDED
@@ -0,0 +1,18 @@
1
+ // Public API of `@aardworx/wombat.rendering`.
2
+ //
3
+ // Default entry re-exports everything: types, resource preparers,
4
+ // command-stream functions, runtime, and the window/canvas glue.
5
+ // Subpath exports give granular access for advanced consumers.
6
+ //
7
+ // import { ... } from "@aardworx/wombat.rendering"; // everything
8
+ // import { ... } from "@aardworx/wombat.rendering/core"; // types only
9
+ // import { ... } from "@aardworx/wombat.rendering/resources";
10
+ // import { ... } from "@aardworx/wombat.rendering/commands";
11
+ // import { ... } from "@aardworx/wombat.rendering/runtime";
12
+ // import { ... } from "@aardworx/wombat.rendering/window";
13
+
14
+ export * from "./core/index.js";
15
+ export * from "./resources/index.js";
16
+ export * from "./commands/index.js";
17
+ export * from "./runtime/index.js";
18
+ export * from "./window/index.js";
@@ -0,0 +1,135 @@
1
+ // prepareAdaptiveBuffer — lift `aval<IBuffer>` to
2
+ // `AdaptiveResource<GPUBuffer>`.
3
+ //
4
+ // Behaviour:
5
+ // - On `acquire` (refCount 0→1): subscribes, allocates nothing yet.
6
+ // - On `compute(token)`:
7
+ // - reads the source `aval<IBuffer>`;
8
+ // - if it's `gpu`: returns the user's handle directly. We
9
+ // don't own it; `destroy()` will not free it.
10
+ // - if it's `host`: ensures we own a `GPUBuffer` of sufficient
11
+ // size + correct usage flags, encodes a `writeBuffer` (via
12
+ // device.queue) to upload the bytes, returns the owned
13
+ // buffer. Reuses the existing buffer when capacity allows;
14
+ // reallocates on growth.
15
+ // - On `destroy`: frees the owned buffer (if any).
16
+ //
17
+ // `writeBuffer` does not require a GPUCommandEncoder — it goes via
18
+ // `GPUQueue.writeBuffer`, scheduled at the next submit. That keeps
19
+ // the upload path simple and avoids needing `RenderContext.encoder`
20
+ // for plain CPU→GPU buffer flow. (Compute-produced buffers will
21
+ // need the encoder; that's a different code path.)
22
+
23
+ import {
24
+ AdaptiveResource,
25
+ tryAcquire,
26
+ tryRelease,
27
+ type IBuffer,
28
+ } from "../core/index.js";
29
+ import {
30
+ type AdaptiveToken,
31
+ type aval,
32
+ } from "@aardworx/wombat.adaptive";
33
+ import { BufferUsage } from "./webgpuFlags.js";
34
+
35
+ export interface PrepareAdaptiveBufferOptions {
36
+ /** Bitwise OR of GPUBufferUsage flags. `COPY_DST` is added automatically when needed. */
37
+ readonly usage: GPUBufferUsageFlags;
38
+ /** Optional debug label forwarded to created GPUBuffers. */
39
+ readonly label?: string;
40
+ /**
41
+ * Reuse the existing GPUBuffer when the new host data fits in
42
+ * the current allocation. Default `true`. Set `false` to force
43
+ * a fresh buffer on every host-side change (rarely useful).
44
+ */
45
+ readonly reuseOnFit?: boolean;
46
+ }
47
+
48
+ class AdaptiveBuffer extends AdaptiveResource<GPUBuffer> {
49
+ private _owned: GPUBuffer | undefined = undefined;
50
+ private _ownedCapacity = 0;
51
+
52
+ constructor(
53
+ private readonly device: GPUDevice,
54
+ private readonly source: aval<IBuffer>,
55
+ private readonly opts: PrepareAdaptiveBufferOptions,
56
+ ) {
57
+ super();
58
+ }
59
+
60
+ protected create(): void {
61
+ tryAcquire(this.source);
62
+ }
63
+
64
+ protected destroy(): void {
65
+ if (this._owned !== undefined) {
66
+ this._owned.destroy();
67
+ this._owned = undefined;
68
+ this._ownedCapacity = 0;
69
+ }
70
+ tryRelease(this.source);
71
+ }
72
+
73
+ override compute(token: AdaptiveToken): GPUBuffer {
74
+ const src = this.source.getValue(token);
75
+ if (src.kind === "gpu") {
76
+ // Caller-owned buffer; drop any owned allocation we still hold.
77
+ if (this._owned !== undefined) {
78
+ this._owned.destroy();
79
+ this._owned = undefined;
80
+ this._ownedCapacity = 0;
81
+ }
82
+ return src.buffer;
83
+ }
84
+ // host — upload via queue.writeBuffer.
85
+ const usage = this.opts.usage | BufferUsage.COPY_DST;
86
+ const reuseOnFit = this.opts.reuseOnFit ?? true;
87
+ const requiredSize = roundUp4(src.sizeBytes);
88
+ if (
89
+ this._owned === undefined
90
+ || !reuseOnFit
91
+ || requiredSize > this._ownedCapacity
92
+ ) {
93
+ if (this._owned !== undefined) this._owned.destroy();
94
+ const desc: GPUBufferDescriptor = {
95
+ size: requiredSize,
96
+ usage,
97
+ ...(this.opts.label !== undefined ? { label: this.opts.label } : {}),
98
+ };
99
+ this._owned = this.device.createBuffer(desc);
100
+ this._ownedCapacity = requiredSize;
101
+ }
102
+ // writeBuffer accepts ArrayBuffer or any ArrayBufferView.
103
+ const data = src.data;
104
+ if (data instanceof ArrayBuffer) {
105
+ this.device.queue.writeBuffer(this._owned, 0, data, 0, src.sizeBytes);
106
+ } else {
107
+ this.device.queue.writeBuffer(
108
+ this._owned,
109
+ 0,
110
+ data.buffer,
111
+ data.byteOffset,
112
+ src.sizeBytes,
113
+ );
114
+ }
115
+ return this._owned;
116
+ }
117
+ }
118
+
119
+ function roundUp4(n: number): number {
120
+ return (n + 3) & ~3;
121
+ }
122
+
123
+ /**
124
+ * Wrap an `aval<IBuffer>` as a ref-counted, adaptively-uploaded
125
+ * `AdaptiveResource<GPUBuffer>`. The resource is itself an
126
+ * `aval<GPUBuffer>` — downstream consumers can `getValue(token)`
127
+ * inside their own adaptive evaluation.
128
+ */
129
+ export function prepareAdaptiveBuffer(
130
+ device: GPUDevice,
131
+ source: aval<IBuffer>,
132
+ opts: PrepareAdaptiveBufferOptions,
133
+ ): AdaptiveResource<GPUBuffer> {
134
+ return new AdaptiveBuffer(device, source, opts);
135
+ }
@@ -0,0 +1,82 @@
1
+ // prepareAdaptiveSampler — lift `aval<ISampler>` to
2
+ // `AdaptiveResource<GPUSampler>`.
3
+ //
4
+ // Samplers are cheap and structurally-identifiable — the device
5
+ // dedupes them via a descriptor cache. Per AdaptiveResource we
6
+ // just track the most recent handle; on descriptor change we
7
+ // fetch a fresh one from the cache.
8
+
9
+ import {
10
+ AdaptiveResource,
11
+ tryAcquire,
12
+ tryRelease,
13
+ type ISampler,
14
+ } from "../core/index.js";
15
+ import {
16
+ type AdaptiveToken,
17
+ type aval,
18
+ } from "@aardworx/wombat.adaptive";
19
+
20
+ class SamplerCache {
21
+ private readonly map = new Map<string, GPUSampler>();
22
+ constructor(private readonly device: GPUDevice) {}
23
+ get(desc: GPUSamplerDescriptor): GPUSampler {
24
+ const k = key(desc);
25
+ let s = this.map.get(k);
26
+ if (s === undefined) {
27
+ s = this.device.createSampler(desc);
28
+ this.map.set(k, s);
29
+ }
30
+ return s;
31
+ }
32
+ }
33
+
34
+ function key(d: GPUSamplerDescriptor): string {
35
+ return [
36
+ d.addressModeU ?? "clamp-to-edge",
37
+ d.addressModeV ?? "clamp-to-edge",
38
+ d.addressModeW ?? "clamp-to-edge",
39
+ d.magFilter ?? "nearest",
40
+ d.minFilter ?? "nearest",
41
+ d.mipmapFilter ?? "nearest",
42
+ d.lodMinClamp ?? 0,
43
+ d.lodMaxClamp ?? 32,
44
+ d.compare ?? "",
45
+ d.maxAnisotropy ?? 1,
46
+ ].join("|");
47
+ }
48
+
49
+ const caches = new WeakMap<GPUDevice, SamplerCache>();
50
+ function cacheFor(device: GPUDevice): SamplerCache {
51
+ let c = caches.get(device);
52
+ if (c === undefined) {
53
+ c = new SamplerCache(device);
54
+ caches.set(device, c);
55
+ }
56
+ return c;
57
+ }
58
+
59
+ class AdaptiveSampler extends AdaptiveResource<GPUSampler> {
60
+ constructor(
61
+ private readonly device: GPUDevice,
62
+ private readonly source: aval<ISampler>,
63
+ ) { super(); }
64
+ protected create(): void { tryAcquire(this.source); }
65
+ protected destroy(): void {
66
+ // We never own the GPUSampler — the cache does. Just propagate
67
+ // release to the source aval (in case it's an AdaptiveResource).
68
+ tryRelease(this.source);
69
+ }
70
+ override compute(token: AdaptiveToken): GPUSampler {
71
+ const s = this.source.getValue(token);
72
+ if (s.kind === "gpu") return s.sampler;
73
+ return cacheFor(this.device).get(s.descriptor);
74
+ }
75
+ }
76
+
77
+ export function prepareAdaptiveSampler(
78
+ device: GPUDevice,
79
+ source: aval<ISampler>,
80
+ ): AdaptiveResource<GPUSampler> {
81
+ return new AdaptiveSampler(device, source);
82
+ }