@codexo/exojs-tilemap 0.15.2 → 0.16.0

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.
Files changed (151) hide show
  1. package/README.md +33 -26
  2. package/dist/esm/ChunkSource.d.ts +33 -0
  3. package/dist/esm/ChunkSource.d.ts.map +1 -0
  4. package/dist/esm/ChunkStreamer.d.ts +98 -0
  5. package/dist/esm/ChunkStreamer.d.ts.map +1 -0
  6. package/dist/esm/ChunkStreamer.js +201 -0
  7. package/dist/esm/ChunkStreamer.js.map +1 -0
  8. package/dist/esm/ImageLayer.d.ts +11 -3
  9. package/dist/esm/ImageLayer.d.ts.map +1 -0
  10. package/dist/esm/ImageLayer.js +71 -64
  11. package/dist/esm/ImageLayer.js.map +1 -1
  12. package/dist/esm/ImageLayerNode.d.ts +89 -0
  13. package/dist/esm/ImageLayerNode.d.ts.map +1 -0
  14. package/dist/esm/ImageLayerNode.js +205 -0
  15. package/dist/esm/ImageLayerNode.js.map +1 -0
  16. package/dist/esm/MapObject.d.ts +70 -0
  17. package/dist/esm/MapObject.d.ts.map +1 -0
  18. package/dist/esm/MapObject.js +34 -0
  19. package/dist/esm/MapObject.js.map +1 -0
  20. package/dist/esm/MapObjectSpawner.d.ts +133 -0
  21. package/dist/esm/MapObjectSpawner.d.ts.map +1 -0
  22. package/dist/esm/MapObjectSpawner.js +147 -0
  23. package/dist/esm/MapObjectSpawner.js.map +1 -0
  24. package/dist/esm/MapSpawnSession.d.ts +53 -0
  25. package/dist/esm/MapSpawnSession.d.ts.map +1 -0
  26. package/dist/esm/MapSpawnSession.js +88 -0
  27. package/dist/esm/MapSpawnSession.js.map +1 -0
  28. package/dist/esm/MapWorld.d.ts +134 -0
  29. package/dist/esm/MapWorld.d.ts.map +1 -0
  30. package/dist/esm/MapWorld.js +150 -0
  31. package/dist/esm/MapWorld.js.map +1 -0
  32. package/dist/esm/MapWorldRuntime.d.ts +182 -0
  33. package/dist/esm/MapWorldRuntime.d.ts.map +1 -0
  34. package/dist/esm/MapWorldRuntime.js +261 -0
  35. package/dist/esm/MapWorldRuntime.js.map +1 -0
  36. package/dist/esm/ObjectLayer.d.ts +93 -7
  37. package/dist/esm/ObjectLayer.d.ts.map +1 -0
  38. package/dist/esm/ObjectLayer.js +184 -167
  39. package/dist/esm/ObjectLayer.js.map +1 -1
  40. package/dist/esm/SampledChunkSource.d.ts +43 -0
  41. package/dist/esm/SampledChunkSource.d.ts.map +1 -0
  42. package/dist/esm/SampledChunkSource.js +48 -0
  43. package/dist/esm/SampledChunkSource.js.map +1 -0
  44. package/dist/esm/TileAnimator.d.ts +12 -2
  45. package/dist/esm/TileAnimator.d.ts.map +1 -0
  46. package/dist/esm/TileAnimator.js +154 -165
  47. package/dist/esm/TileAnimator.js.map +1 -1
  48. package/dist/esm/TileChunk.d.ts +30 -5
  49. package/dist/esm/TileChunk.d.ts.map +1 -0
  50. package/dist/esm/TileChunk.js +190 -188
  51. package/dist/esm/TileChunk.js.map +1 -1
  52. package/dist/esm/TileChunkNode.d.ts +18 -3
  53. package/dist/esm/TileChunkNode.d.ts.map +1 -0
  54. package/dist/esm/TileChunkNode.js +104 -85
  55. package/dist/esm/TileChunkNode.js.map +1 -1
  56. package/dist/esm/TileLayer.d.ts +104 -24
  57. package/dist/esm/TileLayer.d.ts.map +1 -0
  58. package/dist/esm/TileLayer.js +511 -469
  59. package/dist/esm/TileLayer.js.map +1 -1
  60. package/dist/esm/TileLayerNode.d.ts +52 -11
  61. package/dist/esm/TileLayerNode.d.ts.map +1 -0
  62. package/dist/esm/TileLayerNode.js +224 -166
  63. package/dist/esm/TileLayerNode.js.map +1 -1
  64. package/dist/esm/TileMap.d.ts +96 -31
  65. package/dist/esm/TileMap.d.ts.map +1 -0
  66. package/dist/esm/TileMap.js +401 -357
  67. package/dist/esm/TileMap.js.map +1 -1
  68. package/dist/esm/TileMapBand.d.ts +42 -29
  69. package/dist/esm/TileMapBand.d.ts.map +1 -0
  70. package/dist/esm/TileMapBand.js +151 -158
  71. package/dist/esm/TileMapBand.js.map +1 -1
  72. package/dist/esm/TileMapNode.d.ts +34 -18
  73. package/dist/esm/TileMapNode.d.ts.map +1 -0
  74. package/dist/esm/TileMapNode.js +122 -109
  75. package/dist/esm/TileMapNode.js.map +1 -1
  76. package/dist/esm/TileMapView.d.ts +108 -47
  77. package/dist/esm/TileMapView.d.ts.map +1 -0
  78. package/dist/esm/TileMapView.js +385 -338
  79. package/dist/esm/TileMapView.js.map +1 -1
  80. package/dist/esm/TileSet.d.ts +15 -6
  81. package/dist/esm/TileSet.d.ts.map +1 -0
  82. package/dist/esm/TileSet.js +161 -181
  83. package/dist/esm/TileSet.js.map +1 -1
  84. package/dist/esm/WangSet.d.ts +5 -4
  85. package/dist/esm/WangSet.d.ts.map +1 -0
  86. package/dist/esm/WangSet.js +70 -78
  87. package/dist/esm/WangSet.js.map +1 -1
  88. package/dist/esm/WorkerSampledChunkSource.d.ts +95 -0
  89. package/dist/esm/WorkerSampledChunkSource.d.ts.map +1 -0
  90. package/dist/esm/WorkerSampledChunkSource.js +117 -0
  91. package/dist/esm/WorkerSampledChunkSource.js.map +1 -0
  92. package/dist/esm/autoTile.d.ts +10 -5
  93. package/dist/esm/autoTile.d.ts.map +1 -0
  94. package/dist/esm/autoTile.js +164 -205
  95. package/dist/esm/autoTile.js.map +1 -1
  96. package/dist/esm/chunkGeometry.d.ts +8 -7
  97. package/dist/esm/chunkGeometry.d.ts.map +1 -0
  98. package/dist/esm/chunkGeometry.js +83 -94
  99. package/dist/esm/chunkGeometry.js.map +1 -1
  100. package/dist/esm/index.d.ts +1 -0
  101. package/dist/esm/index.d.ts.map +1 -0
  102. package/dist/esm/index.js +26 -15
  103. package/dist/esm/nodeBounds.d.ts +20 -0
  104. package/dist/esm/nodeBounds.d.ts.map +1 -0
  105. package/dist/esm/nodeBounds.js +40 -0
  106. package/dist/esm/nodeBounds.js.map +1 -0
  107. package/dist/esm/pixelSnap.d.ts +4 -3
  108. package/dist/esm/pixelSnap.d.ts.map +1 -0
  109. package/dist/esm/pixelSnap.js +18 -16
  110. package/dist/esm/pixelSnap.js.map +1 -1
  111. package/dist/esm/public.d.ts +23 -3
  112. package/dist/esm/public.d.ts.map +1 -0
  113. package/dist/esm/tileCollision.d.ts +190 -0
  114. package/dist/esm/tileCollision.d.ts.map +1 -0
  115. package/dist/esm/tileCollision.js +367 -0
  116. package/dist/esm/tileCollision.js.map +1 -0
  117. package/dist/esm/tileWord.d.ts +12 -0
  118. package/dist/esm/tileWord.d.ts.map +1 -0
  119. package/dist/esm/tileWord.js +16 -0
  120. package/dist/esm/tileWord.js.map +1 -0
  121. package/dist/esm/tilemapExtension.d.ts +4 -3
  122. package/dist/esm/tilemapExtension.d.ts.map +1 -0
  123. package/dist/esm/tilemapExtension.js +47 -47
  124. package/dist/esm/tilemapExtension.js.map +1 -1
  125. package/dist/esm/tilemapSerializers.d.ts +5 -4
  126. package/dist/esm/tilemapSerializers.d.ts.map +1 -0
  127. package/dist/esm/tilemapSerializers.js +48 -41
  128. package/dist/esm/tilemapSerializers.js.map +1 -1
  129. package/dist/esm/types.d.ts +34 -18
  130. package/dist/esm/types.d.ts.map +1 -0
  131. package/dist/esm/types.js +111 -120
  132. package/dist/esm/types.js.map +1 -1
  133. package/dist/esm/webgl2/WebGl2TileChunkRenderer.d.ts +50 -2
  134. package/dist/esm/webgl2/WebGl2TileChunkRenderer.d.ts.map +1 -0
  135. package/dist/esm/webgl2/WebGl2TileChunkRenderer.js +366 -332
  136. package/dist/esm/webgl2/WebGl2TileChunkRenderer.js.map +1 -1
  137. package/dist/esm/webgl2/shaders/tile-chunk.frag.js +6 -0
  138. package/dist/esm/webgl2/shaders/tile-chunk.frag.js.map +1 -0
  139. package/dist/esm/webgl2/shaders/tile-chunk.vert.js +6 -0
  140. package/dist/esm/webgl2/shaders/tile-chunk.vert.js.map +1 -0
  141. package/dist/esm/webgpu/WebGpuTileChunkRenderer.d.ts +70 -4
  142. package/dist/esm/webgpu/WebGpuTileChunkRenderer.d.ts.map +1 -0
  143. package/dist/esm/webgpu/WebGpuTileChunkRenderer.js +496 -383
  144. package/dist/esm/webgpu/WebGpuTileChunkRenderer.js.map +1 -1
  145. package/dist/esm/webgpu/shaders/tile-chunk.wgsl.js +6 -0
  146. package/dist/esm/webgpu/shaders/tile-chunk.wgsl.js.map +1 -0
  147. package/package.json +8 -15
  148. package/dist/esm/index.js.map +0 -1
  149. package/dist/esm/register.d.ts +0 -1
  150. package/dist/esm/register.js +0 -25
  151. package/dist/esm/register.js.map +0 -1
