@solidrt/core 0.0.50 → 0.0.52

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.
Files changed (42) hide show
  1. package/AGENTS.md +102 -21
  2. package/README.md +1 -1
  3. package/agents/painting.md +61 -0
  4. package/agents/performance.md +216 -0
  5. package/docs/index.md +154 -0
  6. package/docs/reference/detached.md +85 -0
  7. package/docs/reference/drawing.md +95 -0
  8. package/docs/reference/elements.md +56 -0
  9. package/docs/reference/gpu.md +204 -0
  10. package/docs/reference/index.md +50 -0
  11. package/docs/reference/input.md +58 -0
  12. package/docs/reference/layout.md +44 -0
  13. package/docs/reference/shaders.md +46 -0
  14. package/docs/reference/text.md +46 -0
  15. package/docs/reference/transforms.md +35 -0
  16. package/docs/reference/types.md +34 -0
  17. package/examples/README.md +7 -5
  18. package/examples/{sound.tsx → audio.tsx} +1 -1
  19. package/examples/gpu-pipeline.tsx +2 -2
  20. package/examples/gpu-sprites.tsx +102 -0
  21. package/examples/line-points.tsx +145 -0
  22. package/examples/parse-svg.tsx +6 -6
  23. package/examples/responsive-grid.tsx +1 -1
  24. package/examples/scroll.tsx +2 -2
  25. package/examples/snapshot-texture.tsx +72 -0
  26. package/examples/{view-viewbox.tsx → view-design-size.tsx} +33 -15
  27. package/jsx-runtime.d.ts +15 -14
  28. package/package.json +11 -9
  29. package/src/{sound.ts → audio.ts} +68 -15
  30. package/src/color.ts +17 -18
  31. package/src/core.ts +40 -3
  32. package/src/data.ts +99 -0
  33. package/src/gpu.ts +88 -34
  34. package/src/index.ts +9 -3
  35. package/src/logo.tsx +92 -0
  36. package/src/renderer.ts +219 -53
  37. package/src/runtime-modules.d.ts +7 -2
  38. package/src/scroll.ts +51 -15
  39. package/src/svg.ts +1 -1
  40. package/src/text-input.ts +297 -61
  41. package/src/types.d.ts +291 -31
  42. package/src/window.ts +110 -14
package/AGENTS.md CHANGED
@@ -1,12 +1,21 @@
1
1
  # @solidrt/core - agent notes
2
2
 
3
- Dense, self-contained facts for writing a SolidRT app.
4
- Full docs live in docs/ (and the website). When this conflicts with prose docs,
5
- trust this file and the types in src/types.d.ts and jsx-runtime.d.ts.
3
+ Dense, self-contained facts for writing a SolidRT app. The prose lives in
4
+ docs/ (also the website); when this conflicts with it, trust this file and
5
+ the types in src/types.d.ts and jsx-runtime.d.ts.
6
6
 
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,
@@ -26,12 +35,19 @@ hardcode desktop pixels.
26
35
 
27
36
  Exception - fixed-aspect content. For content with fixed internal geometry
28
37
  (diagrams, slides, dashboards, games, emulators), do not branch on window size
29
- at all: author everything in one design space and let `viewBox` fit it.
30
- `<view flex={1} viewBox={[1280, 800]}>` uniformly scales and centers the
38
+ at all: author everything in one design space and let `designSize` fit it.
39
+ `<view flex={1} designSize={[1280, 800]}>` uniformly scales and centers the
31
40
  children (letterboxed), pointer events on them arrive in design coordinates,
32
- and the same code runs unchanged from a desktop window to a phone. Reach for
33
- `windowSizeClass` branching only when the layout genuinely reflows across form
34
- factors.
41
+ and the same code runs unchanged from a desktop window to a phone. Laid-out
42
+ children (flex, percentages, text wrap) resolve against the design size too,
43
+ so a whole panel scales into a smaller box without reflowing; the view itself
44
+ sizes like a replaced element whose intrinsic size is the design size. One
45
+ trap from flexbox, not from designSize: in a flex row a width-only design-size view
46
+ is stretched to the line's height under the default alignment, so give the
47
+ view `alignSelf="flex-start"` (or the row a non-stretch `alignItems`) to get
48
+ the design aspect - `aspectRatio` does not override stretch. Reach for
49
+ `windowSizeClass` branching only when the layout genuinely reflows across
50
+ form factors.
35
51
 
36
52
  `env` and `capabilities` (both exported from `@solidrt/core`) are the two
37
53
  objects that expose this. They are plain objects with REACTIVE GETTERS, not
