@solidrt/2d 0.0.71 → 0.0.73

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/tiles.ts CHANGED
@@ -5,15 +5,18 @@
5
5
  // chunk textures, and scrolling is a transform on the composited world
6
6
  // (see <TileLayer> in components/tile-layer.tsx), never a repaint.
7
7
  //
8
- // Each chunk is a manual target over the layer's ONE sprite pipeline
9
- // (shaders.ts, compiled once per layer for its atlases) with fixed record
8
+ // Each chunk is a manual target over the layer's ONE pipeline (its
9
+ // material's, resolved once per layer for its atlases) with fixed record
10
10
  // slots - record localRow * chunkTiles + localCol IS that tile, an empty
11
11
  // tile is a zero-size quad, instance count is constant per chunk. Records
12
12
  // hold WORLD pixel coordinates; the chunk target's uCamera is its pixel
13
13
  // origin, so the shared vertex stage does the chunk-local mapping (the
14
14
  // same mechanism a camera pass uses, pointed at a chunk rect). A cell's
15
15
  // frame names its atlas like a sprite's (the record's atlas index), so a
16
- // tile world draws from several tilesets in one bake.
16
+ // tile world draws from several tilesets in one bake. The material's
17
+ // style streams are per chunk too (a cell's `style`), plain arrays
18
+ // uploaded whole with the chunk's records; its params reach every
19
+ // resident chunk, each write a re-bake like the layer tint.
17
20
  //
18
21
  // Chunks allocate lazily on the first setTile that gives them content: an
19
22
  // empty chunk costs nothing - no records, no buffer, no texture - so a
@@ -32,20 +35,27 @@ import {
32
35
  destroyBuffer,
33
36
  destroyTexture,
34
37
  endBufferWrite,
38
+ isFloatLayout,
39
+ layoutFields,
40
+ layoutStride,
35
41
  limits,
36
42
  renderTarget,
37
43
  setTargetParams,
38
44
  setTargetSize,
45
+ writeBuffer,
46
+ writeRecord,
39
47
  } from "@solidrt/core/gpu"
40
- import type { BlendMode, BufferId, FilterMode, TextureId } from "@solidrt/core/gpu"
48
+ import type { AttributeAccess, BufferId, FilterMode, ShaderParams, TextureId, VertexAttribute } from "@solidrt/core/gpu"
41
49
  import { checkAtlases, frameIndex } from "./atlas.ts"
42
50
  import type { Atlas } from "./atlas.ts"
43
51
  import { isFrame } from "./frames.ts"
44
52
  import type { Frame } from "./frames.ts"
45
- import { checkTint } from "./layer.ts"
53
+ import { atlasBindings, RECORD_ATTRIBUTES } from "./glsl.ts"
54
+ import { checkParams, checkTint } from "./layer.ts"
55
+ import { materialBlank, materialSamplers, unlit } from "./material.ts"
56
+ import type { Material } from "./material.ts"
46
57
  import { checkOversample, thrashSentinel } from "./oversample.ts"
47
58
  import { INSTANCE_FLOATS } from "./records.ts"
48
- import { atlasBindings, createSpritePipeline, INSTANCE_ATTRIBUTES, VERTEX } from "./shaders.ts"
49
59
  import { checkCell, checkRect, chunkOf, eachChunkSlice, slotOf } from "./tiles-math.ts"
50
60
 
51
61
  const RESOLVED = Promise.resolve()