@@ -1,390 +1,503 @@
1
- import { AbstractWebGpuRenderer, Texture, getWebGpuBlendState, stencilContentDepthStencilState } from '@codexo/exojs/renderer-sdk';
1
+ import { TILE_DIAGONAL_BIT, TILE_ROW_MASK } from "../tileWord.js";
2
+ import tile_chunk_default from "./shaders/tile-chunk.wgsl.js";
3
+ import { AbstractWebGpuRenderer, DataTexture, Texture, fillShaderSource, getWebGpuBlendState, packAffineMat4, packSnapViewport, packedGroupChanged, retainedGroupUniformBytes, stencilContentDepthStencilState } from "@codexo/exojs/renderer-sdk";
4
+ import { Matrix } from "@codexo/exojs";
2
5
 
3
- /// <reference types="@webgpu/types" />
6
+ //#region src/webgpu/WebGpuTileChunkRenderer.ts
4
7
  const instanceStrideBytes = 32;
5
- const wordsPerInstance = instanceStrideBytes / Uint32Array.BYTES_PER_ELEMENT; // = 8
6
- const projectionByteLength = 64;
8
+ const wordsPerInstance = instanceStrideBytes / Uint32Array.BYTES_PER_ELEMENT;
9
+ const projectionByteLength = 144;
7
10
  const initialBatchCapacity = 256;
8
11
  const indicesPerInstance = 6;