@@ -64,7 +80,7 @@ you need the raw fact (e.g. `env.displayScale` for asset sizing below).
64
80
  Because the drawn size is fluid and the display DPI varies, asset format is a
65
81
  real design decision, not an afterthought:
66
82
 
67
- - Prefer VECTORS (`parseSvg` draws mapped to `<d-path>` in a `viewBox` view)
83
+ - Prefer VECTORS (`parseSvg` draws mapped to `<d-path>` in a `designSize` view)
68
84
  whenever the render size is fluid or DPI varies - they stay crisp at any
69
85
  size x `displayScale()`.
70
86
  - RASTER (`<texture>` / `createImage`) needs source resolution >= displayed size
@@ -79,8 +95,9 @@ bun add @solidrt/core # the renderer
79
95
  bun add -d @solidrt/cli # the `srt` tool (see its AGENTS.md)
80
96
  ```
81
97
 
82
- `@solidrt/components` is a separate, optional package of higher-level components
83
- (see its own AGENTS.md); core primitives alone are enough to build a full app.
98
+ Core primitives alone are enough to build a full app. The optional extensions
99
+ (each with its own AGENTS.md) build on it: `@solidrt/components` (themed
100
+ widgets), `@solidrt/2d` (2D graphics and games), `@solidrt/3d` (scene graph).
84
101
 
85
102
  tsconfig.json - the two load-bearing lines are jsx + jsxImportSource:
86
103
 
@@ -94,13 +111,23 @@ tsconfig.json - the two load-bearing lines are jsx + jsxImportSource:
94
111
  ```
95
112
 
96
113
  Peer deps @solidjs/signals and @solidjs/universal must match (currently
97
- 2.0.0-rc.0); bun resolves them from peerDependencies.
114
+ 2.0.0-rc.1); bun resolves them from peerDependencies.
98
115
 
99
116
  ## Element model (the parts that are easy to get wrong)
100
117
 
101
118
  - `render(() => <App />)`. The returned root MUST be a `<window>` or it throws.
102
119
  Call render once, at the top level.
103
120
 
121
+ - Errors never halt the app. One thrown while computing an element's props or
122
+ a child expression is contained at that element: it keeps its last good
123
+ value, one `Contained error` log line names the node and the .tsx line,
124
+ and it recovers when the expression computes again. Anything unclaimed
125
+ beyond that (a throwing `createEffect`, an error while mounting) reaches
126
+ render()'s root boundary, which replaces the app's window with an error
127
+ window (message, stack, a Reset button that retries the failed
128
+ computations) and logs `Uncaught error`. `<Errored>` gives a subtree its
129
+ own in-place fallback.
130
+
104
131
  - Two kinds of element:
105
132
  - Containers - `<window>`, `<view>`. Do layout + transform + pointer events.
106
133
  THEY DO NOT PAINT. A `<view>` has no background/fill prop.
