@solidrt/core 0.0.44 → 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.
@@ -0,0 +1,81 @@
1
+ // A boundary shader: a view with repaintBoundary="snapshot" runs its
2
+ // rasterized subtree through a linked program and composites the result in
3
+ // its place. The program contract matches shader targets, not the window
4
+ // pass: uSource is the subtree's rasterization (top-left origin, so vUV
5
+ // needs no flip), iResolution is the boundary in physical pixels. The pass
6
+ // is split from content invalidation: animating params here re-runs only
7
+ // the pass - the panel's content is rasterized once and stays cached.
8
+ //
9
+ // outset adds a transparent margin the effect may write into: without it the
10
+ // wave clips at the layout box, with it the crests escape the box edge.
11
+ //
12
+ // Click the panel to toggle the warp between 0 and 1: at 0 the program is an
13
+ // identity pass, which must be indistinguishable from the plain snapshot.
14
+ import { render, onFrame, createSignal } from "@solidrt/core"
15
+ import { compileShader, destroyShader, glsl, linkProgram } from "@solidrt/core/gpu"
16
+
17
+ // { header: true } supplies #version and precision; the varyings are the
18
+ // program's own. Unlike the window pass there is no flip here: a target
19
+ // pass's vUV origin already matches the sampled texture's top-left.
20
+ let VERTEX = glsl`
21
+ out vec2 vUV;
22
+ void main() {
23
+ vec2 p = vec2(float((gl_VertexID << 1) & 2), float(gl_VertexID & 2));
24
+ vUV = p;
25
+ gl_Position = vec4(p * 2.0 - 1.0, 0.0, 1.0);
26
+ }
27
+ `
28
+
29
+ let WARP = glsl`
30
+ uniform sampler2D uSource;
31
+ uniform float uAmount;
32
+ uniform float uTime;
33
+ in vec2 vUV;
34
+ void main() {
35
+ vec2 uv = vUV;
36
+ uv.x += sin(uv.y * 24.0 + uTime * 3.0) * 0.03 * uAmount;
37
+ uv.y += sin(uv.x * 18.0 - uTime * 2.0) * 0.03 * uAmount;
38
+ fragColor = texture(uSource, uv);
39
+ }
40
+ `
41
+
42
+ function App() {
43
+ let vs = compileShader("vertex", VERTEX, { header: true })
44
+ let fs = compileShader("fragment", WARP, { header: true })
45
+ let warp = linkProgram(vs, fs, { label: "panel-warp" })
46
+ destroyShader(vs)
47
+ destroyShader(fs)
48
+
49
+ let [time, setTime] = createSignal(0)
50
+ let [amount, setAmount] = createSignal(1)
51
+ onFrame(tick => setTime(tick / 1000))
52
+
53
+ return (
54
+ <window alignItems="center" justifyContent="center">
55
+ <view
56
+ repaintBoundary="snapshot"
57
+ shader={{ program: warp, params: { uTime: time(), uAmount: amount() }, outset: 16 }}
58
+ onPointerDown={() => setAmount(a => (a > 0 ? 0 : 1))}
59
+ flexDirection="column"
60
+ gap={12}
61
+ alignItems="center"
62
+ justifyContent="center"
63
+ width={360}
64
+ height={240}
65
+ >
66
+ {/* Fills the boundary, so the box edge is a content edge: the warp
67
+ visibly ripples it out into the outset margin. */}
68
+ <rect position="absolute" width="100%" height="100%" radius={16} color="#dde3ec" />
69
+ <text fontSize={24} color="#222">Boundary shader</text>
70
+ <view flexDirection="row" gap={12}>
71
+ <rect width={70} height={70} radius={12} color="#0077ff" />
72
+ <rect width={70} height={70} radius={12} color="#ff6a00" />
73
+ <rect width={70} height={70} radius={12} color="#00c46a" />
74
+ </view>
75
+ <text fontSize={13} color="#666">Click to toggle warp (identity at 0)</text>
76
+ </view>
77
+ </window>
78
+ )
79
+ }
80
+
81
+ render(() => <App />)
@@ -0,0 +1,86 @@
1
+ // `viewBox` on a <view> is the fixed-aspect answer to "many screen sizes": you
2
+ // author the whole scene once, in your own made-up design units, and the view
3
+ // scales that space to fit its box. It is SVG's viewBox generalized to any
4
+ // subtree, not an SVG-only thing.
5
+ //
6
+ // Four facts, each demonstrated below:
7
+ // 1. It NEVER sizes the element. `viewBox` is a pure fit transform; the box
8
+ // still takes its size from layout (here `flex={1}`). A view with only a
9
+ // viewBox and no layout size is zero-sized and paints nothing.
10
+ // 2. It fits UNIFORMLY and centers - one scale for both axes, SVG's default
11
+ // preserveAspectRatio. Content never stretches; the leftover on the loose
12
+ // axis is letterbox, showing the window background through it.
13
+ // 3. Children live in DESIGN space. Their x/y, w/h, fontSize, stroke widths -
14
+ // everything resolves against the design size, not the box. The box a child
15
+ // inherits IS the design size, so a bare `d-rect` fills the design space
16
+ // and detached text wraps at its width.
17
+ // 4. Pointer coordinates arrive in design space too. localX/localY on the
18
+ // viewBox view (and on anything under it) read in design units, so no
19
+ // scale factor is threaded through the app's hit math.
20
+ //
21
+ // The payoff: no `windowSizeClass` branching, no per-breakpoint sizes, no
22
+ // scale factor anywhere. The same code runs unchanged from a desktop window to
23
+ // a phone. Reach for reflow (responsive-grid.tsx) only when the layout
24
+ // genuinely rearranges across form factors; for content with fixed internal
25
+ // geometry - diagrams, slides, dashboards, game boards, emulator screens - fit
26
+ // one design space instead.
27
+ //
28
+ // Resize the window and watch: the scene keeps its aspect ratio, everything
29
+ // including text scales together, and the readout reports the same design
30
+ // coordinates for the same spot on the scene at any window size.
31
+ import { render, createSignal, Show } from "@solidrt/core"
32
+
33
+ // The design space. Every number below is in these units - invent whatever
34
+ // suits the content and stay in it. 640x400 is 16:10.
35
+ const DESIGN_W = 640
36
+ const DESIGN_H = 400
37
+
38
+ function App() {
39
+ let [at, setAt] = createSignal<{ x: number; y: number } | null>(null)
40
+ let round = (v: number) => Math.round(v)
41
+
42
+ return (
43
+ // The window background is what shows through the letterbox bars.
44
+ <window>
45
+ <d-rect color="#0b0f17" />
46
+
47
+ {/* flex={1} sizes the box; viewBox only maps content into it (fact 1). */}
48
+ <view
49
+ flex={1}
50
+ viewBox={[DESIGN_W, DESIGN_H]}
51
+ onPointerMove={(e) => setAt({ x: e.localX, y: e.localY })}
52
+ onPointerLeave={() => setAt(null)}
53
+ >
54
+ {/* No w/h, so it fills the box it inherits - which under a viewBox is
55
+ the design space (fact 3). Its edges are the letterbox edges. */}
56
+ <d-rect color="#151b28" />
57
+
58
+ {/* A scene at literal design coordinates. No breakpoints, no
59
+ windowSize() reads, no scale factor: authored once, at this size. */}
60
+ <d-rect x={40} y={40} w={240} h={140} radius={12} color="#1f6feb" />
61
+ <d-rect x={300} y={40} w={300} h={140} radius={12} color="#3fb950" />
62
+ <d-oval x={40} y={220} w={140} h={140} color="#a371f7" />
63
+ <d-line x1={200} y1={300} x2={600} y2={300} color="#e3b341" strokeWidth={6} />
64
+ <d-text x={64} y={95} fontSize={28} color="#e6e6e6">
65
+ fixed design space
66
+ </d-text>
67
+ <d-text x={324} y={95} fontSize={28} color="#0b0f17">
68
+ {DESIGN_W} x {DESIGN_H}
69
+ </d-text>
70
+
71
+ {/* Pointer position in design units (fact 4), drawn in design units.
72
+ Non-keyed Show keeps the marker node mounted across moves (a
73
+ ternary on at() would recreate it every event); the accessor
74
+ reads update its props fine-grained. */}
75
+ <d-text x={200} y={330} fontSize={22} color="#8b949e">
76
+ {at() ? `design x ${round(at()!.x)}, y ${round(at()!.y)}` : "move the pointer over the scene"}
77
+ </d-text>
78
+ <Show when={at()}>
79
+ {(a) => <d-oval x={a().x - 8} y={a().y - 8} w={16} h={16} color="#f85149" />}
80
+ </Show>
81
+ </view>
82
+ </window>
83
+ )
84
+ }
85
+
86
+ render(() => <App />)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@solidrt/core",
3
- "version": "0.0.44",
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.44"
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).
@@ -71,25 +74,34 @@ export type { TextureFormat } from "flux:gpu"
71
74
  // runtime, distinct types to the checker, so a cross-space slip like
