@solidrt/2d 0.0.70 → 0.0.72
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 +222 -40
- package/README.md +7 -3
- package/examples/README.md +1 -1
- package/examples/camera-probe.tsx +4 -4
- package/examples/markers.tsx +3 -3
- package/package.json +5 -4
- package/src/animation.ts +26 -5
- package/src/atlas.ts +8 -4
- package/src/camera.ts +2 -2
- package/src/components/camera2d.tsx +2 -2
- package/src/components/group.tsx +15 -3
- package/src/components/shots.tsx +2 -2
- package/src/components/sprite-layer.tsx +40 -12
- package/src/components/sprite.tsx +31 -3
- package/src/components/text2d.tsx +2 -2
- package/src/components/tile-layer.tsx +21 -7
- package/src/components/view2d.tsx +2 -2
- package/src/dispatch.ts +10 -6
- package/src/frames.ts +1 -1
- package/src/glsl.ts +399 -0
- package/src/index.ts +14 -6
- package/src/layer.ts +260 -113
- package/src/material.ts +368 -0
- package/src/records.ts +198 -104
- package/src/style.ts +98 -0
- package/src/text.ts +64 -11
- package/src/tiles.ts +119 -24
- package/src/views.ts +17 -9
- package/src/shaders.ts +0 -323
package/src/records.ts
CHANGED
|
@@ -5,20 +5,25 @@
|
|
|
5
5
|
// algorithm, later over earlier; `orderBy` swaps that for a core-produced
|
|
6
6
|
// key order at publish, records untouched); mutations batch, and the flush
|
|
7
7
|
// (ahead of the frame's paint, or at a microtask outside a frame)
|
|
8
|
-
// publishes what changed
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
8
|
+
// publishes what changed through the core's record stream
|
|
9
|
+
// (`createRecordStream` in @solidrt/core/gpu, the publish model every
|
|
10
|
+
// JS-stepped population shares): the dirty record range as one buffer
|
|
11
|
+
// write, or under `orderBy` the drawn prefix whole through the zero-copy
|
|
12
|
+
// write lease (the core gathers it into key order during the copy, and a
|
|
13
|
+
// byte range has no stable position under a permutation). A moved sprite
|
|
14
|
+
// is 20 float stores plus its share of one memcpy per dirty frame; a
|
|
15
|
+
// static layer publishes nothing and therefore costs nothing.
|
|
14
16
|
//
|
|
15
17
|
// The record vocabulary is @solidrt/3d's record mesh's, one dimension
|
|
16
|
-
// down: `records(layer)` is the mirror
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
//
|
|
18
|
+
// down: `records(layer, stream?)` is the mirror (stream 0 the layer's
|
|
19
|
+
// own record, 1.. the material's style streams), `updateRecords(layer, {
|
|
20
|
+
// stream?, first?, count? })` publishes a range, `setRecordCount(layer,
|
|
21
|
+
// n)` dials the drawn prefix, `setInstanceStyle(layer, sprite, values)`
|
|
22
|
+
// and `instanceAttribute(layer, name)` write the material's per-sprite
|
|
23
|
+
// style record (style.ts). The POPULATION differs by kind and keeps its
|
|
24
|
+
// own verbs - a record mesh's records are opaque data (setRecords), a
|
|
25
|
+
// record layer's are sprites (addSprite/destroySprite) - so the copy-in
|
|
26
|
+
// here is `records(layer).set(src)` followed by updateRecords.
|
|
22
27
|
//
|
|
23
28
|
// This is NOT the default live layer - that is layer.ts, where sprites are
|
|
24
29
|
// spatial arena nodes core producers can reach. Use this when a JS loop
|
|
@@ -32,15 +37,18 @@
|
|
|
32
37
|
// (addSprite/setSprite/...) are shared with the node layer; picking here is
|
|
33
38
|
// the JS reverse walk (pointInSprite), since records have no nodes.
|
|
34
39
|
import { getOwner, onBeforeRender, onCleanup, runWithOwner } from "@solidrt/core"
|
|
35
|
-
import {
|
|
40
|
+
import { checkScreenSize, createRecordStream, destroyBuffer, screenSizeScale } from "@solidrt/core/gpu"
|
|
36
41
|
import type { BufferId } from "@solidrt/core/gpu"
|
|
42
|
+
import type { AttributeAccess, ShaderParams } from "@solidrt/core/gpu"
|
|
37
43
|
import { checkAtlases, frameIndex } from "./atlas.ts"
|
|
38
44
|
import type { Atlas } from "./atlas.ts"
|
|
39
45
|
import { fullFrame, isFrame, writeFrame } from "./frames.ts"
|
|
40
|
-
import {
|
|
41
|
-
import
|
|
46
|
+
import { atlasBindings, RECORD_ATTRIBUTES } from "./glsl.ts"
|
|
47
|
+
import { checkParams, checkTint, readFrame } from "./layer.ts"
|
|
48
|
+
import type { LayerBase, Sprite, SpriteLayer, SpriteLayerOptions, SpriteOptions, SpriteState } from "./layer.ts"
|
|
49
|
+
import { materialSamplers, unlit } from "./material.ts"
|
|
42
50
|
import { pointInSprite } from "./pick.ts"
|
|
43
|
-
import {
|
|
51
|
+
import { blankStyle, createStyleStreams, growStyle, shiftStyle, writeStyle } from "./style.ts"
|
|
44
52
|
import { createViews } from "./views.ts"
|
|
45
53
|
|
|
46
54
|
// Floats per instance record:
|
|
@@ -83,10 +91,6 @@ export type RecordLayerOptions = Omit<SpriteLayerOptions, "orderBy"> & {
|
|
|
83
91
|
}
|
|
84
92
|
|
|
85
93
|
export type RecordLayer = LayerBase & {
|
|
86
|
-
/** The record mirror; read it through records(). Replaced by growth. */
|
|
87
|
-
_records: Float32Array
|
|
88
|
-
/** The record range [lo, hi) written since the last publish, or null. */
|
|
89
|
-
_dirty: [number, number] | null
|
|
90
94
|
/** The count dial (setRecordCount): how many leading sprites draw and
|
|
91
95
|
* pick; Infinity (the identity of the min against the live count)
|
|
92
96
|
* until a dial is set. */
|
|
@@ -94,48 +98,129 @@ export type RecordLayer = LayerBase & {
|
|
|
94
98
|
_order: SpriteState[]
|
|
95
99
|
}
|
|
96
100
|
|
|
97
|
-
/** Options of `updateRecords`: the
|
|
98
|
-
*
|
|
99
|
-
|
|
101
|
+
/** Options of `updateRecords`: the stream (default 0, the layer's own
|
|
102
|
+
* record; 1.. the material's style streams in its instanceBuffers
|
|
103
|
+
* order) and the record range (default the whole mirror, capacity
|
|
104
|
+
* wide). */
|
|
105
|
+
export type UpdateRecordsOptions = { stream?: number; first?: number; count?: number }
|
|
100
106
|
|
|
101
107
|
/**
|
|
102
|
-
* The layer's record mirror - the raw power path.
|
|
103
|
-
*
|
|
104
|
-
*
|
|
105
|
-
*
|
|
106
|
-
*
|
|
107
|
-
*
|
|
108
|
-
*
|
|
108
|
+
* The layer's record mirror - the raw power path. Stream 0 (the default)
|
|
109
|
+
* is the layer's own record; stream 1.. are the material's style
|
|
110
|
+
* streams in its `instanceBuffers` order, each a Float32Array over an
|
|
111
|
+
* all-float layout and bytes otherwise (write those through
|
|
112
|
+
* instanceAttribute's codecs, or a typed view of your own over the
|
|
113
|
+
* format), slot-indexed like the layer's own, blank (the material's
|
|
114
|
+
* instanceStyle) until written. On a records layer the
|
|
115
|
+
* layout per sprite is INSTANCE_FLOATS floats: [cx, cy, w, h, u0, v0, u1,
|
|
116
|
+
* v1, rot, tintR, tintG, tintB, tintA, minScreenPx, maxScreenPx, atlas,
|
|
117
|
+
* outlineR, outlineG, outlineB, outlineWidth], record i at i *
|
|
118
|
+
* INSTANCE_FLOATS; `atlas` is the frame's texture as its index in the
|
|
119
|
+
* layer's `atlases` list, and the outline is read only by a
|
|
120
|
+
* distance-field atlas (see `Atlas.sdf`): rgb in 0..1 and a width in
|
|
121
|
+
* world pixels, zero for none. Record order is draw order - unless the
|
|
109
122
|
* layer was created with `orderBy`, which draws in key order while record
|
|
110
|
-
* i keeps meaning sprite i.
|
|
111
|
-
*
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
-
*
|
|
123
|
+
* i keeps meaning sprite i. Do not cache indices across destroySprite -
|
|
124
|
+
* records shift.
|
|
125
|
+
*
|
|
126
|
+
* On a node layer (createSpriteLayer) the records are its STYLE records,
|
|
127
|
+
* SPRITE_FLOATS floats per sprite slot: [u0, v0, u1, v1, tintR, tintG,
|
|
128
|
+
* tintB, tintA, renderOrder, minScreenPx, maxScreenPx, atlas, outlineR,
|
|
129
|
+
* outlineG, outlineB, outlineWidth], slot i at i * SPRITE_FLOATS (a
|
|
130
|
+
* sprite's slot is fixed for its life; a freed slot recycles) - the pose
|
|
131
|
+
* lives in the core and is written through setSprite. The 3d
|
|
132
|
+
* `records(instancedMesh)`, one dimension down: a bulk restyle (a palette
|
|
133
|
+
* cycle over thousands of sprites) is one loop over this and one
|
|
134
|
+
* updateRecords.
|
|
135
|
+
*
|
|
136
|
+
* Write fields directly for large per-frame populations, then
|
|
137
|
+
* updateRecords the range. Read it AT USE TIME: an add past capacity
|
|
138
|
+
* replaces the array, and a hoisted reference becomes a dead copy whose
|
|
139
|
+
* writes publish nothing. @solidrt/3d's records(mesh).
|
|
115
140
|
*/
|
|
116
|
-
export function records(layer: RecordLayer): Float32Array
|
|
117
|
-
|
|
141
|
+
export function records(layer: RecordLayer | SpriteLayer): Float32Array
|
|
142
|
+
export function records(layer: RecordLayer | SpriteLayer, stream: number): ArrayBufferView
|
|
143
|
+
export function records(layer: RecordLayer | SpriteLayer, stream = 0): ArrayBufferView {
|
|
144
|
+
if (stream === 0) return layer._records
|
|
145
|
+
return styleStream("records", layer, stream).data
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// The material style stream at index `stream` (1-based after the layer's
|
|
149
|
+
// own record), or a throw naming the range.
|
|
150
|
+
function styleStream(verb: string, layer: RecordLayer | SpriteLayer, stream: number) {
|
|
151
|
+
let s = layer._styles[stream - 1]
|
|
152
|
+
if (!Number.isInteger(stream) || stream < 1 || s === undefined) {
|
|
153
|
+
throw new Error(`${verb}: stream ${stream} is out of range; the layer has record streams 0..${layer._styles.length} (0 its own record, then the material's instance buffers)`)
|
|
154
|
+
}
|
|
155
|
+
return s
|
|
118
156
|
}
|
|
119
157
|
|
|
120
158
|
/**
|
|
121
|
-
*
|
|
159
|
+
* Write a sprite's style record - the material's per-sprite data, its
|
|
160
|
+
* FIRST instance buffer (any format; `values` one per component, in
|
|
161
|
+
* attribute order, as the shader sees them: a unorm8x4 tint as 0..1) -
|
|
162
|
+
* marking the slot for the next flush; a frame-rate path like
|
|
163
|
+
* setSprite, any number of sprites restyled between two frames
|
|
164
|
+
* costing one coalesced buffer write. Throws on a material without an
|
|
165
|
+
* instance buffer or a record of the wrong length; a no-op on a
|
|
166
|
+
* destroyed sprite. instanceAttribute + updateRecords is the same
|
|
167
|
+
* write by attribute name, and the only way into a second stream.
|
|
168
|
+
* @solidrt/3d's setInstanceStyle(instance, values).
|
|
169
|
+
*/
|
|
170
|
+
export function setInstanceStyle(layer: RecordLayer | SpriteLayer, sprite: Sprite, values: ArrayLike<number>): void {
|
|
171
|
+
if (sprite.layer !== layer) {
|
|
172
|
+
if (sprite.layer === null) return
|
|
173
|
+
throw new Error("setInstanceStyle: the sprite belongs to another layer")
|
|
174
|
+
}
|
|
175
|
+
let style = layer._styles[0]
|
|
176
|
+
if (style === undefined) throw new Error("setInstanceStyle: the layer's material declares no instance buffer (no style record)")
|
|
177
|
+
writeStyle(style, sprite._slot, values, "setInstanceStyle")
|
|
178
|
+
layer._schedule()
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* The accessor for the material's per-sprite attribute `name` over the
|
|
183
|
+
* layer's style streams: slot in (a sprite's `_slot`), component values
|
|
184
|
+
* as the shader sees them (a unorm8x4 reads and writes as 0..1),
|
|
185
|
+
* encoded through the format's codec. Write through it, then
|
|
186
|
+
* updateRecords the range of that stream; the accessor stays valid
|
|
187
|
+
* across growth. Null when no stream carries the name (the layer's own
|
|
188
|
+
* record fields are records(layer)'s, not reachable here).
|
|
189
|
+
* @solidrt/3d's instanceAttribute(mesh, name).
|
|
190
|
+
*/
|
|
191
|
+
export function instanceAttribute(layer: RecordLayer | SpriteLayer, name: string): AttributeAccess | null {
|
|
192
|
+
for (let s of layer._styles) {
|
|
193
|
+
let i = s.layout.findIndex(a => a.name === name)
|
|
194
|
+
if (i >= 0) return s.fields[i]!
|
|
195
|
+
}
|
|
196
|
+
return null
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Publish records [first, first + count) of a mirror (`stream` 0 the
|
|
201
|
+
* layer's own record, 1.. the material's style streams) at the layer's
|
|
122
202
|
* next flush - the partial rewrite a population stepped in JS wants (ten
|
|
123
203
|
* moved records of ten thousand cost ten); the whole mirror by default.
|
|
124
204
|
* Write the mirror first, through records(), then call this. The range
|
|
125
205
|
* is against the mirror's capacity, not the live sprites: a record past
|
|
126
|
-
* the last live sprite
|
|
127
|
-
*
|
|
128
|
-
*
|
|
129
|
-
* permutation. @solidrt/3d's
|
|
206
|
+
* the last live sprite (on a node layer, past the highest slot in use)
|
|
207
|
+
* is accepted and never drawn. Under `orderBy` any range republishes the
|
|
208
|
+
* drawn prefix whole - the core gathers it into key order, and a byte
|
|
209
|
+
* range has no stable position under a permutation. @solidrt/3d's
|
|
210
|
+
* updateRecords(mesh).
|
|
130
211
|
*/
|
|
131
|
-
export function updateRecords(layer: RecordLayer, options: UpdateRecordsOptions = {}): void {
|
|
132
|
-
let
|
|
212
|
+
export function updateRecords(layer: RecordLayer | SpriteLayer, options: UpdateRecordsOptions = {}): void {
|
|
213
|
+
let index = options.stream ?? 0
|
|
214
|
+
let stream = index === 0 ? layer._stream : styleStream("updateRecords", layer, index).stream
|
|
215
|
+
let capacity = stream.capacity
|
|
133
216
|
let first = options.first ?? 0
|
|
134
217
|
let count = options.count ?? capacity - first
|
|
135
218
|
if (!Number.isInteger(first) || !Number.isInteger(count) || first < 0 || count < 0 || first + count > capacity) {
|
|
136
219
|
throw new Error("updateRecords: range [" + first + ", " + (first + count) + ") is outside the layer's " + capacity + " records")
|
|
137
220
|
}
|
|
138
|
-
if (count
|
|
221
|
+
if (count === 0) return
|
|
222
|
+
stream.mark(first, first + count)
|
|
223
|
+
layer._schedule()
|
|
139
224
|
}
|
|
140
225
|
|
|
141
226
|
/**
|
|
@@ -154,14 +239,10 @@ export function setRecordCount(layer: RecordLayer, count: number): void {
|
|
|
154
239
|
layer._schedule()
|
|
155
240
|
}
|
|
156
241
|
|
|
157
|
-
//
|
|
158
|
-
//
|
|
159
|
-
function markRecords(layer: RecordLayer, lo: number, hi: number): void {
|
|
160
|
-
|
|
161
|
-
else {
|
|
162
|
-
if (lo < layer._dirty[0]) layer._dirty[0] = lo
|
|
163
|
-
if (hi > layer._dirty[1]) layer._dirty[1] = hi
|
|
164
|
-
}
|
|
242
|
+
// Mark records [lo, hi) of the layer's stream and schedule the flush that
|
|
243
|
+
// publishes them.
|
|
244
|
+
function markRecords(layer: RecordLayer | SpriteLayer, lo: number, hi: number): void {
|
|
245
|
+
layer._stream.mark(lo, hi)
|
|
165
246
|
layer._schedule()
|
|
166
247
|
}
|
|
167
248
|
|
|
@@ -179,12 +260,15 @@ export function createRecordLayer(atlases: Atlas[], opts?: RecordLayerOptions):
|
|
|
179
260
|
throw new Error(`createRecordLayer: capacity must be a positive integer, got ${capacity}`)
|
|
180
261
|
}
|
|
181
262
|
let label = opts?.label ?? "sprites"
|
|
182
|
-
let
|
|
183
|
-
label: `${label}-records`,
|
|
184
|
-
autoFree: false,
|
|
185
|
-
})
|
|
263
|
+
let stream = createRecordStream(RECORD_BYTES, capacity, { label: `${label}-records`, autoFree: false })
|
|
186
264
|
let tint = opts?.tint ?? [1, 1, 1, 1]
|
|
187
265
|
checkTint("createRecordLayer", tint)
|
|
266
|
+
let material = opts?.material ?? unlit()
|
|
267
|
+
checkAtlases("createRecordLayer", atlases, materialSamplers(material))
|
|
268
|
+
let params: ShaderParams = { ...material.params, ...opts?.params }
|
|
269
|
+
// The material's style streams, after the layer's own record: the
|
|
270
|
+
// same publish, the same growth, the same shift on destroy.
|
|
271
|
+
let styles = createStyleStreams(material, capacity, label)
|
|
188
272
|
let orderBy = opts?.orderBy
|
|
189
273
|
let instanceOrder =
|
|
190
274
|
orderBy === undefined
|
|
@@ -193,7 +277,9 @@ export function createRecordLayer(atlases: Atlas[], opts?: RecordLayerOptions):
|
|
|
193
277
|
? { field: Y_FIELD_OFFSET }
|
|
194
278
|
: { field: orderBy.field, descending: orderBy.descending }
|
|
195
279
|
let ordered = instanceOrder !== undefined
|
|
196
|
-
let gpu =
|
|
280
|
+
let gpu = material.pipeline(atlases, [RECORD_ATTRIBUTES], label)
|
|
281
|
+
// The instance buffers in pipeline order after the quad.
|
|
282
|
+
let buffers = () => [stream.buffer, ...styles.map(s => s.stream.buffer)]
|
|
197
283
|
|
|
198
284
|
let disposed = false
|
|
199
285
|
let scheduled = false
|
|
@@ -215,60 +301,53 @@ export function createRecordLayer(atlases: Atlas[], opts?: RecordLayerOptions):
|
|
|
215
301
|
|
|
216
302
|
// How many leading sprites draw: the dial against the live count.
|
|
217
303
|
let drawn = () => Math.min(layer._dial, layer._order.length)
|
|
218
|
-
// Publish the first `n` records whole through the write lease: a
|
|
219
|
-
// replacement buffer's first fill (its contents are unspecified), and
|
|
220
|
-
// every publish of an ordered layer (the gather runs in the copy).
|
|
221
|
-
let publishWhole = (target: BufferId, n: number) => {
|
|
222
|
-
let out = beginBufferWrite(target)
|
|
223
|
-
out.set(layer._records.subarray(0, n * INSTANCE_FLOATS))
|
|
224
|
-
endBufferWrite(target, n * RECORD_BYTES)
|
|
225
|
-
}
|
|
226
304
|
|
|
227
|
-
// The
|
|
228
|
-
//
|
|
229
|
-
//
|
|
230
|
-
//
|
|
231
|
-
//
|
|
232
|
-
let
|
|
305
|
+
// The buffers growth replaced since the last flush (addSprite grows the
|
|
306
|
+
// stream at once; the views still draw from the first of these until the
|
|
307
|
+
// flush swaps the replacement in). The entries hold a swapped-out buffer
|
|
308
|
+
// alive until the swap lands, so the destroys are safe to issue right
|
|
309
|
+
// after.
|
|
310
|
+
let grownFrom: BufferId[] = []
|
|
233
311
|
let flush = () => {
|
|
234
312
|
scheduled = false
|
|
235
313
|
if (disposed) return
|
|
236
|
-
let dirty = layer._dirty
|
|
237
|
-
layer._dirty = null
|
|
238
|
-
let mirror = layer._records
|
|
239
314
|
let live = layer._order.length
|
|
240
315
|
let count = drawn()
|
|
241
|
-
if (
|
|
242
|
-
|
|
243
|
-
let grown = createBuffer(mirror.length * Float32Array.BYTES_PER_ELEMENT, { label: `${label}-records`, autoFree: false })
|
|
244
|
-
// The replacement is filled whole before the swap (the entry must
|
|
316
|
+
if (grownFrom.length > 0) {
|
|
317
|
+
// The replacements are filled whole before the swap (the entry must
|
|
245
318
|
// never point at an unwritten buffer): the live prefix, or under
|
|
246
319
|
// orderBy the drawn one, which is all the gather may see.
|
|
247
|
-
|
|
248
|
-
|
|
320
|
+
stream.publish(true, ordered ? count : live)
|
|
321
|
+
for (let s of styles) s.stream.publish(true, ordered ? count : live)
|
|
322
|
+
views.setBuffers(buffers())
|
|
249
323
|
views.setCount(count)
|
|
250
324
|
if (ordered) {
|
|
251
325
|
// That publish landed BEFORE the swap, so the order had not yet
|
|
252
|
-
// followed to the grown
|
|
326
|
+
// followed to the grown buffers and it went out ungathered. One
|
|
253
327
|
// more publish, now under the swapped-in order, restores key
|
|
254
328
|
// order - growth frames only.
|
|
255
|
-
|
|
329
|
+
stream.publish(true, count)
|
|
330
|
+
for (let s of styles) s.stream.publish(true, count)
|
|
256
331
|
}
|
|
257
|
-
destroyBuffer(
|
|
258
|
-
|
|
332
|
+
for (let old of grownFrom) destroyBuffer(old)
|
|
333
|
+
grownFrom.length = 0
|
|
259
334
|
published = count
|
|
260
335
|
return
|
|
261
336
|
}
|
|
262
337
|
if (ordered) {
|
|
263
338
|
// The gathered set IS the drawn prefix: a dirty record or a moved
|
|
264
|
-
// dial republishes it whole
|
|
265
|
-
|
|
266
|
-
|
|
339
|
+
// dial republishes it whole, every stream alike (the core gathers
|
|
340
|
+
// them under one permutation).
|
|
341
|
+
if (stream.dirty || count !== published) {
|
|
342
|
+
stream.publish(true, count)
|
|
343
|
+
for (let s of styles) s.stream.publish(true, count)
|
|
344
|
+
}
|
|
345
|
+
} else {
|
|
267
346
|
// The dirty range, clipped to the live sprites (a record past them
|
|
268
|
-
// is never drawn): one partial write at its byte offset
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
347
|
+
// is never drawn): one partial write at its byte offset, per
|
|
348
|
+
// dirty stream.
|
|
349
|
+
stream.publish(false, live)
|
|
350
|
+
for (let s of styles) s.stream.publish(false, live)
|
|
272
351
|
}
|
|
273
352
|
if (count !== published) {
|
|
274
353
|
views.setCount(count)
|
|
@@ -334,16 +413,18 @@ export function createRecordLayer(atlases: Atlas[], opts?: RecordLayerOptions):
|
|
|
334
413
|
label,
|
|
335
414
|
pipeline: gpu.pipeline,
|
|
336
415
|
quad: gpu.quad,
|
|
337
|
-
|
|
338
|
-
buffers
|
|
416
|
+
textures: { ...atlasBindings(atlases.map(a => a.texture)), ...material.textures },
|
|
417
|
+
buffers,
|
|
339
418
|
count: () => published,
|
|
340
419
|
tint: () => tint,
|
|
420
|
+
params: () => params,
|
|
341
421
|
pick: (x, y, zoom) => layer.pick(x, y, zoom),
|
|
342
422
|
order: instanceOrder,
|
|
343
423
|
})
|
|
344
424
|
|
|
345
425
|
let layer: RecordLayer = {
|
|
346
426
|
atlases,
|
|
427
|
+
material,
|
|
347
428
|
get count() {
|
|
348
429
|
return layer._order.length
|
|
349
430
|
},
|
|
@@ -353,6 +434,12 @@ export function createRecordLayer(atlases: Atlas[], opts?: RecordLayerOptions):
|
|
|
353
434
|
tint = next
|
|
354
435
|
views.setTint(next)
|
|
355
436
|
},
|
|
437
|
+
setParams(next) {
|
|
438
|
+
if (disposed) return
|
|
439
|
+
checkParams("setParams", next)
|
|
440
|
+
Object.assign(params, next)
|
|
441
|
+
views.setParams(next)
|
|
442
|
+
},
|
|
356
443
|
createView(vopts) {
|
|
357
444
|
if (disposed) throw new Error("createView: layer is disposed")
|
|
358
445
|
return views.create(vopts)
|
|
@@ -383,8 +470,10 @@ export function createRecordLayer(atlases: Atlas[], opts?: RecordLayerOptions):
|
|
|
383
470
|
for (let sprite of layer._order) sprite.layer = null
|
|
384
471
|
layer._order.length = 0
|
|
385
472
|
views.dispose()
|
|
386
|
-
destroyBuffer(
|
|
387
|
-
|
|
473
|
+
for (let old of grownFrom) destroyBuffer(old)
|
|
474
|
+
grownFrom.length = 0
|
|
475
|
+
stream.destroy()
|
|
476
|
+
for (let s of styles) s.stream.destroy()
|
|
388
477
|
},
|
|
389
478
|
_add(opts) {
|
|
390
479
|
if (opts?.parent) {
|
|
@@ -407,12 +496,13 @@ export function createRecordLayer(atlases: Atlas[], opts?: RecordLayerOptions):
|
|
|
407
496
|
}
|
|
408
497
|
let atlas = checkWrite("addSprite", record)
|
|
409
498
|
let index = layer._order.length
|
|
410
|
-
if (
|
|
411
|
-
// Growth replaces the
|
|
412
|
-
//
|
|
413
|
-
let next =
|
|
414
|
-
|
|
415
|
-
|
|
499
|
+
if (index + 1 > stream.capacity) {
|
|
500
|
+
// Growth replaces the mirrors and the buffers (doubling); the
|
|
501
|
+
// flush fills the replacements whole and swaps them in.
|
|
502
|
+
let next = stream.capacity * 2
|
|
503
|
+
grownFrom.push(stream.grow(next))
|
|
504
|
+
for (let s of styles) grownFrom.push(growStyle(s, next))
|
|
505
|
+
layer._records = new Float32Array(stream.bytes.buffer)
|
|
416
506
|
}
|
|
417
507
|
let sprite: SpriteState = { layer, node: null, _slot: index, _x: 0, _y: 0, _w: 0, _h: 0, _rot: 0, _flipX: false, _flipY: false, _visible: true, _parent: null }
|
|
418
508
|
layer._order.push(sprite)
|
|
@@ -420,6 +510,8 @@ export function createRecordLayer(atlases: Atlas[], opts?: RecordLayerOptions):
|
|
|
420
510
|
// No outline until a raw writer sets one: the slot may hold a
|
|
421
511
|
// shifted-out sprite's record otherwise.
|
|
422
512
|
layer._records.fill(0, index * INSTANCE_FLOATS + OUTLINE_FIELD_OFFSET, index * INSTANCE_FLOATS + OUTLINE_FIELD_OFFSET + 4)
|
|
513
|
+
// The material's style slot starts blank for the same reason.
|
|
514
|
+
for (let s of styles) blankStyle(s, index)
|
|
423
515
|
markRecords(layer, index, index + 1)
|
|
424
516
|
return sprite
|
|
425
517
|
},
|
|
@@ -459,6 +551,7 @@ export function createRecordLayer(atlases: Atlas[], opts?: RecordLayerOptions):
|
|
|
459
551
|
let r = layer._records
|
|
460
552
|
let last = order.length - 1
|
|
461
553
|
r.copyWithin(index * INSTANCE_FLOATS, (index + 1) * INSTANCE_FLOATS, order.length * INSTANCE_FLOATS)
|
|
554
|
+
if (index < last) for (let s of styles) shiftStyle(s, index + 1, order.length)
|
|
462
555
|
order.splice(index, 1)
|
|
463
556
|
for (let i = index; i < order.length; i++) order[i]!._slot = i
|
|
464
557
|
if (index < last) markRecords(layer, index, last)
|
|
@@ -469,8 +562,9 @@ export function createRecordLayer(atlases: Atlas[], opts?: RecordLayerOptions):
|
|
|
469
562
|
scheduled = true
|
|
470
563
|
RESOLVED.then(flush)
|
|
471
564
|
},
|
|
472
|
-
|
|
473
|
-
|
|
565
|
+
_stream: stream,
|
|
566
|
+
_records: new Float32Array(stream.bytes.buffer),
|
|
567
|
+
_styles: styles,
|
|
474
568
|
_dial: Infinity,
|
|
475
569
|
_order: [],
|
|
476
570
|
}
|
package/src/style.ts
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
// A material's style streams on a layer: one core record stream per
|
|
2
|
+
// instance buffer the material declares (createRecordStream: the GPU
|
|
3
|
+
// buffer, the byte mirror, the dirty range and the range-or-whole
|
|
4
|
+
// publish), bound after the layer's own records, each slot starting as
|
|
5
|
+
// the material's blank record. The node layer and the records layer
|
|
6
|
+
// hold them beside their own stream and publish them in the same flush
|
|
7
|
+
// (the tile layer bakes per chunk and keeps plain arrays, tiles.ts);
|
|
8
|
+
// the shared verbs (setInstanceStyle, instanceAttribute, records,
|
|
9
|
+
// updateRecords in records.ts) write through the accessors here. The
|
|
10
|
+
// codecs are core's (vertex.ts), the same ones @solidrt/3d's instance
|
|
11
|
+
// streams encode through.
|
|
12
|
+
import { createRecordStream, isFloatLayout, layoutFields, layoutStride, layoutView, writeRecord } from "@solidrt/core/gpu"
|
|
13
|
+
import type { AttributeAccess, BufferId, RecordStream, VertexAttribute } from "@solidrt/core/gpu"
|
|
14
|
+
import { materialBlank } from "./material.ts"
|
|
15
|
+
import type { Material } from "./material.ts"
|
|
16
|
+
|
|
17
|
+
/** One style stream: the layout, the core stream behind it, the blank a
|
|
18
|
+
* fresh slot copies, the accessors (valid across growth), and the
|
|
19
|
+
* mirror's view as the layout wants it (re-derived by growth: read it
|
|
20
|
+
* at use time). */
|
|
21
|
+
export type StyleStream = {
|
|
22
|
+
layout: VertexAttribute[]
|
|
23
|
+
stream: RecordStream
|
|
24
|
+
blank: Uint8Array
|
|
25
|
+
fields: AttributeAccess[]
|
|
26
|
+
/** A Float32Array over an all-float layout, bytes otherwise. */
|
|
27
|
+
data: ArrayBufferView
|
|
28
|
+
/** The float view writeRecord stores through on an all-float layout,
|
|
29
|
+
* null otherwise. */
|
|
30
|
+
_floats: Float32Array | null
|
|
31
|
+
_view: DataView
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** The material's style streams for a layer of `capacity` slots: empty
|
|
35
|
+
* without instance buffers. */
|
|
36
|
+
export function createStyleStreams(material: Material, capacity: number, label: string): StyleStream[] {
|
|
37
|
+
let out: StyleStream[] = []
|
|
38
|
+
material.instanceBuffers?.forEach((buffer, i) => {
|
|
39
|
+
let layout = buffer.attributes
|
|
40
|
+
let stream = createRecordStream(layoutStride(layout), capacity, { label: `${label}-style${i}`, autoFree: false })
|
|
41
|
+
let style: StyleStream = {
|
|
42
|
+
layout,
|
|
43
|
+
stream,
|
|
44
|
+
blank: materialBlank(material, i),
|
|
45
|
+
fields: [],
|
|
46
|
+
data: new Uint8Array(0),
|
|
47
|
+
_floats: null,
|
|
48
|
+
_view: new DataView(new ArrayBuffer(0)),
|
|
49
|
+
}
|
|
50
|
+
viewStyle(style)
|
|
51
|
+
style.fields = layoutFields(layout, () => style._view)
|
|
52
|
+
// Every slot starts blank: a material's instanceStyle is what an
|
|
53
|
+
// unwritten sprite shows, not zeros.
|
|
54
|
+
for (let slot = 0; slot < capacity; slot++) stream.bytes.set(style.blank, slot * stream.stride)
|
|
55
|
+
out.push(style)
|
|
56
|
+
})
|
|
57
|
+
return out
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Point the views at the stream's current mirror.
|
|
61
|
+
function viewStyle(style: StyleStream): void {
|
|
62
|
+
let buffer = style.stream.bytes.buffer
|
|
63
|
+
style.data = layoutView(style.layout, buffer)
|
|
64
|
+
style._floats = isFloatLayout(style.layout) ? (style.data as Float32Array) : null
|
|
65
|
+
style._view = new DataView(buffer)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Grow a style stream to `next` slots (the new slots blank) and return
|
|
69
|
+
* the old buffer for the caller to destroy once its draws point at the
|
|
70
|
+
* new one. */
|
|
71
|
+
export function growStyle(style: StyleStream, next: number): BufferId {
|
|
72
|
+
let previous = style.stream.capacity
|
|
73
|
+
let old = style.stream.grow(next)
|
|
74
|
+
for (let slot = previous; slot < next; slot++) style.stream.bytes.set(style.blank, slot * style.stream.stride)
|
|
75
|
+
viewStyle(style)
|
|
76
|
+
return old
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Reset slot `slot` to the blank record and mark it. */
|
|
80
|
+
export function blankStyle(style: StyleStream, slot: number): void {
|
|
81
|
+
style.stream.bytes.set(style.blank, slot * style.stream.stride)
|
|
82
|
+
style.stream.mark(slot, slot + 1)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Encode `values` (one per component, in layout order, as the shader
|
|
86
|
+
* sees them) into slot `slot` and mark it. */
|
|
87
|
+
export function writeStyle(style: StyleStream, slot: number, values: ArrayLike<number>, site: string): void {
|
|
88
|
+
writeRecord(style.layout, style.fields, style._floats, slot, values, site)
|
|
89
|
+
style.stream.mark(slot, slot + 1)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Shift slots [from, to) down to `from - 1` (a records layer's destroy)
|
|
93
|
+
* and mark the moved range. */
|
|
94
|
+
export function shiftStyle(style: StyleStream, from: number, to: number): void {
|
|
95
|
+
let stride = style.stream.stride
|
|
96
|
+
style.stream.bytes.copyWithin((from - 1) * stride, from * stride, to * stride)
|
|
97
|
+
style.stream.mark(from - 1, to - 1)
|
|
98
|
+
}
|