@solidrt/core 0.0.25 → 0.0.27
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 +1 -1
- package/examples/gpu-pipeline.tsx +82 -0
- package/package.json +5 -5
- package/src/gamepad.ts +56 -0
- package/src/gpu.ts +73 -0
- package/src/index.ts +2 -0
- package/src/renderer.ts +39 -20
- package/src/runtime-modules.d.ts +9 -0
package/AGENTS.md
CHANGED
|
@@ -84,7 +84,7 @@ tsconfig.json - the two load-bearing lines are jsx + jsxImportSource:
|
|
|
84
84
|
```
|
|
85
85
|
|
|
86
86
|
Peer deps @solidjs/signals and @solidjs/universal must match (currently
|
|
87
|
-
2.0.0-beta.
|
|
87
|
+
2.0.0-beta.17); bun resolves them from peerDependencies.
|
|
88
88
|
|
|
89
89
|
## Element model (the parts that are easy to get wrong)
|
|
90
90
|
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
// createPipeline compiles a custom GLSL ES 3.00 vertex+fragment pair that draws
|
|
2
|
+
// an interleaved vertex buffer into a texture, here a spinning cube with depth
|
|
3
|
+
// testing. The vertex shader declares `in` attributes matching the pipeline's
|
|
4
|
+
// attribute list (locations are resolved by name) and its own varyings; the
|
|
5
|
+
// fragment preamble provides fragColor/iResolution/iTime but no vUV. Uniforms
|
|
6
|
+
// are driven exactly like createShader: declaratively via the <texture> params
|
|
7
|
+
// prop, applied at the next repaint.
|
|
8
|
+
import { render, onFrame, createSignal } from "@solidrt/core"
|
|
9
|
+
import { createBuffer, createPipeline } from "@solidrt/core/gpu"
|
|
10
|
+
|
|
11
|
+
let VERTEX = `
|
|
12
|
+
in vec3 aPos;
|
|
13
|
+
in vec3 aColor;
|
|
14
|
+
out vec3 vColor;
|
|
15
|
+
uniform float uTime;
|
|
16
|
+
|
|
17
|
+
void main() {
|
|
18
|
+
float cy = cos(uTime), sy = sin(uTime);
|
|
19
|
+
float cx = cos(uTime * 0.7), sx = sin(uTime * 0.7);
|
|
20
|
+
mat3 rotY = mat3(cy, 0.0, -sy, 0.0, 1.0, 0.0, sy, 0.0, cy);
|
|
21
|
+
mat3 rotX = mat3(1.0, 0.0, 0.0, 0.0, cx, sx, 0.0, -sx, cx);
|
|
22
|
+
vec3 p = rotX * (rotY * aPos);
|
|
23
|
+
p.z += 2.5;
|
|
24
|
+
|
|
25
|
+
// Perspective projection (near 1, far 10). Clip y is negated: the target's
|
|
26
|
+
// memory row 0 is clip y = -1, and Impeller samples row 0 as the top, so
|
|
27
|
+
// camera-up needs the flip to be displayed up.
|
|
28
|
+
float f = 2.0;
|
|
29
|
+
float a = 11.0 / 9.0;
|
|
30
|
+
float b = -20.0 / 9.0;
|
|
31
|
+
gl_Position = vec4(p.x * f, -p.y * f, p.z * a + b, p.z);
|
|
32
|
+
vColor = aColor;
|
|
33
|
+
}
|
|
34
|
+
`
|
|
35
|
+
|
|
36
|
+
let FRAGMENT = `
|
|
37
|
+
in vec3 vColor;
|
|
38
|
+
void main() {
|
|
39
|
+
fragColor = vec4(vColor, 1.0);
|
|
40
|
+
}
|
|
41
|
+
`
|
|
42
|
+
|
|
43
|
+
// Interleaved [pos vec3, color vec3], 6 faces x 2 triangles, one color per face.
|
|
44
|
+
function cube(): Float32Array {
|
|
45
|
+
type Vec3 = [number, number, number]
|
|
46
|
+
let verts: number[] = []
|
|
47
|
+
let quad = (a: Vec3, b: Vec3, c: Vec3, d: Vec3, color: Vec3) => {
|
|
48
|
+
for (let p of [a, b, c, a, c, d]) verts.push(p[0], p[1], p[2], color[0], color[1], color[2])
|
|
49
|
+
}
|
|
50
|
+
let s = 0.5
|
|
51
|
+
quad([-s, -s, s], [s, -s, s], [s, s, s], [-s, s, s], [0.9, 0.3, 0.3]) // front
|
|
52
|
+
quad([s, -s, -s], [-s, -s, -s], [-s, s, -s], [s, s, -s], [0.3, 0.9, 0.4]) // back
|
|
53
|
+
quad([s, -s, s], [s, -s, -s], [s, s, -s], [s, s, s], [0.3, 0.5, 0.9]) // right
|
|
54
|
+
quad([-s, -s, -s], [-s, -s, s], [-s, s, s], [-s, s, -s], [0.9, 0.8, 0.3]) // left
|
|
55
|
+
quad([-s, s, s], [s, s, s], [s, s, -s], [-s, s, -s], [0.8, 0.4, 0.9]) // top
|
|
56
|
+
quad([-s, -s, -s], [s, -s, -s], [s, -s, s], [-s, -s, s], [0.4, 0.9, 0.9]) // bottom
|
|
57
|
+
return new Float32Array(verts)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function App() {
|
|
61
|
+
let bufferId = createBuffer(cube())
|
|
62
|
+
let id = createPipeline(VERTEX, FRAGMENT, 512, 512, {
|
|
63
|
+
params: { uTime: 0 },
|
|
64
|
+
attributes: [
|
|
65
|
+
{ name: "aPos", format: "vec3" },
|
|
66
|
+
{ name: "aColor", format: "vec3" },
|
|
67
|
+
],
|
|
68
|
+
buffer: bufferId,
|
|
69
|
+
depth: true,
|
|
70
|
+
clearColor: [0.08, 0.08, 0.12, 1],
|
|
71
|
+
})
|
|
72
|
+
let [time, setTime] = createSignal(0)
|
|
73
|
+
onFrame((tick) => setTime(tick / 1000))
|
|
74
|
+
|
|
75
|
+
return (
|
|
76
|
+
<window alignItems="center" justifyContent="center">
|
|
77
|
+
<texture src={id} params={{ uTime: time() }} width={400} height={400} />
|
|
78
|
+
</window>
|
|
79
|
+
)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
render(() => <App />)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@solidrt/core",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.27",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"author": "Antoine van Wel",
|
|
6
6
|
"type": "module",
|
|
@@ -27,11 +27,11 @@
|
|
|
27
27
|
"colord": "^2.9.3"
|
|
28
28
|
},
|
|
29
29
|
"devDependencies": {
|
|
30
|
-
"@solidrt/flux-types": "0.0.
|
|
30
|
+
"@solidrt/flux-types": "0.0.27"
|
|
31
31
|
},
|
|
32
32
|
"peerDependencies": {
|
|
33
|
-
"@solidjs/signals": "2.0.0-beta.
|
|
34
|
-
"@solidjs/universal": "2.0.0-beta.
|
|
35
|
-
"solid-js": "2.0.0-beta.
|
|
33
|
+
"@solidjs/signals": "2.0.0-beta.17",
|
|
34
|
+
"@solidjs/universal": "2.0.0-beta.17",
|
|
35
|
+
"solid-js": "2.0.0-beta.17"
|
|
36
36
|
}
|
|
37
37
|
}
|
package/src/gamepad.ts
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { createSignal } from "@solidjs/signals"
|
|
2
|
+
import { on } from "srt:events"
|
|
3
|
+
|
|
4
|
+
// Gamepad State: a reactive mirror of the runtime's sticky "gamepads" event.
|
|
5
|
+
//
|
|
6
|
+
// The runtime coalesces pad activity to at most one snapshot per main-loop
|
|
7
|
+
// iteration and replays the latest one on subscribe, so the first read
|
|
8
|
+
// already sees any connected pads. Runtimes without gamepad support never
|
|
9
|
+
// emit the event and the accessor stays [].
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* One connected gamepad's current state. `buttons` holds the names of the
|
|
13
|
+
* currently-pressed buttons, using SDL3's positional names ("south", "east",
|
|
14
|
+
* "west", "north", "dpadUp", "dpadDown", "dpadLeft", "dpadRight", "start",
|
|
15
|
+
* "back", "guide", "leftShoulder", "rightShoulder", "leftStick",
|
|
16
|
+
* "rightStick"). `axes` has sticks ("leftX", "leftY", "rightX", "rightY") in
|
|
17
|
+
* -1..1 and triggers ("leftTrigger", "rightTrigger") in 0..1.
|
|
18
|
+
*/
|
|
19
|
+
export interface GamepadState {
|
|
20
|
+
/** Runtime instance id: unique per connection, not stable across reconnects. */
|
|
21
|
+
id: number
|
|
22
|
+
name: string
|
|
23
|
+
buttons: string[]
|
|
24
|
+
axes: Record<string, number>
|
|
25
|
+
/**
|
|
26
|
+
* True when the device has an SDL controller-database mapping, so button
|
|
27
|
+
* and axis names reflect verified physical positions. False for raw HID
|
|
28
|
+
* joysticks: they still report, but names are assigned positionally in W3C
|
|
29
|
+
* standard-mapping order (button 0 is "south", ..., overflow "button17"...;
|
|
30
|
+
* axes 0-3 are "leftX"..."rightY", overflow "axis4"...), a d-pad hat folds
|
|
31
|
+
* into the "dpad*" names, and analog triggers, if any, appear wherever the
|
|
32
|
+
* device puts them (e.g. as *button* names "leftTrigger"/"rightTrigger" at
|
|
33
|
+
* indices 6/7) rather than as the trigger axes mapped pads have.
|
|
34
|
+
*/
|
|
35
|
+
mapped: boolean
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
let gamepadsAccessor: (() => (GamepadState | null)[]) | undefined
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Connected gamepads as a reactive accessor. Slots are stable web-style: a
|
|
42
|
+
* pad keeps its index for its whole connection, disconnecting leaves a null
|
|
43
|
+
* hole, and the next connect fills the lowest free slot - so slot index works
|
|
44
|
+
* as a persistent player number. Read inside a tracked scope (JSX, memo,
|
|
45
|
+
* effect, onFrame) to re-run on pad activity.
|
|
46
|
+
*/
|
|
47
|
+
export function gamepads(): (GamepadState | null)[] {
|
|
48
|
+
if (!gamepadsAccessor) {
|
|
49
|
+
// ownedWrite: the sticky event replays synchronously inside on(), which
|
|
50
|
+
// may run within a tracked scope's first read (see environment.ts).
|
|
51
|
+
let [pads, setPads] = createSignal<(GamepadState | null)[]>([], { ownedWrite: true })
|
|
52
|
+
on("gamepads", (e: { pads?: (GamepadState | null)[] }) => setPads(e.pads ?? []))
|
|
53
|
+
gamepadsAccessor = pads
|
|
54
|
+
}
|
|
55
|
+
return gamepadsAccessor()
|
|
56
|
+
}
|
package/src/gpu.ts
CHANGED
|
@@ -19,6 +19,12 @@ import * as gpu from "flux:gpu"
|
|
|
19
19
|
// `<texture params={...}>` when a `<texture>` element is already in the tree.
|
|
20
20
|
export { destroyTexture, setShaderParams, uploadTexture } from "flux:gpu"
|
|
21
21
|
|
|
22
|
+
// Pipeline plumbing re-exported raw: setDrawCount re-renders a pipeline after
|
|
23
|
+
// its buffer gained or lost dynamic geometry; destroyBuffer is the manual
|
|
24
|
+
// cleanup path for buffers created outside a reactive scope.
|
|
25
|
+
export { destroyBuffer, setDrawCount } from "flux:gpu"
|
|
26
|
+
export type { Topology, VertexAttribute } from "flux:gpu"
|
|
27
|
+
|
|
22
28
|
// captureSnapshot renders a node to a texture and readTexture reads any
|
|
23
29
|
// texture's bytes back. Re-exported raw (no reactive auto-cleanup wrapper):
|
|
24
30
|
// captureSnapshot resolves asynchronously, by which point the reactive owner is
|
|
@@ -79,4 +85,71 @@ export function createShader(
|
|
|
79
85
|
let id = gpu.createShader(fragmentSrc, width, height, params, textures)
|
|
80
86
|
if (getOwner()) onCleanup(() => gpu.destroyTexture(id))
|
|
81
87
|
return id
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// View any TypedArray or ArrayBuffer as a Uint8Array over the same memory,
|
|
91
|
+
// without copying, so vertex data can be authored as Float32Array.
|
|
92
|
+
function toUint8(data: ArrayBuffer | ArrayBufferView): Uint8Array {
|
|
93
|
+
if (data instanceof Uint8Array) return data
|
|
94
|
+
if (data instanceof ArrayBuffer) return new Uint8Array(data)
|
|
95
|
+
return new Uint8Array(data.buffer, data.byteOffset, data.byteLength)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Compiles a GLSL ES 3.00 vertex+fragment pipeline and renders it into a
|
|
100
|
+
* texture, returning the texture id (usable anywhere a normal texture id is,
|
|
101
|
+
* e.g. `<texture src>`). Unlike `createShader` the vertex stage is yours:
|
|
102
|
+
* declare `in` attributes matching `opts.attributes` (one interleaved vertex
|
|
103
|
+
* in `opts.buffer`, a {@link createBuffer} id) and your own varyings toward
|
|
104
|
+
* the fragment stage. Both sources may reference `iResolution`/`iTime` and any
|
|
105
|
+
* `uniform float` they declare; drive values with `<texture src={id}
|
|
106
|
+
* params={{...}} />` or `setShaderParams`, exactly like a fragment shader.
|
|
107
|
+
* `opts.depth` attaches a private depth buffer (cleared + tested per render);
|
|
108
|
+
* `opts.vertexCount` defaults to the whole buffer and can be changed later
|
|
109
|
+
* with `setDrawCount`. Frees the texture and GL program when the reactive
|
|
110
|
+
* owner is disposed; create outside any reactive scope for app-lifetime
|
|
111
|
+
* pipelines.
|
|
112
|
+
*/
|
|
113
|
+
export function createPipeline(
|
|
114
|
+
vertexSrc: string,
|
|
115
|
+
fragmentSrc: string,
|
|
116
|
+
width: number,
|
|
117
|
+
height: number,
|
|
118
|
+
opts?: {
|
|
119
|
+
params?: Record<string, number>
|
|
120
|
+
textures?: Record<string, number>
|
|
121
|
+
attributes?: gpu.VertexAttribute[]
|
|
122
|
+
buffer?: number
|
|
123
|
+
topology?: gpu.Topology
|
|
124
|
+
vertexCount?: number
|
|
125
|
+
depth?: boolean
|
|
126
|
+
clearColor?: [number, number, number, number]
|
|
127
|
+
},
|
|
128
|
+
): number {
|
|
129
|
+
let id = gpu.createPipeline(vertexSrc, fragmentSrc, width, height, opts)
|
|
130
|
+
if (getOwner()) onCleanup(() => gpu.destroyTexture(id))
|
|
131
|
+
return id
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Creates a vertex buffer for pipeline attributes from raw data (typically a
|
|
136
|
+
* Float32Array laid out to match the pipeline's interleaved attribute list).
|
|
137
|
+
* Update it later with {@link writeBuffer}; the buffer's byte size is fixed at
|
|
138
|
+
* creation, so reserve room up front for dynamic geometry. Freed automatically
|
|
139
|
+
* when the reactive owner is disposed; created outside a reactive scope you
|
|
140
|
+
* must call `destroyBuffer` yourself. Destroy pipelines before their buffer.
|
|
141
|
+
*/
|
|
142
|
+
export function createBuffer(data: ArrayBuffer | ArrayBufferView): number {
|
|
143
|
+
let id = gpu.createBuffer(toUint8(data))
|
|
144
|
+
if (getOwner()) onCleanup(() => gpu.destroyBuffer(id))
|
|
145
|
+
return id
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Overwrites part of a vertex buffer at `byteOffset` (default 0). Every
|
|
150
|
+
* pipeline drawing from the buffer re-renders with its last-applied params,
|
|
151
|
+
* so geometry-only changes reach the screen without a params update.
|
|
152
|
+
*/
|
|
153
|
+
export function writeBuffer(id: number, data: ArrayBuffer | ArrayBufferView, byteOffset?: number): void {
|
|
154
|
+
gpu.writeBuffer(id, toUint8(data), byteOffset)
|
|
82
155
|
}
|
package/src/index.ts
CHANGED
|
@@ -8,6 +8,8 @@ export { setPointerCapture, releasePointerCapture } from "./window"
|
|
|
8
8
|
export { windowSize, safeArea, displayScale, windowFocused, keyboardHeight } from "./window"
|
|
9
9
|
export { env } from "./environment"
|
|
10
10
|
export type { InputDevices, SystemTheme, Orientation } from "./environment"
|
|
11
|
+
export { gamepads } from "./gamepad"
|
|
12
|
+
export type { GamepadState } from "./gamepad"
|
|
11
13
|
export { capabilities } from "./capabilities"
|
|
12
14
|
export type { Capabilities, WindowSizeClass } from "./capabilities"
|
|
13
15
|
export { createTexture } from "./gpu"
|
package/src/renderer.ts
CHANGED
|
@@ -87,6 +87,34 @@ function removeNode(parent: ProxyNode, node: ProxyNode): void {
|
|
|
87
87
|
}
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
+
// Applies a single prop to a node: routes events to the handler registry,
|
|
91
|
+
// parses color strings/gradients, and forwards everything else to the tree.
|
|
92
|
+
// Shared by the renderer's setProperty hook and by createElement, which since
|
|
93
|
+
// the dom-expressions "universal" template passes static props inline as a
|
|
94
|
+
// second argument rather than as separate setProp calls.
|
|
95
|
+
function applyProp<T>(node: ProxyNode, name: string, value: T): void {
|
|
96
|
+
if (!node) return
|
|
97
|
+
|
|
98
|
+
// console.debug("[srt] applyProp", node.id, name, value)
|
|
99
|
+
|
|
100
|
+
if (/^on[A-Z]/.test(name) && (value == null || typeof value === "function")) {
|
|
101
|
+
setEventHandler(node.id, name, value as Function | null | undefined)
|
|
102
|
+
return
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (name === "color" && isGradient(value)) {
|
|
106
|
+
tree.setProperty(node.id, name, value)
|
|
107
|
+
return
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (name === "color" && typeof value === "string") {
|
|
111
|
+
tree.setProperty(node.id, name, parseColor(value))
|
|
112
|
+
return
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
tree.setProperty(node.id, name, value)
|
|
116
|
+
}
|
|
117
|
+
|
|
90
118
|
export let {
|
|
91
119
|
effect,
|
|
92
120
|
memo,
|
|
@@ -101,7 +129,7 @@ export let {
|
|
|
101
129
|
applyRef,
|
|
102
130
|
ref,
|
|
103
131
|
} = createRenderer<ProxyNode>({
|
|
104
|
-
createElement: (elementType: string): ProxyNode => {
|
|
132
|
+
createElement: (elementType: string, props?: Record<string, any>): ProxyNode => {
|
|
105
133
|
let proxy = createProxyNode(elementType)
|
|
106
134
|
|
|
107
135
|
// console.debug("[srt] createElement", proxy.id, elementType)
|
|
@@ -109,6 +137,15 @@ export let {
|
|
|
109
137
|
if (elementType === "window") tree.createRoot(proxy.id)
|
|
110
138
|
else tree.createNode(proxy.id, elementType)
|
|
111
139
|
|
|
140
|
+
// The universal JSX template hands static props here as an object; children
|
|
141
|
+
// and ref arrive through their own hooks, so skip them.
|
|
142
|
+
if (props) {
|
|
143
|
+
for (let name in props) {
|
|
144
|
+
if (name === "children" || name === "ref") continue
|
|
145
|
+
applyProp(proxy, name, props[name])
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
112
149
|
return proxy
|
|
113
150
|
},
|
|
114
151
|
|
|
@@ -127,26 +164,8 @@ export let {
|
|
|
127
164
|
|
|
128
165
|
isTextNode: (node: ProxyNode): boolean => node?.elementType === "d-span",
|
|
129
166
|
setProperty: <T>(node: ProxyNode, name: string, value: T): void => {
|
|
130
|
-
if (!node) return
|
|
131
|
-
|
|
132
167
|
// console.debug("[srt] setProperty", node.id, name, value)
|
|
133
|
-
|
|
134
|
-
if (/^on[A-Z]/.test(name) && (value == null || typeof value === "function")) {
|
|
135
|
-
setEventHandler(node.id, name, value as Function | null | undefined)
|
|
136
|
-
return
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
if (name === "color" && isGradient(value)) {
|
|
140
|
-
tree.setProperty(node.id, name, value)
|
|
141
|
-
return
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
if (name === "color" && typeof value === "string") {
|
|
145
|
-
tree.setProperty(node.id, name, parseColor(value))
|
|
146
|
-
return
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
tree.setProperty(node.id, name, value)
|
|
168
|
+
applyProp(node, name, value)
|
|
150
169
|
},
|
|
151
170
|
|
|
152
171
|
insertNode: (parent: ProxyNode, node: ProxyNode, anchor?: ProxyNode): void => {
|
package/src/runtime-modules.d.ts
CHANGED
|
@@ -40,6 +40,15 @@ declare module "srt:dev" {
|
|
|
40
40
|
export function connect(address: string): void
|
|
41
41
|
export function discover(): void
|
|
42
42
|
export function stop(): void
|
|
43
|
+
/**
|
|
44
|
+
* Register a named debug command, listable and callable from the dev server
|
|
45
|
+
* (the list_debug / call_debug MCP tools). `args` arrives JSON-parsed; the
|
|
46
|
+
* return value must be JSON-serializable and synchronous (promises are not
|
|
47
|
+
* awaited). Re-registering a name replaces it; registrations reset on hot
|
|
48
|
+
* reload, so register at module init. Callable in every build, but only dev
|
|
49
|
+
* clients ever invoke commands.
|
|
50
|
+
*/
|
|
51
|
+
export function registerDebug(name: string, fn: (args?: any) => unknown): void
|
|
43
52
|
}
|
|
44
53
|
|
|
45
54
|
// Frame draw (lattice runner). renderFrame() synchronously renders the current
|