@solidrt/core 0.0.49 → 0.0.51
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 +23 -0
- package/README.md +1 -1
- package/agents/painting.md +61 -0
- package/agents/performance.md +216 -0
- package/examples/README.md +2 -2
- package/examples/{sound.tsx → audio.tsx} +23 -3
- package/examples/gpu-sprites.tsx +102 -0
- package/jsx-runtime.d.ts +19 -14
- package/package.json +7 -6
- package/src/{sound.ts → audio.ts} +99 -18
- package/src/color.ts +17 -18
- package/src/core.ts +88 -0
- package/src/data.ts +99 -0
- package/src/gpu.ts +32 -10
- package/src/index.ts +4 -4
- package/src/renderer.ts +43 -27
- package/src/runtime-modules.d.ts +3 -2
- package/src/scroll.ts +1 -1
- package/src/text-input.ts +297 -60
- package/src/types.d.ts +233 -5
- package/src/window.ts +58 -7
package/AGENTS.md
CHANGED
|
@@ -7,6 +7,15 @@ trust this file and the types in src/types.d.ts and jsx-runtime.d.ts.
|
|
|
7
7
|
SolidRT is a custom SolidJS renderer: it paints through a Rust runtime, not the
|
|
8
8
|
DOM. There is no HTML, no CSS cascade, no `className`.
|
|
9
9
|
|
|
10
|
+
Two companion files carry the depth this one leaves out; read the one that
|
|
11
|
+
matches the work before starting it:
|
|
12
|
+
- agents/painting.md - what you paint with, and what replaces each CSS
|
|
13
|
+
reflex. Read before styling a screen: a background, a gradient, a shadow,
|
|
14
|
+
an effect, vector art, a chart.
|
|
15
|
+
- agents/performance.md - the performance model, in order of leverage. Read
|
|
16
|
+
before writing any per-frame code, any animation, or anything that writes
|
|
17
|
+
properties in a loop.
|
|
18
|
+
|
|
10
19
|
## The window is device-sized - design fluid
|
|
11
20
|
|
|
12
21
|
A SolidRT window is host-sized and resizable, and the SAME app runs on phones,
|
|
@@ -135,6 +144,8 @@ Peer deps @solidjs/signals and @solidjs/universal must match (currently
|
|
|
135
144
|
box center; a d-view has no box). To scale a detached group around its
|
|
136
145
|
content's center, set the origin explicitly in pixels, e.g.
|
|
137
146
|
`originX={100} originY={50}` for content drawn in a 200x100 local space.
|
|
147
|
+
Avoid pct()/keyword origins on a d-view - they resolve against the box
|
|
148
|
+
inherited from the nearest laid-out ancestor.
|
|
138
149
|
|
|
139
150
|
- Layout-affecting vs not (this matters for per-frame work). Props fall in three
|
|
140
151
|
buckets, split by where they take effect:
|
|
@@ -159,6 +170,18 @@ Peer deps @solidjs/signals and @solidjs/universal must match (currently
|
|
|
159
170
|
expression container preserves it - `<d-text>{"one two"}</d-text>` - and
|
|
160
171
|
`\n` inside one produces a hard line break.
|
|
161
172
|
|
|
173
|
+
- Rich text: `<span>` inside `<text>` restyles a run (`color`, `fontFamily`,
|
|
174
|
+
`fontSize`, `fontWeight`, `fontStyle`, `lineHeight`,
|
|
175
|
+
`textDecoration="underline"`); spans nest and
|
|
176
|
+
inherit inward from the `<text>`. Never lay a paragraph out word by word in
|
|
177
|
+
a wrapping row to mix styles - one `<text>` with spans wraps as a whole.
|
|
178
|
+
A span is content, not a box (no layout or `d-` form, no size, no
|
|
179
|
+
bounding box; it takes its parent's form). A `<span>` takes
|
|
180
|
+
pointer handlers (a link is a span, hit per line it spans), and any other
|
|
181
|
+
element child of `<text>` (`<view>`, `<texture>`, `<path>`, ...) is an
|
|
182
|
+
inline atom flowing with the words as one unbreakable box on the baseline;
|
|
183
|
+
give it margins for spacing, since JSX trims the whitespace around it.
|
|
184
|
+
|
|
162
185
|
- Events: there is NO `onClick`/`onPress`. A "button" is a `<view>`/`<rect>`
|
|
163
186
|
with `onPointerDown`. Handlers: onPointerDown/Up/Move/Enter/Leave, onWheel,
|
|
164
187
|
onKeyDown/Up, onTextInput, onFocus/onBlur. Text entry: focus a node with an
|
package/README.md
CHANGED
|
@@ -53,7 +53,7 @@ Optionally, create a `tsconfig.json` to enable type recognition for SolidRT elem
|
|
|
53
53
|
|
|
54
54
|
## API
|
|
55
55
|
|
|
56
|
-
See [docs/core
|
|
56
|
+
See [docs/20-core](https://github.com/wellawaretech/solidrt/blob/main/docs/20-core/index.md) for the full API reference.
|
|
57
57
|
|
|
58
58
|
## License
|
|
59
59
|
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# What you paint with (there is no CSS layer)
|
|
2
|
+
|
|
3
|
+
Read this before building any screen whose look matters: a background, a
|
|
4
|
+
decoration, an effect, a chart, anything you would have reached for CSS for.
|
|
5
|
+
|
|
6
|
+
Layout and props are half the model. There is no stylesheet: no filters, no
|
|
7
|
+
box-shadow, no keyframes, no canvas. The visual range a web app gets from CSS
|
|
8
|
+
comes from the tiers below instead, and reaching past tier 1 is ordinary
|
|
9
|
+
app-building here, not optimization - a screen built only from view
|
|
10
|
+
backgrounds and text is using a fraction of the runtime. Pick the tier the
|
|
11
|
+
CONTENT calls for, not the one that looks safest.
|
|
12
|
+
|
|
13
|
+
1. Laid-out elements - `<view>`/`<text>`, with `<rect>` (or a filling
|
|
14
|
+
`<d-rect>` child) for background, border, radius. The structure of a
|
|
15
|
+
screen, not its finish.
|
|
16
|
+
2. Vector art, detached from layout - `d-path`/`d-rect`/`d-oval`/`d-line`,
|
|
17
|
+
whose `color` takes a gradient (createLinearGradient /
|
|
18
|
+
createRadialGradient) and which honour `blendMode`, plus `parseSvg` to
|
|
19
|
+
draw a whole SVG document as one subtree. Free-form shapes, decoration,
|
|
20
|
+
diagrams, charts, anything positioned rather than flowed. Examples:
|
|
21
|
+
parse-svg, detached-positioning, text-paint-styling.
|
|
22
|
+
3. GPU textures - `createShaderTexture` puts a fragment shader in a
|
|
23
|
+
`<texture>` (moving gradients, noise, glow, dissolves, a background that
|
|
24
|
+
is alive), `createPipelineTexture` draws geometry you generate yourself
|
|
25
|
+
(particles, point clouds, splats), and the `shader` prop post-processes
|
|
26
|
+
content that already exists: on a `<view>` it grades, warps or dissolves
|
|
27
|
+
that subtree, on `<window>` the whole frame. Stack `<texture>` elements
|
|
28
|
+
with `blendMode` to combine passes. Examples: gpu-shader, gpu-particles,
|
|
29
|
+
gpu-pipeline, gpu-instancing, gpu-texture-blend, view-shader,
|
|
30
|
+
window-shader.
|
|
31
|
+
4. 3D scenes - add `@solidrt/3d` (not a scaffold dependency): meshes,
|
|
32
|
+
materials and a camera declared as Solid components, rendered into a
|
|
33
|
+
texture that sits in the UI tree like any other element.
|
|
34
|
+
|
|
35
|
+
Tier 3 is cheaper than it looks. A shader costs one property write per frame
|
|
36
|
+
no matter how complex the effect, which is why the performance model
|
|
37
|
+
(agents/performance.md) reaches for it first rather than as a last resort.
|
|
38
|
+
|
|
39
|
+
## Web reflexes and what replaces them
|
|
40
|
+
|
|
41
|
+
- gradient background -> a gradient `color` on a `d-rect` (gradients are
|
|
42
|
+
paint values, usable anywhere a color is)
|
|
43
|
+
- `filter: blur/grayscale/hue-rotate`, and any "make this look processed" ->
|
|
44
|
+
a `shader` on the view (requires repaintBoundary="snapshot"), or on
|
|
45
|
+
`<window>` for the whole frame
|
|
46
|
+
- `box-shadow` / `text-shadow` / glow -> no shadow prop exists: draw an
|
|
47
|
+
offset `d-*` shape under the content, or a view shader with `outset` (the
|
|
48
|
+
transparent margin an effect bleeds into)
|
|
49
|
+
- `backdrop-filter` -> no equivalent. A view shader sees only its own
|
|
50
|
+
subtree's pixels, never what is behind it. Frost the whole frame with a
|
|
51
|
+
window shader, or fake the layer with your own content
|
|
52
|
+
- CSS `transition` -> the `transition` prop: declare it on the element and
|
|
53
|
+
keep writing targets; the runtime animates natively (performance rule 1)
|
|
54
|
+
- `@keyframes` -> a `transition` prop when the motion is target-shaped;
|
|
55
|
+
`onFrame` writing a signal for genuinely procedural sequences; a `uTime`
|
|
56
|
+
uniform when the animation is continuous and visual
|
|
57
|
+
- `<canvas>` 2D -> `d-*` primitives (rebuild one `d-path` string per frame
|
|
58
|
+
rather than animating N elements)
|
|
59
|
+
- `<canvas>` WebGL, three.js -> `createPipelineTexture`, or `@solidrt/3d`
|
|
60
|
+
- video background, animated hero, particle field -> a shader texture; this
|
|
61
|
+
is the case the runtime is built for
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
# Performance model (JS is the slow lane)
|
|
2
|
+
|
|
3
|
+
Read this before writing any per-frame code, any animation, or anything that
|
|
4
|
+
writes properties in a loop.
|
|
5
|
+
|
|
6
|
+
The JS engine is interpreted and every property write crosses an FFI boundary
|
|
7
|
+
into the runtime, so per-frame JS work is the expensive path while GPU work is
|
|
8
|
+
nearly free. That holds on desktop and on current mobile hardware; "Where GPU
|
|
9
|
+
work stops being free" below is where it does not. The design answer is not
|
|
10
|
+
"write less JS" but "keep JS off the per-frame path": the platform animates
|
|
11
|
+
(transitions), caches (repaint boundaries), shades (GPU) and computes
|
|
12
|
+
(isolates, wasm) natively, and JS stays the coordinator that sets targets.
|
|
13
|
+
Rules, in order of leverage:
|
|
14
|
+
|
|
15
|
+
1. Motion between states (position, size, opacity, transform components,
|
|
16
|
+
solid colors, enter/exit) belongs in a native transition, never in
|
|
17
|
+
per-frame JS. Declare `transition` on the element and keep writing
|
|
18
|
+
targets the ordinary way; the runtime interpolates every frame on the
|
|
19
|
+
Rust side, so JS runs only when a target changes and the running
|
|
20
|
+
animation costs no JS and no property writes per frame, however many
|
|
21
|
+
elements move. Flat spec, ms durations, kind inferred:
|
|
22
|
+
`{ duration }` / `{ duration, bounce }` is a spring (the default kind;
|
|
23
|
+
springs carry velocity, so a retarget mid-flight stays continuous -
|
|
24
|
+
use them for anything interactive), `{ duration, curve }` is a tween
|
|
25
|
+
(`linear | ease | ease-in | ease-out | ease-in-out` or a cubic-bezier
|
|
26
|
+
array; tweens restart from the current value on retarget, CSS
|
|
27
|
+
semantics). Keys are property names plus `all` as catch-all; a string
|
|
28
|
+
is shorthand (`transition="300ms ease-out"`); `delay` holds each
|
|
29
|
+
write, `from` animates the first attach in (enter), `exit` animates
|
|
30
|
+
removal out before the node frees, `stagger` on a parent cascades its
|
|
31
|
+
children's enters/exits, and `onTransitionEnd` fires per settled
|
|
32
|
+
property. The initial value never animates without `from`; a write to
|
|
33
|
+
a property without a transition snaps, as always. A JS tween loop or
|
|
34
|
+
animation library pushing interpolated values through signals pays the
|
|
35
|
+
whole write path per element per frame - port it to this.
|
|
36
|
+
2. Continuous effects (snow, particles, animated backgrounds) belong in a
|
|
37
|
+
fragment shader: createShaderTexture (from @solidrt/core/gpu) + `<texture
|
|
38
|
+
params={{ uTime }}>` (the shader declares `uniform float uTime;` itself -
|
|
39
|
+
the preamble declares only what the runtime fills). The whole effect then
|
|
40
|
+
costs one setProperty per frame - the uTime write - regardless of visual
|
|
41
|
+
complexity. Shader output
|
|
42
|
+
must be premultiplied alpha (white flakes are `vec4(vec3(a), a)`);
|
|
43
|
+
straight alpha (`vec4(1,1,1,a)`) composites as opaque white. A source that
|
|
44
|
+
starts with `#version 300 es` is compiled exactly as written - no preamble
|
|
45
|
+
is injected, though the built-in vertex stage still supplies `vUV` - so a
|
|
46
|
+
shader ported from elsewhere keeps its own uniform names without dropping
|
|
47
|
+
to compileShader/linkProgram. Params drive any uniform type: a number
|
|
48
|
+
fills a `float`/`int` scalar, a flat number array fills `vec2`/`vec3`/
|
|
49
|
+
`vec4` (2/3/4 numbers) or `mat4` (16, column-major), dispatched by the
|
|
50
|
+
shader's own declaration - a ported shader's `vec2 uCenter` or
|
|
51
|
+
`vec3 iResolution` needs no splitting into scalars. To combine several
|
|
52
|
+
GPU passes, stack `<texture>` elements and set `blendMode` (e.g. a base
|
|
53
|
+
pass plus an additive `blendMode="plus"` pass) rather than writing a
|
|
54
|
+
compositing shader. Within one pipeline draw, createPipelineTexture's
|
|
55
|
+
`blend: "add"` accumulates overlapping geometry additively (soft point
|
|
56
|
+
splats, glow) - pair it with `depthWrite: false` when depth-tested;
|
|
57
|
+
neither option implies the other. A pipeline's own vertex stage writes
|
|
58
|
+
into a y-down clip space: `gl_Position` y = -1 is the top row of the
|
|
59
|
+
target and +1 the bottom, so camera-up geometry must negate y (or fold
|
|
60
|
+
the flip into its projection) or it draws upside down. Sampling is a
|
|
61
|
+
create-time option on every texture: `{ filter: "nearest" }` for
|
|
62
|
+
hard-pixel upscaling (render a small target, display it big - the
|
|
63
|
+
retro/pixel-art path) and `{ wrap: "repeat" }` to tile outside 0..1 in
|
|
64
|
+
shaders; the defaults are linear and clamp, and the choice applies both
|
|
65
|
+
on screen and to shaders sampling the texture.
|
|
66
|
+
3. Reduce setProperty calls wherever possible: one path string rebuilt per
|
|
67
|
+
frame beats N elements with N animated positions; a shader beats the path
|
|
68
|
+
string. get_stats' setPropsPerFrame is the counter to watch. Compiled JSX
|
|
69
|
+
attribute expressions diff before writing, so a per-frame expression that
|
|
70
|
+
returns an unchanged value costs no property write - setPropsPerFrame
|
|
71
|
+
counts values that actually changed, not expressions re-run.
|
|
72
|
+
4. Never leave onFrame registered while nothing animates: a pending onFrame
|
|
73
|
+
is a standing frame request, so the runtime renders and presents every
|
|
74
|
+
vsync even when the callback body does nothing - an invisible 60fps GPU
|
|
75
|
+
burn that also drags the OS compositor along with it. Tweens and
|
|
76
|
+
springs need no pump at all - that is rule 1, and the runtime requests
|
|
77
|
+
frames only while tracks run. For genuinely procedural per-frame motion,
|
|
78
|
+
use a self-rechaining one-shot requestAnimationFrame that stops
|
|
79
|
+
re-requesting when its work list empties. (Registering onFrame outside a
|
|
80
|
+
component body also warns NO_OWNER_CLEANUP - it assumes a reactive owner.)
|
|
81
|
+
5. repaintBoundary works like Flutter's: transforms and opacity on the
|
|
82
|
+
boundary node itself (or any ancestor) are hoisted out of the cache and
|
|
83
|
+
applied at composite time, so animating x/y/scale/rotate/opacity of a
|
|
84
|
+
boundary does NOT re-raster it (verified by A/B measurement - the damage
|
|
85
|
+
system classifies these as Transform and keeps the node's own cache).
|
|
86
|
+
What DOES invalidate the cache is any paint or content change inside the
|
|
87
|
+
subtree - colors, path data, text, a Show toggling - so drive animation
|
|
88
|
+
with transforms and keep the cached content itself static. Off a boundary,
|
|
89
|
+
`opacity` on a view is NOT cheap: it wraps the subtree in a compositing
|
|
90
|
+
layer (save_layer) for as long as it is below 1. To fade a single
|
|
91
|
+
primitive, put the alpha in its `color` (`rgba(...)`) - paint alpha is
|
|
92
|
+
free; reserve view `opacity` for fading a genuine group as a whole.
|
|
93
|
+
Placement rule for animation-heavy screens: a boundary around a node
|
|
94
|
+
that animates its own paint (a moving d-*, a changing color) is useless
|
|
95
|
+
- its interior is damaged every frame, so the cache never survives. The
|
|
96
|
+
win is a boundary around the static bulk NEXT TO the animators: the
|
|
97
|
+
frame then re-records only the moving nodes and replays the fenced
|
|
98
|
+
content as one cached draw, an order-of-magnitude cut when static
|
|
99
|
+
content dominates the node count. get_stats' nodesPainted shows exactly
|
|
100
|
+
what the paint walk still enters. The exception where a boundary on the
|
|
101
|
+
animator itself pays is transform/opacity animation of the boundary
|
|
102
|
+
node - the hoisting described above.
|
|
103
|
+
6. "snapshot" boundaries pay first-frame texture allocation + raster:
|
|
104
|
+
creating many at once (dealing a board of 64 sprites) is a visible
|
|
105
|
+
one-frame hiccup - pool or pre-warm if that moment matters.
|
|
106
|
+
7. Shading pixels the app already drew is a different mechanism from rule 2's
|
|
107
|
+
generated textures, and both forms are a `shader` prop taking a linked
|
|
108
|
+
program from compileShader/linkProgram (@solidrt/core/gpu), not a
|
|
109
|
+
createShaderTexture source. On `<window>`, `shader={{ program, params }}`
|
|
110
|
+
runs the finished frame through the program as the last step before it
|
|
111
|
+
reaches the screen: the frame binds as `uniform sampler2D uSource`,
|
|
112
|
+
`iResolution` fills by name, and `previous: true` retains the last frame as
|
|
113
|
+
`uPrevious` for motion echo or frame differencing. On a `<view>` the same
|
|
114
|
+
prop shades that subtree in place and REQUIRES repaintBoundary="snapshot"
|
|
115
|
+
(without it the shader is ignored with a warning); the pass sees only the
|
|
116
|
+
subtree's own pixels - grading, warping or dissolving the panel works,
|
|
117
|
+
anything needing what is behind it does not - and is split from content
|
|
118
|
+
invalidation, so a params-only change re-runs the pass against the cached
|
|
119
|
+
snapshot instead of re-rasterizing. A window shader's output is invisible
|
|
120
|
+
to get_snapshot and every other MCP tool; `bunx srt render` is the only
|
|
121
|
+
way to see it (see @solidrt/cli AGENTS.md).
|
|
122
|
+
8. `flux:wasm` runs a pure interpreter (wasmi, no JIT), so temper browser
|
|
123
|
+
expectations - but do not write it off for compute. A genuinely numeric
|
|
124
|
+
kernel (typed-array math, tight inner loops, no host calls inside the
|
|
125
|
+
loop) compiled from a systems language can come out a real multiple
|
|
126
|
+
faster than the same loop in interpreted JavaScript, and when profiling
|
|
127
|
+
shows such a kernel is what the app is spending its time on, that
|
|
128
|
+
multiple is worth having: measure the JS loop, port the kernel, measure
|
|
129
|
+
again, keep whichever wins. What wasm does not do is speed up
|
|
130
|
+
render-path work (rules 1-3 are that leverage), and every host call
|
|
131
|
+
costs marshalling, so batch at the boundary - one call over a byte
|
|
132
|
+
buffer, not a call per element. It is also the way to ship one compiled
|
|
133
|
+
module across every target with no native toolchain, and it pairs with
|
|
134
|
+
an isolate when a call runs long enough to block.
|
|
135
|
+
9. `flux:ffi` (dlopen of a native library) is a binding tool, not a
|
|
136
|
+
performance tool. It needs a shared library compiled per platform and
|
|
137
|
+
architecture and shipped under each target's packing rules (Android
|
|
138
|
+
loads only what arrives inside the APK as a lib*.so), so reaching for
|
|
139
|
+
it "to make something fast" buys a build-and-packaging problem on every
|
|
140
|
+
platform the app targets. Use it when the app must call a native
|
|
141
|
+
library that already exists and already ships for those targets; for
|
|
142
|
+
speed, everything above comes first.
|
|
143
|
+
|
|
144
|
+
## Isolates: heavy work off the JS thread
|
|
145
|
+
|
|
146
|
+
A long synchronous computation (a big parse, a simulation step, a blocking
|
|
147
|
+
`flux:ffi`/`flux:wasm` call) freezes rendering and input for its duration.
|
|
148
|
+
Move it into an isolate module: a file whose first statement is the
|
|
149
|
+
`"use isolate"` directive runs in a second runtime on its own thread, and
|
|
150
|
+
main calls its exports as async functions.
|
|
151
|
+
|
|
152
|
+
```ts
|
|
153
|
+
// src/worker.ts
|
|
154
|
+
"use isolate"
|
|
155
|
+
export function crunch(data: Uint8Array): number { /* ... */ }
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
```ts
|
|
159
|
+
// src/index.tsx
|
|
160
|
+
import { isolate } from "flux:isolate"
|
|
161
|
+
import type * as Worker from "./worker"
|
|
162
|
+
let worker = isolate<typeof Worker>("worker") // id = path from src/, no extension
|
|
163
|
+
let n = await worker.crunch(bytes) // main keeps rendering meanwhile
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
The bundler builds each such module as its own bundle and ships it with the
|
|
167
|
+
app (dev pushes and `srt pack` alike). Rules: main may only `import type`
|
|
168
|
+
from an isolate module (a value import is a build error); arguments and
|
|
169
|
+
results are copies (numbers, strings, byte buffers, arrays, plain objects -
|
|
170
|
+
no functions, no class instances); the child has the non-gui `flux:*`
|
|
171
|
+
modules only, so it never touches the render tree; module state persists
|
|
172
|
+
between calls and each `isolate()` call is its own instance. An
|
|
173
|
+
`async function*` export is a stream: `for await (let p of worker.progress())`
|
|
174
|
+
pulls one item per step (progress, ticks, a subscription), `break` ends it in
|
|
175
|
+
the isolate, and streams never block plain calls. Full contract:
|
|
176
|
+
node_modules/@solidrt/flux-types/modules/isolate.d.ts.
|
|
177
|
+
|
|
178
|
+
## Where GPU work stops being free
|
|
179
|
+
|
|
180
|
+
"GPU work is nearly free" is a property of the hardware, not of the engine, and
|
|
181
|
+
the spread is wide enough to design against rather than discover late. The same
|
|
182
|
+
app - two point-cloud pipelines, 233,600 vertices, one params write each per
|
|
183
|
+
onFrame, i.e. exactly what rule 2 recommends - measured 16.7 ms/frame (60 fps,
|
|
184
|
+
vsync-locked) on both desktop and a mid-range 2020 tablet, and 120 ms/frame
|
|
185
|
+
(8.3 fps) on a 2017 Android TV. Roughly 8x for identical work, with the tablet
|
|
186
|
+
indistinguishable from desktop. Measure on a target device if it matters; do
|
|
187
|
+
not infer it from the desktop number.
|
|
188
|
+
|
|
189
|
+
- **On a tiled GPU the budget is primitive count, not pixels.** Every point or
|
|
190
|
+
triangle costs the tiler regardless of how few pixels it covers. On that TV,
|
|
191
|
+
frame time against total vertices with a trivial vertex shader: 20k -> 80 ms,
|
|
192
|
+
35k -> 100 ms, 100k -> 380 ms. Meanwhile `gl_PointSize = 3.0` - nine times
|
|
193
|
+
the fill - measured within one vsync of 1.0, and rendering into a
|
|
194
|
+
quarter-size target measured identical to full size. So for a heavy pass the
|
|
195
|
+
lever is fewer primitives; shrinking the target or the splat usually is not,
|
|
196
|
+
and coverage is far cheaper bought with point size than with more points.
|
|
197
|
+
- **A device's compositor can set the frame budget outright**, in which case
|
|
198
|
+
none of the above moves. That TV never presents faster than every 80 ms -
|
|
199
|
+
four refresh periods blocked inside `eglSwapBuffers` - even for a near-empty
|
|
200
|
+
scene, so its ceiling is ~12 fps whatever you draw. Recognise it by a
|
|
201
|
+
content-independent floor: if a trivial scene and a heavy one present at
|
|
202
|
+
nearly the same rate, you are compositor-bound and tuning the scene is
|
|
203
|
+
wasted effort.
|
|
204
|
+
- **Per-frame writes are gated on the raster thread, not on vsync**, so a pass
|
|
205
|
+
that costs more than a refresh period does not silently pile up. If
|
|
206
|
+
`rasterQueue` climbs across queries while fps drops the raster thread is
|
|
207
|
+
behind; if `fenceTimeoutsPerSec` (in get_stats' window block) is nonzero,
|
|
208
|
+
the GPU is over its pacing budget right now.
|
|
209
|
+
|
|
210
|
+
Finding your own numbers: `get_stats` gives fps, frameMs, setPropsPerFrame,
|
|
211
|
+
the window summary (worst frame, percentiles, GPU rates), rasterQueue and
|
|
212
|
+
fenceTimeouts. When those disagree with what the screen is
|
|
213
|
+
visibly doing, ground truth on Android is
|
|
214
|
+
`adb shell dumpsys SurfaceFlinger --latency <layer>` for real present
|
|
215
|
+
timestamps - engine-reported phase timings can each be honest and still not add
|
|
216
|
+
up to the frame period, because work outside the frame call is not in them.
|
package/examples/README.md
CHANGED
|
@@ -47,8 +47,8 @@ for the element/prop model see `@solidrt/core/AGENTS.md`.
|
|
|
47
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.
|
|
48
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.
|
|
49
49
|
|
|
50
|
-
##
|
|
51
|
-
- `
|
|
50
|
+
## Audio
|
|
51
|
+
- `audio.tsx` - `createSound`: decode a clip once from bytes (here a binary import), replay cheaply; `overlap` stacking vs single-voice, `playing()` signal, release on unmount. `createPcmSound` for a synthesised clip from raw samples (a generated sine sweep). Points to `createSoundStream` for long tracks streamed from a path.
|
|
52
52
|
|
|
53
53
|
## Vector graphics
|
|
54
54
|
- `parse-svg.tsx` - `parseSvg` turns a whole SVG *document string* (not HTML/JSX children) into plain draw data mapped to `<d-path>` inside a `viewBox`-fitted view; per-shape hover highlighting shows the payoff (exact-outline hit testing, recolor without re-parse), plus a `currentColor` icon recolored via the `color` option. This is how to use existing icon libraries (Lucide, Heroicons, etc.) - hand their SVG source to `parseSvg`.
|
|
@@ -14,14 +14,32 @@
|
|
|
14
14
|
// selection tick. playing() is a signal: true from play() until stop() (it
|
|
15
15
|
// does not flip back when a clip ends naturally).
|
|
16
16
|
//
|
|
17
|
+
// Sounds you can compute need no bytes at all: createPcmSound takes raw
|
|
18
|
+
// samples (here a 60 ms sine sweep in an Int16Array) and gives the same handle
|
|
19
|
+
// - generate it, do not ship it. Same reactive ownership as createSound.
|
|
20
|
+
//
|
|
17
21
|
// For a long track (music, ambience) do not load bytes at all: pass a file
|
|
18
22
|
// path to createSoundStream from the same module, which decodes from disk on
|
|
19
23
|
// demand and stays off the heap. Same play()/stop()/playing() surface,
|
|
20
24
|
// always single-voice.
|
|
21
25
|
import { render } from "@solidrt/core"
|
|
22
|
-
import { createSound } from "@solidrt/core/
|
|
26
|
+
import { createPcmSound, createSound } from "@solidrt/core/audio"
|
|
23
27
|
import blipBytes from "./blip.wav" with { type: "binary" }
|
|
24
28
|
|
|
29
|
+
const RATE = 44100
|
|
30
|
+
// A short sweep from 880 Hz down to 440 Hz with a linear fade-out.
|
|
31
|
+
function sweep(): Int16Array {
|
|
32
|
+
let n = Math.round(RATE * 0.06)
|
|
33
|
+
let out = new Int16Array(n)
|
|
34
|
+
let phase = 0
|
|
35
|
+
for (let i = 0; i < n; i++) {
|
|
36
|
+
let t = i / n
|
|
37
|
+
phase += (2 * Math.PI * (880 - 440 * t)) / RATE
|
|
38
|
+
out[i] = Math.round(Math.sin(phase) * (1 - t) * 0.6 * 32767)
|
|
39
|
+
}
|
|
40
|
+
return out
|
|
41
|
+
}
|
|
42
|
+
|
|
25
43
|
function Button(props: { label: string; onTap: () => void }) {
|
|
26
44
|
return (
|
|
27
45
|
<view onPointerDown={props.onTap} padding={12} clipRadius={8}>
|
|
@@ -34,13 +52,15 @@ function Button(props: { label: string; onTap: () => void }) {
|
|
|
34
52
|
function App() {
|
|
35
53
|
let blip = createSound(blipBytes, { gain: 0.8 })
|
|
36
54
|
let tick = createSound(blipBytes, { overlap: false })
|
|
55
|
+
let synth = createPcmSound(sweep(), RATE)
|
|
37
56
|
|
|
38
57
|
return (
|
|
39
58
|
<window padding={20} gap={8} alignItems="flex-start">
|
|
40
59
|
<Button label="Blip (tap fast to stack voices)" onTap={() => blip.play()} />
|
|
41
60
|
<Button label="Tick (overlap: false, restarts)" onTap={() => tick.play()} />
|
|
42
|
-
<Button label="
|
|
43
|
-
<
|
|
61
|
+
<Button label="Sweep (createPcmSound, generated)" onTap={() => synth.play()} />
|
|
62
|
+
<Button label="Stop" onTap={() => { blip.stop(); tick.stop(); synth.stop() }} />
|
|
63
|
+
<text color="#888">{blip.playing() || tick.playing() || synth.playing() ? "playing" : "silent"}</text>
|
|
44
64
|
</window>
|
|
45
65
|
)
|
|
46
66
|
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
// The zero-copy per-frame geometry path: instanced quads whose records are
|
|
2
|
+
// rewritten every frame through a buffer write lease. beginBufferWrite hands
|
|
3
|
+
// back a Float32Array over runtime-owned memory (contents unspecified - fill
|
|
4
|
+
// everything you publish), the frame callback writes every live record into
|
|
5
|
+
// it, and endBufferWrite publishes by MOVING the block to the raster thread:
|
|
6
|
+
// no data copy anywhere on the CPU path, and no per-sprite property writes.
|
|
7
|
+
// Compare gpu-instancing.tsx, where the records are static and only the draw
|
|
8
|
+
// range changes; here the records themselves are the animation.
|
|
9
|
+
//
|
|
10
|
+
// The buffer is created from a byte LENGTH (zeroed storage) - the natural
|
|
11
|
+
// create when every byte arrives through the lease. Publishing a prefix and
|
|
12
|
+
// setDraw({ instanceCount }) keep buffer capacity and live population
|
|
13
|
+
// independent: reserve the max up front, publish what exists this frame.
|
|
14
|
+
import { render, onFrame } from "@solidrt/core"
|
|
15
|
+
import { beginBufferWrite, createBuffer, createPipelineTexture, endBufferWrite, glsl, setDraw } from "@solidrt/core/gpu"
|
|
16
|
+
|
|
17
|
+
const MAX_SPRITES = 2000
|
|
18
|
+
// floats per record: center vec2, half-size f32, tint vec3.
|
|
19
|
+
const RECORD = 6
|
|
20
|
+
|
|
21
|
+
let VERTEX = glsl`
|
|
22
|
+
in vec2 aPos;
|
|
23
|
+
in vec2 iCenter;
|
|
24
|
+
in float iSize;
|
|
25
|
+
in vec3 iTint;
|
|
26
|
+
out vec3 vTint;
|
|
27
|
+
|
|
28
|
+
void main() {
|
|
29
|
+
gl_Position = vec4(iCenter + aPos * iSize, 0.0, 1.0);
|
|
30
|
+
vTint = iTint;
|
|
31
|
+
}
|
|
32
|
+
`
|
|
33
|
+
|
|
34
|
+
let FRAGMENT = glsl`
|
|
35
|
+
in vec3 vTint;
|
|
36
|
+
|
|
37
|
+
void main() {
|
|
38
|
+
fragColor = vec4(vTint, 1.0);
|
|
39
|
+
}
|
|
40
|
+
`
|
|
41
|
+
|
|
42
|
+
// Simulation state lives in plain arrays; the instance buffer holds only
|
|
43
|
+
// this frame's published snapshot of it.
|
|
44
|
+
let x = new Float32Array(MAX_SPRITES)
|
|
45
|
+
let y = new Float32Array(MAX_SPRITES)
|
|
46
|
+
let vx = new Float32Array(MAX_SPRITES)
|
|
47
|
+
let vy = new Float32Array(MAX_SPRITES)
|
|
48
|
+
for (let i = 0; i < MAX_SPRITES; i++) {
|
|
49
|
+
x[i] = Math.random() * 1.9 - 0.95
|
|
50
|
+
y[i] = Math.random() * 1.9 - 0.95
|
|
51
|
+
vx[i] = (Math.random() * 2 - 1) * 0.01
|
|
52
|
+
vy[i] = (Math.random() * 2 - 1) * 0.01
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function App() {
|
|
56
|
+
// One unit quad (triangle strip), reused by every instance.
|
|
57
|
+
let quad = createBuffer(new Float32Array([-0.5, -0.5, 0.5, -0.5, -0.5, 0.5, 0.5, 0.5]), { label: "sprite-quad" })
|
|
58
|
+
let records = createBuffer(MAX_SPRITES * RECORD * 4, { label: "sprite-records" })
|
|
59
|
+
let id = createPipelineTexture(VERTEX, FRAGMENT, 720, 720, null, {
|
|
60
|
+
label: "sprites",
|
|
61
|
+
topology: "triangle-strip",
|
|
62
|
+
vertexCount: 4,
|
|
63
|
+
attributes: [{ name: "aPos", format: "vec2" }],
|
|
64
|
+
buffer: quad,
|
|
65
|
+
instanceAttributes: [
|
|
66
|
+
{ name: "iCenter", format: "vec2" },
|
|
67
|
+
{ name: "iSize", format: "f32" },
|
|
68
|
+
{ name: "iTint", format: "vec3" },
|
|
69
|
+
],
|
|
70
|
+
instanceBuffer: records,
|
|
71
|
+
instanceCount: MAX_SPRITES,
|
|
72
|
+
clearColor: [0.03, 0.03, 0.06, 1],
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
onFrame(() => {
|
|
76
|
+
let out = beginBufferWrite(records)
|
|
77
|
+
for (let i = 0; i < MAX_SPRITES; i++) {
|
|
78
|
+
let nx = x[i]! + vx[i]!
|
|
79
|
+
let ny = y[i]! + vy[i]!
|
|
80
|
+
if (nx < -0.95 || nx > 0.95) vx[i] = -vx[i]!
|
|
81
|
+
else x[i] = nx
|
|
82
|
+
if (ny < -0.95 || ny > 0.95) vy[i] = -vy[i]!
|
|
83
|
+
else y[i] = ny
|
|
84
|
+
let at = i * RECORD
|
|
85
|
+
out[at] = x[i]!
|
|
86
|
+
out[at + 1] = y[i]!
|
|
87
|
+
out[at + 2] = 0.01 + 0.008 * (i % 5)
|
|
88
|
+
out[at + 3] = 0.5 + 0.5 * Math.cos(i * 0.11)
|
|
89
|
+
out[at + 4] = 0.5 + 0.5 * Math.cos(i * 0.13 + 2.1)
|
|
90
|
+
out[at + 5] = 0.5 + 0.5 * Math.cos(i * 0.17 + 4.2)
|
|
91
|
+
}
|
|
92
|
+
endBufferWrite(records)
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
return (
|
|
96
|
+
<window alignItems="center" justifyContent="center">
|
|
97
|
+
<texture src={id} width={480} height={480} />
|
|
98
|
+
</window>
|
|
99
|
+
)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
render(() => <App />)
|
package/jsx-runtime.d.ts
CHANGED
|
@@ -7,6 +7,7 @@ import type {
|
|
|
7
7
|
ViewProps,
|
|
8
8
|
ViewOwnProps,
|
|
9
9
|
TextProps,
|
|
10
|
+
SpanProps,
|
|
10
11
|
TextureProps,
|
|
11
12
|
LayoutProps,
|
|
12
13
|
PositionProps,
|
|
@@ -14,6 +15,7 @@ import type {
|
|
|
14
15
|
OvalGeometryProps,
|
|
15
16
|
TextGeometryProps,
|
|
16
17
|
LineGeometryProps,
|
|
18
|
+
TransitionProps,
|
|
17
19
|
Element as CoreElement,
|
|
18
20
|
ElementChildrenAttribute as CoreElementChildrenAttribute
|
|
19
21
|
} from "./src/types"
|
|
@@ -40,19 +42,22 @@ export namespace JSX {
|
|
|
40
42
|
// layout box; d-* forms compose the paint-space geometry props instead.
|
|
41
43
|
interface IntrinsicElements {
|
|
42
44
|
window: WindowProps & ElementRef
|
|
43
|
-
view: ViewProps & ElementRef
|
|
44
|
-
text: TextProps & LayoutProps & ElementRef
|
|
45
|
-
rect: RectProps & LayoutProps & ElementRef
|
|
46
|
-
oval: OvalProps & LayoutProps & ElementRef
|
|
47
|
-
line: LineProps & LayoutProps & ElementRef
|
|
48
|
-
path: PathProps & LayoutProps & ElementRef
|
|
49
|
-
texture: TextureProps & LayoutProps & ElementRef
|
|
50
|
-
"d-view": ViewOwnProps & ElementRef
|
|
51
|
-
"d-rect": RectProps & GeometryProps & ElementRef
|
|
52
|
-
"d-oval": OvalProps & OvalGeometryProps & ElementRef
|
|
53
|
-
"d-line": LineProps & LineGeometryProps & ElementRef
|
|
54
|
-
"d-path": PathProps & PositionProps & ElementRef
|
|
55
|
-
"d-texture": TextureProps & GeometryProps & ElementRef
|
|
56
|
-
"d-text": TextProps & TextGeometryProps & ElementRef
|
|
45
|
+
view: ViewProps & TransitionProps & ElementRef
|
|
46
|
+
text: TextProps & LayoutProps & TransitionProps & ElementRef
|
|
47
|
+
rect: RectProps & LayoutProps & TransitionProps & ElementRef
|
|
48
|
+
oval: OvalProps & LayoutProps & TransitionProps & ElementRef
|
|
49
|
+
line: LineProps & LayoutProps & TransitionProps & ElementRef
|
|
50
|
+
path: PathProps & LayoutProps & TransitionProps & ElementRef
|
|
51
|
+
texture: TextureProps & LayoutProps & TransitionProps & ElementRef
|
|
52
|
+
"d-view": ViewOwnProps & TransitionProps & ElementRef
|
|
53
|
+
"d-rect": RectProps & GeometryProps & TransitionProps & ElementRef
|
|
54
|
+
"d-oval": OvalProps & OvalGeometryProps & TransitionProps & ElementRef
|
|
55
|
+
"d-line": LineProps & LineGeometryProps & TransitionProps & ElementRef
|
|
56
|
+
"d-path": PathProps & PositionProps & TransitionProps & ElementRef
|
|
57
|
+
"d-texture": TextureProps & GeometryProps & TransitionProps & ElementRef
|
|
58
|
+
"d-text": TextProps & TextGeometryProps & TransitionProps & ElementRef
|
|
59
|
+
// A styled run inside <text>/<d-text>; never has a layout box, so there is
|
|
60
|
+
// no d- form.
|
|
61
|
+
span: SpanProps & ElementRef
|
|
57
62
|
}
|
|
58
63
|
}
|
package/package.json
CHANGED
|
@@ -1,17 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@solidrt/core",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.51",
|
|
4
4
|
"license": "MIT",
|
|
5
|
+
"funding": "https://github.com/sponsors/wellawaretech",
|
|
5
6
|
"author": "Antoine van Wel",
|
|
6
7
|
"type": "module",
|
|
7
8
|
"main": "src/index.ts",
|
|
8
9
|
"exports": {
|
|
9
10
|
".": "./src/index.ts",
|
|
10
11
|
"./camera": "./src/camera.ts",
|
|
12
|
+
"./color": "./src/color.ts",
|
|
13
|
+
"./data": "./src/data.ts",
|
|
11
14
|
"./gpu": "./src/gpu.ts",
|
|
12
15
|
"./image": "./src/image.ts",
|
|
16
|
+
"./audio": "./src/audio.ts",
|
|
13
17
|
"./microphone": "./src/microphone.ts",
|
|
14
|
-
"./sound": "./src/sound.ts",
|
|
15
18
|
"./speech-recognition": "./src/speech-recognition.ts",
|
|
16
19
|
"./text-input": "./src/text-input.ts",
|
|
17
20
|
"./jsx-runtime": "./jsx-runtime.d.ts",
|
|
@@ -21,13 +24,11 @@
|
|
|
21
24
|
"src/",
|
|
22
25
|
"examples/",
|
|
23
26
|
"jsx-runtime.d.ts",
|
|
27
|
+
"agents/",
|
|
24
28
|
"AGENTS.md"
|
|
25
29
|
],
|
|
26
|
-
"dependencies": {
|
|
27
|
-
"colord": "^2.9.3"
|
|
28
|
-
},
|
|
29
30
|
"devDependencies": {
|
|
30
|
-
"@solidrt/flux-types": "0.0.
|
|
31
|
+
"@solidrt/flux-types": "0.0.51"
|
|
31
32
|
},
|
|
32
33
|
"peerDependencies": {
|
|
33
34
|
"@solidjs/signals": "2.0.0-rc.0",
|