72
75
  // destroyBuffer(textureId) fails to compile. Exported so apps can annotate
73
76
  // storage (`let ids: TextureId[]`).
74
- export type { BufferId, ProgramId, RenderPipelineId, ShaderStageId, TextureId } from "flux:gpu"
77
+ export type { BufferId, DrawId, ProgramId, RenderPipelineId, ShaderStageId, TextureId } from "flux:gpu"
75
78
 
76
79
  // Re-exported so callers that depend on @solidrt/core -- like @solidrt/components
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,13 +114,27 @@ 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.
110
121
  export { copyTexture, destroyBuffer, renderTarget, setDraw } from "flux:gpu"
111
- export type { BlendMode, DrawRange, ShaderParams, Topology, VertexAttribute } from "flux:gpu"
122
+ export type { BlendMode, CullMode, DrawRange, IndexBinding, IndexFormat, IndexRange, ShaderParams, Topology, VertexAttribute } from "flux:gpu"
123
+
124
+ // The draw-list verbs, re-exported raw: entries live and die with their draw
125
+ // target (see createDrawTarget below), so there is no per-entry lifetime to
126
+ // wrap. addDraw adds an entry (appended, or inserted via opts.before) and
127
+ // returns its stable DrawId; removeDraw drops one; setDrawParams /
128
+ // setDrawTextures / setDrawRange are the per-entry forms of setTargetParams /
129
+ // setTargetTextures / setDraw, taking (target, draw, value) with identical
130
+ // merge and validation semantics. The per-object hot path is setDrawParams (a
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
134
+ // list order with a full permutation of the live ids - the sorting verb
135
+ // (opaque front-to-back, transparent back-to-front, re-issued when the
136
+ // camera moves).
137
+ export { addDraw, removeDraw, setDrawOrder, setDrawParams, setDrawRange, setDrawTextures } from "flux:gpu"
112
138
 
