@solidrt/core 0.0.45 → 0.0.46

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.
package/AGENTS.md CHANGED
@@ -94,7 +94,7 @@ tsconfig.json - the two load-bearing lines are jsx + jsxImportSource:
94
94
  ```
95
95
 
96
96
  Peer deps @solidjs/signals and @solidjs/universal must match (currently
97
- 2.0.0-beta.26); bun resolves them from peerDependencies.
97
+ 2.0.0-beta.31); bun resolves them from peerDependencies.
98
98
 
99
99
  ## Element model (the parts that are easy to get wrong)
100
100
 
@@ -42,6 +42,8 @@ for the element/prop model see `@solidrt/core/AGENTS.md`.
42
42
  - `gpu-pipeline.tsx` - `createPipelineTexture`: a custom vertex+fragment pair drawing an interleaved vertex buffer (position and color attributes resolved by name), a depth-tested spinning cube whose `uTime` uniform is driven through `<texture params>` exactly like a fragment shader's.
43
43
  - `gpu-particles.tsx` - an additive particle field: `topology: "points"` with `blend: "add"`, so overlapping gaussian splats (`gl_PointSize` from the vertex stage, `gl_PointCoord` falloff, premultiplied output) accumulate into a glowing volume instead of overwriting. The tints are typed vec3 uniforms filled from 3-number array params.
44
44
  - `gpu-instancing.tsx` - instanced drawing: one 3-vertex triangle drawn hundreds of times via `instanceCount`, each instance placed and tinted on a phyllotaxis spiral from `gl_InstanceID` alone. `setDraw(id, { instanceCount })` merges into the draw range per frame (absent keys keep their values, like params), so the population breathes without touching the buffer.
45
+ - `gpu-draw-list.tsx` - `createDrawTarget`: one render target holding an ordered, mutable LIST of draws - two orbiting triangles from different programs sharing one depth buffer (the target owns the depth storage, each pipeline the test/write behavior), a third entry added and removed live via its stable `DrawId`, and `setDrawParams` as the per-object channel.
46
+ - `gpu-shared-params.tsx` - shared target state on a draw target: `setTargetParams`/`setTargetTextures` write values every entry reads - a ring of quads spins and color-cycles from ONE write per frame, and the shared sampler source swaps for the whole target at once. The spin is imperative (`setTargetParams` in `onFrame`) while the tint rides the `<texture params>` prop - the declarative channel, which on a draw target writes the same shared record. The two entries that seeded their own uTint/uMap show the precedence rule (an entry's own value beats the shared one), and mixed programs show partial coverage (a program that does not declare a shared name skips it). `createDrawTarget`'s positional `params` and `opts.textures` seed both channels before any entry exists.
45
47
  - `window-shader.tsx` - the `shader` prop on `<window>`: the finished frame drawn through a raw-linked warp program before present, click to toggle between warp and identity.
46
48
  - `window-shader-history.tsx` - the window shader's frame history: `previous` binds last frame as uPrevious, drawn as a one-frame motion echo behind an orbiting square; click toggles the echo term.
47
49
 
@@ -73,7 +73,7 @@ function App() {
73
73
  attributes: attrs,
74
74
  })
75
75
 
76
- let target = createDrawTarget(512, 512, { depth: true, clearColor: [0.04, 0.04, 0.08, 1], label: "orbits" })
76
+ let target = createDrawTarget(512, 512, null, { depth: true, clearColor: [0.04, 0.04, 0.08, 1], label: "orbits" })
77
77
  let warmDraw = addDraw(target, warm, { uAngle: 0 }, { buffer: triangle })
78
78
  let coolDraw = addDraw(target, cool, { uAngle: Math.PI }, { buffer: triangle })
79
79
 
@@ -0,0 +1,135 @@
1
+ // Shared target state on a draw target: values every entry reads, written
2
+ // ONCE per target instead of once per entry. A ring of quads spins under a
3
+ // single "camera" uniform (uView) and color-cycles under a shared tint
4
+ // (uTint) - one target-level write per frame however many entries the
5
+ // list holds, where per-entry setDrawParams would cost one call and one
6
+ // value's worth of JS arithmetic per quad (the cost profile @solidrt/3d's
7
+ // camera rides on). setTargetTextures is the sampler analog: the patterned
8
+ // quads read one shared uMap source, swapped for the whole target every
9
+ // two seconds.
10
+ //
11
+ // The rules to notice: an entry's OWN value beats the shared one - the
12
+ // amber quad seeds uTint at addDraw and ignores the color cycle, the
13
+ // striped quad binds its own uMap and ignores the swap - and coverage may
14
+ // be partial: the tint program never declares uMap and the patterned
15
+ // program never declares uTint, so each shared write simply skips entries
16
+ // whose program does not declare the name. Shared state is target state:
17
+ // createDrawTarget seeds it (positional params + opts.textures) before any
18
+ // entry exists, and entry add/remove/rebuild cannot lose it.
19
+ //
20
+ // Two channels drive the same shared params. uView goes imperatively
21
+ // (setTargetParams in onFrame), uTint goes declaratively: the `<texture
22
+ // params>` prop means "the target's params" on every target kind, so on a
23
+ // draw target it writes the shared record - a signal into the prop is all
24
+ // the wiring a shared value needs.
25
+ import { render, onFrame, createSignal } from "@solidrt/core"
26
+ import {
27
+ addDraw,
28
+ compileShader,
29
+ createBuffer,
30
+ createDrawTarget,
31
+ createRenderPipeline,
32
+ createTexture,
33
+ glsl,
34
+ linkProgram,
35
+ setTargetParams,
36
+ setTargetTextures,
37
+ } from "@solidrt/core/gpu"
38
+
39
+ let VERTEX = glsl`
40
+ in vec2 aPos;
41
+ uniform float uView;
42
+ uniform vec2 uCenter;
43
+
44
+ void main() {
45
+ vec2 p = uCenter + aPos * 0.13;
46
+ float c = cos(uView), s = sin(uView);
47
+ gl_Position = vec4(c * p.x - s * p.y, s * p.x + c * p.y, 0.0, 1.0);
48
+ }
49
+ `
50
+
51
+ let FRAGMENT_TINT = glsl`
52
+ uniform vec4 uTint;
53
+ void main() {
54
+ fragColor = uTint;
55
+ }
56
+ `
57
+
58
+ let FRAGMENT_MAP = glsl`
59
+ uniform sampler2D uMap;
60
+ void main() {
61
+ fragColor = texture(uMap, gl_FragCoord.xy / 32.0);
62
+ }
63
+ `
64
+
65
+ function App() {
66
+ let quad = createBuffer(new Float32Array([-1, -1, 1, -1, 1, 1, -1, -1, 1, 1, -1, 1]), { label: "quad" })
67
+ let vs = compileShader("vertex", VERTEX, { header: true })
68
+ let attrs = [{ name: "aPos", format: "vec2" as const }]
69
+ let tint = createRenderPipeline(linkProgram(vs, compileShader("fragment", FRAGMENT_TINT, { header: true })), {
70
+ attributes: attrs,
71
+ })
72
+ let mapped = createRenderPipeline(linkProgram(vs, compileShader("fragment", FRAGMENT_MAP, { header: true })), {
73
+ attributes: attrs,
74
+ })
75
+
76
+ // 2x2 patterns, nearest + repeat, so gl_FragCoord tiling shows hard cells.
77
+ let pattern = (a: number[], b: number[]) =>
78
+ createTexture(new Uint8Array([...a, ...b, ...b, ...a]), 2, 2, { filter: "nearest", wrap: "repeat" })
79
+ let checker = pattern([15, 15, 20, 255], [235, 235, 235, 255])
80
+ let ember = pattern([250, 160, 30, 255], [45, 10, 60, 255])
81
+ let stripes = createTexture(new Uint8Array([220, 40, 60, 255, 245, 245, 245, 255]), 2, 1, {
82
+ filter: "nearest",
83
+ wrap: "repeat",
84
+ })
85
+
86
+ // The positional params argument and opts.textures seed the shared state
87
+ // before any entry exists; the entries added below pick it up.
88
+ let target = createDrawTarget(
89
+ 512,
90
+ 512,
91
+ { uView: 0, uTint: [0.3, 0.8, 0.9, 1] },
92
+ { textures: { uMap: checker }, clearColor: [0.05, 0.05, 0.09, 1], label: "shared-ring" },
93
+ )
94
+
95
+ const RING = 8
96
+ for (let i = 0; i < RING; i++) {
97
+ let a = (i / RING) * Math.PI * 2
98
+ let uCenter = [0.62 * Math.cos(a), 0.62 * Math.sin(a)]
99
+ if (i === 1 || i === 5) {
100
+ // Patterned entries read the shared uMap; the one at i === 5 brings
101
+ // its own binding and keeps its stripes through every shared swap.
102
+ addDraw(target, mapped, { uCenter }, i === 5 ? { buffer: quad, textures: { uMap: stripes } } : { buffer: quad })
103
+ } else if (i === 3) {
104
+ // The override quad: its own uTint beats the shared color cycle.
105
+ addDraw(target, tint, { uCenter, uTint: [1, 0.62, 0.1, 1] }, { buffer: quad })
106
+ } else {
107
+ addDraw(target, tint, { uCenter }, { buffer: quad })
108
+ }
109
+ }
110
+
111
+ let [sharedTint, setSharedTint] = createSignal([0.3, 0.8, 0.9, 1])
112
+ let mapFlip = -1
113
+ onFrame(tick => {
114
+ let t = tick / 1000
115
+ // The whole ring, one write: uView spins every entry. uTint takes the
116
+ // declarative channel instead - the signal feeds the params prop below.
117
+ setTargetParams(target, { uView: t * 0.5 })
118
+ setSharedTint([0.45 + 0.45 * Math.sin(t), 0.45 + 0.45 * Math.sin(t + 2.1), 0.45 + 0.45 * Math.sin(t + 4.2), 1])
119
+ // The shared sampler source swaps every two seconds; only the entry
120
+ // with its own uMap keeps its pattern.
121
+ let flip = Math.floor(t / 2) % 2
122
+ if (flip !== mapFlip) {
123
+ mapFlip = flip
124
+ setTargetTextures(target, { uMap: flip === 0 ? checker : ember })
125
+ }
126
+ })
127
+
128
+ return (
129
+ <window alignItems="center" justifyContent="center">
130
+ <texture src={target} width={420} height={420} params={{ uTint: sharedTint() }} />
131
+ </window>
132
+ )
133
+ }
134
+
135
+ render(() => <App />)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@solidrt/core",
3
- "version": "0.0.45",
3
+ "version": "0.0.46",
4
4
  "license": "MIT",
