@solidrt/2d 0.0.71 → 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/src/records.ts CHANGED
@@ -15,12 +15,15 @@
15
15
  // static layer publishes nothing and therefore costs nothing.
16
16
  //
17
17
  // The record vocabulary is @solidrt/3d's record mesh's, one dimension
18
- // down: `records(layer)` is the mirror, `updateRecords(layer, { first?,
19
- // count? })` publishes a range, `setRecordCount(layer, n)` dials the drawn
20
- // prefix. The POPULATION differs by kind and keeps its own verbs - a
21
- // record mesh's records are opaque data (setRecords), a record layer's
22
- // are sprites (addSprite/destroySprite) - so the copy-in here is
23
- // `records(layer).set(src)` followed by updateRecords.
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.
24
27
  //
25
28
  // This is NOT the default live layer - that is layer.ts, where sprites are
26
29
  // spatial arena nodes core producers can reach. Use this when a JS loop
@@ -36,13 +39,16 @@
36
39
  import { getOwner, onBeforeRender, onCleanup, runWithOwner } from "@solidrt/core"
37
40
  import { checkScreenSize, createRecordStream, destroyBuffer, screenSizeScale } from "@solidrt/core/gpu"
38
41
  import type { BufferId } from "@solidrt/core/gpu"
42
+ import type { AttributeAccess, ShaderParams } from "@solidrt/core/gpu"
39
43
  import { checkAtlases, frameIndex } from "./atlas.ts"
40
44
  import type { Atlas } from "./atlas.ts"
41
45
  import { fullFrame, isFrame, writeFrame } from "./frames.ts"
42
- import { checkTint, readFrame } from "./layer.ts"
46
+ import { atlasBindings, RECORD_ATTRIBUTES } from "./glsl.ts"
47
+ import { checkParams, checkTint, readFrame } from "./layer.ts"
43
48
  import type { LayerBase, Sprite, SpriteLayer, SpriteLayerOptions, SpriteOptions, SpriteState } from "./layer.ts"
49
+ import { materialSamplers, unlit } from "./material.ts"
44
50
  import { pointInSprite } from "./pick.ts"
45
- import { createSpritePipeline, INSTANCE_ATTRIBUTES, VERTEX } from "./shaders.ts"
51
+ import { blankStyle, createStyleStreams, growStyle, shiftStyle, writeStyle } from "./style.ts"
46
52
  import { createViews } from "./views.ts"
47
53
 
48
54
  // Floats per instance record:
@@ -92,12 +98,20 @@ export type RecordLayer = LayerBase & {
92
98
  _order: SpriteState[]
93
99
  }
94
100
 
95
- /** Options of `updateRecords`: the record range (default the whole
96
- * mirror, capacity wide). */
97
- export type UpdateRecordsOptions = { first?: number; count?: number }
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 }
98
106
 
99
107
  /**
100
- * The layer's record mirror - the raw power path. On a records layer the
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
101
115
  * layout per sprite is INSTANCE_FLOATS floats: [cx, cy, w, h, u0, v0, u1,
102
116
  * v1, rot, tintR, tintG, tintB, tintA, minScreenPx, maxScreenPx, atlas,
103
117
  * outlineR, outlineG, outlineB, outlineWidth], record i at i *
@@ -110,9 +124,9 @@ export type UpdateRecordsOptions = { first?: number; count?: number }
110
124
  * records shift.
111
125
  *
112
126
  * On a node layer (createSpriteLayer) the records are its STYLE records,
113
- * STYLE_FLOATS floats per sprite slot: [u0, v0, u1, v1, tintR, tintG,
127
+ * SPRITE_FLOATS floats per sprite slot: [u0, v0, u1, v1, tintR, tintG,
114
128
  * tintB, tintA, renderOrder, minScreenPx, maxScreenPx, atlas, outlineR,
115
- * outlineG, outlineB, outlineWidth], slot i at i * STYLE_FLOATS (a
129
+ * outlineG, outlineB, outlineWidth], slot i at i * SPRITE_FLOATS (a
116
130
  * sprite's slot is fixed for its life; a freed slot recycles) - the pose
117
131
  * lives in the core and is written through setSprite. The 3d
118
132
  * `records(instancedMesh)`, one dimension down: a bulk restyle (a palette
@@ -124,12 +138,67 @@ export type UpdateRecordsOptions = { first?: number; count?: number }
124
138
  * replaces the array, and a hoisted reference becomes a dead copy whose
125
139
  * writes publish nothing. @solidrt/3d's records(mesh).
126
140
  */
