@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.
- package/AGENTS.md +1 -1
- package/examples/README.md +4 -1
- package/examples/gpu-draw-list.tsx +110 -0
- package/examples/gpu-instancing.tsx +47 -18
- package/examples/gpu-pipeline.tsx +44 -14
- package/examples/gpu-shared-params.tsx +135 -0
- package/examples/responsive-grid.tsx +5 -0
- package/examples/view-shader-history.tsx +85 -0
- package/examples/view-shader.tsx +81 -0
- package/examples/view-viewbox.tsx +86 -0
- package/package.json +5 -5
- package/src/gpu.ts +151 -59
- package/src/image.ts +8 -20
- package/src/index.ts +1 -1
- package/src/types.d.ts +61 -4
|
@@ -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.
|
|
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).
|
|
@@ -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
|
-
//
|
|
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,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.
|
|
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 `{
|
|
185
|
-
* 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.
|
|
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 (
|
|
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 `{
|
|
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 (
|
|
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 `
|
|
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 `{
|
|
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 (
|
|
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 `
|
|
269
|
-
* 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
|
|
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
|
|
273
|
-
*
|
|
274
|
-
*
|
|
275
|
-
*
|
|
276
|
-
*
|
|
277
|
-
*
|
|
278
|
-
*
|
|
279
|
-
*
|
|
280
|
-
*
|
|
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.
|
|
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 (
|
|
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
|
-
* `
|
|
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.
|
|
466
|
+
gpu.setTargetSize(currentId, next.width, next.height)
|
|
385
467
|
}
|
|
386
468
|
if (!sameRecord(next.params, current.params) && next.params) {
|
|
387
|
-
gpu.
|
|
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
|
-
* `
|
|
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.
|
|
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 `
|
|
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 (
|
|
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 `{
|
|
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 (
|
|
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.
|
|
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"
|