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