127
- export function records(layer: RecordLayer | SpriteLayer): Float32Array {
128
- return layer._records
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
156
+ }
157
+
158
+ /**
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()
129
179
  }
130
180
 
131
181
  /**
132
- * Publish records [first, first + count) of the mirror at the layer's
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
133
202
  * next flush - the partial rewrite a population stepped in JS wants (ten
134
203
  * moved records of ten thousand cost ten); the whole mirror by default.
135
204
  * Write the mirror first, through records(), then call this. The range
@@ -141,13 +210,17 @@ export function records(layer: RecordLayer | SpriteLayer): Float32Array {
141
210
  * updateRecords(mesh).
142
211
  */
143
212
  export function updateRecords(layer: RecordLayer | SpriteLayer, options: UpdateRecordsOptions = {}): void {
144
- let capacity = layer._stream.capacity
213
+ let index = options.stream ?? 0
214
+ let stream = index === 0 ? layer._stream : styleStream("updateRecords", layer, index).stream
215
+ let capacity = stream.capacity
145
216
  let first = options.first ?? 0
146
217
  let count = options.count ?? capacity - first
147
218
  if (!Number.isInteger(first) || !Number.isInteger(count) || first < 0 || count < 0 || first + count > capacity) {
148
219
  throw new Error("updateRecords: range [" + first + ", " + (first + count) + ") is outside the layer's " + capacity + " records")
149
220
  }
150
- if (count > 0) markRecords(layer, first, first + count)
221
+ if (count === 0) return
222
+ stream.mark(first, first + count)
223
+ layer._schedule()
151
224
  }
152
225
 