@@ -77,12 +87,19 @@ export type TileLayerOptions = {
77
87
  */
78
88
  filter?: FilterMode
79
89
  /**
80
- * How the cells blend into their chunk bakes, core gpu's BlendMode;
81
- * default "alpha". "add" for a glow or light map baked over the
82
- * transparent chunk clear, "multiply" to darken. Pipeline state, fixed
83
- * at creation.
90
+ * What the cells look like: the layer's material (see
91
+ * SpriteLayerOptions.material), fixed at creation; default `unlit()`.
92
+ * Its blend is how the cells bake into their chunks ("add" for a glow
93
+ * or light map baked over the transparent chunk clear), its params and
94
+ * textures reach every chunk target, its instance buffers are a style
95
+ * record per cell (`setTile`'s `style`). A tile layer BAKES, so a
96
+ * param change re-renders every resident chunk, like the tint:
97
+ * animated uniforms belong on a sprite layer.
84
98
  */
85
- blend?: BlendMode
99
+ material?: Material
100
+ /** Uniform values seeded on every chunk over the material's own; see
101
+ * TileLayer.setParams. */
102
+ params?: ShaderParams
86
103
  /**
87
104
  * Target texels per world pixel in the baked chunks (positive integer,
88
105
  * default 1); see TileLayer.setOversample. `<TileLayer>` picks it from
@@ -120,10 +137,26 @@ export type TileChunk = {
120
137
  height: number
121
138
  }
122
139
 
140
+ /** A cell write's options: the cell's tint and, under a material with
141
+ * instance buffers, its style record. */
142
+ export type TileCellOptions = {
143
+ /** The cell's tint, multiplied over its texels like a sprite's; absent
144
+ * keeps the cell's current tint. */
145
+ tint?: Tint
146
+ /** The cell's style record (the material's FIRST instance buffer, one
147
+ * value per component in attribute order), absent keeps it; a cell set
148
+ * from empty starts at the material's instanceStyle. Throws on a
149
+ * material without an instance buffer. */
150
+ style?: ArrayLike<number>
151
+ }
152
+
123
153
  export type TileLayer = {
124
154
  /** The atlases the layer bakes from, as declared at creation: every
125
155
  * frame set names one of their textures (see LayerBase.atlases). */
126
156
  readonly atlases: readonly Atlas[]
157
+ /** The material the layer bakes with, as declared at creation (the
158
+ * stock `unlit()` when none was). */
159
+ readonly material: Material
127
160
  /** Grid shape, fixed at creation. */
128
161
  cols: number
129
162
  rows: number
@@ -164,7 +197,7 @@ export type TileLayer = {
164
197
  * a frame) publishes and re-bakes ONLY the chunks that changed, however
165
198
  * many tiles did.
166
199
  */
167
- setTile(col: number, row: number, frame: Frame | null, opts?: { tint?: Tint }): void
200
+ setTile(col: number, row: number, frame: Frame | null, opts?: TileCellOptions): void
168
201
  /**
169
202
  * Set a rect of cells at once: `cols` x `rows` cells from `col`, `row`,
170
203
  * `cells` in row-major order (`cols * rows` long) - frames (null clears)
@@ -174,10 +207,10 @@ export type TileLayer = {
174
207
  * touches, the per-cell loop inside the layer over its own records, the
175
208
  * same batching and flush. A chunk the rect only clears never allocates,
176
209
  * so a whole-world write of a sparse world stays sparse. `tint` applies
177
- * to every cell set, as setTile's; absent, each cell keeps its own. A
178
- * bad entry throws before any cell changes.
210
+ * to every cell set, as setTile's, and so does `style`; absent, each
211
+ * cell keeps its own. A bad entry throws before any cell changes.
179
212
  */
180
- setTiles(col: number, row: number, cols: number, rows: number, cells: ArrayLike<Frame | null> | ArrayLike<number>, opts?: { tint?: Tint }): void
213
+ setTiles(col: number, row: number, cols: number, rows: number, cells: ArrayLike<Frame | null> | ArrayLike<number>, opts?: TileCellOptions): void
181
214
  /** The frame at a cell, or null when empty. */
182
215
  getTile(col: number, row: number): Frame | null
183
216
  /** The frames table given at creation (what setTiles' indices name), or
@@ -191,18 +224,35 @@ export type TileLayer = {
191
224
  * animate it coarsely, not per frame.
192
225
  */
193
226
  setTint(tint: [number, number, number, number]): void
227
+ /**
228
+ * Write uniform values to every resident chunk (the material's prelude
229
+ * uniforms, over its own `params`): LayerBase.setParams for a baked
230
+ * layer, so each write re-renders every resident chunk, like setTint -
231
+ * drive it from slow state, not per frame. Absent names keep their
232
+ * values; a name the program does not declare is skipped.
233
+ */
234
+ setParams(params: ShaderParams): void
194
235
  dispose(): void
195
236
  }
196
237
 
197
238
  type Chunk = TileChunk & {
198
239
  records: Float32Array
199
240
  buffer: BufferId
241
+ /** The material's style records of the chunk, one array and buffer
242
+ * per instance buffer, uploaded whole with the records. */
243
+ styles: ChunkStyle[]
200
244
  dirty: boolean
201
245
  /** Records changed since the last flush (a tint-only or resize re-bake
202
246
  * renders without re-uploading them). */
203
247
  wrote: boolean
204
248
  }
205
249
 
250
+ type ChunkStyle = { bytes: Uint8Array; buffer: BufferId; fields: AttributeAccess[]; floats: Float32Array | null }
251
+
252
+ // One style stream of the material as the tile layer keeps it: the
253
+ // layout, the blank record, and the stride.
254
+ type StyleShape = { layout: VertexAttribute[]; blank: Uint8Array; stride: number }
255
+
206
256
  /**
207
257
  * Create a baked tile layer: `cols` x `rows` cells of `tileW` x `tileH`
208
258
  * pixels, every tile drawing one atlas frame, baked into lazily-allocated
@@ -243,9 +293,12 @@ export function createTileLayer(
243
293
  )
244
294
  }
245
295
  let label = opts?.label ?? "tiles"
246
- let blend: BlendMode = opts?.blend ?? "alpha"
247
296
  let tint: Tint = opts?.tint ?? [1, 1, 1, 1]
248
297
  checkTint("createTileLayer", tint)
298
+ let material = opts?.material ?? unlit()
299
+ checkAtlases("createTileLayer", atlases, materialSamplers(material))
300
+ let params: ShaderParams = { ...material.params, ...opts?.params }
301
+ let shapes: StyleShape[] = (material.instanceBuffers ?? []).map((b, i) => ({ layout: b.attributes, blank: materialBlank(material, i), stride: layoutStride(b.attributes) }))
249
302
  // The frames table, copied (the caller's array may move on) and checked
250
303
  // once, plus its UVs and atlas index as five floats per entry: an index
251
304
  // write copies by offset instead of reading a frame object per cell.
@@ -273,9 +326,10 @@ export function createTileLayer(
273
326
  let chunkCols = Math.ceil(cols / chunkTiles)
274
327
  let perChunk = chunkTiles * chunkTiles
275
328
  // The one pipeline (and unit quad) every chunk target draws with:
276
- // compiled once per layer, however many chunks allocate.
277
- let gpu = createSpritePipeline(label, VERTEX, [INSTANCE_ATTRIBUTES], blend, atlases)
278
- let textures = atlasBindings(atlases.map(a => a.texture))
329
+ // the material's, resolved once per layer, however many chunks
330
+ // allocate.
331
+ let gpu = material.pipeline(atlases, [RECORD_ATTRIBUTES], label)
332
+ let textures = { ...atlasBindings(atlases.map(a => a.texture)), ...material.textures }
279
333
 
280
334
  let disposed = false
281
335
  let scheduled = false
@@ -309,6 +363,7 @@ export function createTileLayer(
309
363
  let out = beginBufferWrite(chunk.buffer)
310
364
  out.set(chunk.records)
311
365
  endBufferWrite(chunk.buffer, chunk.records.byteLength)
366
+ for (let style of chunk.styles) writeBuffer(style.buffer, style.bytes)
312
367
  }
313
368
  renderTarget(chunk.texture)
314
369
  }
@@ -328,16 +383,29 @@ export function createTileLayer(
328
383
  let y = Math.floor(index / chunkCols) * chunkH
329
384
  let records = new Float32Array(perChunk * INSTANCE_FLOATS)
330
385
  let buffer = createBuffer(records.byteLength, { label: `${label}-chunk-records`, autoFree: false })
386
+ // Every cell's style slot starts blank (the material's
387
+ // instanceStyle), so an unwritten cell shows the material's default.
388
+ let styles: ChunkStyle[] = shapes.map((shape, i) => {
389
+ let bytes = new Uint8Array(perChunk * shape.stride)
390
+ for (let slot = 0; slot < perChunk; slot++) bytes.set(shape.blank, slot * shape.stride)
391
+ let view = new DataView(bytes.buffer)
392
+ return {
393
+ bytes,
394
+ buffer: createBuffer(bytes.byteLength, { label: `${label}-chunk-style${i}`, autoFree: false }),
395
+ fields: layoutFields(shape.layout, () => view),
396
+ floats: isFloatLayout(shape.layout) ? new Float32Array(bytes.buffer) : null,
397
+ }
398
+ })
331
399
  let texture = createShaderTarget(
332
400
  gpu.pipeline,
333
401
  chunkW * oversample,
334
402
  chunkH * oversample,
335
403
  // Bake targets pin the camera rotation to identity: the tile camera
336
404
  // rotates the COMPOSITED world (<TileLayer>'s view), never the bake.
337
- { uViewport: [chunkW, chunkH], uCamera: [x, y, 1, 1], uCameraRot: [1, 0, 0, 0], uTint: tint },
405
+ { uViewport: [chunkW, chunkH], uCamera: [x, y, 1, 1], uCameraRot: [1, 0, 0, 0], uTint: tint, ...params },
338
406
  {
339
407
  label: `${label}-chunk`,
340
- buffers: [gpu.quad, buffer],
408
+ buffers: [gpu.quad, buffer, ...styles.map(s => s.buffer)],
341
409
  vertexCount: 4,
342
410
  instanceCount: perChunk,
343
411
  textures,
@@ -347,7 +415,7 @@ export function createTileLayer(
347
415
  autoFree: false,
348
416
  },
349
417
  )
350
- let chunk: Chunk = { texture, x, y, width: chunkW, height: chunkH, records, buffer, dirty: false, wrote: false }
418
+ let chunk: Chunk = { texture, x, y, width: chunkW, height: chunkH, records, buffer, styles, dirty: false, wrote: false }
351
419
  resident.set(index, chunk)
352
420
  layer.chunks.push(chunk)
353
421
  layer.onChunk?.(chunk)
@@ -386,14 +454,27 @@ export function createTileLayer(
386
454
  }
387
455
  }
388
456
  // Clear one cell: a zero-size quad draws nothing (the tint stays, and
389
- // a later set from empty resets it).
457
+ // a later set from empty resets it; the style stays the same way).
390
458
  let clearCell = (r: Float32Array, at: number): void => {
391
459
  r[at + 2] = 0
392
460
  r[at + 3] = 0
393
461
  }
462
+ // A cell write's style, checked before any cell changes: the material
463
+ // must declare an instance buffer, and the record must be its length
464
+ // (the encode throws on a wrong count).
465
+ let checkStyle = (verb: string, style: ArrayLike<number> | undefined): void => {
466
+ if (style === undefined) return
467
+ if (shapes.length === 0) throw new Error(`${verb}: the layer's material declares no instance buffer (no style record)`)
468
+ }
469
+ // Write a cell's style record into its chunk's first style stream.
470
+ let writeStyle = (chunk: Chunk, slot: number, style: ArrayLike<number>, verb: string): void => {
471
+ let s = chunk.styles[0]!
472
+ writeRecord(shapes[0]!.layout, s.fields, s.floats, slot, style, verb)
473
+ }
394
474
 
395
475
  let layer: TileLayer = {
396
476
  atlases,
477
+ material,
397
478
  cols,
398
479
  rows,
399
480
  tileW,
@@ -432,9 +513,11 @@ export function createTileLayer(
432
513
  } else {
433
514
  if (!isFrame(frame)) throw new Error(`setTile: not a frame, got ${JSON.stringify(frame)}`)
434
515
  if (opts?.tint !== undefined) checkTint("setTile", opts.tint)
516
+ checkStyle("setTile", opts?.style)
435
517
  let atlas = frameIndex("setTile", atlasIndex, frame)
436
518
  chunk ??= allocate(index)
437
519
  writeCell(chunk.records, at, col, row, frame.u0, frame.v0, frame.u1, frame.v1, atlas, opts?.tint)
520
+ if (opts?.style !== undefined) writeStyle(chunk, at / INSTANCE_FLOATS, opts.style, "setTile")
438
521
  }
439
522
  touch(chunk)
440
523
  },
@@ -443,7 +526,9 @@ export function createTileLayer(
443
526
  checkRect("setTiles", col, row, w, h, cols, rows)
444
527
  if (cells.length !== w * h) throw new Error(`setTiles: a ${w} x ${h} rect takes ${w * h} cells, got ${cells.length}`)
445
528
  if (opts?.tint !== undefined) checkTint("setTiles", opts.tint)
529
+ checkStyle("setTiles", opts?.style)
446
530
  let cellTint = opts?.tint
531
+ let cellStyle = opts?.style
447
532
  // The two forms: indices into the table, or frames. Every entry is
448
533
  // checked (and a frame resolved to its atlas) before the first write,
449
534
  // so a bad one changes nothing.
@@ -512,6 +597,7 @@ export function createTileLayer(
512
597
  r = chunk.records
513
598
  }
514
599
  writeCell(r, at, x, y, u0, v0, u1, v1, atlas, cellTint)
600
+ if (cellStyle !== undefined) writeStyle(chunk!, at / INSTANCE_FLOATS, cellStyle, "setTiles")
515
601
  }
516
602
  }
517
603
  if (chunk) touch(chunk)
@@ -536,6 +622,15 @@ export function createTileLayer(
536
622
  touch(chunk, false)
537
623
  }
538
624
  },
625
+ setParams(next) {
626
+ if (disposed) return
627
+ checkParams("setParams", next)
628
+ Object.assign(params, next)
629
+ for (let chunk of resident.values()) {
630
+ setTargetParams(chunk.texture, next)
631
+ touch(chunk, false)
632
+ }
633
+ },
539
634
  dispose() {
540
635
  if (disposed) return
541
636
  disposed = true
@@ -543,10 +638,10 @@ export function createTileLayer(
543
638
  for (let chunk of resident.values()) {
544
639
  destroyTexture(chunk.texture)
545
640
  destroyBuffer(chunk.buffer)
641
+ for (let style of chunk.styles) destroyBuffer(style.buffer)
546
642
  }
547
643
  resident.clear()
548
644
  layer.chunks.length = 0
549
- gpu.dispose()
550
645
  },
551
646
  }
552
647
 
package/src/views.ts CHANGED
@@ -18,14 +18,13 @@
18
18
  // a sprite under a minimap gets its ordinary handlers, and the view's
19
19
  // listeners are the last stop.
20
20
  import { addDraw, createDrawTarget, destroyTexture, removeDraw, setDrawBuffers, setDrawRange, setTargetParams, setTargetSize } from "@solidrt/core/gpu"
21
- import type { BufferId, DrawId, InstanceOrder, RenderPipelineId, TextureId } from "@solidrt/core/gpu"
21
+ import type { BufferId, DrawId, InstanceOrder, RenderPipelineId, ShaderParams, TextureBindings, TextureId } from "@solidrt/core/gpu"
22
22
  import { applyCamera, cameraParams, checkCamera, defaultCamera, projectCamera, unprojectCamera } from "./camera.ts"
23
23
  import type { CameraState, CameraUpdate } from "./camera.ts"
24
24
  import { spriteDispatch } from "./dispatch.ts"
25
25
  import type { PointerFeed } from "@solidrt/core"
26
26
  import type { LayerPointerListener, Sprite, LayerHandlers } from "./layer.ts"
27
27
  import { checkOversample, thrashSentinel } from "./oversample.ts"
28
- import { atlasBindings } from "./shaders.ts"
29
28
 
30
29
  export type ViewOptions = {
31
30
  /** View pixels: the viewport its camera maps the world onto. */
@@ -111,16 +110,20 @@ export type ViewDeps = {
111
110
  label: string
112
111
  pipeline: RenderPipelineId
113
112
  quad: BufferId
114
- /** The layer's atlas textures in sampler order (the pipeline was
115
- * compiled for exactly this many). */
116
- atlases: readonly TextureId[]
113
+ /** The sampler bindings every view binds: the layer's atlases by
114
+ * sampler index (the pipeline was compiled for exactly those) and the
115
+ * material's own textures. */
116
+ textures: TextureBindings
117
117
  /** The layer's current instance buffers, in the pipeline's layout order
118
- * after the quad (the pose and style pair on the node layer, the one
119
- * record buffer on records). */
118
+ * after the quad (the pose and sprite pair on the node layer, the one
119
+ * record buffer on records, the material's style buffers after them). */
120
120
  buffers: () => BufferId[]
121
121
  /** The instance count as last published. */
122
122
  count: () => number
123
123
  tint: () => [number, number, number, number]
124
+ /** The params every view seeds beyond the viewport, the camera and
125
+ * the tint: the material's, with the layer's own over them. */
126
+ params: () => ShaderParams
124
127
  /** The layer's pick, `zoom` the camera zoom its floors are measured at. */
125
128
  pick: (x: number, y: number, zoom: number) => Sprite[]
126
129
  /** The layer's key order (`orderBy`), declared by ONE live entry. */
@@ -135,6 +138,8 @@ export type Views = {
135
138
  setBuffers(instanceBuffers: BufferId[]): void
136
139
  setCount(count: number): void
137
140
  setTint(tint: [number, number, number, number]): void
141
+ /** The layer's params changed: every view's target takes the write. */
142
+ setParams(params: ShaderParams): void
138
143
  dispose(): void
139
144
  }
140
145
 
@@ -180,8 +185,8 @@ export function createViews(deps: ViewDeps): Views {
180
185
  let texture = createDrawTarget(
181
186
  width * oversample,
182
187
  height * oversample,
183
- { uViewport: [width, height], ...cameraParams(cam), uTint: deps.tint() },
184
- { textures: atlasBindings(deps.atlases), clearColor: opts.clearColor ?? [0, 0, 0, 0], label, autoFree: false },
188
+ { uViewport: [width, height], ...cameraParams(cam), uTint: deps.tint(), ...deps.params() },
189
+ { textures: deps.textures, clearColor: opts.clearColor ?? [0, 0, 0, 0], label, autoFree: false },
185
190
  )
186
191
  let order = ordered === null ? deps.order : undefined
187
192
  let entry = entryFor(texture, order)
@@ -277,6 +282,9 @@ export function createViews(deps: ViewDeps): Views {
277
282
  setTint(tint) {
278
283
  for (let v of views) setTargetParams(v.texture, { uTint: tint })
279
284
  },
285
+ setParams(params) {
286
+ for (let v of views) setTargetParams(v.texture, params)
287
+ },
280
288
  dispose() {
281
289
  for (let v of [...views]) v.dispose()
282
290
  },