5
5
  "author": "Antoine van Wel",
6
6
  "type": "module",
@@ -27,11 +27,11 @@
27
27
  "colord": "^2.9.3"
28
28
  },
29
29
  "devDependencies": {
30
- "@solidrt/flux-types": "0.0.45"
30
+ "@solidrt/flux-types": "0.0.46"
31
31
  },
32
32
  "peerDependencies": {
33
- "@solidjs/signals": "2.0.0-beta.26",
34
- "@solidjs/universal": "2.0.0-beta.26",
35
- "solid-js": "2.0.0-beta.26"
33
+ "@solidjs/signals": "2.0.0-beta.31",
34
+ "@solidjs/universal": "2.0.0-beta.31",
35
+ "solid-js": "2.0.0-beta.31"
36
36
  }
37
37
  }
package/src/gpu.ts CHANGED
@@ -1,12 +1,14 @@
1
1
  // GPU textures and shaders, reactive (SolidJS) layer: the create* helpers free
2
2
  // their texture automatically when the reactive owner is disposed. Drive a
3
- // shader's uniforms declaratively with `<texture src={id} params={{...}} />`
3
+ // target's uniforms declaratively with `<texture src={id} params={{...}} />`
4
4
  // (see TextureProps) - the preferred way, deferred to the next real repaint so
