@aethervtt/shard-sprite 0.0.1

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/dist/render.js ADDED
@@ -0,0 +1,1216 @@
1
+ import {
2
+ defineResource,
3
+ defineSystem,
4
+ onRemove
5
+ } from "@aethervtt/shard-core";
6
+ import { GpuBuffer } from "@aethervtt/shard-gpu";
7
+ import {
8
+ ComputedVisibility,
9
+ cameraOf,
10
+ Gpu,
11
+ GpuAssetsResource,
12
+ PICK_TARGETS,
13
+ RenderPhase,
14
+ Shaders,
15
+ sceneColor
16
+ } from "@aethervtt/shard-render";
17
+ import { Time } from "@aethervtt/shard-runtime";
18
+ import { Textures } from "@aethervtt/shard-texture";
19
+ import { GlobalTransform } from "@aethervtt/shard-transform";
20
+ import { TextureAtlases } from "./atlas.js";
21
+ import { layerBit, SpriteLighting } from "./lighting.js";
22
+ import {
23
+ describeLights2d,
24
+ lights2dGroup,
25
+ lights2dLayout,
26
+ litView
27
+ } from "./lights2d.js";
28
+ import { Sprite, Sprite2dSettings, SpriteSlot } from "./sprite.js";
29
+ import { Tilemap, TilemapDatas } from "./tilemap.js";
30
+ const SPRITE_FLOATS = 24;
31
+ const SPRITE_BYTES = SPRITE_FLOATS * 4;
32
+ const BLEND_ALPHA = 0;
33
+ const BLEND_ADDITIVE = 1;
34
+ const BLEND_OPAQUE = 2;
35
+ const SPACE_WORLD = 0;
36
+ const SPACE_SCREEN = 1;
37
+ const f32 = Math.fround;
38
+ const orderScratch = new Float32Array(1);
39
+ const orderBits = new Uint32Array(orderScratch.buffer);
40
+ function orderable(v) {
41
+ orderScratch[0] = v;
42
+ const b = orderBits[0];
43
+ return (b & 2147483648 ? ~b : b | 2147483648) >>> 0;
44
+ }
45
+ class SpriteStore {
46
+ capacity = 0;
47
+ high = 0;
48
+ f32 = new Float32Array(0);
49
+ u32 = new Uint32Array(0);
50
+ /** Batch per slot, or -1 (hidden, freed, or waiting on an asset). */
51
+ batchOf = new Int32Array(0);
52
+ keyHi = new Uint32Array(0);
53
+ keyLo = new Uint32Array(0);
54
+ entities = new Float64Array(0);
55
+ free = [];
56
+ dirty = new Uint8Array(0);
57
+ dirtyLo = Number.POSITIVE_INFINITY;
58
+ dirtyHi = -1;
59
+ /** Slots waiting for a texture or atlas to load. */
60
+ pending = /* @__PURE__ */ new Set();
61
+ batches = [];
62
+ byTexture = /* @__PURE__ */ new Map();
63
+ order = new Uint32Array(0);
64
+ orderCount = 0;
65
+ orderDirty = true;
66
+ runs = [];
67
+ runCount = 0;
68
+ /** Sprites per layer in the last sort. */
69
+ perLayer = /* @__PURE__ */ new Map();
70
+ /** Bytes of sprite records uploaded this frame. */
71
+ uploadedBytes = 0;
72
+ sorts = 0;
73
+ records;
74
+ orderBuffer;
75
+ orderUploaded = true;
76
+ /** Hidden sprites per ECS table id, from the last visit. */
77
+ tableSeen = [];
78
+ gpu;
79
+ generation;
80
+ constructor(gpu) {
81
+ this.gpu = gpu;
82
+ this.generation = gpu.generation;
83
+ const storage = GPUBufferUsage.STORAGE;
84
+ this.records = new GpuBuffer(gpu, {
85
+ label: "sprites",
86
+ usage: storage,
87
+ size: SPRITE_BYTES * 256
88
+ });
89
+ this.orderBuffer = new GpuBuffer(gpu, { label: "sprites/order", usage: storage, size: 1024 });
90
+ this.grow(256);
91
+ }
92
+ get live() {
93
+ return this.high - this.free.length;
94
+ }
95
+ grow(capacity) {
96
+ const f = new Float32Array(capacity * SPRITE_FLOATS);
97
+ f.set(this.f32);
98
+ this.f32 = f;
99
+ this.u32 = new Uint32Array(f.buffer);
100
+ const grow32 = (a, make) => {
101
+ const next = make(capacity);
102
+ next.set(a);
103
+ return next;
104
+ };
105
+ this.batchOf = grow32(this.batchOf, (n) => new Int32Array(n).fill(-1));
106
+ this.keyHi = grow32(this.keyHi, (n) => new Uint32Array(n));
107
+ this.keyLo = grow32(this.keyLo, (n) => new Uint32Array(n));
108
+ this.entities = grow32(this.entities, (n) => new Float64Array(n));
109
+ this.dirty = grow32(this.dirty, (n) => new Uint8Array(n));
110
+ this.capacity = capacity;
111
+ }
112
+ alloc(entity) {
113
+ let slot = this.free.pop();
114
+ if (slot === void 0) {
115
+ if (this.high >= this.capacity) this.grow(this.capacity * 2);
116
+ slot = this.high++;
117
+ }
118
+ this.entities[slot] = entity;
119
+ this.batchOf[slot] = -1;
120
+ return slot;
121
+ }
122
+ release(slot) {
123
+ this.assign(slot, -1);
124
+ this.pending.delete(slot);
125
+ this.free.push(slot);
126
+ }
127
+ markDirty(slot) {
128
+ if (this.dirty[slot]) return;
129
+ this.dirty[slot] = 1;
130
+ if (slot < this.dirtyLo) this.dirtyLo = slot;
131
+ if (slot > this.dirtyHi) this.dirtyHi = slot;
132
+ }
133
+ /** The batch for a texture, normal map, blend, and space, made on first use. */
134
+ batchFor(texture, blend, space, normal = null) {
135
+ let list = this.byTexture.get(texture);
136
+ if (!list) {
137
+ list = [];
138
+ this.byTexture.set(texture, list);
139
+ }
140
+ for (const b of list)
141
+ if (b.blend === blend && b.space === space && b.normal === normal) return b;
142
+ const batch = {
143
+ index: this.batches.length,
144
+ texture,
145
+ normal,
146
+ blend,
147
+ space,
148
+ count: 0
149
+ };
150
+ this.batches.push(batch);
151
+ list.push(batch);
152
+ return batch;
153
+ }
154
+ assign(slot, batch) {
155
+ const old = this.batchOf[slot];
156
+ if (old === batch) return;
157
+ if (old >= 0) this.batches[old].count--;
158
+ if (batch >= 0) this.batches[batch].count++;
159
+ this.batchOf[slot] = batch;
160
+ this.orderDirty = true;
161
+ }
162
+ setKey(slot, hi, lo) {
163
+ if (this.keyHi[slot] === hi && this.keyLo[slot] === lo) return;
164
+ this.keyHi[slot] = hi;
165
+ this.keyLo[slot] = lo;
166
+ if (this.batchOf[slot] >= 0) this.orderDirty = true;
167
+ }
168
+ // Radix sort scratch: two ping-pong index lists and a 16-bit histogram.
169
+ sortA = new Uint32Array(0);
170
+ sortB = new Uint32Array(0);
171
+ counts = new Uint32Array(65536);
172
+ /** Sorts drawable slots by (keyHi, keyLo): four 16-bit LSD radix passes. Then builds runs. */
173
+ sort() {
174
+ this.orderDirty = false;
175
+ this.sorts++;
176
+ let n = 0;
177
+ if (this.sortA.length < this.high) {
178
+ this.sortA = new Uint32Array(this.capacity);
179
+ this.sortB = new Uint32Array(this.capacity);
180
+ }
181
+ for (let s = 0; s < this.high; s++) if (this.batchOf[s] >= 0) this.sortA[n++] = s;
182
+ let src = this.sortA;
183
+ let dst = this.sortB;
184
+ const counts = this.counts;
185
+ for (let pass = 0; pass < 4; pass++) {
186
+ const keys = pass < 2 ? this.keyLo : this.keyHi;
187
+ const shift = (pass & 1) * 16;
188
+ counts.fill(0);
189
+ for (let i = 0; i < n; i++) counts[keys[src[i]] >>> shift & 65535]++;
190
+ let sum = 0;
191
+ for (let k = 0; k < 65536; k++) {
192
+ const c = counts[k];
193
+ counts[k] = sum;
194
+ sum += c;
195
+ }
196
+ for (let i = 0; i < n; i++) {
197
+ const slot = src[i];
198
+ dst[counts[keys[slot] >>> shift & 65535]++] = slot;
199
+ }
200
+ const t = src;
201
+ src = dst;
202
+ dst = t;
203
+ }
204
+ if (this.order.length < n) this.order = new Uint32Array(Math.max(1024, n * 2));
205
+ this.order.set(src.subarray(0, n));
206
+ this.orderCount = n;
207
+ this.orderUploaded = false;
208
+ this.runCount = 0;
209
+ this.perLayer.clear();
210
+ let current = -1;
211
+ for (let i = 0; i < n; i++) {
212
+ const slot = this.order[i];
213
+ const b = this.batchOf[slot];
214
+ const layer = (this.keyHi[slot] >>> 16) - 32768;
215
+ this.perLayer.set(layer, (this.perLayer.get(layer) ?? 0) + 1);
216
+ if (b !== current || this.runs[this.runCount - 1].layer !== layer) {
217
+ let run = this.runs[this.runCount];
218
+ if (!run) {
219
+ run = { batch: this.batches[b], first: i, count: 0, layer };
220
+ this.runs.push(run);
221
+ }
222
+ run.batch = this.batches[b];
223
+ run.first = i;
224
+ run.count = 0;
225
+ run.layer = layer;
226
+ this.runCount++;
227
+ current = b;
228
+ }
229
+ this.runs[this.runCount - 1].count++;
230
+ }
231
+ }
232
+ /** Uploads dirty records in coalesced runs, and the order when it changed. */
233
+ upload() {
234
+ if (this.generation !== this.gpu.generation) {
235
+ this.generation = this.gpu.generation;
236
+ this.dirtyLo = 0;
237
+ this.dirtyHi = this.high - 1;
238
+ this.dirty.fill(1, 0, this.high);
239
+ this.orderUploaded = false;
240
+ }
241
+ if (this.records.ensureCapacity(this.capacity * SPRITE_BYTES) && this.high > 0) {
242
+ this.dirtyLo = 0;
243
+ this.dirtyHi = this.high - 1;
244
+ this.dirty.fill(1, 0, this.high);
245
+ }
246
+ let bytes = 0;
247
+ let start = -1;
248
+ for (let s = this.dirtyLo; s <= this.dirtyHi + 1; s++) {
249
+ if (s <= this.dirtyHi && this.dirty[s] === 1) {
250
+ this.dirty[s] = 0;
251
+ if (start < 0) start = s;
252
+ } else if (start >= 0) {
253
+ const n = s - start;
254
+ this.records.write(this.f32, start * SPRITE_BYTES, start * SPRITE_FLOATS, n * SPRITE_FLOATS);
255
+ bytes += n * SPRITE_BYTES;
256
+ start = -1;
257
+ }
258
+ }
259
+ this.dirtyLo = Number.POSITIVE_INFINITY;
260
+ this.dirtyHi = -1;
261
+ if (!this.orderUploaded && this.orderCount > 0) {
262
+ this.orderBuffer.write(this.order, 0, 0, this.orderCount);
263
+ bytes += this.orderCount * 4;
264
+ }
265
+ this.orderUploaded = true;
266
+ this.uploadedBytes = bytes;
267
+ }
268
+ }
269
+ const Sprites = defineResource("sprite/Sprites", {
270
+ description: "The sprite renderer: persistent sprite records, batches, and the sorted draw order."
271
+ });
272
+ const packScratch = new Uint8Array(4);
273
+ const packView = new Uint32Array(packScratch.buffer);
274
+ const halfScratch = new Float32Array(1);
275
+ const halfBits = new Uint32Array(halfScratch.buffer);
276
+ function half(v) {
277
+ halfScratch[0] = v;
278
+ const x = halfBits[0];
279
+ const sign = x >>> 16 & 32768;
280
+ const e = (x >>> 23 & 255) - 127 + 15;
281
+ if (e <= 0) return sign;
282
+ if (e >= 31) return sign | 31743;
283
+ return sign | e << 10 | (x >>> 13 & 1023) + (x >>> 12 & 1);
284
+ }
285
+ function packColor(c, o) {
286
+ for (let k = 0; k < 4; k++) packScratch[k] = Math.round(Math.min(1, Math.max(0, c[o + k])) * 255);
287
+ return packView[0];
288
+ }
289
+ function columns(table) {
290
+ return {
291
+ texture: table.column(Sprite, "texture"),
292
+ atlas: table.column(Sprite, "atlas"),
293
+ region: table.column(Sprite, "region"),
294
+ color: table.column(Sprite, "color"),
295
+ flipX: table.column(Sprite, "flipX"),
296
+ flipY: table.column(Sprite, "flipY"),
297
+ anchor: table.column(Sprite, "anchor"),
298
+ size: table.column(Sprite, "size"),
299
+ layer: table.column(Sprite, "layer"),
300
+ blend: table.column(Sprite, "blend"),
301
+ space: table.column(Sprite, "space"),
302
+ lit: table.column(Sprite, "lit"),
303
+ g: table.column(GlobalTransform, "matrix"),
304
+ visible: table.column(ComputedVisibility, "visible"),
305
+ normal: table.has(SpriteLighting) ? table.column(SpriteLighting, "normal") : void 0,
306
+ emissive: table.has(SpriteLighting) ? table.column(SpriteLighting, "emissive") : void 0,
307
+ normalStrength: table.has(SpriteLighting) ? table.column(SpriteLighting, "normalStrength") : void 0
308
+ };
309
+ }
310
+ function lightFlags(lit, normal, layer) {
311
+ return (lit ? 1 : 0) | (normal ? 2 : 0) | 31 - Math.clz32(layerBit(layer)) << 8;
312
+ }
313
+ function extractSprite(world, store, slot, c, i) {
314
+ const atlases = world.resource(TextureAtlases);
315
+ const textures = world.resource(Textures);
316
+ const settings = world.resource(Sprite2dSettings);
317
+ const f = store.f32;
318
+ const o = slot * SPRITE_FLOATS;
319
+ let texture;
320
+ let rx = 0;
321
+ let ry = 0;
322
+ let rw = 0;
323
+ let rh = 0;
324
+ let ax = c.anchor[i * 2];
325
+ let ay = c.anchor[i * 2 + 1];
326
+ let normal = null;
327
+ const atlasRef = c.atlas[i];
328
+ if (atlasRef && (atlasRef.guid || atlasRef.path) && c.region[i]) {
329
+ const atlas = atlases.get(atlasRef);
330
+ if (!atlas) return false;
331
+ const r = atlas.region(c.region[i]);
332
+ texture = textures.get(atlas.texture);
333
+ if (!texture) return false;
334
+ if (atlas.normals) {
335
+ normal = textures.get(atlas.normals) ?? null;
336
+ if (!normal) return false;
337
+ }
338
+ if (r < 0) {
339
+ store.assign(slot, -1);
340
+ return true;
341
+ }
342
+ rx = atlas.rects[r * 4];
343
+ ry = atlas.rects[r * 4 + 1];
344
+ rw = atlas.rects[r * 4 + 2];
345
+ rh = atlas.rects[r * 4 + 3];
346
+ const px = atlas.pivots[r * 2];
347
+ const py = atlas.pivots[r * 2 + 1];
348
+ if (px !== 0.5 || py !== 0.5) {
349
+ ax = px;
350
+ ay = py;
351
+ }
352
+ } else {
353
+ const ref = c.texture[i];
354
+ if (!ref || !ref.guid && !ref.path) {
355
+ store.assign(slot, -1);
356
+ return true;
357
+ }
358
+ texture = textures.get(ref);
359
+ if (!texture) return false;
360
+ rw = texture.width;
361
+ rh = texture.height;
362
+ }
363
+ const normalRef = c.normal?.[i];
364
+ if (normalRef && (normalRef.guid || normalRef.path)) {
365
+ normal = textures.get(normalRef) ?? null;
366
+ if (!normal) return false;
367
+ }
368
+ const tw = texture.width;
369
+ const th = texture.height;
370
+ let u0 = rx / tw;
371
+ let u1 = (rx + rw) / tw;
372
+ let v0 = ry / th;
373
+ let v1 = (ry + rh) / th;
374
+ if (c.flipX[i]) {
375
+ const t = u0;
376
+ u0 = u1;
377
+ u1 = t;
378
+ }
379
+ if (c.flipY[i]) {
380
+ const t = v0;
381
+ v0 = v1;
382
+ v1 = t;
383
+ }
384
+ const space = c.space[i];
385
+ let w = c.size[i * 2];
386
+ let h = c.size[i * 2 + 1];
387
+ if (w === 0 && h === 0) {
388
+ const ppu = space === SPACE_SCREEN ? 1 : settings.pixelsPerUnit;
389
+ w = rw / ppu;
390
+ h = rh / ppu;
391
+ }
392
+ const g = c.g;
393
+ for (let k = 0; k < 12; k++) f[o + k] = g[i * 12 + k];
394
+ f[o + 12] = u0;
395
+ f[o + 13] = v0;
396
+ f[o + 14] = u1;
397
+ f[o + 15] = v1;
398
+ f[o + 16] = w;
399
+ f[o + 17] = h;
400
+ f[o + 18] = ax;
401
+ f[o + 19] = ay;
402
+ store.u32[o + 20] = packColor(c.color, i * 4);
403
+ store.u32[o + 21] = lightFlags(c.lit[i] !== 0, normal !== null, c.layer[i]);
404
+ store.u32[o + 22] = store.entities[slot] % 4294967296;
405
+ store.u32[o + 23] = (half(c.emissive ? c.emissive[i] : 0) | half(c.normalStrength ? c.normalStrength[i] : 1) << 16) >>> 0;
406
+ store.markDirty(slot);
407
+ const blend = c.blend[i];
408
+ const batch = c.visible[i] ? store.batchFor(texture, blend, space, normal).index : -1;
409
+ store.assign(slot, batch);
410
+ const depth = settings.sort === "y" ? -g[i * 12 + 7] : g[i * 12 + 11];
411
+ const d = orderable(f32(depth));
412
+ store.setKey(
413
+ slot,
414
+ (c.layer[i] + 32768 << 16 | d >>> 16) >>> 0,
415
+ ((d & 65535) << 16 | batch & 65535) >>> 0
416
+ );
417
+ return true;
418
+ }
419
+ function tableChanged(table, since) {
420
+ return table.lastStructural > since || table.lastChanged(Sprite) > since || table.has(SpriteLighting) && table.lastChanged(SpriteLighting) > since || table.lastChanged(GlobalTransform) > since || table.lastChanged(ComputedVisibility) > since || table.lastChanged(SpriteSlot) > since;
421
+ }
422
+ const prepareSprites = defineSystem({
423
+ name: "sprite/prepare",
424
+ description: "Writes changed sprites into the sprite buffer, sorts, and uploads.",
425
+ setup: (world) => ({
426
+ q: world.query({ with: [Sprite, GlobalTransform, ComputedVisibility, SpriteSlot] }),
427
+ atlasVersions: /* @__PURE__ */ new Map(),
428
+ textureVersions: /* @__PURE__ */ new Map()
429
+ }),
430
+ run: ({ q, atlasVersions, textureVersions }, world, ctx) => {
431
+ const store = world.resource(Sprites);
432
+ const since = ctx.lastRunTick;
433
+ let force = false;
434
+ for (const [guid, atlas] of world.resource(TextureAtlases).entries()) {
435
+ if (atlasVersions.get(guid) !== atlas.version) {
436
+ if (atlasVersions.has(guid)) force = true;
437
+ atlasVersions.set(guid, atlas.version);
438
+ }
439
+ }
440
+ for (const batch of store.batches) {
441
+ const v = textureVersions.get(batch.texture);
442
+ if (v !== void 0 && v !== batch.texture.version) force = true;
443
+ textureVersions.set(batch.texture, batch.texture.version);
444
+ }
445
+ for (const table of q.tables) {
446
+ const n = table.count;
447
+ if (n === 0) continue;
448
+ if (!force && store.tableSeen[table.id] === 1 && !tableChanged(table, since)) continue;
449
+ store.tableSeen[table.id] = 1;
450
+ const slots = table.column(SpriteSlot, "slot");
451
+ const sChanged = table.changedTicks(Sprite);
452
+ const gChanged = table.changedTicks(GlobalTransform);
453
+ const vChanged = table.changedTicks(ComputedVisibility);
454
+ const lChanged = table.has(SpriteLighting) ? table.changedTicks(SpriteLighting) : void 0;
455
+ const c = columns(table);
456
+ for (let i = 0; i < n; i++) {
457
+ let slot = slots[i] - 1;
458
+ let fresh = false;
459
+ if (slot < 0) {
460
+ slot = store.alloc(table.entities[i]);
461
+ slots[i] = slot + 1;
462
+ fresh = true;
463
+ }
464
+ if (!fresh && !force && sChanged[i] <= since && gChanged[i] <= since && vChanged[i] <= since && (lChanged === void 0 || lChanged[i] <= since))
465
+ continue;
466
+ if (extractSprite(world, store, slot, c, i)) store.pending.delete(slot);
467
+ else {
468
+ store.assign(slot, -1);
469
+ store.pending.add(slot);
470
+ }
471
+ }
472
+ }
473
+ if (store.pending.size > 0) {
474
+ for (const slot of store.pending) {
475
+ const entity = store.entities[slot];
476
+ if (!world.isAlive(entity)) {
477
+ store.pending.delete(slot);
478
+ continue;
479
+ }
480
+ const table = world.entityTable(entity);
481
+ if (extractSprite(world, store, slot, columns(table), world.entityRow(entity))) {
482
+ store.pending.delete(slot);
483
+ }
484
+ }
485
+ }
486
+ if (store.orderDirty) store.sort();
487
+ store.upload();
488
+ syncTilemaps(world);
489
+ }
490
+ });
491
+ function observeSpriteRemovals(world) {
492
+ world.observe(onRemove(Sprite), ({ entity, world: world2 }) => {
493
+ if (!world2.has(entity, SpriteSlot)) return;
494
+ const store = world2.tryResource(Sprites);
495
+ const value = world2.get(entity, SpriteSlot);
496
+ if (store && value && value.slot > 0) store.release(value.slot - 1);
497
+ world2.entityTable(entity).column(SpriteSlot, "slot")[world2.entityRow(entity)] = 0;
498
+ });
499
+ }
500
+ class TilemapStore {
501
+ maps = /* @__PURE__ */ new Map();
502
+ chunkUploads = 0;
503
+ /** Per view: chunks drawn, chunks in total. */
504
+ drawn = /* @__PURE__ */ new Map();
505
+ }
506
+ const Tilemaps = defineResource("sprite/Tilemaps", {
507
+ description: "Tilemaps on the GPU: chunked tile buffers, and what was drawn per view.",
508
+ init: () => new TilemapStore()
509
+ });
510
+ const paramScratch = new Float32Array(20);
511
+ const paramU32 = new Uint32Array(paramScratch.buffer);
512
+ function uploadChunk(layer, src, cs, chunk, scratch) {
513
+ const cx = chunk % layer.chunksX;
514
+ const cy = Math.floor(chunk / layer.chunksX);
515
+ for (let ly = 0; ly < cs; ly++) {
516
+ const y = cy * cs + ly;
517
+ for (let lx = 0; lx < cs; lx++) {
518
+ const x = cx * cs + lx;
519
+ const inside = x < src.width && y < src.height;
520
+ const i = y * src.width + x;
521
+ scratch[ly * cs + lx] = inside ? (src.tiles[i] | src.flags[i] << 16) >>> 0 : 0;
522
+ }
523
+ }
524
+ layer.buffer.write(scratch, chunk * cs * cs * 4, 0, cs * cs);
525
+ }
526
+ let chunkScratch = new Uint32Array(1024);
527
+ function syncTilemaps(world) {
528
+ const gpu = world.resource(Gpu);
529
+ const tilemaps = world.resource(Tilemaps);
530
+ const datas = world.resource(TilemapDatas);
531
+ const atlases = world.resource(TextureAtlases);
532
+ const textures = world.resource(Textures);
533
+ const time = world.resource(Time).elapsed;
534
+ tilemaps.chunkUploads = 0;
535
+ const seen = /* @__PURE__ */ new Set();
536
+ for (const [entity, value, g] of queryTilemaps(world)) {
537
+ seen.add(entity);
538
+ const data = datas.get(value.data);
539
+ const atlas = atlases.get(value.atlas);
540
+ const texture = atlas ? textures.get(atlas.texture) : void 0;
541
+ if (!data || !atlas || !texture) continue;
542
+ const normal = atlas.normals ? textures.get(atlas.normals) ?? null : null;
543
+ if (atlas.normals && !normal) continue;
544
+ const cs = value.chunkSize;
545
+ let map = tilemaps.maps.get(entity);
546
+ if (!map || map.data !== data || map.version !== data.version || map.chunkSize !== cs) {
547
+ map = {
548
+ entity,
549
+ data,
550
+ version: data.version,
551
+ chunkSize: cs,
552
+ layers: data.layers.map((l, i) => {
553
+ const chunksX = Math.ceil(l.width / cs);
554
+ const chunksY = Math.ceil(l.height / cs);
555
+ return {
556
+ buffer: new GpuBuffer(gpu, {
557
+ label: `tilemap/${entity}/${i}`,
558
+ usage: GPUBufferUsage.STORAGE,
559
+ size: Math.max(16, chunksX * chunksY * cs * cs * 4)
560
+ }),
561
+ chunksX,
562
+ chunksY,
563
+ cursor: 0,
564
+ base: -1,
565
+ params: new GpuBuffer(gpu, {
566
+ label: `tilemap/${entity}/${i}/params`,
567
+ usage: GPUBufferUsage.UNIFORM,
568
+ size: 80
569
+ })
570
+ };
571
+ }),
572
+ regions: map?.regions ?? new GpuBuffer(gpu, {
573
+ label: `tilemap/${entity}/regions`,
574
+ usage: GPUBufferUsage.STORAGE,
575
+ size: 256
576
+ }),
577
+ regionKey: "",
578
+ remap: map?.remap ?? new GpuBuffer(gpu, {
579
+ label: `tilemap/${entity}/remap`,
580
+ usage: GPUBufferUsage.STORAGE,
581
+ size: 256
582
+ }),
583
+ remapData: new Uint32Array(0),
584
+ layer: 0,
585
+ tileSize: [1, 1],
586
+ affine: new Float32Array(12),
587
+ bounds: [],
588
+ atlas: null,
589
+ texture: void 0,
590
+ normal: null,
591
+ lit: true
592
+ };
593
+ tilemaps.maps.set(entity, map);
594
+ }
595
+ map.layer = value.layer;
596
+ map.tileSize = [value.tileSize[0], value.tileSize[1]];
597
+ map.affine.set(g);
598
+ map.atlas = value.atlas;
599
+ map.texture = texture;
600
+ map.normal = normal;
601
+ map.lit = value.lit;
602
+ if (chunkScratch.length < cs * cs) chunkScratch = new Uint32Array(cs * cs);
603
+ data.layers.forEach((l, li) => {
604
+ const layer = map.layers[li];
605
+ if (layer.base !== l.editBase) {
606
+ for (let chunk = 0; chunk < layer.chunksX * layer.chunksY; chunk++) {
607
+ uploadChunk(layer, l, cs, chunk, chunkScratch);
608
+ }
609
+ tilemaps.chunkUploads += layer.chunksX * layer.chunksY;
610
+ layer.base = l.editBase;
611
+ layer.cursor = l.edits.length;
612
+ } else if (layer.cursor < l.edits.length) {
613
+ const chunks = /* @__PURE__ */ new Set();
614
+ for (let k = layer.cursor; k < l.edits.length; k++) {
615
+ const t = l.edits[k];
616
+ const x = t % l.width;
617
+ const y = Math.floor(t / l.width);
618
+ chunks.add(Math.floor(y / cs) * layer.chunksX + Math.floor(x / cs));
619
+ }
620
+ for (const chunk of chunks) uploadChunk(layer, l, cs, chunk, chunkScratch);
621
+ tilemaps.chunkUploads += chunks.size;
622
+ layer.cursor = l.edits.length;
623
+ }
624
+ paramScratch.set(g, 0);
625
+ paramScratch[12] = map.tileSize[0];
626
+ paramScratch[13] = map.tileSize[1];
627
+ paramScratch[14] = l.width;
628
+ paramScratch[15] = l.height;
629
+ paramU32[16] = cs;
630
+ paramU32[17] = layer.chunksX;
631
+ paramU32[18] = lightFlags(map.lit, normal !== null, value.layer);
632
+ paramU32[19] = 0;
633
+ layer.params.write(paramScratch);
634
+ const n = layer.chunksX * layer.chunksY;
635
+ let b = map.bounds[li];
636
+ if (!b || b.length !== n * 6) {
637
+ b = new Float32Array(n * 6);
638
+ map.bounds[li] = b;
639
+ }
640
+ for (let chunk = 0; chunk < n; chunk++) {
641
+ const cx = chunk % layer.chunksX;
642
+ const cy = Math.floor(chunk / layer.chunksX);
643
+ const x0 = cx * cs * map.tileSize[0];
644
+ const x1 = Math.min((cx + 1) * cs, l.width) * map.tileSize[0];
645
+ const y0 = -Math.min((cy + 1) * cs, l.height) * map.tileSize[1];
646
+ const y1 = -cy * cs * map.tileSize[1];
647
+ chunkBounds(g, x0, y0, x1, y1, b, chunk * 6);
648
+ }
649
+ });
650
+ const key = `${atlas.version}/${texture.width}x${texture.height}/${atlas.count}`;
651
+ if (map.regionKey !== key) {
652
+ const uv = new Float32Array(Math.max(4, atlas.count * 4));
653
+ for (let r = 0; r < atlas.count; r++) {
654
+ uv[r * 4] = atlas.rects[r * 4] / texture.width;
655
+ uv[r * 4 + 1] = atlas.rects[r * 4 + 1] / texture.height;
656
+ uv[r * 4 + 2] = (atlas.rects[r * 4] + atlas.rects[r * 4 + 2]) / texture.width;
657
+ uv[r * 4 + 3] = (atlas.rects[r * 4 + 1] + atlas.rects[r * 4 + 3]) / texture.height;
658
+ }
659
+ map.regions.write(uv);
660
+ map.regionKey = key;
661
+ map.remapData = new Uint32Array(atlas.count + 1);
662
+ for (let t = 0; t < map.remapData.length; t++) map.remapData[t] = t;
663
+ map.remap.write(map.remapData);
664
+ }
665
+ if (data.animations.length > 0) {
666
+ let changed = false;
667
+ for (const a of data.animations) {
668
+ if (a.frames.length === 0 || a.tile >= map.remapData.length) continue;
669
+ const frame = Math.min(
670
+ a.frames[Math.floor(time / a.frameTime) % a.frames.length],
671
+ atlas.count
672
+ );
673
+ if (map.remapData[a.tile] !== frame) {
674
+ map.remapData[a.tile] = frame;
675
+ changed = true;
676
+ }
677
+ }
678
+ if (changed) map.remap.write(map.remapData);
679
+ }
680
+ }
681
+ for (const entity of tilemaps.maps.keys()) if (!seen.has(entity)) tilemaps.maps.delete(entity);
682
+ }
683
+ function chunkBounds(g, x0, y0, x1, y1, out, o) {
684
+ out[o] = out[o + 1] = out[o + 2] = Number.POSITIVE_INFINITY;
685
+ out[o + 3] = out[o + 4] = out[o + 5] = Number.NEGATIVE_INFINITY;
686
+ for (let k = 0; k < 4; k++) {
687
+ const x = k & 1 ? x1 : x0;
688
+ const y = k & 2 ? y1 : y0;
689
+ for (let a = 0; a < 3; a++) {
690
+ const v = g[a * 4] * x + g[a * 4 + 1] * y + g[a * 4 + 3];
691
+ if (v < out[o + a]) out[o + a] = v;
692
+ if (v > out[o + 3 + a]) out[o + 3 + a] = v;
693
+ }
694
+ }
695
+ }
696
+ function boxInFrustum(planes, b, o) {
697
+ for (let p = 0; p < 6; p++) {
698
+ const nx = planes[p * 4];
699
+ const ny = planes[p * 4 + 1];
700
+ const nz = planes[p * 4 + 2];
701
+ const x = nx >= 0 ? b[o + 3] : b[o];
702
+ const y = ny >= 0 ? b[o + 4] : b[o + 1];
703
+ const z = nz >= 0 ? b[o + 5] : b[o + 2];
704
+ if (nx * x + ny * y + nz * z + planes[p * 4 + 3] < 0) return false;
705
+ }
706
+ return true;
707
+ }
708
+ function* queryTilemaps(world) {
709
+ for (const table of world.query({ with: [Tilemap, GlobalTransform, ComputedVisibility] }).tables) {
710
+ const g = table.column(GlobalTransform, "matrix");
711
+ const vis = table.column(ComputedVisibility, "visible");
712
+ const atlas = table.column(Tilemap, "atlas");
713
+ const data = table.column(Tilemap, "data");
714
+ const tileSize = table.column(Tilemap, "tileSize");
715
+ const chunkSize = table.column(Tilemap, "chunkSize");
716
+ const layer = table.column(Tilemap, "layer");
717
+ const lit = table.column(Tilemap, "lit");
718
+ for (let i = 0; i < table.count; i++) {
719
+ if (!vis[i]) continue;
720
+ yield [
721
+ table.entities[i],
722
+ {
723
+ atlas: atlas[i],
724
+ data: data[i],
725
+ tileSize: [tileSize[i * 2], tileSize[i * 2 + 1]],
726
+ chunkSize: chunkSize[i],
727
+ layer: layer[i],
728
+ lit: lit[i] !== 0
729
+ },
730
+ g.subarray(i * 12, i * 12 + 12)
731
+ ];
732
+ }
733
+ }
734
+ }
735
+ const SpriteDrawCaches = defineResource("sprite/DrawCaches", {
736
+ description: "GPU objects of the sprite and tilemap passes.",
737
+ init: () => ({
738
+ generation: -1,
739
+ pipelines: /* @__PURE__ */ new Map(),
740
+ groups: /* @__PURE__ */ new Map(),
741
+ views: /* @__PURE__ */ new Map(),
742
+ batchParams: /* @__PURE__ */ new Map()
743
+ })
744
+ });
745
+ const ids = /* @__PURE__ */ new WeakMap();
746
+ let nextId = 1;
747
+ function idOf(o) {
748
+ let id = ids.get(o);
749
+ if (id === void 0) {
750
+ id = nextId++;
751
+ ids.set(o, id);
752
+ }
753
+ return id;
754
+ }
755
+ function check(ctx) {
756
+ const gpu = ctx.gpu;
757
+ const caches = ctx.world.initResource(SpriteDrawCaches);
758
+ if (caches.generation !== gpu.generation) {
759
+ caches.generation = gpu.generation;
760
+ caches.layouts = void 0;
761
+ caches.pipelines.clear();
762
+ caches.groups.clear();
763
+ caches.views.clear();
764
+ caches.batchParams.clear();
765
+ caches.samplers = void 0;
766
+ caches.flatNormal = void 0;
767
+ }
768
+ if (!caches.layouts) {
769
+ const V = GPUShaderStage.VERTEX;
770
+ const F = GPUShaderStage.FRAGMENT;
771
+ const storage = (binding) => ({
772
+ binding,
773
+ visibility: V,
774
+ buffer: { type: "read-only-storage" }
775
+ });
776
+ caches.layouts = {
777
+ view: gpu.layouts.bindGroupLayout({
778
+ label: "sprites/view",
779
+ entries: [{ binding: 0, visibility: V, buffer: { type: "uniform" } }]
780
+ }),
781
+ sprites: gpu.layouts.bindGroupLayout({
782
+ label: "sprites/records",
783
+ entries: [storage(0), storage(1)]
784
+ }),
785
+ tilemap: gpu.layouts.bindGroupLayout({
786
+ label: "tilemap",
787
+ entries: [
788
+ storage(0),
789
+ storage(1),
790
+ storage(2),
791
+ storage(3),
792
+ { binding: 4, visibility: V, buffer: { type: "uniform" } }
793
+ ]
794
+ }),
795
+ texture: gpu.layouts.bindGroupLayout({
796
+ label: "sprites/texture",
797
+ entries: [
798
+ { binding: 0, visibility: F, texture: { sampleType: "float" } },
799
+ { binding: 1, visibility: F, sampler: { type: "filtering" } },
800
+ { binding: 2, visibility: F, buffer: { type: "uniform" } }
801
+ ]
802
+ }),
803
+ litTexture: gpu.layouts.bindGroupLayout({
804
+ label: "sprites/texture-lit",
805
+ entries: [
806
+ { binding: 0, visibility: F, texture: { sampleType: "float" } },
807
+ { binding: 1, visibility: F, sampler: { type: "filtering" } },
808
+ { binding: 2, visibility: F, buffer: { type: "uniform" } },
809
+ { binding: 3, visibility: F, texture: { sampleType: "float" } }
810
+ ]
811
+ })
812
+ };
813
+ }
814
+ if (!caches.flatNormal) {
815
+ const t = gpu.device.createTexture({
816
+ label: "sprites/flat-normal",
817
+ size: [1, 1],
818
+ format: "rgba8unorm",
819
+ usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST
820
+ });
821
+ gpu.device.queue.writeTexture({ texture: t }, new Uint8Array([128, 128, 255, 255]), {}, [1, 1]);
822
+ caches.flatNormal = t.createView();
823
+ }
824
+ caches.samplers ??= {
825
+ linear: gpu.device.createSampler({
826
+ label: "sprites/linear",
827
+ magFilter: "linear",
828
+ minFilter: "linear",
829
+ mipmapFilter: "linear"
830
+ }),
831
+ nearest: gpu.device.createSampler({
832
+ label: "sprites/nearest",
833
+ magFilter: "nearest",
834
+ minFilter: "nearest"
835
+ })
836
+ };
837
+ return caches;
838
+ }
839
+ const PREMULTIPLIED_ALPHA = {
840
+ color: { srcFactor: "one", dstFactor: "one-minus-src-alpha", operation: "add" },
841
+ alpha: { srcFactor: "one", dstFactor: "one-minus-src-alpha", operation: "add" }
842
+ };
843
+ const ADDITIVE = {
844
+ color: { srcFactor: "one", dstFactor: "one", operation: "add" },
845
+ alpha: { srcFactor: "zero", dstFactor: "one", operation: "add" }
846
+ };
847
+ function pipeline(ctx, kind, blend, screen, format, msaa, pick = false, lit = false) {
848
+ const gpu = ctx.gpu;
849
+ const c = check(ctx);
850
+ const key = `${kind}/${blend}/${screen ? "s" : "w"}/${format}/${msaa}${pick ? "/pick" : ""}${lit ? "/lit" : ""}`;
851
+ const cached = c.pipelines.get(key);
852
+ if (cached) return cached;
853
+ const module = ctx.world.resource(Shaders).module(gpu, {
854
+ root: kind === "sprite" ? "shard::sprite" : "shard::tilemap",
855
+ defines: lit ? { SCREEN: screen, SRGB_TARGET: format.endsWith("-srgb"), LIT: true } : { SCREEN: screen, SRGB_TARGET: format.endsWith("-srgb") }
856
+ });
857
+ if (!module) {
858
+ gpu.pipelines.skipped++;
859
+ return void 0;
860
+ }
861
+ const l = c.layouts;
862
+ const p = gpu.pipelines.render({
863
+ label: key,
864
+ layout: gpu.layouts.pipelineLayout({
865
+ label: lit ? `${kind}/lit` : kind,
866
+ bindGroupLayouts: lit ? [l.view, kind === "sprite" ? l.sprites : l.tilemap, l.litTexture, lights2dLayout(gpu)] : [l.view, kind === "sprite" ? l.sprites : l.tilemap, l.texture]
867
+ }),
868
+ vertex: { module, entryPoint: "vs" },
869
+ fragment: pick ? { module, entryPoint: "fs_pick", targets: PICK_TARGETS } : {
870
+ module,
871
+ entryPoint: "fs",
872
+ targets: [
873
+ {
874
+ format,
875
+ blend: blend === BLEND_OPAQUE ? void 0 : blend === BLEND_ADDITIVE ? ADDITIVE : PREMULTIPLIED_ALPHA
876
+ }
877
+ ]
878
+ },
879
+ primitive: { topology: "triangle-list" },
880
+ depthStencil: screen ? void 0 : {
881
+ format: "depth32float",
882
+ depthWriteEnabled: pick || blend === BLEND_OPAQUE,
883
+ depthCompare: "greater-equal"
884
+ },
885
+ multisample: { count: msaa }
886
+ });
887
+ if (p) c.pipelines.set(key, p);
888
+ return p;
889
+ }
890
+ const viewScratch = new Float32Array(24);
891
+ let chunkList = new Uint32Array(256);
892
+ function viewGroup(ctx, cam) {
893
+ const gpu = ctx.gpu;
894
+ const c = check(ctx);
895
+ let v = c.views.get(ctx.view.name);
896
+ if (!v) {
897
+ v = {
898
+ uniform: new GpuBuffer(gpu, {
899
+ label: `${ctx.view.name}/sprites`,
900
+ usage: GPUBufferUsage.UNIFORM,
901
+ size: 96
902
+ }),
903
+ chunks: new GpuBuffer(gpu, {
904
+ label: `${ctx.view.name}/tilemap-chunks`,
905
+ usage: GPUBufferUsage.STORAGE,
906
+ size: 1024
907
+ })
908
+ };
909
+ c.views.set(ctx.view.name, v);
910
+ }
911
+ viewScratch.set(cam.viewProj, 0);
912
+ viewScratch[16] = cam.displayWidth;
913
+ viewScratch[17] = cam.displayHeight;
914
+ viewScratch[18] = 1 / cam.displayWidth;
915
+ viewScratch[19] = 1 / cam.displayHeight;
916
+ viewScratch[20] = cam.pixelPerfect?.snap ? cam.pixelPerfect.pixelsPerUnit : 0;
917
+ viewScratch[21] = viewScratch[22] = viewScratch[23] = 0;
918
+ v.uniform.write(viewScratch);
919
+ const uniform = v.uniform;
920
+ return group(ctx, `${ctx.view.name}/view`, `${idOf(uniform.buffer)}`, c.layouts.view, () => [
921
+ { binding: 0, resource: { buffer: uniform.buffer } }
922
+ ]);
923
+ }
924
+ function group(ctx, slot, key, layout, entries) {
925
+ const gpu = ctx.gpu;
926
+ const c = check(ctx);
927
+ let g = c.groups.get(slot);
928
+ if (!g || g.key !== key) {
929
+ g = { key, group: gpu.device.createBindGroup({ label: slot, layout, entries: entries() }) };
930
+ c.groups.set(slot, g);
931
+ }
932
+ return g.group;
933
+ }
934
+ const batchScratch = new Uint32Array(4);
935
+ function textureGroup(ctx, texture, blend, nearest, owner, normal) {
936
+ const gpu = ctx.gpu;
937
+ const c = check(ctx);
938
+ const gt = ctx.world.resource(GpuAssetsResource).texture(texture);
939
+ if (!gt) return void 0;
940
+ let params = c.batchParams.get(owner);
941
+ if (!params) {
942
+ params = new GpuBuffer(gpu, { label: "sprites/batch", usage: GPUBufferUsage.UNIFORM, size: 16 });
943
+ c.batchParams.set(owner, params);
944
+ }
945
+ batchScratch[0] = texture.premultiplied ? 1 : 0;
946
+ batchScratch[1] = blend;
947
+ params.write(batchScratch);
948
+ const view = texture.usage === "color" ? gt.srgb : gt.linear;
949
+ const sampler = nearest ? c.samplers.nearest : c.samplers.linear;
950
+ const buffer = params;
951
+ if (normal !== void 0) {
952
+ const gn = normal ? ctx.world.resource(GpuAssetsResource).texture(normal) : void 0;
953
+ if (normal && !gn) return void 0;
954
+ const nview = gn ? gn.linear : c.flatNormal;
955
+ return group(
956
+ ctx,
957
+ `tex-lit/${idOf(owner)}/${nearest ? "n" : "l"}`,
958
+ `${idOf(gt.texture)}/${idOf(buffer.buffer)}/${idOf(nview)}`,
959
+ c.layouts.litTexture,
960
+ () => [
961
+ { binding: 0, resource: view },
962
+ { binding: 1, resource: sampler },
963
+ { binding: 2, resource: { buffer: buffer.buffer } },
964
+ { binding: 3, resource: nview }
965
+ ]
966
+ );
967
+ }
968
+ return group(
969
+ ctx,
970
+ `tex/${idOf(owner)}/${nearest ? "n" : "l"}`,
971
+ `${idOf(gt.texture)}/${idOf(buffer.buffer)}`,
972
+ c.layouts.texture,
973
+ () => [
974
+ { binding: 0, resource: view },
975
+ { binding: 1, resource: sampler },
976
+ { binding: 2, resource: { buffer: buffer.buffer } }
977
+ ]
978
+ );
979
+ }
980
+ function drawSpace(ctx, space) {
981
+ const cam = cameraOf(ctx.view);
982
+ if (!cam) return;
983
+ const store = ctx.world.tryResource(Sprites);
984
+ const tilemaps = ctx.world.resource(Tilemaps);
985
+ const pass = ctx.renderPass;
986
+ const c = check(ctx);
987
+ const format = space === SPACE_SCREEN ? ctx.texture("view-target").format : "rgba16float";
988
+ const msaa = space === SPACE_SCREEN ? 1 : cam.msaa;
989
+ const nearest = cam.pixelPerfect !== void 0;
990
+ const lights = space === SPACE_WORLD ? litView(ctx.world, ctx.view.name) : void 0;
991
+ const lit = lights !== void 0;
992
+ pass.setBindGroup(0, viewGroup(ctx, cam));
993
+ if (lights) pass.setBindGroup(3, lights2dGroup(ctx.gpu, lights));
994
+ const drawn = { visible: 0, total: 0, draws: 0 };
995
+ const maps = [];
996
+ if (space === SPACE_WORLD) for (const m of tilemaps.maps.values()) maps.push(m);
997
+ maps.sort((a, b) => a.layer - b.layer);
998
+ const v = c.views.get(ctx.view.name);
999
+ let chunkCount = 0;
1000
+ const tileDraws = [];
1001
+ for (const m of maps) {
1002
+ m.layers.forEach((layer, li) => {
1003
+ const bounds = m.bounds[li];
1004
+ if (!bounds) return;
1005
+ const first = chunkCount;
1006
+ const n = layer.chunksX * layer.chunksY;
1007
+ drawn.total += n;
1008
+ for (let chunk = 0; chunk < n; chunk++) {
1009
+ if (!boxInFrustum(cam.frustum, bounds, chunk * 6)) continue;
1010
+ if (chunkCount >= chunkList.length) {
1011
+ const grown = new Uint32Array(chunkList.length * 2);
1012
+ grown.set(chunkList);
1013
+ chunkList = grown;
1014
+ }
1015
+ chunkList[chunkCount++] = chunk;
1016
+ }
1017
+ if (chunkCount > first)
1018
+ tileDraws.push({ map: m, layer: li, first, count: chunkCount - first });
1019
+ });
1020
+ }
1021
+ drawn.visible = chunkCount;
1022
+ if (chunkCount > 0) v.chunks.write(chunkList, 0, 0, chunkCount);
1023
+ let t = 0;
1024
+ let r = 0;
1025
+ const runs = store ? store.runs : [];
1026
+ const runCount = store ? store.runCount : 0;
1027
+ while (t < tileDraws.length || r < runCount) {
1028
+ const tl = t < tileDraws.length ? tileDraws[t].map.layer : Number.POSITIVE_INFINITY;
1029
+ const rl = r < runCount ? runs[r].layer : Number.POSITIVE_INFINITY;
1030
+ if (tl <= rl) {
1031
+ const d = tileDraws[t++];
1032
+ const p = pipeline(ctx, "tilemap", BLEND_ALPHA, false, format, msaa, false, lit);
1033
+ const tex = d.map.texture && textureGroup(
1034
+ ctx,
1035
+ d.map.texture,
1036
+ BLEND_ALPHA,
1037
+ nearest,
1038
+ d.map.layers[d.layer],
1039
+ lit ? d.map.normal : void 0
1040
+ );
1041
+ if (!p || !tex) continue;
1042
+ const layer = d.map.layers[d.layer];
1043
+ const cs = d.map.chunkSize;
1044
+ pass.setPipeline(p);
1045
+ pass.setBindGroup(
1046
+ 1,
1047
+ group(
1048
+ ctx,
1049
+ `${ctx.view.name}/tilemap/${d.map.entity}/${d.layer}`,
1050
+ `${idOf(layer.buffer.buffer)}/${idOf(v.chunks.buffer)}/${idOf(d.map.regions.buffer)}/${idOf(d.map.remap.buffer)}/${idOf(layer.params.buffer)}`,
1051
+ c.layouts.tilemap,
1052
+ () => [
1053
+ { binding: 0, resource: { buffer: layer.buffer.buffer } },
1054
+ { binding: 1, resource: { buffer: v.chunks.buffer } },
1055
+ { binding: 2, resource: { buffer: d.map.regions.buffer } },
1056
+ { binding: 3, resource: { buffer: d.map.remap.buffer } },
1057
+ { binding: 4, resource: { buffer: layer.params.buffer } }
1058
+ ]
1059
+ )
1060
+ );
1061
+ pass.setBindGroup(2, tex);
1062
+ pass.draw(6, d.count * cs * cs, 0, d.first * cs * cs);
1063
+ drawn.draws++;
1064
+ } else {
1065
+ const run = runs[r++];
1066
+ if (run.batch.space !== space) continue;
1067
+ let count = run.count;
1068
+ while (r < runCount) {
1069
+ const next = runs[r];
1070
+ const b = next.batch;
1071
+ if (next.layer !== run.layer || next.first !== run.first + count || b.texture !== run.batch.texture || b.blend !== run.batch.blend || b.space !== space || lit && b.normal !== run.batch.normal)
1072
+ break;
1073
+ count += next.count;
1074
+ r++;
1075
+ }
1076
+ const p = pipeline(
1077
+ ctx,
1078
+ "sprite",
1079
+ run.batch.blend,
1080
+ space === SPACE_SCREEN,
1081
+ format,
1082
+ msaa,
1083
+ false,
1084
+ lit
1085
+ );
1086
+ const tex = textureGroup(
1087
+ ctx,
1088
+ run.batch.texture,
1089
+ run.batch.blend,
1090
+ nearest,
1091
+ run.batch,
1092
+ lit ? run.batch.normal : void 0
1093
+ );
1094
+ if (!p || !tex || !store) continue;
1095
+ pass.setPipeline(p);
1096
+ pass.setBindGroup(
1097
+ 1,
1098
+ group(
1099
+ ctx,
1100
+ "sprites/records",
1101
+ `${idOf(store.records.buffer)}/${idOf(store.orderBuffer.buffer)}`,
1102
+ c.layouts.sprites,
1103
+ () => [
1104
+ { binding: 0, resource: { buffer: store.records.buffer } },
1105
+ { binding: 1, resource: { buffer: store.orderBuffer.buffer } }
1106
+ ]
1107
+ )
1108
+ );
1109
+ pass.setBindGroup(2, tex);
1110
+ pass.draw(6, count, 0, run.first);
1111
+ drawn.draws++;
1112
+ }
1113
+ }
1114
+ const key = space === SPACE_SCREEN ? `${ctx.view.name}/screen` : ctx.view.name;
1115
+ tilemaps.drawn.set(key, drawn);
1116
+ }
1117
+ function drawSpritePicks(ctx, cam) {
1118
+ const store = ctx.world.tryResource(Sprites);
1119
+ if (!store || store.runCount === 0) return;
1120
+ const pass = ctx.renderPass;
1121
+ const c = check(ctx);
1122
+ pass.setBindGroup(0, viewGroup(ctx, cam));
1123
+ for (let r = 0; r < store.runCount; r++) {
1124
+ const run = store.runs[r];
1125
+ if (run.batch.space !== SPACE_WORLD) continue;
1126
+ const p = pipeline(ctx, "sprite", run.batch.blend, false, "rgba16float", 1, true);
1127
+ const tex = textureGroup(
1128
+ ctx,
1129
+ run.batch.texture,
1130
+ run.batch.blend,
1131
+ cam.pixelPerfect !== void 0,
1132
+ run.batch
1133
+ );
1134
+ if (!p || !tex) continue;
1135
+ pass.setPipeline(p);
1136
+ pass.setBindGroup(
1137
+ 1,
1138
+ group(
1139
+ ctx,
1140
+ "sprites/records",
1141
+ `${idOf(store.records.buffer)}/${idOf(store.orderBuffer.buffer)}`,
1142
+ c.layouts.sprites,
1143
+ () => [
1144
+ { binding: 0, resource: { buffer: store.records.buffer } },
1145
+ { binding: 1, resource: { buffer: store.orderBuffer.buffer } }
1146
+ ]
1147
+ )
1148
+ );
1149
+ pass.setBindGroup(2, tex);
1150
+ pass.draw(6, run.count, 0, run.first);
1151
+ }
1152
+ }
1153
+ function hasSpace(world, space) {
1154
+ const store = world.tryResource(Sprites);
1155
+ if (store) {
1156
+ for (const b of store.batches) if (b.space === space && b.count > 0) return true;
1157
+ }
1158
+ return space === SPACE_WORLD && world.resource(Tilemaps).maps.size > 0;
1159
+ }
1160
+ function spriteNode(world) {
1161
+ return {
1162
+ kind: "render",
1163
+ phase: RenderPhase.Sprites,
1164
+ enabled: (view) => cameraOf(view) !== void 0 && hasSpace(world, SPACE_WORLD),
1165
+ reads: ["culled", "lights2d"],
1166
+ writes: ["scene-color", "scene-depth", "hdr"],
1167
+ color: (view) => sceneColor(view),
1168
+ depth: { resource: "scene-depth" },
1169
+ run: (ctx) => drawSpace(ctx, SPACE_WORLD)
1170
+ };
1171
+ }
1172
+ function overlayNode(world) {
1173
+ return {
1174
+ kind: "render",
1175
+ phase: RenderPhase.Overlay,
1176
+ enabled: (view) => cameraOf(view) !== void 0 && hasSpace(world, SPACE_SCREEN),
1177
+ writes: ["view-target"],
1178
+ color: [{ resource: "view-target" }],
1179
+ run: (ctx) => drawSpace(ctx, SPACE_SCREEN)
1180
+ };
1181
+ }
1182
+ function describeSprites(world) {
1183
+ const store = world.tryResource(Sprites);
1184
+ const tilemaps = world.tryResource(Tilemaps);
1185
+ if (!store) return void 0;
1186
+ return {
1187
+ sprites: store.live,
1188
+ drawn: store.orderCount,
1189
+ pending: store.pending.size,
1190
+ batches: store.batches.filter((b) => b.count > 0).length,
1191
+ drawCalls: store.runCount,
1192
+ perLayer: Object.fromEntries([...store.perLayer].sort((a, b) => a[0] - b[0])),
1193
+ uploadedBytes: store.uploadedBytes,
1194
+ sorts: store.sorts,
1195
+ lighting: describeLights2d(world),
1196
+ tilemaps: tilemaps ? {
1197
+ maps: tilemaps.maps.size,
1198
+ chunkUploads: tilemaps.chunkUploads,
1199
+ views: Object.fromEntries(tilemaps.drawn)
1200
+ } : void 0
1201
+ };
1202
+ }
1203
+ export {
1204
+ SPRITE_FLOATS,
1205
+ SpriteDrawCaches,
1206
+ SpriteStore,
1207
+ Sprites,
1208
+ TilemapStore,
1209
+ Tilemaps,
1210
+ describeSprites,
1211
+ drawSpritePicks,
1212
+ observeSpriteRemovals,
1213
+ overlayNode,
1214
+ prepareSprites,
1215
+ spriteNode
1216
+ };