@@ -113,11 +140,13 @@ Peer deps @solidjs/signals and @solidjs/universal must match (currently
113
140
  Outlines: `drawStyle="stroke"` (or "stroke-and-fill") plus `strokeWidth`.
114
141
  Corner radius on draw primitives: `radius` (number or [tl, tr, br, bl]).
115
142
 
116
- - Registered JSX intrinsics: `window`, `view`, `text`, `rect`, `oval`, `line`,
117
- `path`, `texture`, `audio`, plus the `d-` variants `d-view`, `d-rect`,
143
+ - Registered JSX intrinsics: `window`, `view`, `text`, `span`, `rect`, `oval`,
144
+ `line`, `path`, `texture`, `audio`, plus the `d-` variants `d-view`, `d-rect`,
118
145
  `d-oval`, `d-line`, `d-path`, `d-texture`, `d-text`. Line endpoints
119
146
  (`x1`/`y1`/`x2`/`y2`) exist only on `d-line`; a laid-out `<line>` has no
120
- endpoint props and spans its layout box corner to corner.
147
+ endpoint props and spans its layout box corner to corner. `points` (a flat
148
+ `[x0, y0, x1, y1, ...]` array, plus `closed`) turns either form into a
149
+ polyline and wins over the endpoints while set.
121
150
 
122
151
  - Plain vs `d-` variant (the `d-` prefix means "detached" - detached from the
123
152
  layout engine, Taffy): a plain element (e.g. `rect`) is `RectProps &
@@ -135,6 +164,8 @@ Peer deps @solidjs/signals and @solidjs/universal must match (currently
135
164
  box center; a d-view has no box). To scale a detached group around its
136
165
  content's center, set the origin explicitly in pixels, e.g.
137
166
  `originX={100} originY={50}` for content drawn in a 200x100 local space.
167
+ Avoid pct()/keyword origins on a d-view - they resolve against the box
168
+ inherited from the nearest laid-out ancestor.
138
169
 
139
170
  - Layout-affecting vs not (this matters for per-frame work). Props fall in three
140
171
  buckets, split by where they take effect:
@@ -154,13 +185,18 @@ Peer deps @solidjs/signals and @solidjs/universal must match (currently
154
185
  `position:absolute` at `left:0,top:0`, or just let normal flow place it) and
155
186
  then translate it with `x`/`y`.
156
187
 
188
+ - Text `lineHeight` is a MULTIPLIER of fontSize (1.3-1.6 is typical), not
189
+ pixels. A CSS-reflex value like 22 makes each line box 22x the font size:
190
+ the text becomes blank space and the parent balloons.
191
+
157
192
  - JSX text children collapse whitespace (ordinary JSX semantics): runs of
158
193
  spaces become one, so space-padding a mono label collapses silently. An
159
194
  expression container preserves it - `<d-text>{"one two"}</d-text>` - and
160
195
  `\n` inside one produces a hard line break.
161
196
 
162
197
  - Rich text: `<span>` inside `<text>` restyles a run (`color`, `fontFamily`,
163
- `fontSize`, `fontWeight`, `fontStyle`, `lineHeight`); spans nest and
198
+ `fontSize`, `fontWeight`, `fontStyle`, `lineHeight`,
199
+ `textDecoration="underline"`); spans nest and
164
200
  inherit inward from the `<text>`. Never lay a paragraph out word by word in
165
201
  a wrapping row to mix styles - one `<text>` with spans wraps as a whole.
166
202
  A span is content, not a box (no layout or `d-` form, no size, no
@@ -206,9 +242,55 @@ Peer deps @solidjs/signals and @solidjs/universal must match (currently
206
242
  TRACKED compute that reads signals and returns a value, then an UNTRACKED
207
243
  effect that receives it - `createEffect(() => count(), (c) => ...)`. The 1.x
208
244
  single-callback form `createEffect(() => { ...count()... })` does NOT track
209
- here. Per-frame work: `onFrame((tick, frame) => {})` (returns a cleanup;
210
- auto-cleaned inside a reactive scope) or standard `requestAnimationFrame`.
211
- Also onResize, onLayout, onWindowFocus, onWindowBlur.
245
+ here.
246
+ Reading a signal/prop/store at the top level of a component body (not
247
+ inside JSX, a `createMemo`, or an effect's compute phase) reads it
248
+ untracked: it silently freezes at the initial value.
249
+ Writing a signal or store from inside an owned scope (a component body, a
250
+ `createMemo`, an effect's compute phase) throws
251
+ `REACTIVE_WRITE_IN_OWNED_SCOPE` in dev; a loader called in the component
252
+ body that sets state is the classic React / Solid 1.x reflex that hits
253
+ this. Move the write into an event handler, an effect's apply phase,
254
+ `onSettled`, or an `untrack` block; opt in narrowly with
255
+ `createSignal(v, { ownedWrite: true })` for a signal that genuinely is
256
+ internal state.
257
+ Signal writes flush on a microtask: a handler that sets a signal and
258
+ immediately reads it back gets the OLD value. Read it in an effect, or
259
+ call `flush()` (from @solidjs/signals) to force it through.
260
+
261
+ - An element-valued prop (children, a content/icon slot) compiles to a getter
262
+ that builds a fresh native subtree on EVERY read, and a subtree that is
263
+ never inserted is never freed - native nodes are not garbage collected, so
264
+ what is only wasted work in DOM Solid is a permanent memory leak here. Read
265
+ such props exactly once, at the place they are mounted. To inspect
266
+ children (a typeof probe, counting), resolve them first with the
267
+ `children()` helper (re-exported from @solidrt/core) and probe the resolved
268
+ memo - never `typeof props.children` on the raw prop.
269
+
270
+ - Animation is target-shaped first: declare `transition` on the element and
271
+ write targets, and the runtime animates natively with no per-frame JS.
272
+ Reach for per-frame work only for genuinely procedural motion:
273
+ `onFrame((tick, frame) => {})` is the native hook (runtime-paced, returns a
274
+ cleanup, auto-cleaned inside a reactive scope); `requestAnimationFrame`
275
+ exists as a web-standard one-shot but is not the preferred driver. A JS
276
+ tween loop or an animation library pushing interpolated values through
277
+ signals is the single most expensive mistake available here - read
278
+ agents/performance.md before writing either.
279
+ Window state: onResize, onLayout, onWindowFocus, onWindowBlur exist, but
280
+ prefer the reactive reads (`env`/`capabilities` above, or the accessors
281
+ `windowSize()`, `safeArea()`, `displayScale()`, `windowFocused()`,
282
+ `keyboardHeight()`, `pointerLocked()`) for reading layout and window
283
+ state. For mouse look, `lockPointer(true)` enters relative mouse mode
284
+ (cursor hidden and confined, positions freeze) and pointer events keep
285
+ reporting motion through `movementX`/`movementY`.
286
+
287
+ - `createPortal` cannot mount during the app's initial render (it throws
288
+ "no mount target"): gate portal content behind a signal that starts false
289
+ and open it after startup - overlay content is opened, not born open.
290
+ `createScroll` containers need an explicit main-axis size (a height, or
291
+ flex inside a sized parent); with neither they resolve to 0 and the
292
+ content silently vanishes (`maxHeight` alone does not size it). The
293
+ runtime warns when this happens.
212
294
 
213
295
  - Device/GPU access via subpath imports: @solidrt/core/camera, /microphone,
214
296
  /speech, /gpu. Image flow: `decodeImage(bytes)` ->
@@ -241,5 +323,4 @@ render(() => <App />)
241
323
  Note the two `<d-rect>` underlays: a `<view>`/`<window>` does not paint, so a
242
324
  background is a draw-primitive child placed behind the content.
243
325
 
244
- To run and verify (incl. headless), see @solidrt/cli (its AGENTS.md). For
245
- higher-level components, see @solidrt/components (its AGENTS.md).
326
+ To run and verify (incl. headless), see @solidrt/cli (its AGENTS.md).
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.md](https://github.com/wellawaretech/solidrt/blob/main/docs/core.md) for the full API reference.
56
+ See [docs/](https://github.com/wellawaretech/solidrt/blob/main/packages/core/docs/index.md) (shipped in the package) 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/docs/index.md ADDED
@@ -0,0 +1,154 @@
1
+ # Core
2
+
3
+ `@solidrt/core` is the spine of SolidRT. It links SolidJS reactivity
4
+ to the native rendertree: an element vocabulary, layout, input, frames, and
5
+ the environment model for adapting to the device you are running on.
6
+
7
+ If you only learn one layer, learn this one. Extensions and tools are built
8
+ on it and are replaceable; Core is the part that changes least (SolidRT is
9
+ in alpha, so "least" is not "never").
10
+
11
+ ## Elements
12
+
13
+ There is no DOM. JSX elements are native rendertree nodes, and the
14
+ vocabulary is deliberately small:
15
+
16
+ | Element | Purpose |
17
+ | --- | --- |
18
+ | `window` | The app window. One per app, the root of the tree. |
19
+ | `view` | Layout and input. Boxes, flex containers, hit targets. |
20
+ | `text`, `span` | A shaped paragraph, and a styled run inside it. |
21
+ | `rect`, `oval`, `line`, `path` | Painted shapes. |
22
+ | `texture` | A GPU texture: a decoded image, a camera frame, a shader target. |
23
+
24
+ Layout and paint are separate jobs, which is the one place the vocabulary
25
+ diverges sharply from HTML. A `view` never paints, so there is no
26
+ `backgroundColor`; you put a `rect` behind the content, and by default a
27
+ shape fills the layout box it sits in:
28
+
29
+ ```tsx
30
+ <view padding={16} alignItems="center">
31
+ <rect color="#1b2440" radius={12} />
32
+ <text color="white">Boxed</text>
33
+ </view>
34
+ ```
35
+
36
+ Each painting element also has a detached twin: `d-view`, `d-rect`,
37
+ `d-path`, `d-text`, and so on. Detached elements are positioned by their
38
+ parent's coordinate system rather than by layout, so changing one costs no
39
+ reflow. Use them for anything that moves at animation frequency.
40
+
41
+ ## Reactivity
42
+
43
+ Props are reactive values, not snapshots. A signal read inside JSX
44
+ subscribes exactly one native property to exactly one signal, and an update
45
+ writes that property directly. Nothing re-renders, and there is no virtual
46
+ DOM to diff:
47
+
48
+ ```tsx
49
+ let [x, setX] = createSignal(0)
50
+
51
+ <d-rect x={x()} w={40} h={40} color="tomato" />
52
+ ```
53
+
54
+ The reactive and control-flow vocabulary comes from SolidJS 2.0 and is
55
+ re-exported from `@solidrt/core`, so an app imports everything from one
56
+ place: `createSignal`, `createMemo`, `createEffect`, `createStore`,
57
+ `onCleanup`, and the control-flow components `For`, `Show`, `Switch`,
58
+ `Match`, `Loading`, `Errored`.
59
+
60
+ Because props are values rather than accessors, the usual Solid rules apply:
61
+ do not destructure props, and read reactive values inside the expression
62
+ that uses them.
63
+
64
+ An error thrown while computing an element's props or a child expression is
65
+ contained at that element: it keeps its last good value, the error is logged
66
+ once with the node and source line, and the element recovers when the
67
+ expression computes again. An error nothing claims (a throwing effect, an
68
+ error during mount) replaces the app's window with an error window showing
69
+ the message and stack, with a Reset that retries; the reactive system keeps
70
+ running either way. `<Errored>` gives a subtree its own fallback.
71
+
72
+ ## Layout
73
+
74
+ Layout is flexbox, plus a line-based subset of CSS grid, over the whole
75
+ element tree. Prop names match CSS: `flexDirection`, `alignItems`,
76
+ `justifyContent`, `gap`, `padding`, `width`, `position`, `top`.
77
+
78
+ Units are simpler than CSS. A bare number is pixels; a percentage is
79
+ `pct(50)`, a branded value rather than a parsed string:
80
+
81
+ ```tsx
82
+ <view flexDirection="row" gap={8} padding={16}>
83
+ <view width={pct(50)} />
84
+ </view>
85
+ ```
86
+
87
+ `position` has `relative` and `absolute` only, and an absolute element does
88
+ not itself become a containing block: it resolves against the nearest
89
+ ancestor with `position="relative"`.
90
+
91
+ ## Input
92
+
93
+ Pointer, wheel, and key events are props on any element:
94
+ `onPointerDown`, `onPointerMove`, `onPointerUp`, `onPointerEnter`,
95
+ `onPointerLeave`, `onWheel`, `onKeyDown`, `onKeyUp`. Events travel from the
96
+ hit leaf up to the root, and `stopPropagation()` ends the walk. Key events
97
+ bubble the same way, starting at the focused node - or at the window root
98
+ when nothing is focused, so `onKeyDown` on the window is where app-global
99
+ shortcuts live.
100
+
101
+ Text entry goes to the focused node's `onTextInput`. Focusing a field never
102
+ raises the on-screen keyboard by itself - a tap on the field (or an explicit
103
+ `startTextInput()`) does, and never while a physical keyboard is attached.
104
+
105
+ Coordinates are logical points, so a handler reads the same numbers on a
106
+ high-density phone screen as on a desktop monitor.
107
+
108
+ ## Frames and animation
109
+
110
+ `onFrame(callback)` runs before every painted frame with the frame time in
111
+ ms, the frame count, and the display refresh rate; it returns a disposer and
112
+ cleans itself up with the reactive scope it was called in. Rendering is
113
+ demand-driven: the runtime does not spin a render loop when nothing changed,
114
+ so an idle app is genuinely idle.
115
+
116
+ ```tsx
117
+ let [t, setT] = createSignal(0)
118
+ onFrame((tick) => setT(tick))
119
+
120
+ <d-view rotate={t() / 1000}>...</d-view>
121
+ ```
122
+
123
+ ## Environment and devices
124
+
125
+ `env` and `capabilities` describe where the app is running: `env` is what
126
+ is observed (system theme, text scale, orientation, visibility, connected
127
+ input devices), `capabilities` what follows from it for behavior (hover,
128
+ touch, precise pointer, keyboard navigation, window size class). Which
129
+ runtime features exist on this build is `Flux.capabilities`, by name, never
130
+ by guessing from the OS.
131
+
132
+ Window-shaped values are reactive too: `windowSize()`, `safeArea()`,
133
+ `displayScale()`, `keyboardHeight()`, `windowFocused()`.
134
+
135
+ Device access follows the same reactive shape, as `create*` primitives
136
+ imported from Core subpaths rather than an imperative API:
137
+
138
+ ```tsx
139
+ import { createCamera } from "@solidrt/core/camera"
140
+
141
+ let camera = createCamera()
142
+
143
+ <texture src={camera.texture()} fit="cover" />
144
+ ```
145
+
146
+ The same pattern covers `@solidrt/core/microphone`, `/sound`,
147
+ `/speech-recognition`, `/text-input`, `/image`, `/color`, and `/gpu`.
148
+
149
+ ## Reference
150
+
151
+ The [reference](/core/reference/) covers the API by subject: the element
152
+ vocabulary, drawing, text, detached elements, layout, transforms, input,
153
+ shaders, the GPU module, and the shared types. It shows the shipped declarations themselves,
154
+ so it says exactly what your editor says.