5
- // a fast-changing signal stays paced to actual frames. setShaderParams is the
6
- // imperative exception: reach for it only when there is no `<texture>` element
7
- // to hold a params prop, e.g. a shader that only feeds another shader as a
8
- // sampler2D input. The imperative primitives (uploadTexture, setShaderParams,
9
- // destroyTexture, ...) live in the `flux:gpu` module.
5
+ // a fast-changing signal stays paced to actual frames; the prop means "the
6
+ // target's params" on every kind (on a draw target, its shared params).
7
+ // setTargetParams is the imperative exception: reach for it only when there
8
+ // is no `<texture>` element to hold a params prop, e.g. a target that only
9
+ // feeds another shader as a sampler2D input. The imperative primitives
10
+ // (uploadTexture, setTargetParams, destroyTexture, ...) live in the
11
+ // `flux:gpu` module.
10
12
  //
11
13
  // Sampling is a per-texture property declared at creation: `filter`
12
14
  // ("linear" default | "nearest") and `wrap` ("clamp" default | "repeat") on
@@ -45,15 +47,16 @@
45
47
  import { createEffect, createSignal, getOwner, onCleanup, untrack } from "@solidjs/signals"
46
48
  import * as gpu from "flux:gpu"
47
49
 
48
- // The create* helpers accept { manual: true } to opt out of the owner-scoped
49
- // auto-free, for resources whose lifetime is managed by hand (rebuilt on
50
- // signal changes inside a long-lived component, handed across owners, ...).
51
- // Without it, each rebuild would stack another onCleanup on the component
52
- // owner: a leak until unmount, then a double-free against manual destroys.
50
+ // The create* helpers accept { autoFree: false } to opt out of the
51
+ // owner-scoped auto-free, for resources whose lifetime is managed by hand
52
+ // (rebuilt on signal changes inside a long-lived component, handed across
53
+ // owners, ...). Without the opt-out, each rebuild would stack another
54
+ // onCleanup on the component owner: a leak until unmount, then a double-free
55
+ // against the by-hand destroys.
53
56
  // `label` is a free-form debug name (WebGPU's label): surfaced by the dev
