@solidrt/cli 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.
- package/AGENTS.md +79 -36
- package/README.md +86 -15
- package/agents/assets.md +46 -0
- package/agents/debugging.md +294 -0
- package/dist/console.srtapp +94123 -73
- package/dist/server.js +3936 -0
- package/package.json +11 -10
- package/src/android/docs.md +21 -0
- package/src/android/main.ts +286 -0
- package/src/{bundler.ts → bundle/bundler.ts} +163 -76
- package/src/bundle/docs.md +12 -0
- package/src/bundle/main.ts +203 -0
- package/src/check/docs.md +10 -0
- package/src/check/main.ts +85 -0
- package/src/{commands/check.ts → check/typecheck.ts} +11 -34
- package/src/client/docs.md +9 -0
- package/src/client/main.ts +33 -0
- package/src/console/docs.md +14 -0
- package/src/console/main.ts +21 -0
- package/src/demo/docs.md +27 -0
- package/src/demo/main.ts +67 -0
- package/src/init/docs.md +11 -0
- package/src/{commands/init.ts → init/main.ts} +31 -25
- package/src/init/scaffold/AGENTS.md +98 -0
- package/src/init/scaffold/package.json +23 -0
- package/{scaffold → src/init/scaffold}/templates/components/index.tsx +2 -3
- package/{scaffold → src/init/scaffold}/templates/default/index.tsx +2 -3
- package/src/lib/args.ts +194 -0
- package/src/{artifacts.ts → lib/artifacts.ts} +41 -1
- package/src/{dev-dir.ts → lib/dev-dir.ts} +10 -8
- package/src/{fonts.ts → lib/fonts.ts} +12 -26
- package/src/lib/mode.ts +77 -0
- package/src/{project.ts → lib/project.ts} +109 -61
- package/src/lib/registry.ts +120 -0
- package/src/lib/server-bundle.ts +24 -0
- package/src/lib/usage.ts +117 -0
- package/src/lib/util.ts +36 -0
- package/src/main.ts +109 -31
- package/src/mcp/docs.md +22 -0
- package/src/mcp/main.ts +719 -0
- package/src/pack/docs.md +18 -0
- package/src/{pack-folder.ts → pack/layout.ts} +13 -22
- package/src/pack/main.ts +86 -0
- package/src/pack/trailer.ts +97 -0
- package/src/render/docs.md +19 -0
- package/src/render/main.ts +49 -0
- package/src/server/args.ts +126 -0
- package/src/server/binaries.ts +47 -0
- package/src/server/config.ts +54 -0
- package/{server → src/server}/control.ts +246 -78
- package/src/server/docs.md +51 -0
- package/src/server/line-editor.ts +200 -0
- package/src/server/main.ts +473 -0
- package/src/server/mode.ts +92 -0
- package/src/server/rebuild.ts +90 -0
- package/src/server/registry.ts +138 -0
- package/src/server/remap.ts +60 -0
- package/src/server/repl.ts +223 -0
- package/src/server/state.ts +54 -0
- package/{server → src/server}/tsconfig.json +1 -1
- package/{server → src/server}/tunnel.ts +6 -6
- package/src/server/watcher.ts +121 -0
- package/src/tool/main.ts +70 -0
- package/src/types/bundle.d.ts +24 -0
- package/src/types/control.d.ts +90 -0
- package/src/types/registry.d.ts +16 -0
- package/scaffold/AGENTS.md +0 -630
- package/scaffold/package.json +0 -22
- package/scaffold/templates/components/icon.tsx +0 -48
- package/scaffold/templates/default/icon.tsx +0 -48
- package/server/main.ts +0 -308
- package/server/rebuild.ts +0 -68
- package/server/remap.ts +0 -47
- package/server/state.ts +0 -93
- package/src/args.ts +0 -212
- package/src/bundle-cli.ts +0 -13
- package/src/commands/bundle.ts +0 -76
- package/src/commands/client.ts +0 -34
- package/src/commands/mcp.ts +0 -603
- package/src/commands/pack.ts +0 -65
- package/src/commands/render.ts +0 -24
- package/src/commands/server.ts +0 -73
- package/src/dev-android.ts +0 -176
- package/src/dev-client.ts +0 -29
- package/src/dev-server.ts +0 -302
- package/src/packer.ts +0 -103
- package/src/repl.ts +0 -233
- package/src/util.ts +0 -121
- package/src/watcher.ts +0 -69
- /package/src/{untyped-deps.d.ts → bundle/untyped-deps.d.ts} +0 -0
- /package/src/{prompt.ts → init/prompt.ts} +0 -0
- /package/{scaffold → src/init/scaffold}/gitignore +0 -0
- /package/{scaffold → src/init/scaffold}/icon.svg +0 -0
- /package/{scaffold → src/init/scaffold}/mcp.json +0 -0
- /package/{scaffold → src/init/scaffold}/tsconfig.json +0 -0
- /package/{server → src/server}/cache.ts +0 -0
- /package/{server → src/server}/proxy.ts +0 -0
- /package/{server → src/server}/qr.ts +0 -0
package/scaffold/AGENTS.md
DELETED
|
@@ -1,630 +0,0 @@
|
|
|
1
|
-
# SolidRT app - agent notes
|
|
2
|
-
|
|
3
|
-
This project uses SolidRT: a custom SolidJS renderer that paints through a Rust
|
|
4
|
-
runtime. No DOM, no HTML, no CSS cascade. If you are an AI assistant, read this
|
|
5
|
-
before writing or editing code here.
|
|
6
|
-
|
|
7
|
-
## Levels: core, and frameworks on top
|
|
8
|
-
|
|
9
|
-
- @solidrt/core is the low-level foundation: host intrinsics (`<window>`,
|
|
10
|
-
`<view>`, `<text>`, the detached `d-*` drawing primitives) with flat props
|
|
11
|
-
that feed the layout and paint engine directly. An app can be written
|
|
12
|
-
entirely at this level.
|
|
13
|
-
- Higher-level component frameworks build on core. @solidrt/components is
|
|
14
|
-
the first-party one: themed widgets (Window, View, Text, Button,
|
|
15
|
-
ScrollView, SafeArea, ...) with the `layout={{...}}`/`style={{...}}` prop
|
|
16
|
-
split. It is not privileged - a framework is just functions returning core
|
|
17
|
-
JSX, and an app can use a third-party one or grow its own.
|
|
18
|
-
|
|
19
|
-
Match the level the code you are editing already uses. package.json shows
|
|
20
|
-
the choice this app made: if no component framework is among the
|
|
21
|
-
dependencies, the app is core-only - do not add one for a change core
|
|
22
|
-
covers.
|
|
23
|
-
|
|
24
|
-
Authoritative references ship inside the installed packages - read them:
|
|
25
|
-
- node_modules/solid-js/CHEATSHEET.md - SolidJS 2.0 reactivity/control-flow model
|
|
26
|
-
- node_modules/@solidrt/components/AGENTS.md - the component vocabulary; build UI from these
|
|
27
|
-
- node_modules/@solidrt/components/README.md - full prop tables for every component
|
|
28
|
-
- node_modules/@solidrt/components/examples/ - single-concept usage patterns to copy (see its README.md index)
|
|
29
|
-
- node_modules/@solidrt/core/AGENTS.md - the underlying element/prop/reactivity model
|
|
30
|
-
- node_modules/@solidrt/core/examples/ - single-concept usage patterns to copy (see its README.md index)
|
|
31
|
-
- node_modules/@solidrt/cli/AGENTS.md - running, bundling, headless verify
|
|
32
|
-
- node_modules/@solidrt/core/src/types.d.ts and jsx-runtime.d.ts - source of truth
|
|
33
|
-
|
|
34
|
-
<!-- Claude Code auto-imports these; other tools read the paths above. -->
|
|
35
|
-
@./node_modules/solid-js/CHEATSHEET.md
|
|
36
|
-
@./node_modules/@solidrt/components/AGENTS.md
|
|
37
|
-
@./node_modules/@solidrt/core/AGENTS.md
|
|
38
|
-
@./node_modules/@solidrt/cli/AGENTS.md
|
|
39
|
-
|
|
40
|
-
## What you paint with (there is no CSS layer)
|
|
41
|
-
|
|
42
|
-
Layout and props are half the model. There is no stylesheet: no filters, no
|
|
43
|
-
box-shadow, no keyframes, no canvas. The visual range a web app gets from CSS
|
|
44
|
-
comes from the tiers below instead, and reaching past tier 1 is ordinary
|
|
45
|
-
app-building here, not optimization - a screen built only from view
|
|
46
|
-
backgrounds and text is using a fraction of the runtime. Pick the tier the
|
|
47
|
-
CONTENT calls for, not the one that looks safest.
|
|
48
|
-
|
|
49
|
-
1. Laid-out elements - `<view>`/`<text>`, with `<rect>` (or a filling
|
|
50
|
-
`<d-rect>` child) for background, border, radius. The structure of a
|
|
51
|
-
screen, not its finish.
|
|
52
|
-
2. Vector art, detached from layout - `d-path`/`d-rect`/`d-oval`/`d-line`,
|
|
53
|
-
whose `color` takes a gradient (createLinearGradient /
|
|
54
|
-
createRadialGradient) and which honour `blendMode`, plus `parseSvg` to
|
|
55
|
-
draw a whole SVG document as one subtree. Free-form shapes, decoration,
|
|
56
|
-
diagrams, charts, anything positioned rather than flowed. Examples:
|
|
57
|
-
parse-svg, detached-positioning, text-paint-styling.
|
|
58
|
-
3. GPU textures - `createShaderTexture` puts a fragment shader in a
|
|
59
|
-
`<texture>` (moving gradients, noise, glow, dissolves, a background that
|
|
60
|
-
is alive), `createPipelineTexture` draws geometry you generate yourself
|
|
61
|
-
(particles, point clouds, splats), and the `shader` prop post-processes
|
|
62
|
-
content that already exists: on a `<view>` it grades, warps or dissolves
|
|
63
|
-
that subtree, on `<window>` the whole frame. Stack `<texture>` elements
|
|
64
|
-
with `blendMode` to combine passes. Examples: gpu-shader, gpu-particles,
|
|
65
|
-
gpu-pipeline, gpu-instancing, gpu-texture-blend, view-shader,
|
|
66
|
-
window-shader.
|
|
67
|
-
4. 3D scenes - add `@solidrt/3d` (not a scaffold dependency): meshes,
|
|
68
|
-
materials and a camera declared as Solid components, rendered into a
|
|
69
|
-
texture that sits in the UI tree like any other element.
|
|
70
|
-
|
|
71
|
-
Tier 3 is cheaper than it looks. A shader costs one property write per frame
|
|
72
|
-
no matter how complex the effect, which is why the performance notes below
|
|
73
|
-
reach for it first rather than as a last resort.
|
|
74
|
-
|
|
75
|
-
Web reflexes and what replaces them:
|
|
76
|
-
- gradient background -> a gradient `color` on a `d-rect` (gradients are
|
|
77
|
-
paint values, usable anywhere a color is)
|
|
78
|
-
- `filter: blur/grayscale/hue-rotate`, and any "make this look processed" ->
|
|
79
|
-
a `shader` on the view (requires repaintBoundary="snapshot"), or on
|
|
80
|
-
`<window>` for the whole frame
|
|
81
|
-
- `box-shadow` / `text-shadow` / glow -> no shadow prop exists: draw an
|
|
82
|
-
offset `d-*` shape under the content, or a view shader with `outset` (the
|
|
83
|
-
transparent margin an effect bleeds into)
|
|
84
|
-
- `backdrop-filter` -> no equivalent. A view shader sees only its own
|
|
85
|
-
subtree's pixels, never what is behind it. Frost the whole frame with a
|
|
86
|
-
window shader, or fake the layer with your own content
|
|
87
|
-
- `@keyframes` / transitions -> `onFrame` writing a signal for discrete
|
|
88
|
-
motion; a `uTime` uniform when the animation is continuous and visual
|
|
89
|
-
- `<canvas>` 2D -> `d-*` primitives (rebuild one `d-path` string per frame
|
|
90
|
-
rather than animating N elements)
|
|
91
|
-
- `<canvas>` WebGL, three.js -> `createPipelineTexture`, or `@solidrt/3d`
|
|
92
|
-
- video background, animated hero, particle field -> a shader texture; this
|
|
93
|
-
is the case the runtime is built for
|
|
94
|
-
|
|
95
|
-
## The things assistants get wrong (this is not React/DOM)
|
|
96
|
-
|
|
97
|
-
1. This is SolidJS 2.0 (see CHEATSHEET.md for the reactivity/control-flow
|
|
98
|
-
model), rendering through a custom Rust runtime instead of the DOM. Build
|
|
99
|
-
UI at the level the app uses (see "Levels" above): core intrinsics
|
|
100
|
-
directly, or a component framework such as @solidrt/components (Window,
|
|
101
|
-
View, Text, Image, TextInput, ScrollView, Pressable, Button, SafeArea,
|
|
102
|
-
theme/setTheme) - the first-party, batteries-included one.
|
|
103
|
-
2. Most components split their props into two objects: `layout={{...}}` for
|
|
104
|
-
anything that feeds the layout engine (flex/grid, sizing, padding/margin,
|
|
105
|
-
position; font fields for Text) and `style={{...}}` for paint-only
|
|
106
|
-
properties that never relayout (`backgroundColor`, `borderColor`,
|
|
107
|
-
`borderWidth`, `borderRadius`, `color`, and the transform `x`/`y`/
|
|
108
|
-
`rotate`/`scale`). Event handlers (`onPointerDown`, `onKeyDown`, ...) are
|
|
109
|
-
top-level props, not inside `layout`/`style`.
|
|
110
|
-
3. `render(() => <App/>)` once, top level. The root MUST be a `<Window>`
|
|
111
|
-
(from @solidrt/components) or the core `<window>` - it throws otherwise.
|
|
112
|
-
4. Components' `Window`/`View` do not paint on their own; they only paint
|
|
113
|
-
when you set `style.backgroundColor`/`borderColor` etc - there is no
|
|
114
|
-
separate background element to place by hand. That is the components level
|
|
115
|
-
only: the core `<view>`/`<window>` have no background prop at all - in a
|
|
116
|
-
core-only app the background is a draw-primitive child
|
|
117
|
-
(`<d-rect color={...} />`) behind the content.
|
|
118
|
-
5. There is no onClick/onPress on host elements. Use `Pressable`/`Button`
|
|
119
|
-
from components (`onPress`), or `onPointerDown` on a `View` for anything
|
|
120
|
-
custom.
|
|
121
|
-
6. Reactive window state: prefer the accessors windowSize(), safeArea(),
|
|
122
|
-
displayScale(), windowFocused(), keyboardHeight() (re-exported from
|
|
123
|
-
@solidrt/core) over onResize/onLayout callbacks for reading layout and
|
|
124
|
-
window state. SafeArea (the component) is usually the simpler fix for
|
|
125
|
-
avoiding notches/system UI.
|
|
126
|
-
7. Per-frame animation: onFrame((tick, frame) => {}) is the native hook
|
|
127
|
-
(runtime-paced, auto-cleans), re-exported from @solidrt/core.
|
|
128
|
-
requestAnimationFrame(t => {}) exists as a web-standard one-shot but is
|
|
129
|
-
not the preferred animation driver.
|
|
130
|
-
8. In a components-based app, reach for @solidrt/core directly only for
|
|
131
|
-
what components doesn't wrap:
|
|
132
|
-
raw host intrinsics and the `d-` (detached, non-layout) primitives like
|
|
133
|
-
`d-rect`/`d-path`/`d-oval` for vector art or perf-sensitive positioned
|
|
134
|
-
drawing, device/GPU subpath imports (@solidrt/core/camera, /microphone,
|
|
135
|
-
/gpu), gradients (createLinearGradient/createRadialGradient), and
|
|
136
|
-
createImage/decodeImage for images below the `Image` component's level.
|
|
137
|
-
Components and core primitives compose freely in the same tree - a
|
|
138
|
-
components-based app can drop to a `<d-path>` for one custom shape without
|
|
139
|
-
giving up `View`/`Text` everywhere else.
|
|
140
|
-
9. tsconfig needs jsx:"preserve" + jsxImportSource:"@solidrt/core" (still
|
|
141
|
-
true even when you build almost entirely with @solidrt/components -
|
|
142
|
-
components are plain functions returning core JSX). Solid peer deps are
|
|
143
|
-
pinned betas - do not bump them casually.
|
|
144
|
-
10. Use ASCII characters whenever possible in code and text - for example, no
|
|
145
|
-
em-dashes (use a hyphen), no smart/curly quotes, no unicode symbols.
|
|
146
|
-
11. Prefer let over const. Use const only for real constants - a single fixed
|
|
147
|
-
string or number value - and name those in ALL_CAPS.
|
|
148
|
-
12. Reading a signal/prop/store at the top level of a component body (not
|
|
149
|
-
inside JSX, a `createMemo`, or an effect's compute phase) reads it
|
|
150
|
-
untracked - it silently freezes at the initial value instead of updating
|
|
151
|
-
on change. `createEffect` takes two arguments now: `(compute, apply)`.
|
|
152
|
-
`compute` is the tracked read phase; `apply(value, prev)` runs untracked
|
|
153
|
-
and is where side effects/DOM-equivalent writes belong. The old
|
|
154
|
-
single-arg `createEffect(fn)` form is gone - using it is an error.
|
|
155
|
-
13. A scroll container (ScrollView, or anything on createScroll) needs an
|
|
156
|
-
explicit main-axis size - a height, or flex inside a sized parent. With
|
|
157
|
-
neither it resolves to 0 and its content silently vanishes; maxHeight
|
|
158
|
-
alone does not size it (the auto size it would clamp is already 0). The
|
|
159
|
-
runtime warns when this happens.
|
|
160
|
-
14. Text `lineHeight` is a MULTIPLIER of fontSize (the theme uses 1.3-1.6),
|
|
161
|
-
not pixels. A CSS-reflex value like 22 makes each line box 22x the font
|
|
162
|
-
size: the text becomes blank space and the parent balloons.
|
|
163
|
-
15. Signal writes flush on a microtask: a handler that sets a signal and
|
|
164
|
-
immediately reads it back gets the OLD value. Read the new value in an
|
|
165
|
-
effect, or call `flush()` (from @solidjs/signals) to force it through.
|
|
166
|
-
16. Portals cannot mount during the app's initial render: a Modal (or any
|
|
167
|
-
createPortal content) that is visible at first mount throws "no mount
|
|
168
|
-
target". Gate it behind a signal that starts false and open it after
|
|
169
|
-
startup - overlay content is opened, not born open.
|
|
170
|
-
17. An element-valued prop (children, a content/icon slot) compiles to a
|
|
171
|
-
getter that builds a fresh native subtree on EVERY read, and a subtree
|
|
172
|
-
that is never inserted is never freed - native nodes are not garbage
|
|
173
|
-
collected, so what is only wasted work in DOM Solid is a permanent
|
|
174
|
-
memory leak here. Read such props exactly once, at the place they are
|
|
175
|
-
mounted. To inspect children (a typeof probe, counting), resolve them
|
|
176
|
-
first with the children() helper (re-exported from @solidrt/core) and
|
|
177
|
-
probe the resolved memo - never `typeof props.children` on the raw prop.
|
|
178
|
-
18. Writing a signal or store from inside an owned scope - a component body, a
|
|
179
|
-
`createMemo`, an effect's compute phase - throws
|
|
180
|
-
`REACTIVE_WRITE_IN_OWNED_SCOPE` in dev. Calling a loader/init function in
|
|
181
|
-
the component body that sets state is the classic React / Solid 1.x
|
|
182
|
-
reflex and hits this every time. Move the write into an event handler, an
|
|
183
|
-
effect's apply phase, or `onSettled`; opt in narrowly with
|
|
184
|
-
`createSignal(v, { ownedWrite: true })` for a signal that genuinely is
|
|
185
|
-
internal state.
|
|
186
|
-
19. Transform origin on a `d-view`: unset `originX`/`originY` pivots
|
|
187
|
-
scale/rotate at the view's local (0,0), the point its children's
|
|
188
|
-
coordinates are drawn against (a laid-out view pivots at its own box
|
|
189
|
-
center; a d-view has no box). To pivot a detached group around its
|
|
190
|
-
content's center, set the origin explicitly in pixels
|
|
191
|
-
(`originX={100} originY={50}` for content drawn in a 200x100 local
|
|
192
|
-
space). Avoid pct()/keyword origins on a d-view - they resolve against
|
|
193
|
-
the box inherited from the nearest laid-out ancestor.
|
|
194
|
-
20. Cover/contain images: give `Image` a `fit` prop ("fill" | "cover" |
|
|
195
|
-
"contain" | "none" | "scale-down", CSS object-fit semantics, centered)
|
|
196
|
-
plus a box via `layout` in any form - numbers, pct(), flex. Without
|
|
197
|
-
`fit`, only NUMERIC layout sizes reach the image; `width: pct(100)`
|
|
198
|
-
alone draws at intrinsic size. `fit="cover"` is the answer for the
|
|
199
|
-
ported-web hero-image/thumbnail pattern.
|
|
200
|
-
|
|
201
|
-
## Performance model (JS is the slow lane)
|
|
202
|
-
|
|
203
|
-
The JS engine is interpreted and every property write crosses an FFI boundary
|
|
204
|
-
into the runtime, so per-frame JS work is the expensive path while GPU work is
|
|
205
|
-
nearly free. That holds on desktop and on current mobile hardware; "Where GPU
|
|
206
|
-
work stops being free" below is where it does not. Rules, in order of leverage:
|
|
207
|
-
|
|
208
|
-
1. Continuous effects (snow, particles, animated backgrounds) belong in a
|
|
209
|
-
fragment shader: createShaderTexture (from @solidrt/core/gpu) + `<texture
|
|
210
|
-
params={{ uTime }}>` (the shader declares `uniform float uTime;` itself -
|
|
211
|
-
the preamble declares only what the runtime fills). The whole effect then
|
|
212
|
-
costs one setProperty per frame - the uTime write - regardless of visual
|
|
213
|
-
complexity. Shader output
|
|
214
|
-
must be premultiplied alpha (white flakes are `vec4(vec3(a), a)`);
|
|
215
|
-
straight alpha (`vec4(1,1,1,a)`) composites as opaque white. A source that
|
|
216
|
-
starts with `#version 300 es` is compiled exactly as written - no preamble
|
|
217
|
-
is injected, though the built-in vertex stage still supplies `vUV` - so a
|
|
218
|
-
shader ported from elsewhere keeps its own uniform names without dropping
|
|
219
|
-
to compileShader/linkProgram. Params drive any uniform type: a number
|
|
220
|
-
fills a `float`/`int` scalar, a flat number array fills `vec2`/`vec3`/
|
|
221
|
-
`vec4` (2/3/4 numbers) or `mat4` (16, column-major), dispatched by the
|
|
222
|
-
shader's own declaration - a ported shader's `vec2 uCenter` or
|
|
223
|
-
`vec3 iResolution` needs no splitting into scalars. To combine several
|
|
224
|
-
GPU passes, stack `<texture>` elements and set `blendMode` (e.g. a base
|
|
225
|
-
pass plus an additive `blendMode="plus"` pass) rather than writing a
|
|
226
|
-
compositing shader. Within one pipeline draw, createPipelineTexture's
|
|
227
|
-
`blend: "add"` accumulates overlapping geometry additively (soft point
|
|
228
|
-
splats, glow) - pair it with `depthWrite: false` when depth-tested;
|
|
229
|
-
neither option implies the other. A pipeline's own vertex stage writes
|
|
230
|
-
into a y-down clip space: `gl_Position` y = -1 is the top row of the
|
|
231
|
-
target and +1 the bottom, so camera-up geometry must negate y (or fold
|
|
232
|
-
the flip into its projection) or it draws upside down. Sampling is a
|
|
233
|
-
create-time option on every texture: `{ filter: "nearest" }` for
|
|
234
|
-
hard-pixel upscaling (render a small target, display it big - the
|
|
235
|
-
retro/pixel-art path) and `{ wrap: "repeat" }` to tile outside 0..1 in
|
|
236
|
-
shaders; the defaults are linear and clamp, and the choice applies both
|
|
237
|
-
on screen and to shaders sampling the texture.
|
|
238
|
-
2. Reduce setProperty calls wherever possible: one path string rebuilt per
|
|
239
|
-
frame beats N elements with N animated positions; a shader beats the path
|
|
240
|
-
string. get_stats' setPropsPerFrame is the counter to watch. Compiled JSX
|
|
241
|
-
attribute expressions diff before writing, so a per-frame expression that
|
|
242
|
-
returns an unchanged value costs no property write - setPropsPerFrame
|
|
243
|
-
counts values that actually changed, not expressions re-run.
|
|
244
|
-
3. Never leave onFrame registered while nothing animates: a pending onFrame
|
|
245
|
-
is a standing frame request, so the runtime renders and presents every
|
|
246
|
-
vsync even when the callback body does nothing - an invisible 60fps GPU
|
|
247
|
-
burn that also drags the OS compositor along with it. For an on-demand
|
|
248
|
-
animation pump (tweens), use a self-rechaining one-shot
|
|
249
|
-
requestAnimationFrame that stops re-requesting when its work list
|
|
250
|
-
empties. (Registering onFrame outside a component body also warns
|
|
251
|
-
NO_OWNER_CLEANUP - it assumes a reactive owner.)
|
|
252
|
-
4. repaintBoundary works like Flutter's: transforms and opacity on the
|
|
253
|
-
boundary node itself (or any ancestor) are hoisted out of the cache and
|
|
254
|
-
applied at composite time, so animating x/y/scale/rotate/opacity of a
|
|
255
|
-
boundary does NOT re-raster it (verified by A/B measurement - the damage
|
|
256
|
-
system classifies these as Transform and keeps the node's own cache).
|
|
257
|
-
What DOES invalidate the cache is any paint or content change inside the
|
|
258
|
-
subtree - colors, path data, text, a Show toggling - so drive animation
|
|
259
|
-
with transforms and keep the cached content itself static. Off a boundary,
|
|
260
|
-
`opacity` on a view is NOT cheap: it wraps the subtree in a compositing
|
|
261
|
-
layer (save_layer) for as long as it is below 1. To fade a single
|
|
262
|
-
primitive, put the alpha in its `color` (`rgba(...)`) - paint alpha is
|
|
263
|
-
free; reserve view `opacity` for fading a genuine group as a whole.
|
|
264
|
-
5. "snapshot" boundaries pay first-frame texture allocation + raster:
|
|
265
|
-
creating many at once (dealing a board of 64 sprites) is a visible
|
|
266
|
-
one-frame hiccup - pool or pre-warm if that moment matters.
|
|
267
|
-
6. Shading pixels the app already drew is a different mechanism from rule 1's
|
|
268
|
-
generated textures, and both forms are a `shader` prop taking a linked
|
|
269
|
-
program from compileShader/linkProgram (@solidrt/core/gpu), not a
|
|
270
|
-
createShaderTexture source. On `<window>`, `shader={{ program, params }}`
|
|
271
|
-
runs the finished frame through the program as the last step before it
|
|
272
|
-
reaches the screen: the frame binds as `uniform sampler2D uSource`,
|
|
273
|
-
`iResolution` fills by name, and `previous: true` retains the last frame as
|
|
274
|
-
`uPrevious` for motion echo or frame differencing. On a `<view>` the same
|
|
275
|
-
prop shades that subtree in place and REQUIRES repaintBoundary="snapshot"
|
|
276
|
-
(without it the shader is ignored with a warning); the pass sees only the
|
|
277
|
-
subtree's own pixels - grading, warping or dissolving the panel works,
|
|
278
|
-
anything needing what is behind it does not - and is split from content
|
|
279
|
-
invalidation, so a params-only change re-runs the pass against the cached
|
|
280
|
-
snapshot instead of re-rasterizing. A window shader's output is invisible
|
|
281
|
-
to get_snapshot and every other MCP tool; `srt render` is the only way to
|
|
282
|
-
see it (Run / verify below).
|
|
283
|
-
7. `flux:wasm` is not the fast lane. It runs a pure interpreter (wasmi, no
|
|
284
|
-
JIT), so tight typed compute gains a small constant factor over the same
|
|
285
|
-
loop in JavaScript, nowhere near browser wasm speed, and every host call
|
|
286
|
-
costs marshalling. Use it to ship one compiled module across every target
|
|
287
|
-
without native binaries, not to speed up per-frame work; for that, rules
|
|
288
|
-
1-2 (move it to the GPU, cut property writes) are the leverage.
|
|
289
|
-
|
|
290
|
-
### Isolates: heavy work off the JS thread
|
|
291
|
-
|
|
292
|
-
A long synchronous computation (a big parse, a simulation step, a blocking
|
|
293
|
-
`flux:ffi`/`flux:wasm` call) freezes rendering and input for its duration.
|
|
294
|
-
Move it into an isolate module: a file whose first statement is the
|
|
295
|
-
`"use isolate"` directive runs in a second runtime on its own thread, and
|
|
296
|
-
main calls its exports as async functions.
|
|
297
|
-
|
|
298
|
-
```ts
|
|
299
|
-
// src/worker.ts
|
|
300
|
-
"use isolate"
|
|
301
|
-
export function crunch(data: Uint8Array): number { /* ... */ }
|
|
302
|
-
```
|
|
303
|
-
|
|
304
|
-
```ts
|
|
305
|
-
// src/index.tsx
|
|
306
|
-
import { isolate } from "flux:isolate"
|
|
307
|
-
import type * as Worker from "./worker"
|
|
308
|
-
let worker = isolate<typeof Worker>("worker") // id = path from src/, no extension
|
|
309
|
-
let n = await worker.crunch(bytes) // main keeps rendering meanwhile
|
|
310
|
-
```
|
|
311
|
-
|
|
312
|
-
The bundler builds each such module as its own bundle and ships it with the
|
|
313
|
-
app (dev pushes and `srt pack` alike). Rules: main may only `import type`
|
|
314
|
-
from an isolate module (a value import is a build error); arguments and
|
|
315
|
-
results are copies (numbers, strings, byte buffers, arrays, plain objects -
|
|
316
|
-
no functions, no class instances); the child has the non-gui `flux:*`
|
|
317
|
-
modules only, so it never touches the render tree; module state persists
|
|
318
|
-
between calls and each `isolate()` call is its own instance. An
|
|
319
|
-
`async function*` export is a stream: `for await (let p of worker.progress())`
|
|
320
|
-
pulls one item per step (progress, ticks, a subscription), `break` ends it in
|
|
321
|
-
the isolate, and streams never block plain calls. Full contract:
|
|
322
|
-
node_modules/@solidrt/flux-types/modules/isolate.d.ts.
|
|
323
|
-
|
|
324
|
-
### Where GPU work stops being free
|
|
325
|
-
|
|
326
|
-
"GPU work is nearly free" is a property of the hardware, not of the engine, and
|
|
327
|
-
the spread is wide enough to design against rather than discover late. The same
|
|
328
|
-
app - two point-cloud pipelines, 233,600 vertices, one params write each per
|
|
329
|
-
onFrame, i.e. exactly what rule 1 recommends - measured 16.7 ms/frame (60 fps,
|
|
330
|
-
vsync-locked) on both desktop and a mid-range 2020 tablet, and 120 ms/frame
|
|
331
|
-
(8.3 fps) on a 2017 Android TV. Roughly 8x for identical work, with the tablet
|
|
332
|
-
indistinguishable from desktop. Measure on a target device if it matters; do
|
|
333
|
-
not infer it from the desktop number.
|
|
334
|
-
|
|
335
|
-
- **On a tiled GPU the budget is primitive count, not pixels.** Every point or
|
|
336
|
-
triangle costs the tiler regardless of how few pixels it covers. On that TV,
|
|
337
|
-
frame time against total vertices with a trivial vertex shader: 20k -> 80 ms,
|
|
338
|
-
35k -> 100 ms, 100k -> 380 ms. Meanwhile `gl_PointSize = 3.0` - nine times
|
|
339
|
-
the fill - measured within one vsync of 1.0, and rendering into a
|
|
340
|
-
quarter-size target measured identical to full size. So for a heavy pass the
|
|
341
|
-
lever is fewer primitives; shrinking the target or the splat usually is not,
|
|
342
|
-
and coverage is far cheaper bought with point size than with more points.
|
|
343
|
-
- **A device's compositor can set the frame budget outright**, in which case
|
|
344
|
-
none of the above moves. That TV never presents faster than every 80 ms -
|
|
345
|
-
four refresh periods blocked inside `eglSwapBuffers` - even for a near-empty
|
|
346
|
-
scene, so its ceiling is ~12 fps whatever you draw. Recognise it by a
|
|
347
|
-
content-independent floor: if a trivial scene and a heavy one present at
|
|
348
|
-
nearly the same rate, you are compositor-bound and tuning the scene is
|
|
349
|
-
wasted effort.
|
|
350
|
-
- **Per-frame writes are gated on the raster thread, not on vsync**, so a pass
|
|
351
|
-
that costs more than a refresh period does not silently pile up. If
|
|
352
|
-
`rasterQueue` sits persistently above 0 the raster thread is behind; if
|
|
353
|
-
`fenceTimeouts` climbs, the GPU is over its pacing budget.
|
|
354
|
-
|
|
355
|
-
Finding your own numbers: `get_stats` gives fps, frameMs, setPropsPerFrame,
|
|
356
|
-
rasterQueue and fenceTimeouts. When those disagree with what the screen is
|
|
357
|
-
visibly doing, ground truth on Android is
|
|
358
|
-
`adb shell dumpsys SurfaceFlinger --latency <layer>` for real present
|
|
359
|
-
timestamps - engine-reported phase timings can each be honest and still not add
|
|
360
|
-
up to the frame period, because work outside the frame call is not in them.
|
|
361
|
-
|
|
362
|
-
## Assets and app identity
|
|
363
|
-
|
|
364
|
-
- Everything under `assets/` ships with the app: the folder is collected
|
|
365
|
-
wholesale into each build's version manifest (no bundler analysis, no
|
|
366
|
-
registration step). Reference assets by path - `file("assets/sounds/x.ogg")`
|
|
367
|
-
from `flux:fs` - and treat them as read-only at runtime; writes belong in
|
|
368
|
-
plain relative paths, which land in the app's private data dir.
|
|
369
|
-
- Small text-like assets (SVG documents, shaders) can instead be inlined via
|
|
370
|
-
imports. An import attribute picks the form and works on any extension:
|
|
371
|
-
`import src from "./effect.glsl" with { type: "text" }` yields the file's
|
|
372
|
-
contents as a string, `with { type: "binary" }` yields a Uint8Array. `.svg`
|
|
373
|
-
is text-loaded with no attribute needed. Shader sources (`.glsl`/`.vert`/
|
|
374
|
-
`.frag`) are declared as text modules out of the box, so they typecheck
|
|
375
|
-
without setup. Inlining trades update granularity for zero I/O - keep big or
|
|
376
|
-
streamable files (audio, images) in `assets/`.
|
|
377
|
-
- Custom fonts go in `assets/fonts/` and are declared in the `solidrt.fonts`
|
|
378
|
-
map in package.json (alias -> file path; role aliases `sans`/`serif`/`mono`
|
|
379
|
-
replace the built-in defaults, `false` drops one, other keys add fonts
|
|
380
|
-
selectable via fontFamily). A newly added font shows after restarting the
|
|
381
|
-
client.
|
|
382
|
-
- The `solidrt` key in package.json is the app's identity: set a stable
|
|
383
|
-
reverse-DNS `appId` before distributing - it keys the app's storage
|
|
384
|
-
folder, defaults from the package name in dev, and `srt pack` warns
|
|
385
|
-
while defaulted. `org` and `displayName` are optional display metadata
|
|
386
|
-
(future launcher/window naming) with no storage meaning.
|
|
387
|
-
- `bunx srt pack src/index.tsx` builds a single-file executable;
|
|
388
|
-
`bunx srt pack --folder src/index.tsx` writes the flat app folder
|
|
389
|
-
(runner + manifest.json + bundle + assets/, plus the runner's GL
|
|
390
|
-
libraries on Windows and macOS) to `dist/`.
|
|
391
|
-
|
|
392
|
-
## Run / verify
|
|
393
|
-
|
|
394
|
-
- FIRST check whether a dev server and a client are already running (MCP
|
|
395
|
-
list_clients, see below) and build/test against those: `reload` pushes your
|
|
396
|
-
edits to the live app, get_logs and get_snapshot verify them. Do not start a
|
|
397
|
-
second `srt run` when one is already up.
|
|
398
|
-
- bunx srt run src/index.tsx - dev server + window (needs a display)
|
|
399
|
-
- bunx srt check src/index.tsx - exit 0 means it compiles and the app's
|
|
400
|
-
types hold (dependency-internal type errors are hidden). Builds in memory:
|
|
401
|
-
writes nothing and never triggers a dev-server reload, so use this while
|
|
402
|
-
iterating - `srt bundle` writes output files and reloads connected clients
|
|
403
|
-
- bunx srt render src/index.tsx --size 480x640 --duration 1 --fps 2 - headless
|
|
404
|
-
render to PNG frames (proves it renders; see the cli AGENTS.md for where the
|
|
405
|
-
frames land). It is also the ONLY way to see the output of a window shader
|
|
406
|
-
(the `shader` prop on `<window>`): that pass runs on the finished frame on
|
|
407
|
-
its way to the screen, past the point every other capture reads, so `render`
|
|
408
|
-
frames are the only programmatic view of what it produces
|
|
409
|
-
|
|
410
|
-
## MCP: inspect the running app
|
|
411
|
-
|
|
412
|
-
The project ships an MCP server (.mcp.json, `srt mcp`) that talks to the dev
|
|
413
|
-
server `bunx srt run` starts. When it is loaded in your environment, prefer
|
|
414
|
-
its tools over guessing at runtime state:
|
|
415
|
-
|
|
416
|
-
- list_clients: connected app clients, their platform and runtime
|
|
417
|
-
capabilities, plus the server's `entry` (the app source it serves) and
|
|
418
|
-
`projectDir` - check entry matches the app you think you are driving. Each
|
|
419
|
-
tool call finds the dev server currently serving this project (by its
|
|
420
|
-
project root), so a server restarted on another port/session is followed
|
|
421
|
-
automatically; no server serving this project is an error, not a wrong
|
|
422
|
-
server.
|
|
423
|
-
Each client also lists `queries`, the dev-tool query kinds its runtime
|
|
424
|
-
answers - check it before planning verification against a mixed-version
|
|
425
|
-
fleet (no "input" = the client predates send_input)
|
|
426
|
-
- get_logs: console output and runtime errors (seq cursor; `wait_ms` long-poll
|
|
427
|
-
to catch output right after a reload; `level`/`contains` filters; repeated
|
|
428
|
-
lines collapse into one entry with a `repeats` count)
|
|
429
|
-
- get_render_tree: what the app actually rendered - node kinds, text, and
|
|
430
|
-
window-relative boxes. Pass `props: true` for each node's current
|
|
431
|
-
property values (JSX names, off-default only - "is rotate/color/overflow/d
|
|
432
|
-
applied right now" is one call, not a probe entry) and, on transformed nodes, the
|
|
433
|
-
painted `quad` (four corners after transforms; the box is just its
|
|
434
|
-
axis-aligned bounds). Whole trees get large: `query` finds nodes by
|
|
435
|
-
kind/text, then `root` + `depth` inspect just that region
|
|
436
|
-
- client ids and log cursors die with the dev server: list_clients and
|
|
437
|
-
get_logs responses carry `generation`, and a changed generation means
|
|
438
|
-
re-fetch ids and restart cursors
|
|
439
|
-
- get_stats: fps, CPU/memory, frame phase timings, setProperty rate, plus
|
|
440
|
-
layout-activity counters for the last rebuild (nodes, measureCalls,
|
|
441
|
-
paraShapes/wordHits, dirtiedNodes, cacheGets/cacheHits) - when layoutMs looks
|
|
442
|
-
wrong, these say whether the cost is text shaping, invalidation breadth,
|
|
443
|
-
or a defeated layout cache (healthy incremental rebuilds show a near-100%
|
|
444
|
-
cacheHits rate). reusedPerSec/skippedPerSec are the demand gate's visible
|
|
445
|
-
signal: frames presented from the cached display list without a rebuild
|
|
446
|
-
(texture content changed, no property writes - expect reusedPerSec near
|
|
447
|
-
fps on texture-driven apps) and frames skipped entirely (nothing
|
|
448
|
-
requested one)
|
|
449
|
-
- get_snapshot: PNG capture of any render-tree node's pixels (get node ids
|
|
450
|
-
from get_render_tree; the window node captures everything). A subtree
|
|
451
|
-
capture renders with NO ancestor paint: pixels the subtree does not draw
|
|
452
|
-
come back transparent, not the background behind the node. Captures
|
|
453
|
-
re-rasterize the tree offscreen, so they are also PRE window shader: an app
|
|
454
|
-
with a `shader` on its `<window>` snapshots as its unshaded content, window
|
|
455
|
-
node included, and no MCP tool reads the shaded result (its layer is
|
|
456
|
-
runtime-owned, so get_texture has no id for it; get_gpu_resources reports
|
|
457
|
-
only that the pass exists). Use `srt render` for that one. Crop with
|
|
458
|
-
x/y/width/height (captured-image pixels) and magnify with `scale` (1-8,
|
|
459
|
-
nearest-neighbour) - a tight crop at 4-8x is how small geometry gets
|
|
460
|
-
verified. Pass `save_to` on get_snapshot or get_texture to also write the
|
|
461
|
-
PNG to a file - the image in the tool result cannot be saved afterwards,
|
|
462
|
-
so decide before capturing (e.g. keep a before/after pair to diff)
|
|
463
|
-
- set_time_scale / step_frames: the runtime clock. `set_time_scale 0`
|
|
464
|
-
freezes app time (onFrame, requestAnimationFrame, timers, and
|
|
465
|
-
performance.now all stop; Date.now stays wall time), so a snapshot can
|
|
466
|
-
catch an exact frame of any animation instead of racing it; `step_frames
|
|
467
|
-
n` then advances exactly n frames (one refresh period each). Pause,
|
|
468
|
-
snapshot, step, snapshot again to see precisely what changed. ALWAYS set
|
|
469
|
-
the scale back to 1 when done - a paused client looks wedged to the human
|
|
470
|
-
watching - though reload/load also reset it
|
|
471
|
-
- send_input: synthetic pointer/key/wheel/text events through the REAL
|
|
472
|
-
input pipeline (hit testing, focus, bubbling) - the way to verify an
|
|
473
|
-
interaction actually works, where call_debug would bypass it. A click is
|
|
474
|
-
one call ({type: "pointer", action: "tap", x, y} - logical points, the
|
|
475
|
-
same space get_render_tree reports); a key hold is {type: "key", action:
|
|
476
|
-
"tap", key: "w", holdMs: 500}; text needs the field focused first (tap
|
|
477
|
-
it), then {type: "text", text: "go"}; drags are down + moves (delayMs
|
|
478
|
-
~16 apiece) + up. Sequences run in order with per-event delayMs and the
|
|
479
|
-
call returns after the last event is delivered, so a following snapshot
|
|
480
|
-
sees the result. A synthetic mouse keeps hovering at its last position
|
|
481
|
-
(like a real cursor at rest); use pointerType: "touch" for gestures that
|
|
482
|
-
should end hover-free. Composes with the clock: pause, send_input,
|
|
483
|
-
step_frames, snapshot = a deterministic interaction test
|
|
484
|
-
- get_gpu_resources: inventory of GPU state - textures (size, render target
|
|
485
|
-
or not), vertex buffers (byteLength), pipelines (draw count, attribute
|
|
486
|
-
layout, bound textures, current uniform values - the most recent writes,
|
|
487
|
-
which the next frame or readback draws with)
|
|
488
|
-
- get_texture: any GPU texture read back as a PNG by id - atlases, data
|
|
489
|
-
textures, and shader/pipeline render targets alike (a render target reads
|
|
490
|
-
as its current output, pending writes included, with no frame or snapshot
|
|
491
|
-
needed); crop with x/y/width/height, magnify with `scale`
|
|
492
|
-
- get_buffer: a vertex-buffer range decoded to numbers (f32/u16/u8, 64 KiB
|
|
493
|
-
per call) - verify geometry after a writeBuffer instead of inferring it
|
|
494
|
-
from pixels
|
|
495
|
-
- list_debug / call_debug: the app's own debug commands (registered with
|
|
496
|
-
`registerDebug` from `srt:dev`) - list them, then invoke by name with a
|
|
497
|
-
JSON argument. Per client, like get_snapshot
|
|
498
|
-
- reload: rebuild from source and push to every client - THE dev loop is
|
|
499
|
-
edit -> reload -> get_logs -> get_snapshot. reload surfaces build errors
|
|
500
|
-
but not type errors; run `bunx srt check` for those.
|
|
501
|
-
- load: bundle a given source file and push it to every client, replacing
|
|
502
|
-
the running app; later reloads rebuild that entry. Use it when the dev
|
|
503
|
-
server has no app loaded yet, or to switch apps without restarting srt.
|
|
504
|
-
- watch: pause (enabled: false) or resume the automatic reload-on-save.
|
|
505
|
-
Pause BEFORE creating or editing source files so half-finished work is
|
|
506
|
-
not pushed to the user's screens mid-burst; a successful reload or load
|
|
507
|
-
resumes it, so pause again before the next burst. Never leave it paused
|
|
508
|
-
when you stop working - the user's own saves rely on it.
|
|
509
|
-
|
|
510
|
-
The tools need a running app: if list_clients is empty, ask the user to start
|
|
511
|
-
`bunx srt run src/index.tsx`. The bridge dials the dev server's default port
|
|
512
|
-
(34884), so if the user started it with `--port N`, .mcp.json needs the same
|
|
513
|
-
flag: `"args": [..., "mcp", "--port", "N"]`.
|
|
514
|
-
|
|
515
|
-
- Permission prompts: agents typically ask approval per MCP tool. All of
|
|
516
|
-
these tools only talk to the local dev server the user started with
|
|
517
|
-
`bunx srt run` - nothing leaves the machine - so approving the server as
|
|
518
|
-
a whole is a reasonable default. If repeated prompts get in the way, do
|
|
519
|
-
not work around them; tell the user they can pre-approve the server in
|
|
520
|
-
their agent's settings (most agents have a per-server trust or allowlist
|
|
521
|
-
setting - in Claude Code, add "mcp__solidrt" to `permissions.allow` in
|
|
522
|
-
~/.claude/settings.json to cover every solidrt project). This is the
|
|
523
|
-
user's call to make, once, in their own tooling.
|
|
524
|
-
- Multiple clients: several clients may be attached (desktop window,
|
|
525
|
-
phone, tablet) with different sizes, display scales, and safe areas.
|
|
526
|
-
reload pushes to all of them, but call_debug / send_input / get_snapshot
|
|
527
|
-
/ log cursors are per client, and interactive state does NOT sync - a flow
|
|
528
|
-
driven on one client leaves the others sitting on the initial screen,
|
|
529
|
-
which reads as a crash to a human holding that device. So: when driving
|
|
530
|
-
state via call_debug, send the same call to every client (or say which
|
|
531
|
-
client you are using); and before calling a visual change done,
|
|
532
|
-
snapshot each distinct form factor at least once - a layout that fits
|
|
533
|
-
one window can clip or overflow another.
|
|
534
|
-
|
|
535
|
-
## Debugging a running app (lessons that cost real time)
|
|
536
|
-
|
|
537
|
-
- console.log + get_logs is your primary probe into runtime state. For state
|
|
538
|
-
you will want repeatedly (a pose, a mode, a counter), bind a debug key that
|
|
539
|
-
logs it and read it back via get_logs.
|
|
540
|
-
- Better than debug keys when driving the app over MCP: register debug
|
|
541
|
-
COMMANDS - `registerDebug(name, fn)` from `srt:dev`, invoked via the
|
|
542
|
-
list_debug/call_debug tools. Use them to SET UP state (jump to a level,
|
|
543
|
-
force a mode, seed a scenario); then the runtime-level tools take over -
|
|
544
|
-
set_time_scale 0 freezes the result for as many snapshots as you need,
|
|
545
|
-
and step_frames walks it forward deterministically. Set state, pause,
|
|
546
|
-
snapshot. Registrations reset on hot reload, so register at module init;
|
|
547
|
-
sync return values only - and note a signal you just wrote flushes on a
|
|
548
|
-
microtask, so returning a signal read straight after setting it returns
|
|
549
|
-
the OLD value.
|
|
550
|
-
- call_debug sets state directly, skipping focus, key routing, and
|
|
551
|
-
TextInput - fine for SETUP, but "the interaction works" is only shown by
|
|
552
|
-
the real pipeline: verify clicks, typing, and drags with send_input,
|
|
553
|
-
which enters events where SDL input does.
|
|
554
|
-
- Key events start at the focused node and bubble to the window root; with
|
|
555
|
-
nothing focused they go to the window root alone. So a debug key bound via
|
|
556
|
-
`<window onKeyDown>` always fires (unless a focused component consumes the
|
|
557
|
-
key with stopPropagation, as TextInput does for editing keys). `key` and
|
|
558
|
-
`code` are W3C KeyboardEvent values, so arrow keys arrive as "ArrowLeft"/
|
|
559
|
-
"ArrowRight"/"ArrowUp"/"ArrowDown" (not "Left"), alongside "Enter",
|
|
560
|
-
"Escape", "a".
|
|
561
|
-
- Idle frames skip work: shaders/pipelines only re-render when an input
|
|
562
|
-
changes - their own params/geometry, or a sampled texture (a data upload,
|
|
563
|
-
or a sampled target re-rendering; chains propagate automatically). Measure
|
|
564
|
-
performance while inputs are actually changing. get_snapshot works on an
|
|
565
|
-
idle client (it requests its own frame); a timeout means the JS thread is
|
|
566
|
-
busy or wedged. get_texture on a pipeline's render target reads the
|
|
567
|
-
current output, pending writes included, without needing a new frame.
|
|
568
|
-
- When a human reports a visual bug: capture a snapshot and SAY WHAT YOU SEE
|
|
569
|
-
in it before investigating, so you agree on the symptom. If you cannot see
|
|
570
|
-
the problem in the capture, say that instead of guessing.
|
|
571
|
-
- Snapshots are downscaled by the time you see them, so a full-window capture
|
|
572
|
-
cannot show you a defect a few pixels across. Whenever you hand-author
|
|
573
|
-
geometry - a `d-path` from raw path math, a `radius` where two shapes meet,
|
|
574
|
-
a stroke join - inspect it MAGNIFIED once, when you write it: get_snapshot
|
|
575
|
-
with a tight crop at scale 4-8 shows the actual rendered pixels enlarged,
|
|
576
|
-
in one call, on the real app. Verifying that a shape is in the right place
|
|
577
|
-
is not the same check as verifying it is drawn right - and get_render_tree
|
|
578
|
-
props answers the third question, whether the value you set is the value
|
|
579
|
-
the renderer holds.
|
|
580
|
-
- GPU/geometry bugs: inspect the actual GPU data FIRST - get_gpu_resources
|
|
581
|
-
for draw counts/uniforms/sizes, get_texture for atlas or data-texture
|
|
582
|
-
contents ("is this tile blank?" is a ten-second question), get_buffer for
|
|
583
|
-
vertex data. The pixels only tell you THAT something is wrong; the
|
|
584
|
-
resources tell you WHERE the data stops being right. In a one-big-pipeline
|
|
585
|
-
app the render tree is a single <texture> leaf and tells you nothing -
|
|
586
|
-
these tools are the visibility layer behind it. Only when the GPU data is
|
|
587
|
-
all correct (so the bug is in producing it, or in the shader), reproduce
|
|
588
|
-
the math CPU-side in a scratch bun script against the app's real data and
|
|
589
|
-
print values.
|
|
590
|
-
- Validate assets at load time and log anomalies (missing lumps/files,
|
|
591
|
-
fully-transparent composites, zero-sized images). Silent fallbacks hide
|
|
592
|
-
bugs for days; a one-line warning surfaces them the first run.
|
|
593
|
-
- After every reload the app restarts from its initial state. If reaching
|
|
594
|
-
the bug site takes navigation, add a dev shortcut (teleport key, noclip,
|
|
595
|
-
initial-state override) before iterating - the round trips add up fast.
|
|
596
|
-
- Clamp onFrame time deltas to [0, cap], not just capped: across a hot
|
|
597
|
-
reload the runtime's tick counter resets AFTER the new instance's first
|
|
598
|
-
frame, so the second frame computes a hugely NEGATIVE delta.
|
|
599
|
-
Math.min(dt, cap) lets it through, and one bad frame can corrupt anything
|
|
600
|
-
integrated from dt (positions fly off, accumulators go so negative they
|
|
601
|
-
never recover). Math.max(0, Math.min(dt, cap)) costs nothing.
|
|
602
|
-
- A registered onFrame is a standing request, not demand-gated: it re-requests
|
|
603
|
-
the next frame every time it runs, so the runtime keeps calling it - and
|
|
604
|
-
presents - every frame at the refresh rate until you deregister it (fps stays
|
|
605
|
-
at the refresh rate on an idle screen; that is the 60fps burn called out in
|
|
606
|
-
the performance notes above). The upside is that a self-running loop - a game
|
|
607
|
-
clock, a shader driver, a stepped VM doing silent CPU work with no console
|
|
608
|
-
output - keeps advancing on its own: it does NOT stall when the body changes
|
|
609
|
-
nothing and needs no startup "prime" write. Deregister onFrame (return its
|
|
610
|
-
cleanup, or let onCleanup fire) whenever there is nothing left to advance.
|
|
611
|
-
- Layout is incremental: a change re-solves only the dirty path, and clean
|
|
612
|
-
subtrees answer from a per-node cache, so long lists no longer cap layout
|
|
613
|
-
(a thousand-node tree relays out in well under a millisecond). If layoutMs
|
|
614
|
-
still grows with tree size, read the get_stats counters - a low
|
|
615
|
-
cacheHits/cacheGets ratio means the layout cache is being defeated, high
|
|
616
|
-
paraShapes means text is actually reshaping. Very long lists still pay
|
|
617
|
-
for the initial mount and for memory, so windowing stays sensible at the
|
|
618
|
-
thousands-of-rows scale.
|
|
619
|
-
- Remote images: createImage (and Image) dedupes repeated URLs, caches the
|
|
620
|
-
bytes on disk, and the runtime rate-limits concurrent asset fetches per
|
|
621
|
-
host - do not build your own promise cache around it. Images are fetched
|
|
622
|
-
with no freshness check (an already-cached URL is never re-checked), so
|
|
623
|
-
use versioned URLs for content that changes. Use Image's `fallback` prop
|
|
624
|
-
(an image source) for the broken-image case instead of catching errors
|
|
625
|
-
yourself.
|
|
626
|
-
- fetch() never caches by default and ignores server cache headers. Caching
|
|
627
|
-
is explicit and per call: `fetch(url, { cache: "force-cache" })` for
|
|
628
|
-
assets (serve from disk or fetch-and-store, no freshness),
|
|
629
|
-
`{ cache: "reload" }` to refresh an entry. Image/createImage already do
|
|
630
|
-
this for you.
|
package/scaffold/package.json
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "solidrt-app",
|
|
3
|
-
"type": "module",
|
|
4
|
-
"private": true,
|
|
5
|
-
"scripts": {
|
|
6
|
-
"dev": "srt run src/index.tsx",
|
|
7
|
-
"pack": "srt pack -o out src/index.tsx",
|
|
8
|
-
"server": "srt server",
|
|
9
|
-
"client": "srt client",
|
|
10
|
-
"android": "srt client --android"
|
|
11
|
-
},
|
|
12
|
-
"dependencies": {
|
|
13
|
-
"@solidrt/core": "0.0.50",
|
|
14
|
-
"@solidrt/components": "0.0.50",
|
|
15
|
-
"@solidrt/3d": "0.0.50"
|
|
16
|
-
},
|
|
17
|
-
"devDependencies": {
|
|
18
|
-
"@solidrt/cli": "0.0.50",
|
|
19
|
-
"@solidrt/flux-types": "0.0.50",
|
|
20
|
-
"typescript": "^7"
|
|
21
|
-
}
|
|
22
|
-
}
|