9
- const quadIndices = new Uint16Array([0, 1, 2, 0, 2, 3]);
10
- const TILE_ROW_MASK = 0x1fffffff;
11
- const TILE_DIAGONAL_BIT = 0x20000000;
12
- const tileShaderSource = `
13
- struct ProjectionUniforms {
14
- matrix: mat4x4<f32>,
15
- };
16
-
17
- struct TransformSlot {
18
- m0: vec4<f32>,
19
- m1: vec4<f32>,
20
- m2: vec4<f32>,
21
- };
22
-
23
- @group(0) @binding(0)
24
- var<uniform> projection: ProjectionUniforms;
25
- @group(0) @binding(1)
26
- var<storage, read> transforms: array<TransformSlot>;
27
-
28
- @group(1) @binding(0)
29
- var tileTexture: texture_2d<f32>;
30
- @group(1) @binding(1)
31
- var tileSampler: sampler;
32
-
33
- struct VertexInput {
34
- @location(0) quadBounds: vec4<f32>, // x0, y0, x1, y1
35
- @location(1) uvBounds: vec4<f32>, // uMin, vMin, uMax, vMax (flipX/Y + texture flipY baked)
36
- @location(2) color: vec4<f32>, // RGBA tint (layer opacity in alpha)
37
- @location(3) tileWord: u32, // transform row (bits 0..28) | diagonal (bit 29)
38
- };
39
-
40
- struct VertexOutput {
41
- @builtin(position) position: vec4<f32>,
42
- @location(0) texcoord: vec2<f32>,
43
- @location(1) color: vec4<f32>,
44
- };
45
-
46
- @vertex
47
- fn vertexMain(input: VertexInput, @builtin(vertex_index) vid: u32) -> VertexOutput {
48
- var output: VertexOutput;
49
-
50
- // vid 0..3 → TL, TR, BR, BL (matches static index buffer [0,1,2,0,2,3])
51
- let cornerX = ((vid + 1u) >> 1u) & 1u;
52
- let cornerY = vid >> 1u;
53
-
54
- let localX = select(input.quadBounds.x, input.quadBounds.z, cornerX == 1u);
55
- let localY = select(input.quadBounds.y, input.quadBounds.w, cornerY == 1u);
56
-
57
- let row = input.tileWord & ${TILE_ROW_MASK}u;
58
- let diagonal = (input.tileWord & ${TILE_DIAGONAL_BIT}u) != 0u;
59
-
60
- let slot = transforms[row];
61
- let worldX = slot.m0.x * localX + slot.m0.y * localY + slot.m1.x;
62
- let worldY = slot.m0.z * localX + slot.m0.w * localY + slot.m1.y;
63
-
64
- output.position = projection.matrix * vec4<f32>(worldX, worldY, 0.0, 1.0);
65
-
66
- // Tile orientation: diagonal transposes the corner-coordinate axes; flipX/Y
67
- // are baked into the UV corner ordering by the CPU writer.
68
- var su = cornerX;
69
- var sv = cornerY;
70
- if (diagonal) {
71
- let t = su;
72
- su = sv;
73
- sv = t;
74
- }
75
-
76
- let u = select(input.uvBounds.x, input.uvBounds.z, su == 1u);
77
- let v = select(input.uvBounds.y, input.uvBounds.w, sv == 1u);
78
- output.texcoord = vec2<f32>(u, v);
79
-
80
- output.color = vec4(input.color.rgb * input.color.a, input.color.a);
81
-
82
- return output;
83
- }
84
-
85
- @fragment
86
- fn fragmentMain(input: VertexOutput) -> @location(0) vec4<f32> {
87
- let sample = textureSample(tileTexture, tileSampler, input.texcoord);
88
- return sample * input.color;
89
- }
90
- `;
12
+ const quadIndices = new Uint16Array([
13
+ 0,
14
+ 1,
15
+ 2,
16
+ 0,
17
+ 2,
18
+ 3
19
+ ]);
20
+ const tileShaderSource = fillShaderSource(tile_chunk_default, {
21
+ tileRowMask: TILE_ROW_MASK,
22
+ tileDiagonalBit: TILE_DIAGONAL_BIT
23
+ });
91
24
  /**
92
- * Instanced WebGPU renderer for {@link TileChunkNode}, the parity counterpart of
93
- * the WebGL2 renderer. Same instance layout and orientation handling; per-chunk
94
- * transforms ride on the backend's shared transform storage buffer, and the
95
- * instance buffer grows on demand rather than flushing in fixed runs.
96
- * @internal
97
- */
98
- class WebGpuTileChunkRenderer extends AbstractWebGpuRenderer {
99
- _projectionData = new Float32Array(projectionByteLength / Float32Array.BYTES_PER_ELEMENT);
100
- _device = null;
101
- _shaderModule = null;
102
- _uniformBindGroupLayout = null;
103
- _textureBindGroupLayout = null;
104
- _pipelineLayout = null;
105
- _uniformBuffer = null;
106
- _transformBindGroup = null;
107
- _transformStorageBuffer = null;
108
- _indexBuffer = null;
109
- _instanceBuffer = null;
110
- _instanceCapacity = 0;
111
- _instanceData = new ArrayBuffer(0);
112
- _instanceFloat32 = new Float32Array(this._instanceData);
113
- _instanceUint32 = new Uint32Array(this._instanceData);
114
- _pipelines = new Map();
115
- _quadIndex = 0;
116
- _maxNodeIndex = 0;
117
- _currentBlendMode = null;
118
- _currentTexture = null;
119
- onConnect(backend) {
120
- if (this._device) {
121
- return;
122
- }
123
- this._device = backend.device;
124
- this._shaderModule = this._device.createShaderModule({ code: tileShaderSource });
125
- this._uniformBindGroupLayout = this._device.createBindGroupLayout({
126
- entries: [
127
- { binding: 0, visibility: GPUShaderStage.VERTEX, buffer: { type: 'uniform' } },
128
- { binding: 1, visibility: GPUShaderStage.VERTEX, buffer: { type: 'read-only-storage' } },
129
- ],
130
- });
131
- this._textureBindGroupLayout = this._device.createBindGroupLayout({
132
- entries: [
133
- { binding: 0, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: 'float' } },
134
- { binding: 1, visibility: GPUShaderStage.FRAGMENT, sampler: { type: 'filtering' } },
135
- ],
136
- });
137
- this._pipelineLayout = this._device.createPipelineLayout({
138
- bindGroupLayouts: [this._uniformBindGroupLayout, this._textureBindGroupLayout],
139
- });
140
- this._uniformBuffer = this._device.createBuffer({
141
- size: projectionByteLength,
142
- usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
143
- });
144
- this._indexBuffer = this._device.createBuffer({
145
- size: quadIndices.byteLength,
146
- usage: GPUBufferUsage.INDEX | GPUBufferUsage.COPY_DST,
147
- });
148
- this._device.queue.writeBuffer(this._indexBuffer, 0, quadIndices.buffer, quadIndices.byteOffset, quadIndices.byteLength);
149
- }
150
- onDisconnect() {
151
- this._instanceBuffer?.destroy();
152
- this._indexBuffer?.destroy();
153
- this._uniformBuffer?.destroy();
154
- this._pipelines.clear();
155
- this._instanceBuffer = null;
156
- this._indexBuffer = null;
157
- this._transformBindGroup = null;
158
- this._transformStorageBuffer = null;
159
- this._uniformBuffer = null;
160
- this._pipelineLayout = null;
161
- this._textureBindGroupLayout = null;
162
- this._uniformBindGroupLayout = null;
163
- this._shaderModule = null;
164
- this._device = null;
165
- this._backend = null;
166
- this._instanceCapacity = 0;
167
- this._instanceData = new ArrayBuffer(0);
168
- this._instanceFloat32 = new Float32Array(this._instanceData);
169
- this._instanceUint32 = new Uint32Array(this._instanceData);
170
- this._quadIndex = 0;
171
- this._maxNodeIndex = 0;
172
- this._currentBlendMode = null;
173
- this._currentTexture = null;
174
- }
175
- render(node) {
176
- const backend = this._backend;
177
- if (backend === null) {
178
- return;
179
- }
180
- const pages = node.pages;
181
- if (pages.length === 0) {
182
- return;
183
- }
184
- const blendMode = node.blendMode;
185
- const tintRgba = node.tint.toRgba();
186
- const command = backend.activeDrawCommand;
187
- const nodeIndex = command !== null ? command.nodeIndex : backend._pushTransform(node);
188
- for (const page of pages) {
189
- this._renderPage(backend, page.texture, page.quads, blendMode, tintRgba, nodeIndex);
190
- }
191
- }
192
- _renderPage(backend, texture, quads, blendMode, tintRgba, nodeIndex) {
193
- if (quads.length === 0) {
194
- return;
195
- }
196
- if (texture.width === 0 || texture.height === 0) {
197
- return;
198
- }
199
- if (texture instanceof Texture && texture.source === null) {
200
- return;
201
- }
202
- const blendModeChanged = this._currentBlendMode !== null && blendMode !== this._currentBlendMode;
203
- const textureChanged = this._currentTexture !== null && texture !== this._currentTexture;
204
- const willExceed = this._quadIndex + quads.length > this._instanceCapacity && this._instanceCapacity > 0;
205
- if ((blendModeChanged || textureChanged || willExceed) && this._quadIndex > 0) {
206
- this.flush();
207
- }
208
- this._currentBlendMode = blendMode;
209
- this._currentTexture = texture;
210
- backend.setBlendMode(blendMode);
211
- this._ensureInstanceCapacity(this._quadIndex + quads.length);
212
- const f32 = this._instanceFloat32;
213
- const u32 = this._instanceUint32;
214
- const flipYTexture = texture.flipY;
215
- const baseWord = nodeIndex & TILE_ROW_MASK;
216
- for (const q of quads) {
217
- const idx = this._quadIndex * wordsPerInstance;
218
- f32[idx + 0] = q.x0;
219
- f32[idx + 1] = q.y0;
220
- f32[idx + 2] = q.x1;
221
- f32[idx + 3] = q.y1;
222
- const flipX = (q.orient & 1) !== 0;
223
- const tileFlipY = (q.orient & 2) !== 0;
224
- const diagonal = (q.orient & 4) !== 0;
225
- const uA = flipX ? q.u1 : q.u0;
226
- const uB = flipX ? q.u0 : q.u1;
227
- let vA = tileFlipY ? q.v1 : q.v0;
228
- let vB = tileFlipY ? q.v0 : q.v1;
229
- if (flipYTexture) {
230
- const swap = vA;
231
- vA = vB;
232
- vB = swap;
233
- }
234
- const uMin = (uA * 0xffff) & 0xffff;
235
- const vMin = (vA * 0xffff) & 0xffff;
236
- const uMax = (uB * 0xffff) & 0xffff;
237
- const vMax = (vB * 0xffff) & 0xffff;
238
- u32[idx + 4] = uMin | (vMin << 16);
239
- u32[idx + 5] = uMax | (vMax << 16);
240
- u32[idx + 6] = tintRgba;
241
- u32[idx + 7] = (diagonal ? baseWord | TILE_DIAGONAL_BIT : baseWord) >>> 0;
242
- this._quadIndex++;
243
- }
244
- if (nodeIndex > this._maxNodeIndex) {
245
- this._maxNodeIndex = nodeIndex;
246
- }
247
- }
248
- flush() {
249
- const backend = this._backend;
250
- const device = this._device;
251
- const uniformBuffer = this._uniformBuffer;
252
- if (!backend || !device || !uniformBuffer) {
253
- return;
254
- }
255
- if (this._quadIndex === 0 && !backend.clearRequested) {
256
- return;
257
- }
258
- const viewMatrix = backend.view.getTransform();
259
- this._projectionData.set([viewMatrix.a, viewMatrix.c, 0, 0, viewMatrix.b, viewMatrix.d, 0, 0, 0, 0, 1, 0, viewMatrix.x, viewMatrix.y, 0, viewMatrix.z]);
260
- device.queue.writeBuffer(uniformBuffer, 0, this._projectionData.buffer, this._projectionData.byteOffset, this._projectionData.byteLength);
261
- const scissor = backend.getScissorRect();
262
- const maskClipsAll = scissor !== null && (scissor.width <= 0 || scissor.height <= 0);
263
- const pass = backend._passCoordinator.acquirePass().pass;
264
- if (this._quadIndex > 0 && !maskClipsAll && this._instanceBuffer !== null && this._indexBuffer !== null && this._currentBlendMode !== null && this._currentTexture !== null) {
265
- device.queue.writeBuffer(this._instanceBuffer, 0, this._instanceData, 0, this._quadIndex * instanceStrideBytes);
266
- const storage = backend.getTransformStorageBuffer(this._maxNodeIndex + 1);
267
- const transformBindGroup = this._getOrCreateTransformBindGroup(device, uniformBuffer, storage.buffer);
268
- const textureBindGroup = this._createTextureBindGroup(device, backend, this._currentTexture);
269
- const stencil = backend._passCoordinator.stencilActive;
270
- const pipeline = this._getPipeline(this._currentBlendMode, backend.renderTargetFormat, stencil);
271
- pass.setPipeline(pipeline);
272
- pass.setBindGroup(0, transformBindGroup);
273
- pass.setBindGroup(1, textureBindGroup);
274
- pass.setVertexBuffer(0, this._instanceBuffer);
275
- pass.setIndexBuffer(this._indexBuffer, 'uint16');
276
- pass.drawIndexed(indicesPerInstance, this._quadIndex, 0, 0, 0);
277
- backend.stats.batches++;
278
- backend.stats.drawCalls++;
279
- }
280
- backend._passCoordinator.endPass();
281
- this._quadIndex = 0;
282
- this._maxNodeIndex = 0;
283
- this._currentBlendMode = null;
284
- this._currentTexture = null;
285
- }
286
- destroy() {
287
- this.disconnect();
288
- }
289
- _getOrCreateTransformBindGroup(device, uniformBuffer, storageBuffer) {
290
- if (this._transformBindGroup !== null && this._transformStorageBuffer === storageBuffer) {
291
- return this._transformBindGroup;
292
- }
293
- this._transformStorageBuffer = storageBuffer;
294
- this._transformBindGroup = device.createBindGroup({
295
- layout: this._uniformBindGroupLayout,
296
- entries: [
297
- { binding: 0, resource: { buffer: uniformBuffer } },
298
- { binding: 1, resource: { buffer: storageBuffer } },
299
- ],
300
- });
301
- return this._transformBindGroup;
302
- }
303
- _createTextureBindGroup(device, backend, texture) {
304
- const binding = backend.getTextureBinding(texture);
305
- return device.createBindGroup({
306
- layout: this._textureBindGroupLayout,
307
- entries: [
308
- { binding: 0, resource: binding.view },
309
- { binding: 1, resource: binding.sampler },
310
- ],
311
- });
312
- }
313
- _getPipeline(blendMode, format, stencil) {
314
- const key = `${blendMode}:${format}:${stencil ? 's' : 'n'}`;
315
- const existing = this._pipelines.get(key);
316
- if (existing) {
317
- return existing;
318
- }
319
- if (!this._device || !this._shaderModule || !this._pipelineLayout) {
320
- throw new Error('WebGpuTileChunkRenderer: renderer must be connected first.');
321
- }
322
- const descriptor = {
323
- layout: this._pipelineLayout,
324
- vertex: {
325
- module: this._shaderModule,
326
- entryPoint: 'vertexMain',
327
- buffers: [
328
- {
329
- arrayStride: instanceStrideBytes,
330
- stepMode: 'instance',
331
- attributes: [
332
- { shaderLocation: 0, offset: 0, format: 'float32x4' },
333
- { shaderLocation: 1, offset: 16, format: 'unorm16x4' },
334
- { shaderLocation: 2, offset: 24, format: 'unorm8x4' },
335
- { shaderLocation: 3, offset: 28, format: 'uint32' },
336
- ],
337
- },
338
- ],
339
- },
340
- fragment: {
341
- module: this._shaderModule,
342
- entryPoint: 'fragmentMain',
343
- targets: [
344
- {
345
- format,
346
- blend: getWebGpuBlendState(blendMode),
347
- writeMask: GPUColorWrite.ALL,
348
- },
349
- ],
350
- },
351
- primitive: {
352
- topology: 'triangle-list',
353
- },
354
- };
355
- if (stencil) {
356
- descriptor.depthStencil = stencilContentDepthStencilState();
357
- }
358
- const pipeline = this._device.createRenderPipeline(descriptor);
359
- this._pipelines.set(key, pipeline);
360
- return pipeline;
361
- }
362
- _ensureInstanceCapacity(instanceCount) {
363
- if (!this._device || instanceCount <= this._instanceCapacity) {
364
- return;
365
- }
366
- let nextCapacity = Math.max(this._instanceCapacity, initialBatchCapacity);
367
- while (nextCapacity < instanceCount) {
368
- nextCapacity *= 2;
369
- }
370
- const oldData = this._instanceData;
371
- const carryBytes = Math.min(this._quadIndex * instanceStrideBytes, oldData.byteLength);
372
- const instanceData = new ArrayBuffer(nextCapacity * instanceStrideBytes);
373
- if (carryBytes > 0) {
374
- new Uint8Array(instanceData).set(new Uint8Array(oldData, 0, carryBytes));
375
- }
376
- const instanceBuffer = this._device.createBuffer({
377
- size: instanceData.byteLength,
378
- usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
379
- });
380
- this._instanceBuffer?.destroy();
381
- this._instanceCapacity = nextCapacity;
382
- this._instanceData = instanceData;
383
- this._instanceFloat32 = new Float32Array(instanceData);
384
- this._instanceUint32 = new Uint32Array(instanceData);
385
- this._instanceBuffer = instanceBuffer;
386
- }
387
- }
25
+ * Instanced WebGPU renderer for {@link TileChunkNode}, the parity counterpart of
26
+ * the WebGL2 renderer. Same instance layout and orientation handling; per-chunk
27
+ * transforms ride on the backend's shared transform storage buffer, and the
28
+ * instance buffer grows on demand rather than flushing in fixed runs.
29
+ * @internal
30
+ */
31
+ var WebGpuTileChunkRenderer = class extends AbstractWebGpuRenderer {
32
+ /**
33
+ * Retained-batch capability opt-in: a tile chunk's per-flush
34
+ * instanced batches (fixed 32-byte layout, tile word at word 7) record and
35
+ * replay from group-owned resources. Pixel-snapped draws are excluded by
36
+ * the collect-time recordability predicate (and belt-and-braces poisoning
37
+ * in {@link render}); tile chunks have no custom-material path to exclude.
38
+ * @internal
39
+ */
40
+ _supportsRetainedBatches = true;
41
+ _projectionData = new Float32Array(projectionByteLength / Float32Array.BYTES_PER_ELEMENT);
42
+ _writtenView = null;
43
+ _writtenViewUpdateId = -1;
44
+ _hasWrittenProjection = false;
45
+ _stagedGroupData = /* @__PURE__ */ new Float32Array(16);
46
+ _device = null;
47
+ _shaderModule = null;
48
+ _uniformBindGroupLayout = null;
49
+ _textureBindGroupLayout = null;
50
+ _pipelineLayout = null;
51
+ _uniformBuffer = null;
52
+ _transformBindGroup = null;
53
+ _transformStorageBuffer = null;
54
+ _indexBuffer = null;
55
+ _instanceBuffer = null;
56
+ _instanceBufferCapacity = 0;
57
+ _instanceStagingCapacity = 0;
58
+ _instanceData = /* @__PURE__ */ new ArrayBuffer(0);
59
+ _instanceFloat32 = new Float32Array(this._instanceData);
60
+ _instanceUint32 = new Uint32Array(this._instanceData);
61
+ _pipelines = /* @__PURE__ */ new Map();
62
+ _textureBindGroups = /* @__PURE__ */ new WeakMap();
63
+ _quadIndex = 0;
64
+ _maxNodeIndex = 0;
65
+ _batchNodeCount = 0;
66
+ _nodeBooked = false;
67
+ _currentBlendMode = null;
68
+ _currentTexture = null;
69
+ _passDraws = null;
70
+ _instancePassBytes = 0;
71
+ _stagedReplayGroupData = /* @__PURE__ */ new Float32Array(16);
72
+ _recordTextures = [null];
73
+ onConnect(backend) {
74
+ if (this._device) return;
75
+ this._device = backend.device;
76
+ this._shaderModule = this._device.createShaderModule({ code: tileShaderSource });
77
+ this._uniformBindGroupLayout = this._device.createBindGroupLayout({ entries: [{
78
+ binding: 0,
79
+ visibility: GPUShaderStage.VERTEX,
80
+ buffer: { type: "uniform" }
81
+ }, {
82
+ binding: 1,
83
+ visibility: GPUShaderStage.VERTEX,
84
+ buffer: { type: "read-only-storage" }
85
+ }] });
86
+ this._textureBindGroupLayout = this._device.createBindGroupLayout({ entries: [{
87
+ binding: 0,
88
+ visibility: GPUShaderStage.FRAGMENT,
89
+ texture: { sampleType: "float" }
90
+ }, {
91
+ binding: 1,
92
+ visibility: GPUShaderStage.FRAGMENT,
93
+ sampler: { type: "filtering" }
94
+ }] });
95
+ this._pipelineLayout = this._device.createPipelineLayout({ bindGroupLayouts: [this._uniformBindGroupLayout, this._textureBindGroupLayout] });
96
+ this._uniformBuffer = this._device.createBuffer({
97
+ size: projectionByteLength,
98
+ usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
99
+ });
100
+ this._indexBuffer = this._device.createBuffer({
101
+ size: quadIndices.byteLength,
102
+ usage: GPUBufferUsage.INDEX | GPUBufferUsage.COPY_DST
103
+ });
104
+ this._device.queue.writeBuffer(this._indexBuffer, 0, quadIndices.buffer, quadIndices.byteOffset, quadIndices.byteLength);
105
+ }
106
+ onDisconnect() {
107
+ const coordinator = this._backend?._passCoordinator ?? null;
108
+ if (coordinator !== null && this._passDraws !== null && this._passDraws === coordinator.activePass) coordinator.endPass();
109
+ this._instanceBuffer?.destroy();
110
+ this._indexBuffer?.destroy();
111
+ this._uniformBuffer?.destroy();
112
+ this._pipelines.clear();
113
+ this._instanceBuffer = null;
114
+ this._indexBuffer = null;
115
+ this._transformBindGroup = null;
116
+ this._transformStorageBuffer = null;
117
+ this._textureBindGroups = /* @__PURE__ */ new WeakMap();
118
+ this._uniformBuffer = null;
119
+ this._pipelineLayout = null;
120
+ this._textureBindGroupLayout = null;
121
+ this._uniformBindGroupLayout = null;
122
+ this._shaderModule = null;
123
+ this._device = null;
124
+ this._backend = null;
125
+ this._instanceBufferCapacity = 0;
126
+ this._instanceStagingCapacity = 0;
127
+ this._instanceData = /* @__PURE__ */ new ArrayBuffer(0);
128
+ this._instanceFloat32 = new Float32Array(this._instanceData);
129
+ this._instanceUint32 = new Uint32Array(this._instanceData);
130
+ this._passDraws = null;
131
+ this._instancePassBytes = 0;
132
+ this._quadIndex = 0;
133
+ this._maxNodeIndex = 0;
134
+ this._batchNodeCount = 0;
135
+ this._nodeBooked = false;
136
+ this._currentBlendMode = null;
137
+ this._currentTexture = null;
138
+ this._writtenView = null;
139
+ this._writtenViewUpdateId = -1;
140
+ this._hasWrittenProjection = false;
141
+ }
142
+ render(node) {
143
+ const backend = this._backend;
144
+ if (backend === null) return;
145
+ const pages = node.pages;
146
+ this._nodeBooked = false;
147
+ if (pages.length === 0) return;
148
+ const blendMode = node.blendMode;
149
+ const tintRgba = node.tint.toRgba8();
150
+ const command = backend.activeDrawCommand;
151
+ const nodeIndex = command !== null ? command.nodeIndex : backend._pushTransform(node);
152
+ for (const page of pages) this._renderPage(backend, page.texture, page.quads, blendMode, tintRgba, nodeIndex);
153
+ }
154
+ _renderPage(backend, texture, quads, blendMode, tintRgba, nodeIndex) {
155
+ if (quads.length === 0) return;
156
+ if (texture.width === 0 || texture.height === 0) return;
157
+ if (texture instanceof Texture && !(texture instanceof DataTexture) && texture.source === null) return;
158
+ const blendModeChanged = this._currentBlendMode !== null && blendMode !== this._currentBlendMode;
159
+ const textureChanged = this._currentTexture !== null && texture !== this._currentTexture;
160
+ const willExceed = this._quadIndex + quads.length > this._instanceStagingCapacity && this._instanceStagingCapacity > 0;
161
+ if ((blendModeChanged || textureChanged || willExceed) && this._quadIndex > 0) this.flush();
162
+ this._currentBlendMode = blendMode;
163
+ this._currentTexture = texture;
164
+ backend.setBlendMode(blendMode);
165
+ this._ensureStagingCapacity(this._quadIndex + quads.length);
166
+ if (!this._nodeBooked) {
167
+ this._nodeBooked = true;
168
+ this._batchNodeCount++;
169
+ }
170
+ const f32 = this._instanceFloat32;
171
+ const u32 = this._instanceUint32;
172
+ const flipYTexture = texture.flipY;
173
+ const baseWord = nodeIndex & TILE_ROW_MASK;
174
+ for (const q of quads) {
175
+ const idx = this._quadIndex * wordsPerInstance;
176
+ f32[idx + 0] = q.x0;
177
+ f32[idx + 1] = q.y0;
178
+ f32[idx + 2] = q.x1;
179
+ f32[idx + 3] = q.y1;
180
+ const flipX = (q.orient & 1) !== 0;
181
+ const tileFlipY = (q.orient & 2) !== 0;
182
+ const diagonal = (q.orient & 4) !== 0;
183
+ const uA = flipX ? q.u1 : q.u0;
184
+ const uB = flipX ? q.u0 : q.u1;
185
+ let vA = tileFlipY ? q.v1 : q.v0;
186
+ let vB = tileFlipY ? q.v0 : q.v1;
187
+ if (flipYTexture) {
188
+ const swap = vA;
189
+ vA = vB;
190
+ vB = swap;
191
+ }
192
+ const uMin = uA * 65535 & 65535;
193
+ const vMin = vA * 65535 & 65535;
194
+ const uMax = uB * 65535 & 65535;
195
+ const vMax = vB * 65535 & 65535;
196
+ u32[idx + 4] = uMin | vMin << 16;
197
+ u32[idx + 5] = uMax | vMax << 16;
198
+ u32[idx + 6] = tintRgba;
199
+ u32[idx + 7] = (diagonal ? baseWord | TILE_DIAGONAL_BIT : baseWord) >>> 0;
200
+ this._quadIndex++;
201
+ }
202
+ if (nodeIndex > this._maxNodeIndex) this._maxNodeIndex = nodeIndex;
203
+ }
204
+ flush() {
205
+ const backend = this._backend;
206
+ const device = this._device;
207
+ const uniformBuffer = this._uniformBuffer;
208
+ if (!backend || !device || !uniformBuffer) return;
209
+ if (this._quadIndex === 0 && !backend.clearRequested) return;
210
+ const view = backend.view;
211
+ const viewportChanged = packSnapViewport(backend, this._projectionData, 32);
212
+ const projectionChanged = !this._hasWrittenProjection || this._writtenView !== view || this._writtenViewUpdateId !== view.updateId || viewportChanged || this._groupContentChanged(backend);
213
+ const scissor = backend.getScissorRect();
214
+ const maskClipsAll = scissor !== null && (scissor.width <= 0 || scissor.height <= 0);
215
+ const coordinator = backend._passCoordinator;
216
+ const texture = this._currentTexture;
217
+ const blendMode = this._currentBlendMode;
218
+ const indexBuffer = this._indexBuffer;
219
+ const willDraw = this._quadIndex > 0 && !maskClipsAll && texture !== null && blendMode !== null && indexBuffer !== null;
220
+ const flushBytes = this._quadIndex * instanceStrideBytes;
221
+ const targetInstanceBytes = this._instancePassBytes + flushBytes;
222
+ if (this._passDraws !== null && this._passDraws === coordinator.activePass && (projectionChanged || willDraw && targetInstanceBytes > this._instanceBufferCapacity) || willDraw && coordinator.passHasDraws && (backend._textureUploadWouldMutate(texture) || backend._transformStorageWouldGrow(this._maxNodeIndex + 1))) coordinator.endPass();
223
+ if (this._passDraws !== coordinator.activePass) {
224
+ this._passDraws = null;
225
+ this._instancePassBytes = 0;
226
+ }
227
+ if (projectionChanged) {
228
+ packAffineMat4(view.getTransform(), this._projectionData, 0);
229
+ packAffineMat4(backend.renderGroupTransform ?? Matrix.identity, this._projectionData, 16);
230
+ this._writtenView = view;
231
+ this._writtenViewUpdateId = view.updateId;
232
+ this._hasWrittenProjection = true;
233
+ device.queue.writeBuffer(uniformBuffer, 0, this._projectionData.buffer, this._projectionData.byteOffset, this._projectionData.byteLength);
234
+ }
235
+ if (willDraw || backend.clearRequested) {
236
+ const active = coordinator.acquirePass();
237
+ const pass = active.pass;
238
+ if (willDraw) {
239
+ const instanceBuffer = this._ensureInstanceBufferCapacity(device, targetInstanceBytes);
240
+ const instanceByteOffset = this._instancePassBytes;
241
+ device.queue.writeBuffer(instanceBuffer, instanceByteOffset, this._instanceData, 0, flushBytes);
242
+ const storage = backend.getTransformStorageBuffer(this._maxNodeIndex + 1);
243
+ const transformBindGroup = this._getOrCreateTransformBindGroup(device, uniformBuffer, storage.buffer);
244
+ const textureBindGroup = this._getOrCreateTextureBindGroup(device, backend, texture);
245
+ const stencil = coordinator.stencilActive;
246
+ const pipeline = this._getPipeline(blendMode, backend.renderTargetFormat, stencil);
247
+ pass.setPipeline(pipeline);
248
+ pass.setBindGroup(0, transformBindGroup);
249
+ pass.setBindGroup(1, textureBindGroup);
250
+ pass.setVertexBuffer(0, instanceBuffer, instanceByteOffset);
251
+ pass.setIndexBuffer(indexBuffer, "uint16");
252
+ pass.drawIndexed(indicesPerInstance, this._quadIndex, 0, 0, 0);
253
+ this._instancePassBytes = instanceByteOffset + flushBytes;
254
+ this._passDraws = active;
255
+ coordinator.markPassDraws();
256
+ backend.stats.batches++;
257
+ backend.stats.drawCalls++;
258
+ }
259
+ }
260
+ if (this._quadIndex > 0 && backend._retainedCaptureActive && blendMode !== null && texture !== null) {
261
+ this._recordTextures[0] = texture;
262
+ backend._recordRetainedBatch(this, this._instanceData, flushBytes, this._quadIndex, blendMode, this._recordTextures, 1, null, null, this._batchNodeCount);
263
+ }
264
+ this._quadIndex = 0;
265
+ this._maxNodeIndex = 0;
266
+ this._batchNodeCount = 0;
267
+ this._currentBlendMode = null;
268
+ this._currentTexture = null;
269
+ }
270
+ /**
271
+ * Whether the packed floats of the active group matrix differ from what the
272
+ * shared projection UBO currently holds at [16, 32). Stages the packed matrix
273
+ * into `_stagedGroupData` as a side effect (idempotent - safe to call more
274
+ * than once per flush).
275
+ */
276
+ _groupContentChanged(backend) {
277
+ packAffineMat4(backend.renderGroupTransform ?? Matrix.identity, this._stagedGroupData, 0);
278
+ if (!this._hasWrittenProjection) return true;
279
+ return packedGroupChanged(this._stagedGroupData, this._projectionData, 16);
280
+ }
281
+ destroy() {
282
+ this.disconnect();
283
+ }
284
+ _getOrCreateTransformBindGroup(device, uniformBuffer, storageBuffer) {
285
+ if (this._transformBindGroup !== null && this._transformStorageBuffer === storageBuffer) return this._transformBindGroup;
286
+ this._transformStorageBuffer = storageBuffer;
287
+ this._transformBindGroup = device.createBindGroup({
288
+ layout: this._uniformBindGroupLayout,
289
+ entries: [{
290
+ binding: 0,
291
+ resource: { buffer: uniformBuffer }
292
+ }, {
293
+ binding: 1,
294
+ resource: { buffer: storageBuffer }
295
+ }]
296
+ });
297
+ return this._transformBindGroup;
298
+ }
299
+ /**
300
+ * Build (or reuse) the group(1) texture bind group for `texture`. The
301
+ * binding is resolved BEFORE the cache lookup on purpose: resolving is what
302
+ * syncs a dirty/mutated texture's content to the GPU, so it must run every
303
+ * flush/replay even when the bind group itself is served from cache.
304
+ */
305
+ _getOrCreateTextureBindGroup(device, backend, texture) {
306
+ const { view, sampler } = backend.getTextureBinding(texture);
307
+ const cached = this._textureBindGroups.get(texture);
308
+ if (cached?.view === view && cached.sampler === sampler) return cached.group;
309
+ const group = device.createBindGroup({
310
+ layout: this._textureBindGroupLayout,
311
+ entries: [{
312
+ binding: 0,
313
+ resource: view
314
+ }, {
315
+ binding: 1,
316
+ resource: sampler
317
+ }]
318
+ });
319
+ this._textureBindGroups.set(texture, {
320
+ group,
321
+ view,
322
+ sampler
323
+ });
324
+ return group;
325
+ }
326
+ /** @internal See {@link WebGpuRetainedBatchReplayer._scanRetainedNodeIndexRange}. */
327
+ _scanRetainedNodeIndexRange(bytes, range) {
328
+ const words = new Uint32Array(bytes.buffer, bytes.byteOffset, bytes.byteLength / Uint32Array.BYTES_PER_ELEMENT);
329
+ for (let i = 7; i < words.length; i += 8) {
330
+ const row = words[i] & TILE_ROW_MASK;
331
+ if (row < range.min) range.min = row;
332
+ if (row > range.max) range.max = row;
333
+ }
334
+ }
335
+ /** @internal See {@link WebGpuRetainedBatchReplayer._rebaseRetainedNodeIndices} (group-local indices). */
336
+ _rebaseRetainedNodeIndices(bytes, base) {
337
+ const words = new Uint32Array(bytes.buffer, bytes.byteOffset, bytes.byteLength / Uint32Array.BYTES_PER_ELEMENT);
338
+ for (let i = 7; i < words.length; i += 8) {
339
+ const word = words[i];
340
+ const diagonal = word & TILE_DIAGONAL_BIT;
341
+ const row = word & TILE_ROW_MASK;
342
+ words[i] = (diagonal | row - base & TILE_ROW_MASK) >>> 0;
343
+ }
344
+ }
345
+ /**
346
+ * Replay one recorded batch from its group-owned bundle into the OPEN pass.
347
+ * Reuses only recorded DATA (instance bytes, transform rows, texture, blend
348
+ * mode); every piece of STATE is resolved live - pipeline via the
349
+ * `_getPipeline` cache, the group(1) texture bind group via the live
350
+ * texture-set cache (resolving re-syncs dirty content), and the group's
351
+ * 128-byte UBO (projection from the live view + the live composed group
352
+ * matrix) written only when its content changed. The same-frame
353
+ * double-replay hazard (one group under two views while the open pass
354
+ * already holds this bundle's draws) ends the pass first.
355
+ * @internal
356
+ */
357
+ _replayRetainedBatch(payload) {
358
+ const backend = this._backend;
359
+ const device = this._device;
360
+ const bundle = payload.bundle;
361
+ if (!backend || !device || this._indexBuffer === null || !bundle.isReady) return;
362
+ this.flush();
363
+ const scissor = backend.getScissorRect();
364
+ if (scissor !== null && (scissor.width <= 0 || scissor.height <= 0)) return;
365
+ const coordinator = backend._passCoordinator;
366
+ if (coordinator.passHasDraws) {
367
+ for (const texture of payload.textures) if (backend._textureUploadWouldMutate(texture)) {
368
+ coordinator.endPass();
369
+ break;
370
+ }
371
+ }
372
+ const textureBindGroup = this._getOrCreateTextureBindGroup(device, backend, payload.textures[0]);
373
+ const view = backend.view;
374
+ const scratch = this._stagedReplayGroupData;
375
+ packAffineMat4(backend.renderGroupTransform ?? Matrix.identity, scratch, 0);
376
+ const viewportChanged = packSnapViewport(backend, bundle.uboData, 32);
377
+ let uboDirty = !bundle.uboWritten || bundle.uboView !== view || bundle.uboViewUpdateId !== view.updateId || viewportChanged;
378
+ if (!uboDirty) {
379
+ for (let i = 0; i < 16; i++) if (scratch[i] !== bundle.uboData[16 + i]) {
380
+ uboDirty = true;
381
+ break;
382
+ }
383
+ }
384
+ if (uboDirty) {
385
+ const activePass = coordinator.activePass;
386
+ if (activePass !== null && bundle.drawsInPass === activePass) coordinator.endPass();
387
+ packAffineMat4(view.getTransform(), bundle.uboData, 0);
388
+ bundle.uboData.set(scratch, 16);
389
+ bundle.uboView = view;
390
+ bundle.uboViewUpdateId = view.updateId;
391
+ bundle.uboWritten = true;
392
+ device.queue.writeBuffer(bundle.uniformBuffer, 0, bundle.uboData.buffer, bundle.uboData.byteOffset, retainedGroupUniformBytes);
393
+ }
394
+ const active = coordinator.acquirePass();
395
+ const pass = active.pass;
396
+ pass.setPipeline(this._getPipeline(payload.blendMode, backend.renderTargetFormat, coordinator.stencilActive));
397
+ pass.setBindGroup(0, bundle.getBindGroup(device, this._uniformBindGroupLayout, false));
398
+ pass.setBindGroup(1, textureBindGroup);
399
+ pass.setVertexBuffer(0, bundle.instanceBuffer, payload.byteOffset);
400
+ pass.setIndexBuffer(this._indexBuffer, "uint16");
401
+ pass.drawIndexed(indicesPerInstance, payload.instanceCount, 0, 0, 0);
402
+ bundle.drawsInPass = active;
403
+ coordinator.markPassDraws();
404
+ backend.stats.batches++;
405
+ backend.stats.drawCalls++;
406
+ }
407
+ _getPipeline(blendMode, format, stencil) {
408
+ const key = `${blendMode}:${format}:${stencil ? "s" : "n"}`;
409
+ const existing = this._pipelines.get(key);
410
+ if (existing) return existing;
411
+ if (!this._device || !this._shaderModule || !this._pipelineLayout) throw new Error("WebGpuTileChunkRenderer: renderer must be connected first.");
412
+ const descriptor = {
413
+ layout: this._pipelineLayout,
414
+ vertex: {
415
+ module: this._shaderModule,
416
+ entryPoint: "vertexMain",
417
+ buffers: [{
418
+ arrayStride: instanceStrideBytes,
419
+ stepMode: "instance",
420
+ attributes: [
421
+ {
422
+ shaderLocation: 0,
423
+ offset: 0,
424
+ format: "float32x4"
425
+ },
426
+ {
427
+ shaderLocation: 1,
428
+ offset: 16,
429
+ format: "unorm16x4"
430
+ },
431
+ {
432
+ shaderLocation: 2,
433
+ offset: 24,
434
+ format: "unorm8x4"
435
+ },
436
+ {
437
+ shaderLocation: 3,
438
+ offset: 28,
439
+ format: "uint32"
440
+ }
441
+ ]
442
+ }]
443
+ },
444
+ fragment: {
445
+ module: this._shaderModule,
446
+ entryPoint: "fragmentMain",
447
+ targets: [{
448
+ format,
449
+ blend: getWebGpuBlendState(blendMode),
450
+ writeMask: GPUColorWrite.ALL
451
+ }]
452
+ },
453
+ primitive: { topology: "triangle-list" }
454
+ };
455
+ if (stencil) descriptor.depthStencil = stencilContentDepthStencilState();
456
+ const pipeline = this._device.createRenderPipeline(descriptor);
457
+ this._pipelines.set(key, pipeline);
458
+ return pipeline;
459
+ }
460
+ /**
461
+ * Grow the CPU staging array to hold `instanceCount` instances, carrying the
462
+ * quads packed so far. Flush-local: the array only ever holds the pending
463
+ * batch, so it is sized independently of the pass-scoped GPU buffer.
464
+ */
465
+ _ensureStagingCapacity(instanceCount) {
466
+ if (instanceCount <= this._instanceStagingCapacity) return;
467
+ let nextCapacity = Math.max(this._instanceStagingCapacity, initialBatchCapacity);
468
+ while (nextCapacity < instanceCount) nextCapacity *= 2;
469
+ const oldData = this._instanceData;
470
+ const carryBytes = Math.min(this._quadIndex * instanceStrideBytes, oldData.byteLength);
471
+ const instanceData = /* @__PURE__ */ new ArrayBuffer(nextCapacity * instanceStrideBytes);
472
+ if (carryBytes > 0) new Uint8Array(instanceData).set(new Uint8Array(oldData, 0, carryBytes));
473
+ this._instanceStagingCapacity = nextCapacity;
474
+ this._instanceData = instanceData;
475
+ this._instanceFloat32 = new Float32Array(instanceData);
476
+ this._instanceUint32 = new Uint32Array(instanceData);
477
+ }
478
+ /**
479
+ * Resolve the GPU instance buffer for `requiredBytes` - the total this pass
480
+ * would reach by appending, not this flush's own bytes. Reallocation frees the
481
+ * buffer earlier draws in the open pass read, so the caller must have ended
482
+ * that pass (and restarted the cursor) before a growing call. Capacity
483
+ * doubles, so the split a growth costs converges away within a frame or two.
484
+ */
485
+ _ensureInstanceBufferCapacity(device, requiredBytes) {
486
+ const existing = this._instanceBuffer;
487
+ if (existing !== null && requiredBytes <= this._instanceBufferCapacity) return existing;
488
+ const nextCapacity = Math.max(this._instanceBufferCapacity * 2, requiredBytes, initialBatchCapacity * instanceStrideBytes);
489
+ const instanceBuffer = device.createBuffer({
490
+ label: "tile-chunk:instance-buffer",
491
+ size: nextCapacity,
492
+ usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST
493
+ });
494
+ existing?.destroy();
495
+ this._instanceBufferCapacity = nextCapacity;
496
+ this._instanceBuffer = instanceBuffer;
497
+ return instanceBuffer;
498
+ }
499
+ };
388
500
 
501
+ //#endregion
389
502
  export { WebGpuTileChunkRenderer };
390
- //# sourceMappingURL=WebGpuTileChunkRenderer.js.map
503
+ //# sourceMappingURL=WebGpuTileChunkRenderer.js.map