@solidrt/core 0.0.40 → 0.0.42
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 +14 -3
- package/README.md +1 -1
- package/examples/README.md +5 -4
- package/examples/gpu-instancing.tsx +70 -0
- package/examples/gpu-particles.tsx +35 -35
- package/examples/gpu-pipeline.tsx +40 -37
- package/examples/gpu-raw-program.tsx +59 -40
- package/examples/gpu-shader.tsx +48 -41
- package/examples/gpu-texture-blend.tsx +20 -19
- package/examples/inline-image.tsx +1 -1
- package/examples/parse-svg.tsx +94 -0
- package/examples/text-import.tsx +2 -2
- package/examples/wave.glsl +5 -3
- package/examples/window-shader-history.tsx +23 -23
- package/examples/window-shader.tsx +30 -28
- package/jsx-runtime.d.ts +17 -9
- package/package.json +2 -2
- package/src/camera.ts +8 -4
- package/src/color.ts +14 -2
- package/src/core.ts +254 -15
- package/src/environment.ts +16 -3
- package/src/gpu.ts +176 -75
- package/src/image.ts +12 -11
- package/src/index.ts +6 -2
- package/src/renderer.ts +32 -11
- package/src/runtime-modules.d.ts +2 -2
- package/src/speech-recognition.ts +2 -1
- package/src/svg.ts +71 -0
- package/src/types.d.ts +111 -38
- package/src/window.ts +39 -18
- package/examples/svg.tsx +0 -49
package/src/gpu.ts
CHANGED
|
@@ -21,6 +21,26 @@
|
|
|
21
21
|
// both. WITHIN one pipeline draw, `blend: "add"` accumulates overlapping
|
|
22
22
|
// geometry additively (order-independent, no sorting); anything else draws
|
|
23
23
|
// with GL blending disabled and overwrites.
|
|
24
|
+
//
|
|
25
|
+
// The pixel contract. Three facts hold for every texture and target:
|
|
26
|
+
//
|
|
27
|
+
// - Clip space is y-down. `gl_Position` y = -1 is the top of the target, +1
|
|
28
|
+
// the bottom (GL's row 0 is clip y = -1, and Impeller samples row 0 as the
|
|
29
|
+
// top). A vertex stage carrying camera-up geometry must negate y, or fold
|
|
30
|
+
// the flip into its projection, or it draws upside down: Vulkan's
|
|
31
|
+
// convention, not desktop GL's. The fragment path absorbs the same flip
|
|
32
|
+
// already, so `vUV` is 0..1 with top-left origin and a fragment-only shader
|
|
33
|
+
// never sees it.
|
|
34
|
+
// - Color is premultiplied alpha. A target's RGB is expected already
|
|
35
|
+
// multiplied by its A - `vec4(rgb * a, a)`, not `vec4(rgb, a)`, which
|
|
36
|
+
// composites as opaque. That is what Impeller composites and what
|
|
37
|
+
// `<texture blendMode>` blends; `clearColor` is premultiplied too, so the
|
|
38
|
+
// default transparent black needs no thought.
|
|
39
|
+
// - Values are non-linear RGBA8, with no color-space concept. Every texture
|
|
40
|
+
// and target holds 8-bit RGBA UNORM exactly as written; nothing converts to
|
|
41
|
+
// or from linear light. `filter: "linear"` averages and `blend: "add"`
|
|
42
|
+
// accumulates non-linear values - the usual approximation, stated so
|
|
43
|
+
// shaders written today stay correct if a format vocabulary arrives.
|
|
24
44
|
|
|
25
45
|
import { createEffect, createSignal, getOwner, onCleanup, untrack } from "@solidjs/signals"
|
|
26
46
|
import * as gpu from "flux:gpu"
|
|
@@ -30,13 +50,22 @@ import * as gpu from "flux:gpu"
|
|
|
30
50
|
// signal changes inside a long-lived component, handed across owners, ...).
|
|
31
51
|
// Without it, each rebuild would stack another onCleanup on the component
|
|
32
52
|
// owner: a leak until unmount, then a double-free against manual destroys.
|
|
33
|
-
|
|
53
|
+
// `label` is a free-form debug name (WebGPU's label): surfaced by the dev
|
|
54
|
+
// tooling's GPU inventory and engine log messages, never interpreted, kept
|
|
55
|
+
// across id-stable resizes.
|
|
56
|
+
export type CreateOptions = { manual?: boolean; label?: string }
|
|
34
57
|
|
|
35
58
|
// Sampling options every texture-producing create* helper accepts, applied at
|
|
36
59
|
// creation as a property of the texture id (there is no set-sampler-later).
|
|
37
60
|
export type SamplerOptions = { filter?: gpu.FilterMode; wrap?: gpu.WrapMode }
|
|
38
61
|
export type { FilterMode, WrapMode } from "flux:gpu"
|
|
39
62
|
|
|
63
|
+
// The branded id types, one per id space (see flux:gpu): plain numbers at
|
|
64
|
+
// runtime, distinct types to the checker, so a cross-space slip like
|
|
65
|
+
// destroyBuffer(textureId) fails to compile. Exported so apps can annotate
|
|
66
|
+
// storage (`let ids: TextureId[]`).
|
|
67
|
+
export type { BufferId, ProgramId, RenderPipelineId, ShaderStageId, TextureId } from "flux:gpu"
|
|
68
|
+
|
|
40
69
|
// Re-exported so callers that depend on @solidrt/core -- like @solidrt/components
|
|
41
70
|
// -- need not import flux directly: destroyTexture for the manual-cleanup path
|
|
42
71
|
// (textures made outside a reactive scope, e.g. after an await, are not
|
|
@@ -57,20 +86,64 @@ export {
|
|
|
57
86
|
uploadTexture,
|
|
58
87
|
} from "flux:gpu"
|
|
59
88
|
|
|
60
|
-
// Pipeline plumbing re-exported raw:
|
|
61
|
-
// its buffer gained or lost dynamic
|
|
62
|
-
//
|
|
63
|
-
|
|
64
|
-
|
|
89
|
+
// Pipeline plumbing re-exported raw: setDraw re-renders a pipeline with an
|
|
90
|
+
// updated draw range (vertexCount after its buffer gained or lost dynamic
|
|
91
|
+
// geometry, firstVertex for a different window of a shared buffer,
|
|
92
|
+
// instanceCount for an instanced population; absent keys keep their current
|
|
93
|
+
// value, like params); destroyBuffer is the manual
|
|
94
|
+
// cleanup path for buffers created outside a reactive scope. renderTarget is
|
|
95
|
+
// the explicit render verb for `render: "manual"` targets - targets whose
|
|
96
|
+
// pass is state (accumulation, feedback) rather than a pure function of its
|
|
97
|
+
// inputs, which the runtime therefore never renders on its own; the app
|
|
98
|
+
// steps them, usually from onFrame. (`render: "manual"` is the render mode;
|
|
99
|
+
// the unrelated `manual: true` create option is the lifetime opt-out above.)
|
|
100
|
+
// copyTexture overwrites a manual target with another texture's pixels
|
|
101
|
+
// GPU-side (exact, same size): seed a loadOp "load" accumulator, snapshot a
|
|
102
|
+
// ping-pong buffer, reset state to a known image.
|
|
103
|
+
export { copyTexture, destroyBuffer, renderTarget, setDraw } from "flux:gpu"
|
|
104
|
+
export type { BlendMode, DrawRange, ShaderParams, Topology, VertexAttribute } from "flux:gpu"
|
|
105
|
+
|
|
106
|
+
// The device ceilings (max texture/target size, sampler inputs per pass,
|
|
107
|
+
// vertex attributes per pipeline), queried once at startup. Creates and binds
|
|
108
|
+
// validate against them and throw naming the limit; read these to size within
|
|
109
|
+
// the device instead (e.g. clamp a supersampled target to maxTextureSize).
|
|
110
|
+
export { limits } from "flux:gpu"
|
|
65
111
|
|
|
66
112
|
// The raw shading layer, re-exported as-is - no reactive wrapper, the app
|
|
67
113
|
// owns these lifetimes. compileShader compiles one stage from complete GLSL
|
|
68
114
|
// ES (or with the standard header via { header: true }); linkProgram links a
|
|
69
|
-
// vertex and a fragment stage into a program handle
|
|
70
|
-
//
|
|
71
|
-
//
|
|
72
|
-
//
|
|
73
|
-
|
|
115
|
+
// vertex and a fragment stage into a program handle; createRenderPipeline
|
|
116
|
+
// pairs a program with draw state (vertex layout, topology, blend, depth -
|
|
117
|
+
// how it draws) into a pipeline handle that backs any number of
|
|
118
|
+
// createShaderTarget calls (and compiles nothing per pipeline or target);
|
|
119
|
+
// destroyShader / destroyProgram / destroyRenderPipeline free by id space,
|
|
120
|
+
// any order safe against live users. createShaderTexture/createPipelineTexture
|
|
121
|
+
// remain the fused conveniences on top.
|
|
122
|
+
export {
|
|
123
|
+
compileShader,
|
|
124
|
+
createRenderPipeline,
|
|
125
|
+
destroyProgram,
|
|
126
|
+
destroyRenderPipeline,
|
|
127
|
+
destroyShader,
|
|
128
|
+
linkProgram,
|
|
129
|
+
} from "flux:gpu"
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Tags an inline GLSL source, returning it unchanged. Shaders small enough to
|
|
133
|
+
* belong beside the code that uses them stay in the file; the tag is what makes
|
|
134
|
+
* them legible there, because editors highlight GLSL inside a template literal
|
|
135
|
+
* only when a known tag marks it (the name matters - `glsl` is the one the
|
|
136
|
+
* grammars look for).
|
|
137
|
+
*
|
|
138
|
+
* Interpolated values are stringified verbatim, with no GLSL-aware formatting:
|
|
139
|
+
* `${2}` splices in the int literal `2`, which will not assign to a float. Pass
|
|
140
|
+
* anything that varies as a uniform instead of building it into the source.
|
|
141
|
+
*
|
|
142
|
+
* Raw semantics, so backslashes reach the compiler as written: the GLSL
|
|
143
|
+
* preprocessor continues a line with a trailing `\`, which a cooked template
|
|
144
|
+
* would reject as an invalid escape and silently pass through as `undefined`.
|
|
145
|
+
*/
|
|
146
|
+
export let glsl = String.raw
|
|
74
147
|
|
|
75
148
|
// captureSnapshot renders a node to a texture and readTexture reads any
|
|
76
149
|
// texture's bytes back. A laid-out node captures its layout box; a `d-*` node
|
|
@@ -108,7 +181,7 @@ export function createTexture(
|
|
|
108
181
|
width: number,
|
|
109
182
|
height: number,
|
|
110
183
|
opts?: CreateOptions & SamplerOptions,
|
|
111
|
-
):
|
|
184
|
+
): gpu.TextureId {
|
|
112
185
|
let id = gpu.createTexture(data, width, height, opts)
|
|
113
186
|
if (!opts?.manual && getOwner()) onCleanup(() => gpu.destroyTexture(id))
|
|
114
187
|
return id
|
|
@@ -128,7 +201,7 @@ export function createMutableTexture(
|
|
|
128
201
|
width: number,
|
|
129
202
|
height: number,
|
|
130
203
|
opts?: CreateOptions & SamplerOptions,
|
|
131
|
-
):
|
|
204
|
+
): gpu.TextureId {
|
|
132
205
|
let id = gpu.createMutableTexture(data, width, height, opts)
|
|
133
206
|
if (!opts?.manual && getOwner()) onCleanup(() => gpu.destroyTexture(id))
|
|
134
207
|
return id
|
|
@@ -137,13 +210,17 @@ export function createMutableTexture(
|
|
|
137
210
|
/**
|
|
138
211
|
* Compiles a GLSL ES 3.00 fragment shader and renders it into a texture,
|
|
139
212
|
* returning the texture id (usable anywhere a normal texture id is, e.g.
|
|
140
|
-
* `<texture src>`)
|
|
141
|
-
*
|
|
213
|
+
* `<texture src>`) - hence the name: what comes back is a texture, not a
|
|
214
|
+
* shader object. The fragment body may reference `vUV` (0..1, top-left
|
|
215
|
+
* origin), `iResolution`, and any uniform it declares (`float`/`int`
|
|
142
216
|
* scalars from a number, `vec2`/`vec3`/`vec4`/`mat4` from a flat number
|
|
143
217
|
* array); drive their values with `<texture src={id} params={{...}} />`
|
|
144
218
|
* (preferred) or, when there is no `<texture>` element for it, imperatively
|
|
145
|
-
* with `setShaderParams`.
|
|
146
|
-
*
|
|
219
|
+
* with `setShaderParams`. `params` is its own argument - it seeds the same
|
|
220
|
+
* live channel those two drive - and takes `null` (or nothing) for a shader
|
|
221
|
+
* without uniforms. A time-driven shader declares its own time uniform
|
|
222
|
+
* (`uniform float uTime;`) and the app drives it like any other value.
|
|
223
|
+
* `opts.textures` binds each declared `uniform sampler2D` to an existing texture id
|
|
147
224
|
* (e.g. a camera or decoded image, or another shader/pipeline target) so the
|
|
148
225
|
* shader can read it; bound inputs are live dependencies, so the shader
|
|
149
226
|
* re-renders whenever a source changes - including a sampled target
|
|
@@ -151,72 +228,85 @@ export function createMutableTexture(
|
|
|
151
228
|
* texture and shader program when the reactive owner is disposed (opt out
|
|
152
229
|
* with `{ manual: true }`); create outside any reactive scope for
|
|
153
230
|
* app-lifetime shaders. For a shader whose source or inputs change
|
|
154
|
-
* reactively, use {@link
|
|
231
|
+
* reactively, use {@link createShaderTextureMemo} instead.
|
|
155
232
|
*
|
|
156
|
-
* That preamble (`#version 300 es`, precision, `vUV`, `iResolution`,
|
|
233
|
+
* That preamble (`#version 300 es`, precision, `vUV`, `iResolution`,
|
|
157
234
|
* `fragColor`) is injected only into sources that do not declare their own
|
|
158
|
-
* `#version` line
|
|
235
|
+
* `#version` line, and declares exactly what the runtime provides - nothing
|
|
236
|
+
* app-driven. A source starting with `#version 300 es` compiles exactly
|
|
159
237
|
* as written, so a shader carrying its own uniform names - one ported from
|
|
160
238
|
* elsewhere - runs unchanged here without dropping to compileShader /
|
|
161
239
|
* linkProgram. The built-in vertex stage still supplies `vUV`; declare
|
|
162
240
|
* `in vec2 vUV;` yourself to read it.
|
|
163
241
|
*/
|
|
164
|
-
export function
|
|
242
|
+
export function createShaderTexture(
|
|
165
243
|
fragmentSrc: string,
|
|
166
244
|
width: number,
|
|
167
245
|
height: number,
|
|
168
|
-
params?: gpu.ShaderParams,
|
|
169
|
-
textures?: Record<string,
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
let id = gpu.createShader(fragmentSrc, width, height, params, textures, opts)
|
|
246
|
+
params?: gpu.ShaderParams | null,
|
|
247
|
+
opts?: CreateOptions & SamplerOptions & { textures?: Record<string, gpu.TextureId> },
|
|
248
|
+
): gpu.TextureId {
|
|
249
|
+
let id = gpu.createShaderTexture(fragmentSrc, width, height, params, opts)
|
|
173
250
|
if (!opts?.manual && getOwner()) onCleanup(() => gpu.destroyTexture(id))
|
|
174
251
|
return id
|
|
175
252
|
}
|
|
176
253
|
|
|
177
254
|
/**
|
|
178
|
-
* Creates a render target over a
|
|
179
|
-
* once, returning the texture id (usable anywhere a normal
|
|
180
|
-
* e.g. `<texture src>`; resize with `setShaderSize`, drive
|
|
181
|
-
* `<texture params>` or `setShaderParams`). Many targets may
|
|
182
|
-
*
|
|
183
|
-
*
|
|
184
|
-
*
|
|
185
|
-
*
|
|
186
|
-
*
|
|
255
|
+
* Creates a render target over a pipeline from `createRenderPipeline` and
|
|
256
|
+
* renders it once, returning the texture id (usable anywhere a normal
|
|
257
|
+
* texture id is, e.g. `<texture src>`; resize with `setShaderSize`, drive
|
|
258
|
+
* uniforms with `<texture params>` or `setShaderParams`). Many targets may
|
|
259
|
+
* share one pipeline, and creating a target compiles nothing. The target
|
|
260
|
+
* brings the per-target half: size, the concrete vertex `buffer` the
|
|
261
|
+
* pipeline's attribute layout describes, the draw range (`vertexCount`
|
|
262
|
+
* defaults to the rest of the buffer from `firstVertex` on, `instanceCount`
|
|
263
|
+
* repeats it as instances told apart by `gl_InstanceID`; a fullscreen pass
|
|
264
|
+
* over an attributeless pipeline is `{ vertexCount: 3 }` with a
|
|
265
|
+
* covering-triangle vertex stage), uniforms, and
|
|
266
|
+
* `clearColor`. Draw state (`attributes`, `topology`, `blend`, `depth`,
|
|
267
|
+
* `depthWrite`) lives on the pipeline and throws here. Frees the target when
|
|
268
|
+
* the reactive owner is disposed (opt out with `opts.manual`); the pipeline
|
|
269
|
+
* is yours and outlives it.
|
|
270
|
+
*
|
|
271
|
+
* `render: "manual"` makes it a manual target: the runtime never renders it
|
|
272
|
+
* (it starts cleared to `clearColor`), only an explicit `renderTarget(id)`
|
|
273
|
+
* does, in call order - which is what legalizes feedback state stepped by
|
|
274
|
+
* the app. `loadOp: "load"` (manual-only, throws otherwise) keeps the
|
|
275
|
+
* previous contents under each draw - single-target accumulation - while
|
|
276
|
+
* the default `"clear"` clears to `clearColor` per render; state that must
|
|
277
|
+
* read its own pixels (decay, blur, simulation) still ping-pongs across two
|
|
278
|
+
* manual targets, and `copyTexture` seeds either shape. Distinct from the
|
|
279
|
+
* `manual` lifetime option: `render` is who renders, `manual` is who frees.
|
|
187
280
|
*/
|
|
188
281
|
export function createShaderTarget(
|
|
189
|
-
|
|
282
|
+
pipeline: gpu.RenderPipelineId,
|
|
190
283
|
width: number,
|
|
191
284
|
height: number,
|
|
285
|
+
params?: gpu.ShaderParams | null,
|
|
192
286
|
opts?: {
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
attributes?: gpu.VertexAttribute[]
|
|
196
|
-
buffer?: number
|
|
197
|
-
topology?: gpu.Topology
|
|
198
|
-
vertexCount?: number
|
|
199
|
-
depth?: boolean
|
|
200
|
-
depthWrite?: boolean
|
|
201
|
-
blend?: gpu.BlendMode
|
|
287
|
+
textures?: Record<string, gpu.TextureId>
|
|
288
|
+
buffer?: gpu.BufferId
|
|
202
289
|
clearColor?: [number, number, number, number]
|
|
203
|
-
|
|
290
|
+
render?: "auto" | "manual"
|
|
291
|
+
loadOp?: "clear" | "load"
|
|
292
|
+
} & gpu.DrawRange &
|
|
293
|
+
CreateOptions &
|
|
204
294
|
SamplerOptions,
|
|
205
|
-
):
|
|
206
|
-
let id = gpu.createShaderTarget(
|
|
295
|
+
): gpu.TextureId {
|
|
296
|
+
let id = gpu.createShaderTarget(pipeline, width, height, params, opts)
|
|
207
297
|
if (!opts?.manual && getOwner()) onCleanup(() => gpu.destroyTexture(id))
|
|
208
298
|
return id
|
|
209
299
|
}
|
|
210
300
|
|
|
211
|
-
/** The reactive shader description `
|
|
212
|
-
* (`filter`/`wrap`) is creation-time state, so changing it rebuilds
|
|
213
|
-
* fresh id, like a fragment-source or sampler-binding change. */
|
|
301
|
+
/** The reactive shader description `createShaderTextureMemo` builds from.
|
|
302
|
+
* Sampling (`filter`/`wrap`) is creation-time state, so changing it rebuilds
|
|
303
|
+
* at a fresh id, like a fragment-source or sampler-binding change. */
|
|
214
304
|
export type ShaderSpec = {
|
|
215
305
|
fragmentSrc: string
|
|
216
306
|
width: number
|
|
217
307
|
height: number
|
|
218
308
|
params?: gpu.ShaderParams
|
|
219
|
-
textures?: Record<string,
|
|
309
|
+
textures?: Record<string, gpu.TextureId>
|
|
220
310
|
} & SamplerOptions
|
|
221
311
|
|
|
222
312
|
// Shallow name->value equality for params/textures records; treats undefined
|
|
@@ -261,12 +351,12 @@ function sameRecord(
|
|
|
261
351
|
* covered: it throws at the call site, where an ordinary try/catch works and
|
|
262
352
|
* there is no previous shader to fall back to.
|
|
263
353
|
*/
|
|
264
|
-
export function
|
|
354
|
+
export function createShaderTextureMemo(
|
|
265
355
|
spec: () => ShaderSpec,
|
|
266
356
|
opts?: { onError?: (error: unknown) => void },
|
|
267
|
-
): () =>
|
|
357
|
+
): () => gpu.TextureId {
|
|
268
358
|
let make = (s: ShaderSpec) =>
|
|
269
|
-
gpu.
|
|
359
|
+
gpu.createShaderTexture(s.fragmentSrc, s.width, s.height, s.params, { textures: s.textures, filter: s.filter, wrap: s.wrap })
|
|
270
360
|
let current = untrack(spec)
|
|
271
361
|
let currentId = make(current)
|
|
272
362
|
let [id, setId] = createSignal(currentId)
|
|
@@ -317,45 +407,56 @@ function toUint8(data: ArrayBuffer | ArrayBufferView): Uint8Array {
|
|
|
317
407
|
/**
|
|
318
408
|
* Compiles a GLSL ES 3.00 vertex+fragment pipeline and renders it into a
|
|
319
409
|
* texture, returning the texture id (usable anywhere a normal texture id is,
|
|
320
|
-
* e.g. `<texture src>`)
|
|
410
|
+
* e.g. `<texture src>`) - named, like `createShaderTexture`, for what comes
|
|
411
|
+
* back. Unlike `createShaderTexture` the vertex stage is yours:
|
|
321
412
|
* declare `in` attributes matching `opts.attributes` (one interleaved vertex
|
|
322
413
|
* in `opts.buffer`, a {@link createBuffer} id) and your own varyings toward
|
|
323
|
-
* the fragment stage.
|
|
324
|
-
*
|
|
325
|
-
*
|
|
326
|
-
*
|
|
327
|
-
*
|
|
414
|
+
* the fragment stage. Clip space is y-down: `gl_Position` y = -1 is the top
|
|
415
|
+
* row of the target and +1 the bottom, so camera-up geometry must negate y
|
|
416
|
+
* (or fold the flip into its projection) to display up. Both sources may
|
|
417
|
+
* reference `iResolution` and any uniform they declare (`float`/`int`
|
|
418
|
+
* scalars from a number, `vec2`/`vec3`/`vec4`/`mat4` from a flat number
|
|
419
|
+
* array); drive values with `<texture src={id} params={{...}} />` or
|
|
420
|
+
* `setShaderParams`, exactly like a fragment shader.
|
|
328
421
|
* `opts.depth` attaches a private depth buffer (cleared + tested per render);
|
|
329
422
|
* `opts.depthWrite: false` (requires depth) keeps the test but stops the
|
|
330
423
|
* draw from writing depth. `opts.blend: "add"` makes the draw accumulate
|
|
331
424
|
* overlapping geometry additively (order-independent, no sorting) instead of
|
|
332
425
|
* overwriting; a depth-tested additive pass is `{ depth: true, blend: "add",
|
|
333
426
|
* depthWrite: false }` - each option only does what it says, neither implies
|
|
334
|
-
* the other.
|
|
335
|
-
*
|
|
427
|
+
* the other. The draw range (`firstVertex`, `vertexCount`, `instanceCount` -
|
|
428
|
+
* see DrawRange) defaults to the whole buffer drawn once and can be changed
|
|
429
|
+
* later with `setDraw`; `instanceCount` is the standard answer to particles
|
|
430
|
+
* and repeated meshes, N copies of the range told apart by `gl_InstanceID`
|
|
431
|
+
* in the vertex stage. `opts.render: "manual"` and
|
|
432
|
+
* `opts.loadOp` behave exactly as on {@link createShaderTarget}: step the
|
|
433
|
+
* target with `renderTarget(id)`, and `loadOp: "load"` (manual-only) keeps
|
|
434
|
+
* the previous contents under each draw. Frees the texture and GL program when the reactive
|
|
336
435
|
* owner is disposed (opt out with `opts.manual`); create outside any reactive
|
|
337
436
|
* scope for app-lifetime pipelines.
|
|
338
437
|
*/
|
|
339
|
-
export function
|
|
438
|
+
export function createPipelineTexture(
|
|
340
439
|
vertexSrc: string,
|
|
341
440
|
fragmentSrc: string,
|
|
342
441
|
width: number,
|
|
343
442
|
height: number,
|
|
443
|
+
params?: gpu.ShaderParams | null,
|
|
344
444
|
opts?: {
|
|
345
|
-
|
|
346
|
-
textures?: Record<string, number>
|
|
445
|
+
textures?: Record<string, gpu.TextureId>
|
|
347
446
|
attributes?: gpu.VertexAttribute[]
|
|
348
|
-
buffer?:
|
|
447
|
+
buffer?: gpu.BufferId
|
|
349
448
|
topology?: gpu.Topology
|
|
350
|
-
vertexCount?: number
|
|
351
449
|
depth?: boolean
|
|
352
450
|
depthWrite?: boolean
|
|
353
451
|
blend?: gpu.BlendMode
|
|
354
452
|
clearColor?: [number, number, number, number]
|
|
355
|
-
|
|
453
|
+
render?: "auto" | "manual"
|
|
454
|
+
loadOp?: "clear" | "load"
|
|
455
|
+
} & gpu.DrawRange &
|
|
456
|
+
CreateOptions &
|
|
356
457
|
SamplerOptions,
|
|
357
|
-
):
|
|
358
|
-
let id = gpu.
|
|
458
|
+
): gpu.TextureId {
|
|
459
|
+
let id = gpu.createPipelineTexture(vertexSrc, fragmentSrc, width, height, params, opts)
|
|
359
460
|
if (!opts?.manual && getOwner()) onCleanup(() => gpu.destroyTexture(id))
|
|
360
461
|
return id
|
|
361
462
|
}
|
|
@@ -367,10 +468,10 @@ export function createPipeline(
|
|
|
367
468
|
* creation, so reserve room up front for dynamic geometry. Freed automatically
|
|
368
469
|
* when the reactive owner is disposed (opt out with `{ manual: true }`);
|
|
369
470
|
* created outside a reactive scope you must call `destroyBuffer` yourself.
|
|
370
|
-
*
|
|
471
|
+
* (Destruction order relative to pipelines does not matter.)
|
|
371
472
|
*/
|
|
372
|
-
export function createBuffer(data: ArrayBuffer | ArrayBufferView, opts?: CreateOptions):
|
|
373
|
-
let id = gpu.createBuffer(toUint8(data))
|
|
473
|
+
export function createBuffer(data: ArrayBuffer | ArrayBufferView, opts?: CreateOptions): gpu.BufferId {
|
|
474
|
+
let id = gpu.createBuffer(toUint8(data), opts)
|
|
374
475
|
if (!opts?.manual && getOwner()) onCleanup(() => gpu.destroyBuffer(id))
|
|
375
476
|
return id
|
|
376
477
|
}
|
|
@@ -380,6 +481,6 @@ export function createBuffer(data: ArrayBuffer | ArrayBufferView, opts?: CreateO
|
|
|
380
481
|
* pipeline drawing from the buffer re-renders with its last-applied params,
|
|
381
482
|
* so geometry-only changes reach the screen without a params update.
|
|
382
483
|
*/
|
|
383
|
-
export function writeBuffer(id:
|
|
484
|
+
export function writeBuffer(id: gpu.BufferId, data: ArrayBuffer | ArrayBufferView, byteOffset?: number): void {
|
|
384
485
|
gpu.writeBuffer(id, toUint8(data), byteOffset)
|
|
385
486
|
}
|
package/src/image.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
// CPU image codec plus the reactive load-and-upload convenience. decodeImage is
|
|
2
2
|
// the raw primitive (no GPU involved); createImage is the owner-aware layer on
|
|
3
3
|
// top that fetches/decodes/uploads for you and swaps the texture when the source
|
|
4
|
-
// changes - the same relationship createTexture/
|
|
4
|
+
// changes - the same relationship createTexture/createShaderTexture have to
|
|
5
|
+
// flux:gpu.
|
|
5
6
|
|
|
6
7
|
import { createMemo, onCleanup } from "@solidjs/signals"
|
|
7
|
-
import { createTexture, destroyTexture } from "./gpu"
|
|
8
|
+
import { createTexture, destroyTexture, type TextureId } from "./gpu"
|
|
8
9
|
|
|
9
10
|
export type DecodedImage = {
|
|
10
11
|
data: Uint8Array
|
|
@@ -32,13 +33,13 @@ export type ImageSource = string | Uint8Array
|
|
|
32
33
|
// Uint8Array sources bypass all of this: no key, per-mount texture.
|
|
33
34
|
type ImageEntry = {
|
|
34
35
|
refs: number
|
|
35
|
-
texture:
|
|
36
|
-
promise: Promise<
|
|
36
|
+
texture: TextureId | undefined
|
|
37
|
+
promise: Promise<TextureId>
|
|
37
38
|
}
|
|
38
39
|
|
|
39
40
|
let imageCache = new Map<string, ImageEntry>()
|
|
40
41
|
|
|
41
|
-
async function loadImage(url: string): Promise<
|
|
42
|
+
async function loadImage(url: string): Promise<TextureId> {
|
|
42
43
|
// Images are assets: cache to disk, no freshness. Use a versioned URL (or
|
|
43
44
|
// fetch + decodeImage manually) when a URL's content must be re-checked.
|
|
44
45
|
let res = await fetch(url, { cache: "force-cache" })
|
|
@@ -56,7 +57,7 @@ async function loadImage(url: string): Promise<number> {
|
|
|
56
57
|
function acquireImage(url: string): ImageEntry {
|
|
57
58
|
let entry = imageCache.get(url)
|
|
58
59
|
if (!entry) {
|
|
59
|
-
let e: ImageEntry = { refs: 0, texture:
|
|
60
|
+
let e: ImageEntry = { refs: 0, texture: undefined, promise: undefined as never }
|
|
60
61
|
e.promise = loadImage(url).then(
|
|
61
62
|
id => {
|
|
62
63
|
// Everyone released while the load was in flight: nothing owns the
|
|
@@ -91,7 +92,7 @@ function releaseImage(url: string): void {
|
|
|
91
92
|
if (!entry) return
|
|
92
93
|
entry.refs--
|
|
93
94
|
if (entry.refs > 0) return
|
|
94
|
-
if (entry.texture
|
|
95
|
+
if (entry.texture !== undefined) {
|
|
95
96
|
destroyTexture(entry.texture)
|
|
96
97
|
imageCache.delete(url)
|
|
97
98
|
}
|
|
@@ -122,10 +123,10 @@ function releaseImage(url: string): void {
|
|
|
122
123
|
* `createImage` earns its async only for a fetched string URL or a reactive
|
|
123
124
|
* source.
|
|
124
125
|
*/
|
|
125
|
-
export function createImage(src: ImageSource | (() => ImageSource)): () =>
|
|
126
|
+
export function createImage(src: ImageSource | (() => ImageSource)): () => TextureId {
|
|
126
127
|
let getSrc = typeof src === "function" ? src : () => src
|
|
127
128
|
|
|
128
|
-
return createMemo<
|
|
129
|
+
return createMemo<TextureId>(async () => {
|
|
129
130
|
let source = getSrc()
|
|
130
131
|
|
|
131
132
|
if (typeof source === "string") {
|
|
@@ -138,9 +139,9 @@ export function createImage(src: ImageSource | (() => ImageSource)): () => numbe
|
|
|
138
139
|
}
|
|
139
140
|
|
|
140
141
|
// Byte sources decode and upload synchronously; this run owns the texture.
|
|
141
|
-
let holder = { id:
|
|
142
|
+
let holder: { id: TextureId | undefined } = { id: undefined }
|
|
142
143
|
onCleanup(() => {
|
|
143
|
-
if (holder.id
|
|
144
|
+
if (holder.id !== undefined) destroyTexture(holder.id)
|
|
144
145
|
})
|
|
145
146
|
let decoded: DecodedImage
|
|
146
147
|
try {
|
package/src/index.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export * from "./renderer"
|
|
2
|
-
export { setFocus,
|
|
3
|
-
export type { BoundingBox } from "./core"
|
|
2
|
+
export { setFocus, focusedNode, startTextInput, textInputActive, getFocusables, measureText, getBoundingBox, getBoundingBoxViewport, onPointerMove } from "./core"
|
|
3
|
+
export type { BoundingBox, GlobalPointerEvent } from "./core"
|
|
4
4
|
export { parseColor, mixColors, brightness, createLinearGradient, createRadialGradient } from "./color"
|
|
5
5
|
export type { Gradient, GradientStop } from "./color"
|
|
6
6
|
export { onFrame, onLayout, onResize, onWindowFocus, onWindowBlur, onBack, exit } from "./window"
|
|
@@ -13,8 +13,11 @@ export type { GamepadState } from "./gamepad"
|
|
|
13
13
|
export { capabilities } from "./capabilities"
|
|
14
14
|
export type { Capabilities, WindowSizeClass } from "./capabilities"
|
|
15
15
|
export { createTexture } from "./gpu"
|
|
16
|
+
export type { TextureId } from "./gpu"
|
|
16
17
|
export { createImage, decodeImage } from "./image"
|
|
17
18
|
export type { DecodedImage, ImageSource } from "./image"
|
|
19
|
+
export { parseSvg, svg } from "./svg"
|
|
20
|
+
export type { SvgDocument, SvgDraw } from "./svg"
|
|
18
21
|
export { createScroll } from "./scroll"
|
|
19
22
|
export type { Scroll, ScrollAxis, ScrollOffset, ScrollOptions } from "./scroll"
|
|
20
23
|
export type {
|
|
@@ -25,6 +28,7 @@ export type {
|
|
|
25
28
|
WheelEvent,
|
|
26
29
|
KeyEvent,
|
|
27
30
|
TextEvent,
|
|
31
|
+
TextInputHints,
|
|
28
32
|
PaintProps,
|
|
29
33
|
WindowProps,
|
|
30
34
|
WindowShaderProps,
|
package/src/renderer.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { createRenderer } from "@solidjs/universal"
|
|
|
3
3
|
import type { Element } from "solid-js"
|
|
4
4
|
import * as tree from "flux:rendertree"
|
|
5
5
|
import { attachWindow } from "./window"
|
|
6
|
-
import { setEventHandler,
|
|
6
|
+
import { setEventHandler, setFocusable, setTextInputHints, cleanupNode, focusedNode, setFocus } from "./core"
|
|
7
7
|
import { parseColor, isGradient } from "./color"
|
|
8
8
|
|
|
9
9
|
export { getEventHandler } from "./core"
|
|
@@ -31,6 +31,16 @@ function createProxyNode(elementType: ElementType): ProxyNode {
|
|
|
31
31
|
return node
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
// Ancestor chain for event dispatch: node ids from `id` (inclusive) to the
|
|
35
|
+
// root, following the mount tree (a portaled node reports its mount point's
|
|
36
|
+
// chain, not its lexical one). Empty when the id is unknown.
|
|
37
|
+
export function getNodePath(id: number): number[] {
|
|
38
|
+
let path: number[] = []
|
|
39
|
+
let node: ProxyNode | undefined = nodes.get(id)
|
|
40
|
+
for (; node; node = node.parent) path.push(node.id)
|
|
41
|
+
return path
|
|
42
|
+
}
|
|
43
|
+
|
|
34
44
|
// Nodes detached this tick and awaiting the destroy sweep, keyed by id so a
|
|
35
45
|
// re-insert can cancel one. See removeNode / flushDestroy.
|
|
36
46
|
let pendingDestroy = new Map<number, ProxyNode>()
|
|
@@ -43,9 +53,9 @@ function destroyNode(node: ProxyNode): void {
|
|
|
43
53
|
tree.destroyNode(node.id)
|
|
44
54
|
let cleanup = (n: ProxyNode) => {
|
|
45
55
|
for (let child of n.children) if (child.parent === n) cleanup(child)
|
|
46
|
-
if (n.id ===
|
|
56
|
+
if (n.id === focusedNode()) setFocus(null)
|
|
47
57
|
nodes.delete(n.id)
|
|
48
|
-
|
|
58
|
+
cleanupNode(n.id)
|
|
49
59
|
}
|
|
50
60
|
cleanup(node)
|
|
51
61
|
}
|
|
@@ -129,21 +139,22 @@ export function scanForOrphans(now: number): void {
|
|
|
129
139
|
}
|
|
130
140
|
|
|
131
141
|
// A property the native tree rejected must not take down the reactive system:
|
|
132
|
-
// a typo'd
|
|
133
|
-
// element kind + property with a stack (the dev server remaps
|
|
134
|
-
// the .tsx source), then ignore further writes of the same pair.
|
|
135
|
-
let
|
|
142
|
+
// a typo'd, not-yet-implemented, or detached-only prop poisons only itself.
|
|
143
|
+
// Warn once per element kind + property with a stack (the dev server remaps
|
|
144
|
+
// its frames to the .tsx source), then ignore further writes of the same pair.
|
|
145
|
+
let warnedRejectedProps = new Set<string>()
|
|
136
146
|
|
|
137
147
|
function setTreeProperty(node: ProxyNode, name: string, value: unknown): void {
|
|
138
148
|
try {
|
|
139
149
|
tree.setProperty(node.id, name, value)
|
|
140
150
|
} catch (e) {
|
|
141
|
-
|
|
151
|
+
let message = String(e)
|
|
152
|
+
if (!message.includes("unknown property") && !message.includes("detached-only")) throw e
|
|
142
153
|
let key = node.elementType + "." + name
|
|
143
|
-
if (
|
|
144
|
-
|
|
154
|
+
if (warnedRejectedProps.has(key)) return
|
|
155
|
+
warnedRejectedProps.add(key)
|
|
145
156
|
let stack = new Error().stack ?? ""
|
|
146
|
-
console.warn(`Ignoring
|
|
157
|
+
console.warn(`Ignoring property '${name}' on <${node.elementType}>: ${message}\n${stack}`)
|
|
147
158
|
}
|
|
148
159
|
}
|
|
149
160
|
|
|
@@ -162,6 +173,16 @@ function applyProp<T>(node: ProxyNode, name: string, value: T): void {
|
|
|
162
173
|
return
|
|
163
174
|
}
|
|
164
175
|
|
|
176
|
+
if (name === "focusable") {
|
|
177
|
+
setFocusable(node.id, value === true)
|
|
178
|
+
return
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (name === "textInputHints") {
|
|
182
|
+
setTextInputHints(node.id, value as any)
|
|
183
|
+
return
|
|
184
|
+
}
|
|
185
|
+
|
|
165
186
|
if (name === "color" && isGradient(value)) {
|
|
166
187
|
setTreeProperty(node, name, value)
|
|
167
188
|
return
|
package/src/runtime-modules.d.ts
CHANGED
|
@@ -105,8 +105,8 @@ declare module "srt:apps" {
|
|
|
105
105
|
* installs nothing and leaves it alone. `size` is the version's
|
|
106
106
|
* manifest-declared size (bundle plus assets) - claimed rather than walked,
|
|
107
107
|
* so that listing stays cheap; `info()` reports what is actually on disk.
|
|
108
|
-
* `icon` is the manifest-declared icon's SVG source, ready for
|
|
109
|
-
*
|
|
108
|
+
* `icon` is the manifest-declared icon's SVG source, ready for `parseSvg`;
|
|
109
|
+
* absent when the app declares none (or the file is unreadable).
|
|
110
110
|
*/
|
|
111
111
|
export type InstalledApp = {
|
|
112
112
|
id: string
|
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
// final transcripts through onResult. With wakeWord the session starts
|
|
4
4
|
// asleep behind an efficient wake word detector (livekit-wakeword) and only
|
|
5
5
|
// transcribes after the wake word. startRecognition resolves once the models
|
|
6
|
-
// are loaded and listening has begun; it rejects when
|
|
6
|
+
// are loaded and listening has begun; it rejects when the microphone cannot be
|
|
7
|
+
// opened or the models fail to load.
|
|
7
8
|
// Models are passed as bytes so any source composes: flux:fs file(), fetch
|
|
8
9
|
// (incl. the dev-server file proxy), or a download cache layered on top.
|
|
9
10
|
// Requires a runtime built with speech support.
|