113
139
  // The device ceilings (max texture/target size, sampler inputs per pass,
114
140
  // vertex attributes per pipeline), queried once at startup. Creates and binds
@@ -181,8 +207,9 @@ export { captureSnapshot, readTexture } from "flux:gpu"
181
207
  * reactive scope the texture is freed automatically once that owner is
182
208
  * disposed; when called outside one (e.g. after an `await`, where the owner
183
209
  * is no longer current) nothing is registered and you must call
184
- * `destroyTexture` (from flux:gpu) yourself. Pass `{ manual: true }` to skip
185
- * 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.
186
213
  */
187
214
  export function createTexture(
188
215
  data: Uint8Array,
@@ -191,7 +218,7 @@ export function createTexture(
191
218
  opts?: CreateOptions & SamplerOptions & TextureFormatOptions,
192
219
  ): gpu.TextureId {
193
220
  let id = gpu.createTexture(data, width, height, opts)
194
- if (!opts?.manual && getOwner()) onCleanup(() => gpu.destroyTexture(id))
221
+ if (opts?.autoFree !== false && getOwner()) onCleanup(() => gpu.destroyTexture(id))
195
222
  return id
196
223
  }
197
224
 
@@ -201,7 +228,7 @@ export function createTexture(
201
228
  * `data` must hold at least `width * height` pixels at the declared format's
202
229
  * size (`* 4` bytes for the default "rgba8", `* 1` for "r8"; it may hold
203
230
  * several frames). Like `createTexture`, the texture is freed automatically
204
- * when the reactive owner is disposed (opt out with `{ manual: true }`);
231
+ * when the reactive owner is disposed (opt out with `{ autoFree: false }`);
205
232
  * created outside a reactive scope you must call `destroyTexture` (from
206
233
  * flux:gpu) yourself.
207
234
  */
@@ -212,7 +239,7 @@ export function createMutableTexture(
212
239
  opts?: CreateOptions & SamplerOptions & TextureFormatOptions,
213
240
  ): gpu.TextureId {
214
241
  let id = gpu.createMutableTexture(data, width, height, opts)
215
- if (!opts?.manual && getOwner()) onCleanup(() => gpu.destroyTexture(id))
242
+ if (opts?.autoFree !== false && getOwner()) onCleanup(() => gpu.destroyTexture(id))
216
243
  return id
217
244
  }
218
245
 
@@ -225,7 +252,7 @@ export function createMutableTexture(
225
252
  * scalars from a number, `vec2`/`vec3`/`vec4`/`mat4` from a flat number
226
253
  * array); drive their values with `<texture src={id} params={{...}} />`
227
254
  * (preferred) or, when there is no `<texture>` element for it, imperatively
228
- * with `setShaderParams`. `params` is its own argument - it seeds the same
255
+ * with `setTargetParams`. `params` is its own argument - it seeds the same
229
256
  * live channel those two drive - and takes `null` (or nothing) for a shader
230
257
  * without uniforms. A time-driven shader declares its own time uniform
231
258
  * (`uniform float uTime;`) and the app drives it like any other value.
@@ -235,7 +262,7 @@ export function createMutableTexture(
235
262
  * re-renders whenever a source changes - including a sampled target
236
263
  * re-rendering, transitively through chains. Frees the
237
264
  * texture and shader program when the reactive owner is disposed (opt out
238
- * with `{ manual: true }`); create outside any reactive scope for
265
+ * with `{ autoFree: false }`); create outside any reactive scope for
239
266
  * app-lifetime shaders. For a shader whose source or inputs change
240
267
  * reactively, use {@link createShaderTextureMemo} instead.
241
268
  *
@@ -258,26 +285,31 @@ export function createShaderTexture(
258
285
  opts?: CreateOptions & SamplerOptions & { textures?: Record<string, gpu.TextureId> },
259
286
  ): gpu.TextureId {
260
287
  let id = gpu.createShaderTexture(fragmentSrc, width, height, params, opts)
261
- if (!opts?.manual && getOwner()) onCleanup(() => gpu.destroyTexture(id))
288
+ if (opts?.autoFree !== false && getOwner()) onCleanup(() => gpu.destroyTexture(id))
262
289
  return id
263
290
  }
264
291
 
265
292
  /**
266
293
  * Creates a render target over a pipeline from `createRenderPipeline` and
267
294
  * renders it once, returning the texture id (usable anywhere a normal
268
- * texture id is, e.g. `<texture src>`; resize with `setShaderSize`, drive
269
- * 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
270
297
  * share one pipeline, and creating a target compiles nothing. The target
271
298
  * brings the per-target half: size, the concrete vertex `buffer` the
272
- * pipeline's attribute layout describes, the draw range (`vertexCount`
273
- * defaults to the rest of the buffer from `firstVertex` on, `instanceCount`
274
- * repeats it as instances told apart by `gl_InstanceID`; a fullscreen pass
275
- * over an attributeless pipeline is `{ vertexCount: 3 }` with a
276
- * covering-triangle vertex stage), uniforms, and
277
- * `clearColor`. Draw state (`attributes`, `topology`, `blend`, `depth`,
278
- * `depthWrite`) lives on the pipeline and throws here. Frees the target when
279
- * the reactive owner is disposed (opt out with `opts.manual`); the pipeline
280
- * is yours and outlives it.
299
+ * pipeline's attribute layout describes, the `instanceBuffer` its
300
+ * `instanceAttributes` describe (required exactly when it declares any),
301
+ * the draw range (`vertexCount` defaults to the rest of the buffer from
302
+ * `firstVertex` on, `instanceCount` repeats it as instances told apart by
303
+ * `gl_InstanceID` and defaults to one per instance-buffer record; a
304
+ * fullscreen pass over an attributeless pipeline is `{ vertexCount: 3 }`
305
+ * with a covering-triangle vertex stage), uniforms, and
306
+ * `clearColor`. An `indexBuffer` + `indexFormat` pair makes the draw indexed
307
+ * (shared vertices stored once), with the range in `firstIndex`/`indexCount`
308
+ * spelling - see IndexBinding/IndexRange. Draw state (`attributes`,
309
+ * `instanceAttributes`, `topology`, `blend`, `cull`, `depth`, `depthWrite`)
310
+ * lives on the pipeline
311
+ * and throws here. Frees the target when the reactive owner is disposed (opt
312
+ * out with `autoFree: false`); the pipeline is yours and outlives it.
281
313
  *
282
314
  * `render: "manual"` makes it a manual target: the runtime never renders it
283
315
  * (it starts cleared to `clearColor`), only an explicit `renderTarget(id)`
@@ -286,8 +318,7 @@ export function createShaderTexture(
286
318
  * previous contents under each draw - single-target accumulation - while
287
319
  * the default `"clear"` clears to `clearColor` per render; state that must
288
320
  * read its own pixels (decay, blur, simulation) still ping-pongs across two
289
- * manual targets, and `copyTexture` seeds either shape. Distinct from the
290
- * `manual` lifetime option: `render` is who renders, `manual` is who frees.
321
+ * manual targets, and `copyTexture` seeds either shape.
291
322
  */
292
323
  export function createShaderTarget(
293
324
  pipeline: gpu.RenderPipelineId,
@@ -297,15 +328,66 @@ export function createShaderTarget(
297
328
  opts?: {
298
329
  textures?: Record<string, gpu.TextureId>
299
330
  buffer?: gpu.BufferId
331
+ instanceBuffer?: gpu.BufferId
300
332
  clearColor?: [number, number, number, number]
301
333
  render?: "auto" | "manual"
302
334
  loadOp?: "clear" | "load"
303
- } & gpu.DrawRange &
335
+ } & (gpu.DrawRange | (gpu.IndexBinding & gpu.IndexRange)) &
304
336
  CreateOptions &
305
337
  SamplerOptions,
306
338
  ): gpu.TextureId {
307
339
  let id = gpu.createShaderTarget(pipeline, width, height, params, opts)
308
- if (!opts?.manual && getOwner()) onCleanup(() => gpu.destroyTexture(id))
340
+ if (opts?.autoFree !== false && getOwner()) onCleanup(() => gpu.destroyTexture(id))
341
+ return id
342
+ }
343
+
344
+ /**
345
+ * Creates a draw target: a render target holding an ordered, MUTABLE list of
346
+ * draws, rendered as one pass - clear once (color, and depth when declared),
347
+ * then every entry in list order into the same storage. This is the
348
+ * multi-pass primitive (N meshes x N pipelines sharing one depth buffer -
349
+ * what every 3D API calls a render pass), retained: build the list with
350
+ * `addDraw`, prune it with `removeDraw`, and drive per-entry state with
351
+ * `setDrawParams` / `setDrawTextures` / `setDrawRange`. `depth: true` gives
352
+ * the target the depth storage all entries share (cross-entry occlusion);
353
+ * whether an entry tests/writes it stays pipeline state, and a depth-testing
354
+ * pipeline into a depthless target throws at `addDraw`.
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
+ *
368
+ * The render contract is unchanged: the list is input data, so an ordinary
369
+ * (`render: "auto"`) draw target re-renders exactly when its entries or
370
+ * their inputs change - a static scene costs zero passes, and one render is
371
+ * one pass regardless of entry count. `render: "manual"` and `loadOp` work
372
+ * as on `createShaderTarget`. Returns the texture id; frees on owner
373
+ * disposal (opt out with `autoFree: false`), taking its entries with it - the
374
+ * entries' pipelines and buffers are yours and outlive it.
375
+ */
376
+ export function createDrawTarget(
377
+ width: number,
378
+ height: number,
379
+ params?: gpu.ShaderParams | null,
380
+ opts?: {
381
+ depth?: boolean
382
+ textures?: Record<string, gpu.TextureId>
383
+ clearColor?: [number, number, number, number]
384
+ render?: "auto" | "manual"
385
+ loadOp?: "clear" | "load"
386
+ } & CreateOptions &
387
+ SamplerOptions,
388
+ ): gpu.TextureId {
389
+ let id = gpu.createDrawTarget(width, height, params, opts)
390
+ if (opts?.autoFree !== false && getOwner()) onCleanup(() => gpu.destroyTexture(id))
309
391
  return id
310
392
  }
311
393
 
@@ -344,7 +426,7 @@ function sameRecord(
344
426
  * current texture id (use it as `<texture src={id()} />`) and keeps the GPU
345
427
  * resource in step with `spec` from then on. Changes that keep the compiled
346
428
  * program valid mutate in place at a stable id - a size change routes to
347
- * `setShaderSize`, a params change to `setShaderParams` - while a change to
429
+ * `setTargetSize`, a params change to `setTargetParams` - while a change to
348
430
  * the fragment source or the sampler bindings rebuilds at a fresh id, updates
349
431
  * the accessor, and destroys the old id. That destroy is frame-safe (the
350
432
  * runtime reclaims an id only once the render tree no longer references it),
@@ -381,10 +463,10 @@ export function createShaderTextureMemo(
381
463
  ) {
382
464
  // Program and inputs unchanged: mutate in place, the id stays stable.
383
465
  if (next.width !== current.width || next.height !== current.height) {
384
- gpu.setShaderSize(currentId, next.width, next.height)
466
+ gpu.setTargetSize(currentId, next.width, next.height)
385
467
  }
386
468
  if (!sameRecord(next.params, current.params) && next.params) {
387
- gpu.setShaderParams(currentId, next.params)
469
+ gpu.setTargetParams(currentId, next.params)
388
470
  }
389
471
  current = next
390
472
  return
@@ -428,7 +510,7 @@ function toUint8(data: ArrayBuffer | ArrayBufferView): Uint8Array {
428
510
  * reference `iResolution` and any uniform they declare (`float`/`int`
429
511
  * scalars from a number, `vec2`/`vec3`/`vec4`/`mat4` from a flat number
430
512
  * array); drive values with `<texture src={id} params={{...}} />` or
431
- * `setShaderParams`, exactly like a fragment shader.
513
+ * `setTargetParams`, exactly like a fragment shader.
432
514
  * `opts.depth` attaches a private depth buffer (cleared + tested per render);
433
515
  * `opts.depthWrite: false` (requires depth) keeps the test but stops the
434
516
  * draw from writing depth. `opts.blend: "add"` makes the draw accumulate
@@ -439,11 +521,18 @@ function toUint8(data: ArrayBuffer | ArrayBufferView): Uint8Array {
439
521
  * see DrawRange) defaults to the whole buffer drawn once and can be changed
440
522
  * later with `setDraw`; `instanceCount` is the standard answer to particles
441
523
  * and repeated meshes, N copies of the range told apart by `gl_InstanceID`
442
- * in the vertex stage. `opts.render: "manual"` and
524
+ * in the vertex stage. `opts.instanceAttributes` + `opts.instanceBuffer`
525
+ * (declare both or neither) give each instance its own interleaved record -
526
+ * real per-instance state instead of `gl_InstanceID` arithmetic - and
527
+ * `instanceCount` then defaults to one instance per record. An
528
+ * `indexBuffer` + `indexFormat` pair makes the draw
529
+ * indexed (shared vertices stored once), with the range in
530
+ * `firstIndex`/`indexCount` spelling; `opts.cull` discards one face set by
531
+ * winding (counter-clockwise as displayed = front). `opts.render: "manual"` and
443
532
  * `opts.loadOp` behave exactly as on {@link createShaderTarget}: step the
444
533
  * target with `renderTarget(id)`, and `loadOp: "load"` (manual-only) keeps
445
534
  * the previous contents under each draw. Frees the texture and GL program when the reactive
446
- * 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
447
536
  * scope for app-lifetime pipelines.
448
537
  */
449
538
  export function createPipelineTexture(
@@ -456,19 +545,22 @@ export function createPipelineTexture(
456
545
  textures?: Record<string, gpu.TextureId>
457
546
  attributes?: gpu.VertexAttribute[]
458
547
  buffer?: gpu.BufferId
548
+ instanceAttributes?: gpu.VertexAttribute[]
549
+ instanceBuffer?: gpu.BufferId
459
550
  topology?: gpu.Topology
460
551
  depth?: boolean
461
552
  depthWrite?: boolean
462
553
  blend?: gpu.BlendMode
554
+ cull?: gpu.CullMode
463
555
  clearColor?: [number, number, number, number]
464
556
  render?: "auto" | "manual"
465
557
  loadOp?: "clear" | "load"
466
- } & gpu.DrawRange &
558
+ } & (gpu.DrawRange | (gpu.IndexBinding & gpu.IndexRange)) &
467
559
  CreateOptions &
468
560
  SamplerOptions,
469
561
  ): gpu.TextureId {
470
562
  let id = gpu.createPipelineTexture(vertexSrc, fragmentSrc, width, height, params, opts)
471
- if (!opts?.manual && getOwner()) onCleanup(() => gpu.destroyTexture(id))
563
+ if (opts?.autoFree !== false && getOwner()) onCleanup(() => gpu.destroyTexture(id))
472
564
  return id
473
565
  }
474
566
 
@@ -477,13 +569,13 @@ export function createPipelineTexture(
477
569
  * Float32Array laid out to match the pipeline's interleaved attribute list).
478
570
  * Update it later with {@link writeBuffer}; the buffer's byte size is fixed at
479
571
  * creation, so reserve room up front for dynamic geometry. Freed automatically
480
- * when the reactive owner is disposed (opt out with `{ manual: true }`);
572
+ * when the reactive owner is disposed (opt out with `{ autoFree: false }`);
481
573
  * created outside a reactive scope you must call `destroyBuffer` yourself.
482
574
  * (Destruction order relative to pipelines does not matter.)
483
575
  */
484
576
  export function createBuffer(data: ArrayBuffer | ArrayBufferView, opts?: CreateOptions): gpu.BufferId {
485
577
  let id = gpu.createBuffer(toUint8(data), opts)
486
- if (!opts?.manual && getOwner()) onCleanup(() => gpu.destroyBuffer(id))
578
+ if (opts?.autoFree !== false && getOwner()) onCleanup(() => gpu.destroyBuffer(id))
487
579
  return id
488
580
  }
489
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"