@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 +1 -1
- package/examples/README.md +2 -0
- package/examples/gpu-draw-list.tsx +1 -1
- package/examples/gpu-shared-params.tsx +135 -0
- package/package.json +5 -5
- package/src/gpu.ts +79 -52
- package/src/image.ts +8 -20
- package/src/index.ts +1 -1
- package/src/types.d.ts +0 -4
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.
|
|
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
|
|
package/examples/README.md
CHANGED
|
@@ -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.
|
|
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.
|
|
30
|
+
"@solidrt/flux-types": "0.0.46"
|
|
31
31
|
},
|
|
32
32
|
"peerDependencies": {
|
|
33
|
-
"@solidjs/signals": "2.0.0-beta.
|
|
34
|
-
"@solidjs/universal": "2.0.0-beta.
|
|
35
|
-
"solid-js": "2.0.0-beta.
|
|
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
|
-
//
|
|
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
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
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 {
|
|
49
|
-
// auto-free, for resources whose lifetime is managed by hand
|
|
50
|
-
// signal changes inside a long-lived component, handed across
|
|
51
|
-
// Without
|
|
52
|
-
// owner: a leak until unmount, then a double-free
|
|
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 = {
|
|
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
|
-
//
|
|
81
|
-
//
|
|
82
|
-
//
|
|
83
|
-
//
|
|
84
|
-
//
|
|
85
|
-
//
|
|
86
|
-
//
|
|
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
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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.
|
|
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
|
|
118
|
-
//
|
|
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)
|
|
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 `{
|
|
198
|
-
* the auto-free and own the disposal yourself even inside a reactive
|
|
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 (
|
|
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 `{
|
|
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 (
|
|
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 `
|
|
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 `{
|
|
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 (
|
|
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 `
|
|
282
|
-
* uniforms with `<texture params>` or `
|
|
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 `
|
|
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.
|
|
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 (
|
|
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 `
|
|
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 (
|
|
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
|
-
* `
|
|
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.
|
|
466
|
+
gpu.setTargetSize(currentId, next.width, next.height)
|
|
440
467
|
}
|
|
441
468
|
if (!sameRecord(next.params, current.params) && next.params) {
|
|
442
|
-
gpu.
|
|
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
|
-
* `
|
|
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 `
|
|
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 (
|
|
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 `{
|
|
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 (
|
|
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.
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
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
|
|
11
|
-
|
|
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,
|