@solidrt/core 0.0.33 → 0.0.35

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@solidrt/core",
3
- "version": "0.0.33",
3
+ "version": "0.0.35",
4
4
  "license": "MIT",
5
5
  "author": "Antoine van Wel",
6
6
  "type": "module",
@@ -27,7 +27,7 @@
27
27
  "colord": "^2.9.3"
28
28
  },
29
29
  "devDependencies": {
30
- "@solidrt/flux-types": "0.0.33"
30
+ "@solidrt/flux-types": "0.0.35"
31
31
  },
32
32
  "peerDependencies": {
33
33
  "@solidjs/signals": "2.0.0-beta.20",
package/src/gpu.ts CHANGED
@@ -8,16 +8,35 @@
8
8
  // sampler2D input. The imperative primitives (uploadTexture, setShaderParams,
9
9
  // destroyTexture, ...) live in the `flux:gpu` module.
10
10
 
11
- import { getOwner, onCleanup } from "@solidjs/signals"
11
+ import { createEffect, createSignal, getOwner, onCleanup, untrack } from "@solidjs/signals"
12
12
  import * as gpu from "flux:gpu"
13
13
 
14
+ // The create* helpers accept { manual: true } to opt out of the owner-scoped
15
+ // auto-free, for resources whose lifetime is managed by hand (rebuilt on
16
+ // signal changes inside a long-lived component, handed across owners, ...).
17
+ // Without it, each rebuild would stack another onCleanup on the component
18
+ // owner: a leak until unmount, then a double-free against manual destroys.
19
+ export type CreateOptions = { manual?: boolean }
20
+
14
21
  // Re-exported so callers that depend on @solidrt/core -- like @solidrt/components
15
22
  // -- need not import flux directly: destroyTexture for the manual-cleanup path
16
23
  // (textures made outside a reactive scope, e.g. after an await, are not
17
24
  // auto-freed), uploadTexture to push new pixels into a mutable texture, and
18
25
  // setShaderParams as the non-reactive exception described above - prefer
19
26
  // `<texture params={...}>` when a `<texture>` element is already in the tree.
20
- export { destroyTexture, setShaderParams, uploadTexture } from "flux:gpu"
27
+ // resizeTexture and setShaderSize resize in place at a stable id (so
28
+ // `<texture src>` and sampler bindings stay valid); because the id survives,
29
+ // the owner-scoped auto-free registered at creation keeps working and no
30
+ // re-registration is needed. setShaderTextures is the sampler analog of
31
+ // setShaderParams: retarget a shader's sampler2D inputs without recompiling.
32
+ export {
33
+ destroyTexture,
34
+ resizeTexture,
35
+ setShaderParams,
36
+ setShaderSize,
37
+ setShaderTextures,
38
+ uploadTexture,
39
+ } from "flux:gpu"
21
40
 
22
41
  // Pipeline plumbing re-exported raw: setDrawCount re-renders a pipeline after
23
42
  // its buffer gained or lost dynamic geometry; destroyBuffer is the manual
@@ -40,11 +59,12 @@ export { captureSnapshot, readTexture } from "flux:gpu"
40
59
  * texture is freed automatically once that owner is disposed; when called
41
60
  * outside one (e.g. after an `await`, where the owner is no longer current)
42
61
  * nothing is registered and you must call `destroyTexture` (from flux:gpu)
43
- * yourself.
62
+ * yourself. Pass `{ manual: true }` to skip the auto-free and own the
63
+ * disposal yourself even inside a reactive scope.
44
64
  */
45
- export function createTexture(data: Uint8Array, width: number, height: number): number {
65
+ export function createTexture(data: Uint8Array, width: number, height: number, opts?: CreateOptions): number {
46
66
  let id = gpu.createTexture(data, width, height)
47
- if (getOwner()) onCleanup(() => gpu.destroyTexture(id))
67
+ if (!opts?.manual && getOwner()) onCleanup(() => gpu.destroyTexture(id))
48
68
  return id
49
69
  }
50
70
 
@@ -53,12 +73,13 @@ export function createTexture(data: Uint8Array, width: number, height: number):
53
73
  * then call `uploadTexture(id, data)` (from flux:gpu) to push new pixels. `data`
54
74
  * is RGBA8 and must hold at least `width * height * 4` bytes (it may hold several
55
75
  * frames). Like `createTexture`, the texture is freed automatically when the
56
- * reactive owner is disposed; created outside a reactive scope you must call
57
- * `destroyTexture` (from flux:gpu) yourself.
76
+ * reactive owner is disposed (opt out with `{ manual: true }`); created
77
+ * outside a reactive scope you must call `destroyTexture` (from flux:gpu)
78
+ * yourself.
58
79
  */
59
- export function createMutableTexture(data: Uint8Array, width: number, height: number): number {
80
+ export function createMutableTexture(data: Uint8Array, width: number, height: number, opts?: CreateOptions): number {
60
81
  let id = gpu.createMutableTexture(data, width, height)
61
- if (getOwner()) onCleanup(() => gpu.destroyTexture(id))
82
+ if (!opts?.manual && getOwner()) onCleanup(() => gpu.destroyTexture(id))
62
83
  return id
63
84
  }
64
85
 
@@ -72,8 +93,10 @@ export function createMutableTexture(data: Uint8Array, width: number, height: nu
72
93
  * `textures` binds each declared `uniform sampler2D` to an existing texture id
73
94
  * (e.g. a camera or decoded image) so the shader can read it; those inputs are
74
95
  * re-sampled on every params update, so live sources stay current. Frees the
75
- * texture and shader program when the reactive owner is disposed; create
76
- * outside any reactive scope for app-lifetime shaders.
96
+ * texture and shader program when the reactive owner is disposed (opt out
97
+ * with `{ manual: true }`); create outside any reactive scope for
98
+ * app-lifetime shaders. For a shader whose source or inputs change
99
+ * reactively, use {@link createShaderMemo} instead.
77
100
  */
78
101
  export function createShader(
79
102
  fragmentSrc: string,
@@ -81,9 +104,67 @@ export function createShader(
81
104
  height: number,
82
105
  params?: Record<string, number>,
83
106
  textures?: Record<string, number>,
107
+ opts?: CreateOptions,
84
108
  ): number {
85
109
  let id = gpu.createShader(fragmentSrc, width, height, params, textures)
86
- if (getOwner()) onCleanup(() => gpu.destroyTexture(id))
110
+ if (!opts?.manual && getOwner()) onCleanup(() => gpu.destroyTexture(id))
111
+ return id
112
+ }
113
+
114
+ /** The reactive shader description `createShaderMemo` builds from. */
115
+ export type ShaderSpec = {
116
+ fragmentSrc: string
117
+ width: number
118
+ height: number
119
+ params?: Record<string, number>
120
+ textures?: Record<string, number>
121
+ }
122
+
123
+ // Shallow name->number equality for params/textures records; treats undefined
124
+ // as the empty record.
125
+ function sameRecord(a: Record<string, number> | undefined, b: Record<string, number> | undefined): boolean {
126
+ if (a === b) return true
127
+ let ka = a ? Object.keys(a) : []
128
+ let kb = b ? Object.keys(b) : []
129
+ return ka.length === kb.length && ka.every(k => a![k] === b![k])
130
+ }
131
+
132
+ /**
133
+ * A fragment shader whose spec is reactive: returns an accessor for the
134
+ * current texture id (use it as `<texture src={id()} />`) and keeps the GPU
135
+ * resource in step with `spec` from then on. Changes that keep the compiled
136
+ * program valid mutate in place at a stable id - a size change routes to
137
+ * `setShaderSize`, a params change to `setShaderParams` - while a change to
138
+ * the fragment source or the sampler bindings rebuilds at a fresh id, updates
139
+ * the accessor, and destroys the old id. That destroy is frame-safe (the
140
+ * runtime reclaims an id only once the render tree no longer references it),
141
+ * so the swap never paints a blank frame. The current id is freed when the
142
+ * owning scope is disposed. Data textures need no analog: `uploadTexture` and
143
+ * `resizeTexture` already cover their reactive changes id-stably.
144
+ */
145
+ export function createShaderMemo(spec: () => ShaderSpec): () => number {
146
+ let current = untrack(spec)
147
+ let currentId = gpu.createShader(current.fragmentSrc, current.width, current.height, current.params, current.textures)
148
+ let [id, setId] = createSignal(currentId)
149
+ createEffect(spec, next => {
150
+ if (next.fragmentSrc === current.fragmentSrc && sameRecord(next.textures, current.textures)) {
151
+ // Program and inputs unchanged: mutate in place, the id stays stable.
152
+ if (next.width !== current.width || next.height !== current.height) {
153
+ gpu.setShaderSize(currentId, next.width, next.height)
154
+ }
155
+ if (!sameRecord(next.params, current.params) && next.params) {
156
+ gpu.setShaderParams(currentId, next.params)
157
+ }
158
+ current = next
159
+ return
160
+ }
161
+ let old = currentId
162
+ current = next
163
+ currentId = gpu.createShader(next.fragmentSrc, next.width, next.height, next.params, next.textures)
164
+ setId(currentId)
165
+ gpu.destroyTexture(old)
166
+ })
167
+ if (getOwner()) onCleanup(() => gpu.destroyTexture(currentId))
87
168
  return id
88
169
  }
89
170
 
@@ -107,8 +188,8 @@ function toUint8(data: ArrayBuffer | ArrayBufferView): Uint8Array {
107
188
  * `opts.depth` attaches a private depth buffer (cleared + tested per render);
108
189
  * `opts.vertexCount` defaults to the whole buffer and can be changed later
109
190
  * 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.
191
+ * owner is disposed (opt out with `opts.manual`); create outside any reactive
192
+ * scope for app-lifetime pipelines.
112
193
  */
113
194
  export function createPipeline(
114
195
  vertexSrc: string,
@@ -124,10 +205,10 @@ export function createPipeline(
124
205
  vertexCount?: number
125
206
  depth?: boolean
126
207
  clearColor?: [number, number, number, number]
127
- },
208
+ } & CreateOptions,
128
209
  ): number {
129
210
  let id = gpu.createPipeline(vertexSrc, fragmentSrc, width, height, opts)
130
- if (getOwner()) onCleanup(() => gpu.destroyTexture(id))
211
+ if (!opts?.manual && getOwner()) onCleanup(() => gpu.destroyTexture(id))
131
212
  return id
132
213
  }
133
214
 
@@ -136,12 +217,13 @@ export function createPipeline(
136
217
  * Float32Array laid out to match the pipeline's interleaved attribute list).
137
218
  * Update it later with {@link writeBuffer}; the buffer's byte size is fixed at
138
219
  * 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.
220
+ * when the reactive owner is disposed (opt out with `{ manual: true }`);
221
+ * created outside a reactive scope you must call `destroyBuffer` yourself.
222
+ * Destroy pipelines before their buffer.
141
223
  */
142
- export function createBuffer(data: ArrayBuffer | ArrayBufferView): number {
224
+ export function createBuffer(data: ArrayBuffer | ArrayBufferView, opts?: CreateOptions): number {
143
225
  let id = gpu.createBuffer(toUint8(data))
144
- if (getOwner()) onCleanup(() => gpu.destroyBuffer(id))
226
+ if (!opts?.manual && getOwner()) onCleanup(() => gpu.destroyBuffer(id))
145
227
  return id
146
228
  }
147
229
 
@@ -51,6 +51,57 @@ declare module "srt:dev" {
51
51
  export function registerDebug(name: string, fn: (args?: any) => unknown): void
52
52
  }
53
53
 
54
+ // Installed-app management (lattice), the launcher's surface over the client's
55
+ // version store. Present only in go/dev client builds; elsewhere `available`
56
+ // is false, `list` returns [], and launch/remove are no-ops.
57
+ declare module "srt:apps" {
58
+ export const available: boolean
59
+ /**
60
+ * An installed app: id, display name (the installed manifest's displayName,
61
+ * defaulting to the id) and current version id (manifest hash).
62
+ */
63
+ export type InstalledApp = { id: string; name: string; version: string }
64
+ /** Installed apps, sorted by name. */
65
+ export function list(): InstalledApp[]
66
+ /** A stored version: id (manifest hash), bytes on disk, whether it is the current one. */
67
+ export type AppVersion = { id: string; size: number; current: boolean }
68
+ /** One file in a listing: a relative path and its size in bytes. */
69
+ export type AppFile = { path: string; size: number }
70
+ /**
71
+ * Usage details for one installed app: total bytes of its stored versions
72
+ * (assets shared between versions via hardlinks count in each) and of its
73
+ * data sandbox, plus the stored versions (current first, then newest first)
74
+ * and three file listings, each sorted by path. `assets` is the current
75
+ * version's manifest claim (declared sizes); `files` and `data` are disk
76
+ * walks of the current version dir and the data sandbox - the truth, so a
77
+ * divergence from the manifest is visible.
78
+ */
79
+ export type AppInfo = {
80
+ id: string
81
+ name: string
82
+ version: string
83
+ installSize: number
84
+ dataSize: number
85
+ versions: AppVersion[]
86
+ assets: AppFile[]
87
+ files: AppFile[]
88
+ data: AppFile[]
89
+ }
90
+ /** Usage details for an installed app. Throws when the app is not installed. */
91
+ export function info(id: string): AppInfo
92
+ /**
93
+ * Boot the app's installed current version, replacing the running app (the
94
+ * launcher). Throws when the app is not installed. Custom fonts of the
95
+ * launched app register at client startup only, not mid-session.
96
+ */
97
+ export function launch(id: string): void
98
+ /**
99
+ * Full uninstall: the app's versions, state and data sandbox. Throws when
100
+ * the app is not installed.
101
+ */
102
+ export function remove(id: string): void
103
+ }
104
+
54
105
  // Frame draw (lattice runner). renderFrame() synchronously renders the current
55
106
  // frame: layout, the postLayout hook, paint and hover refresh, then builds and
56
107
  // submits the display list. To schedule a future frame instead, use