54
57
  // tooling's GPU inventory and engine log messages, never interpreted, kept
55
58
  // across id-stable resizes.
56
- export type CreateOptions = { manual?: boolean; label?: string }
59
+ export type CreateOptions = { autoFree?: boolean; label?: string }
57
60
 
58
61
  // Sampling options every texture-producing create* helper accepts, applied at
59
62
  // creation as a property of the texture id (there is no set-sampler-later).
@@ -77,19 +80,28 @@ export type { BufferId, DrawId, ProgramId, RenderPipelineId, ShaderStageId, Text
77
80
  // -- need not import flux directly: destroyTexture for the manual-cleanup path
78
81
  // (textures made outside a reactive scope, e.g. after an await, are not
79
82
  // auto-freed), uploadTexture to push new pixels into a mutable texture, and
80
- // setShaderParams as the non-reactive exception described above - prefer
81
- // `<texture params={...}>` when a `<texture>` element is already in the tree.
82
- // resizeTexture and setShaderSize resize in place at a stable id (so
83
- // `<texture src>` and sampler bindings stay valid); because the id survives,
84
- // the owner-scoped auto-free registered at creation keeps working and no
85
- // re-registration is needed. setShaderTextures is the sampler analog of
86
- // setShaderParams: retarget a shader's sampler2D inputs without recompiling.
83
+ // the target-level verbs. setTargetParams writes a target's params on ANY
84
+ // target kind - the non-reactive exception described above, so prefer
85
+ // `<texture params={...}>` when a `<texture>` element is already in the
86
+ // tree. On a single-program target (fragment texture, pipeline target) the
87
+ // names validate strictly against its one program; on a draw target they are
88
+ // the SHARED params every entry reads (a camera's view-projection: one write
89
+ // per camera move instead of one per mesh), applied before each entry's own
90
+ // params so an entry naming the same uniform overrides the shared value, and
91
+ // a name only some entries' programs declare applies where declared.
92
+ // setTargetTextures is its sampler analog: retarget sampler2D inputs without
93
+ // recompiling (on a draw target, shared sources every entry reads - an
94
+ // environment map, a LUT - bound where an entry's program declares the name
95
+ // and its own bindings do not override it). resizeTexture and setTargetSize
96
+ // resize in place at a stable id (so `<texture src>` and sampler bindings
97
+ // stay valid); because the id survives, the owner-scoped auto-free
98
+ // registered at creation keeps working and no re-registration is needed.
87
99
  export {
88
100
  destroyTexture,
89
101
  resizeTexture,
90
- setShaderParams,
91
- setShaderSize,
92
- setShaderTextures,
102
+ setTargetParams,
103
+ setTargetSize,
104
+ setTargetTextures,
93
105
  uploadTexture,
94
106
  } from "flux:gpu"
95
107
 
@@ -102,8 +114,7 @@ export {
102
114
  // the explicit render verb for `render: "manual"` targets - targets whose
103
115
  // pass is state (accumulation, feedback) rather than a pure function of its
104
116
  // inputs, which the runtime therefore never renders on its own; the app
105
- // steps them, usually from onFrame. (`render: "manual"` is the render mode;
106
- // the unrelated `manual: true` create option is the lifetime opt-out above.)
117
+ // steps them, usually from onFrame.
107
118
  // copyTexture overwrites a manual target with another texture's pixels
108
119
  // GPU-side (exact, same size): seed a loadOp "load" accumulator, snapshot a
109
120
  // ping-pong buffer, reset state to a known image.
@@ -114,10 +125,12 @@ export type { BlendMode, CullMode, DrawRange, IndexBinding, IndexFormat, IndexRa
114
125
  // target (see createDrawTarget below), so there is no per-entry lifetime to
115
126
  // wrap. addDraw adds an entry (appended, or inserted via opts.before) and
116
127
  // returns its stable DrawId; removeDraw drops one; setDrawParams /
117
- // setDrawTextures / setDrawRange are the per-entry forms of setShaderParams /
118
- // setShaderTextures / setDraw, taking (target, draw, value) with identical
128
+ // setDrawTextures / setDrawRange are the per-entry forms of setTargetParams /
129
+ // setTargetTextures / setDraw, taking (target, draw, value) with identical
119
130
  // merge and validation semantics. The per-object hot path is setDrawParams (a
120
- // moved mesh = one call with its new matrix). setDrawOrder replaces the whole
131
+ // moved mesh = one call with its new matrix); the per-target one is
132
+ // setTargetParams (exported above), which on a draw target writes the SHARED
133
+ // params every entry reads. setDrawOrder replaces the whole
121
134
  // list order with a full permutation of the live ids - the sorting verb
122
135
  // (opaque front-to-back, transparent back-to-front, re-issued when the
123
136
  // camera moves).
@@ -194,8 +207,9 @@ export { captureSnapshot, readTexture } from "flux:gpu"
194
207
  * reactive scope the texture is freed automatically once that owner is
195
208
  * disposed; when called outside one (e.g. after an `await`, where the owner
196
209
  * is no longer current) nothing is registered and you must call
197
- * `destroyTexture` (from flux:gpu) yourself. Pass `{ manual: true }` to skip
198
- * the auto-free and own the disposal yourself even inside a reactive scope.
210
+ * `destroyTexture` (from flux:gpu) yourself. Pass `{ autoFree: false }` to
211
+ * skip the auto-free and own the disposal yourself even inside a reactive
212
+ * scope.
199
213
  */
200
214
  export function createTexture(
201
215
  data: Uint8Array,
@@ -204,7 +218,7 @@ export function createTexture(
204
218
  opts?: CreateOptions & SamplerOptions & TextureFormatOptions,
205
219
  ): gpu.TextureId {
206
220
  let id = gpu.createTexture(data, width, height, opts)
207
- if (!opts?.manual && getOwner()) onCleanup(() => gpu.destroyTexture(id))
221
+ if (opts?.autoFree !== false && getOwner()) onCleanup(() => gpu.destroyTexture(id))
208
222
  return id
209
223
  }
210
224
 
@@ -214,7 +228,7 @@ export function createTexture(
214
228
  * `data` must hold at least `width * height` pixels at the declared format's
215
229
  * size (`* 4` bytes for the default "rgba8", `* 1` for "r8"; it may hold
216
230
  * several frames). Like `createTexture`, the texture is freed automatically
217
- * when the reactive owner is disposed (opt out with `{ manual: true }`);
231
+ * when the reactive owner is disposed (opt out with `{ autoFree: false }`);
218
232
  * created outside a reactive scope you must call `destroyTexture` (from
219
233
  * flux:gpu) yourself.
220
234
  */
@@ -225,7 +239,7 @@ export function createMutableTexture(
225
239
  opts?: CreateOptions & SamplerOptions & TextureFormatOptions,
226
240
  ): gpu.TextureId {
227
241
  let id = gpu.createMutableTexture(data, width, height, opts)
228
- if (!opts?.manual && getOwner()) onCleanup(() => gpu.destroyTexture(id))
242
+ if (opts?.autoFree !== false && getOwner()) onCleanup(() => gpu.destroyTexture(id))
229
243
  return id
230
244
  }
231
245
 
@@ -238,7 +252,7 @@ export function createMutableTexture(
238
252
  * scalars from a number, `vec2`/`vec3`/`vec4`/`mat4` from a flat number
239
253
  * array); drive their values with `<texture src={id} params={{...}} />`
240
254
  * (preferred) or, when there is no `<texture>` element for it, imperatively
241
- * with `setShaderParams`. `params` is its own argument - it seeds the same
255
+ * with `setTargetParams`. `params` is its own argument - it seeds the same
242
256
  * live channel those two drive - and takes `null` (or nothing) for a shader
243
257
  * without uniforms. A time-driven shader declares its own time uniform
244
258
  * (`uniform float uTime;`) and the app drives it like any other value.
@@ -248,7 +262,7 @@ export function createMutableTexture(
248
262
  * re-renders whenever a source changes - including a sampled target
249
263
  * re-rendering, transitively through chains. Frees the
250
264
  * texture and shader program when the reactive owner is disposed (opt out
251
- * with `{ manual: true }`); create outside any reactive scope for
265
+ * with `{ autoFree: false }`); create outside any reactive scope for
252
266
  * app-lifetime shaders. For a shader whose source or inputs change
253
267
  * reactively, use {@link createShaderTextureMemo} instead.
254
268
  *
@@ -271,15 +285,15 @@ export function createShaderTexture(
271
285
  opts?: CreateOptions & SamplerOptions & { textures?: Record<string, gpu.TextureId> },
272
286
  ): gpu.TextureId {
273
287
  let id = gpu.createShaderTexture(fragmentSrc, width, height, params, opts)
274
- if (!opts?.manual && getOwner()) onCleanup(() => gpu.destroyTexture(id))
288
+ if (opts?.autoFree !== false && getOwner()) onCleanup(() => gpu.destroyTexture(id))
275
289
  return id
276
290
  }
277
291
 
278
292
  /**
279
293
  * Creates a render target over a pipeline from `createRenderPipeline` and
280
294
  * renders it once, returning the texture id (usable anywhere a normal
281
- * texture id is, e.g. `<texture src>`; resize with `setShaderSize`, drive
282
- * uniforms with `<texture params>` or `setShaderParams`). Many targets may
295
+ * texture id is, e.g. `<texture src>`; resize with `setTargetSize`, drive
296
+ * uniforms with `<texture params>` or `setTargetParams`). Many targets may
283
297
  * share one pipeline, and creating a target compiles nothing. The target
284
298
  * brings the per-target half: size, the concrete vertex `buffer` the
285
299
  * pipeline's attribute layout describes, the `instanceBuffer` its
@@ -295,7 +309,7 @@ export function createShaderTexture(
295
309
  * `instanceAttributes`, `topology`, `blend`, `cull`, `depth`, `depthWrite`)
296
310
  * lives on the pipeline
297
311
  * and throws here. Frees the target when the reactive owner is disposed (opt
298
- * out with `opts.manual`); the pipeline is yours and outlives it.
312
+ * out with `autoFree: false`); the pipeline is yours and outlives it.
299
313
  *
300
314
  * `render: "manual"` makes it a manual target: the runtime never renders it
301
315
  * (it starts cleared to `clearColor`), only an explicit `renderTarget(id)`
@@ -304,8 +318,7 @@ export function createShaderTexture(
304
318
  * previous contents under each draw - single-target accumulation - while
305
319
  * the default `"clear"` clears to `clearColor` per render; state that must
306
320
  * read its own pixels (decay, blur, simulation) still ping-pongs across two
307
- * manual targets, and `copyTexture` seeds either shape. Distinct from the
308
- * `manual` lifetime option: `render` is who renders, `manual` is who frees.
321
+ * manual targets, and `copyTexture` seeds either shape.
309
322
  */
310
323
  export function createShaderTarget(
311
324
  pipeline: gpu.RenderPipelineId,
@@ -324,7 +337,7 @@ export function createShaderTarget(
324
337
  SamplerOptions,
325
338
  ): gpu.TextureId {
326
339
  let id = gpu.createShaderTarget(pipeline, width, height, params, opts)
327
- if (!opts?.manual && getOwner()) onCleanup(() => gpu.destroyTexture(id))
340
+ if (opts?.autoFree !== false && getOwner()) onCleanup(() => gpu.destroyTexture(id))
328
341
  return id
329
342
  }
330
343
 
@@ -340,27 +353,41 @@ export function createShaderTarget(
340
353
  * whether an entry tests/writes it stays pipeline state, and a depth-testing
341
354
  * pipeline into a depthless target throws at `addDraw`.
342
355
  *
356
+ * `params` seeds the target's SHARED params - values every entry reads,
357
+ * written once per target instead of once per entry (a camera's
358
+ * view-projection is the motivating case: one `setTargetParams` per camera
359
+ * move instead of one `setDrawParams` per mesh). Shared values apply before
360
+ * each entry's own params, so an entry naming the same uniform overrides
361
+ * the shared value; a name only some entries' programs declare is applied
362
+ * where declared and skipped elsewhere. They are target state: entry
363
+ * add/remove/rebuild cannot lose them. `opts.textures` is the sampler
364
+ * analog - shared sources every entry reads (an environment map, a LUT),
365
+ * driven later with `setTargetTextures`, same precedence and coverage
366
+ * rules.
367
+ *
343
368
  * The render contract is unchanged: the list is input data, so an ordinary
344
369
  * (`render: "auto"`) draw target re-renders exactly when its entries or
345
370
  * their inputs change - a static scene costs zero passes, and one render is
346
371
  * one pass regardless of entry count. `render: "manual"` and `loadOp` work
347
372
  * as on `createShaderTarget`. Returns the texture id; frees on owner
348
- * disposal (opt out with `opts.manual`), taking its entries with it - the
373
+ * disposal (opt out with `autoFree: false`), taking its entries with it - the
349
374
  * entries' pipelines and buffers are yours and outlive it.
350
375
  */
351
376
  export function createDrawTarget(
352
377
  width: number,
353
378
  height: number,
379
+ params?: gpu.ShaderParams | null,
354
380
  opts?: {
355
381
  depth?: boolean
382
+ textures?: Record<string, gpu.TextureId>
356
383
  clearColor?: [number, number, number, number]
357
384
  render?: "auto" | "manual"
358
385
  loadOp?: "clear" | "load"
359
386
  } & CreateOptions &
360
387
  SamplerOptions,
361
388
  ): gpu.TextureId {
362
- let id = gpu.createDrawTarget(width, height, opts)
363
- if (!opts?.manual && getOwner()) onCleanup(() => gpu.destroyTexture(id))
389
+ let id = gpu.createDrawTarget(width, height, params, opts)
390
+ if (opts?.autoFree !== false && getOwner()) onCleanup(() => gpu.destroyTexture(id))
364
391
  return id
365
392
  }
366
393
 
@@ -399,7 +426,7 @@ function sameRecord(
399
426
  * current texture id (use it as `<texture src={id()} />`) and keeps the GPU
400
427
  * resource in step with `spec` from then on. Changes that keep the compiled
401
428
  * program valid mutate in place at a stable id - a size change routes to
402
- * `setShaderSize`, a params change to `setShaderParams` - while a change to
429
+ * `setTargetSize`, a params change to `setTargetParams` - while a change to
403
430
  * the fragment source or the sampler bindings rebuilds at a fresh id, updates
404
431
  * the accessor, and destroys the old id. That destroy is frame-safe (the
405
432
  * runtime reclaims an id only once the render tree no longer references it),
@@ -436,10 +463,10 @@ export function createShaderTextureMemo(
436
463
  ) {
437
464
  // Program and inputs unchanged: mutate in place, the id stays stable.
438
465
  if (next.width !== current.width || next.height !== current.height) {
439
- gpu.setShaderSize(currentId, next.width, next.height)
466
+ gpu.setTargetSize(currentId, next.width, next.height)
440
467
  }
441
468
  if (!sameRecord(next.params, current.params) && next.params) {
442
- gpu.setShaderParams(currentId, next.params)
469
+ gpu.setTargetParams(currentId, next.params)
443
470
  }
444
471
  current = next
445
472
  return
@@ -483,7 +510,7 @@ function toUint8(data: ArrayBuffer | ArrayBufferView): Uint8Array {
483
510
  * reference `iResolution` and any uniform they declare (`float`/`int`
484
511
  * scalars from a number, `vec2`/`vec3`/`vec4`/`mat4` from a flat number
485
512
  * array); drive values with `<texture src={id} params={{...}} />` or
486
- * `setShaderParams`, exactly like a fragment shader.
513
+ * `setTargetParams`, exactly like a fragment shader.
487
514
  * `opts.depth` attaches a private depth buffer (cleared + tested per render);
488
515
  * `opts.depthWrite: false` (requires depth) keeps the test but stops the
489
516
  * draw from writing depth. `opts.blend: "add"` makes the draw accumulate
@@ -505,7 +532,7 @@ function toUint8(data: ArrayBuffer | ArrayBufferView): Uint8Array {
505
532
  * `opts.loadOp` behave exactly as on {@link createShaderTarget}: step the
506
533
  * target with `renderTarget(id)`, and `loadOp: "load"` (manual-only) keeps
507
534
  * the previous contents under each draw. Frees the texture and GL program when the reactive
508
- * owner is disposed (opt out with `opts.manual`); create outside any reactive
535
+ * owner is disposed (opt out with `autoFree: false`); create outside any reactive
509
536
  * scope for app-lifetime pipelines.
510
537
  */
511
538
  export function createPipelineTexture(
@@ -533,7 +560,7 @@ export function createPipelineTexture(
533
560
  SamplerOptions,
534
561
  ): gpu.TextureId {
535
562
  let id = gpu.createPipelineTexture(vertexSrc, fragmentSrc, width, height, params, opts)
536
- if (!opts?.manual && getOwner()) onCleanup(() => gpu.destroyTexture(id))
563
+ if (opts?.autoFree !== false && getOwner()) onCleanup(() => gpu.destroyTexture(id))
537
564
  return id
538
565
  }
539
566
 
@@ -542,13 +569,13 @@ export function createPipelineTexture(
542
569
  * Float32Array laid out to match the pipeline's interleaved attribute list).
543
570
  * Update it later with {@link writeBuffer}; the buffer's byte size is fixed at
544
571
  * creation, so reserve room up front for dynamic geometry. Freed automatically
545
- * when the reactive owner is disposed (opt out with `{ manual: true }`);
572
+ * when the reactive owner is disposed (opt out with `{ autoFree: false }`);
546
573
  * created outside a reactive scope you must call `destroyBuffer` yourself.
547
574
  * (Destruction order relative to pipelines does not matter.)
548
575
  */
549
576
  export function createBuffer(data: ArrayBuffer | ArrayBufferView, opts?: CreateOptions): gpu.BufferId {
550
577
  let id = gpu.createBuffer(toUint8(data), opts)
551
- if (!opts?.manual && getOwner()) onCleanup(() => gpu.destroyBuffer(id))
578
+ if (opts?.autoFree !== false && getOwner()) onCleanup(() => gpu.destroyBuffer(id))
552
579
  return id
553
580
  }
554
581
 
package/src/image.ts CHANGED
@@ -1,27 +1,15 @@
1
- // CPU image codec plus the reactive load-and-upload convenience. decodeImage is
2
- // the raw primitive (no GPU involved); createImage is the owner-aware layer on
3
- // top that fetches/decodes/uploads for you and swaps the texture when the source
4
- // changes - the same relationship createTexture/createShaderTexture have to
5
- // flux:gpu.
1
+ // CPU image codec plus the reactive load-and-upload convenience. The codec is
2
+ // flux:image, re-exported here (like the flux:gpu re-exports in gpu.ts) so
3
+ // applications import everything image-shaped from one place; createImage is
4
+ // the owner-aware layer on top that fetches/decodes/uploads for you and swaps
5
+ // the texture when the source changes.
6
6
 
7
7
  import { createMemo, onCleanup } from "@solidjs/signals"
8
+ import { decodeImage, type DecodedImage } from "flux:image"
8
9
  import { createTexture, destroyTexture, type TextureId } from "./gpu"
9
10
 
10
- export type DecodedImage = {
11
- data: Uint8Array
12
- width: number
13
- height: number
14
- }
15
-
16
- /**
17
- * Decodes encoded image bytes (PNG, JPEG, and the other formats the runtime's
18
- * image decoder supports) into raw, tightly-packed RGBA8 pixels plus the
19
- * decoded dimensions. Feed the result straight into `createTexture`. Use this
20
- * when you want manual control; for the common case reach for `createImage`.
21
- */
22
- export function decodeImage(bytes: Uint8Array): DecodedImage {
23
- return image.decodeImage(bytes)
24
- }
11
+ export { decodeImage, encodeImage } from "flux:image"
12
+ export type { DecodedImage } from "flux:image"
25
13
 
26
14
  export type ImageSource = string | Uint8Array
27
15
 
package/src/index.ts CHANGED
@@ -14,7 +14,7 @@ export { capabilities } from "./capabilities"
14
14
  export type { Capabilities, WindowSizeClass } from "./capabilities"
15
15
  export { createTexture } from "./gpu"
16
16
  export type { TextureId } from "./gpu"
17
- export { createImage, decodeImage } from "./image"
17
+ export { createImage, decodeImage, encodeImage } from "./image"
18
18
  export type { DecodedImage, ImageSource } from "./image"
19
19
  export { parseSvg, svg } from "./svg"
20
20
  export type { SvgDocument, SvgDraw } from "./svg"
package/src/types.d.ts CHANGED
@@ -23,10 +23,6 @@ declare global {
23
23
  readonly env: { readonly DEV: boolean }
24
24
  }
25
25
 
26
- let image: {
27
- decodeImage(bytes: Uint8Array): { data: Uint8Array, width: number, height: number }
28
- }
29
-
30
26
  let speech: {
31
27
  start(options: {
32
28
  model: Uint8Array, vadModel: Uint8Array, lang?: string, microphone?: number,