153
226
  /**
@@ -190,6 +263,12 @@ export function createRecordLayer(atlases: Atlas[], opts?: RecordLayerOptions):
190
263
  let stream = createRecordStream(RECORD_BYTES, capacity, { label: `${label}-records`, autoFree: false })
191
264
  let tint = opts?.tint ?? [1, 1, 1, 1]
192
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)
193
272
  let orderBy = opts?.orderBy
194
273
  let instanceOrder =
195
274
  orderBy === undefined
@@ -198,7 +277,9 @@ export function createRecordLayer(atlases: Atlas[], opts?: RecordLayerOptions):
198
277
  ? { field: Y_FIELD_OFFSET }
199
278
  : { field: orderBy.field, descending: orderBy.descending }
200
279
  let ordered = instanceOrder !== undefined
201
- let gpu = createSpritePipeline(label, VERTEX, [INSTANCE_ATTRIBUTES], opts?.blend ?? "alpha", atlases)
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)]
202
283
 
203
284
  let disposed = false
204
285
  let scheduled = false
@@ -233,18 +314,20 @@ export function createRecordLayer(atlases: Atlas[], opts?: RecordLayerOptions):
233
314
  let live = layer._order.length
234
315
  let count = drawn()
235
316
  if (grownFrom.length > 0) {
236
- // The replacement is filled whole before the swap (the entry must
317
+ // The replacements are filled whole before the swap (the entry must
237
318
  // never point at an unwritten buffer): the live prefix, or under
238
319
  // orderBy the drawn one, which is all the gather may see.
239
320
  stream.publish(true, ordered ? count : live)
240
- views.setBuffers([stream.buffer])
321
+ for (let s of styles) s.stream.publish(true, ordered ? count : live)
322
+ views.setBuffers(buffers())
241
323
  views.setCount(count)
242
324
  if (ordered) {
243
325
  // That publish landed BEFORE the swap, so the order had not yet
244
- // followed to the grown buffer and it went out ungathered. One
326
+ // followed to the grown buffers and it went out ungathered. One
245
327
  // more publish, now under the swapped-in order, restores key
246
328
  // order - growth frames only.
247
329
  stream.publish(true, count)
330
+ for (let s of styles) s.stream.publish(true, count)
248
331
  }
249
332
  for (let old of grownFrom) destroyBuffer(old)
250
333
  grownFrom.length = 0
@@ -253,12 +336,18 @@ export function createRecordLayer(atlases: Atlas[], opts?: RecordLayerOptions):
253
336
  }
254
337
  if (ordered) {
255
338
  // The gathered set IS the drawn prefix: a dirty record or a moved
256
- // dial republishes it whole.
257
- if (stream.dirty || count !== published) stream.publish(true, count)
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
+ }
258
345
  } else {
259
346
  // The dirty range, clipped to the live sprites (a record past them
260
- // is never drawn): one partial write at its byte offset.
347
+ // is never drawn): one partial write at its byte offset, per
348
+ // dirty stream.
261
349
  stream.publish(false, live)
350
+ for (let s of styles) s.stream.publish(false, live)
262
351
  }
263
352
  if (count !== published) {
264
353
  views.setCount(count)
@@ -324,16 +413,18 @@ export function createRecordLayer(atlases: Atlas[], opts?: RecordLayerOptions):
324
413
  label,
325
414
  pipeline: gpu.pipeline,
326
415
  quad: gpu.quad,
327
- atlases: atlases.map(a => a.texture),
328
- buffers: () => [stream.buffer],
416
+ textures: { ...atlasBindings(atlases.map(a => a.texture)), ...material.textures },
417
+ buffers,
329
418
  count: () => published,
330
419
  tint: () => tint,
420
+ params: () => params,
331
421
  pick: (x, y, zoom) => layer.pick(x, y, zoom),
332
422
  order: instanceOrder,
333
423
  })
334
424
 
335
425
  let layer: RecordLayer = {
336
426
  atlases,
427
+ material,
337
428
  get count() {
338
429
  return layer._order.length
339
430
  },
@@ -343,6 +434,12 @@ export function createRecordLayer(atlases: Atlas[], opts?: RecordLayerOptions):
343
434
  tint = next
344
435
  views.setTint(next)
345
436
  },
437
+ setParams(next) {
438
+ if (disposed) return
439
+ checkParams("setParams", next)
440
+ Object.assign(params, next)
441
+ views.setParams(next)
442
+ },
346
443
  createView(vopts) {
347
444
  if (disposed) throw new Error("createView: layer is disposed")
348
445
  return views.create(vopts)
@@ -376,7 +473,7 @@ export function createRecordLayer(atlases: Atlas[], opts?: RecordLayerOptions):
376
473
  for (let old of grownFrom) destroyBuffer(old)
377
474
  grownFrom.length = 0
378
475
  stream.destroy()
379
- gpu.dispose()
476
+ for (let s of styles) s.stream.destroy()
380
477
  },
381
478
  _add(opts) {
382
479
  if (opts?.parent) {
@@ -400,9 +497,11 @@ export function createRecordLayer(atlases: Atlas[], opts?: RecordLayerOptions):
400
497
  let atlas = checkWrite("addSprite", record)
401
498
  let index = layer._order.length
402
499
  if (index + 1 > stream.capacity) {
403
- // Growth replaces the mirror and the buffer (doubling); the flush
404
- // fills the replacement whole and swaps it in.
405
- grownFrom.push(stream.grow(stream.capacity * 2))
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))
406
505
  layer._records = new Float32Array(stream.bytes.buffer)
407
506
  }
408
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 }
@@ -411,6 +510,8 @@ export function createRecordLayer(atlases: Atlas[], opts?: RecordLayerOptions):
411
510
  // No outline until a raw writer sets one: the slot may hold a
412
511
  // shifted-out sprite's record otherwise.
413
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)
414
515
  markRecords(layer, index, index + 1)
415
516
  return sprite
416
517
  },
@@ -450,6 +551,7 @@ export function createRecordLayer(atlases: Atlas[], opts?: RecordLayerOptions):
450
551
  let r = layer._records
451
552
  let last = order.length - 1
452
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)
453
555
  order.splice(index, 1)
454
556
  for (let i = index; i < order.length; i++) order[i]!._slot = i
455
557
  if (index < last) markRecords(layer, index, last)
@@ -462,6 +564,7 @@ export function createRecordLayer(atlases: Atlas[], opts?: RecordLayerOptions):
462
564
  },
463
565
  _stream: stream,
464
566
  _records: new Float32Array(stream.bytes.buffer),
567
+ _styles: styles,
465
568
  _dial: Infinity,
466
569
  _order: [],
467
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
+ }