minecraft-renderer 0.1.78 → 0.1.79

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.
@@ -0,0 +1,99 @@
1
+ //@ts-nocheck
2
+ import { MAX_OPAQUE_SPANS } from './globalLegacyBuffer'
3
+
4
+ export type LegacyMultiDrawTier = 'A' | 'B' | 'C'
5
+
6
+ export type LegacyDrawSpan = { indexStart: number, indexCount: number }
7
+
8
+ export type LegacyMultiDrawCaps = {
9
+ tier: LegacyMultiDrawTier
10
+ ext: MultiDrawElementsExt | null
11
+ }
12
+
13
+ type MultiDrawElementsExt = {
14
+ multiDrawElementsWEBGL: (
15
+ mode: number,
16
+ counts: Int32Array,
17
+ countsOffset: number,
18
+ type: number,
19
+ offsets: Int32Array,
20
+ offsetsOffset: number,
21
+ drawCount: number,
22
+ ) => void
23
+ }
24
+
25
+ export type LegacyMultiDrawScratch = {
26
+ counts: Int32Array
27
+ offsets: Int32Array
28
+ }
29
+
30
+ export function createLegacyMultiDrawScratch (): LegacyMultiDrawScratch {
31
+ return {
32
+ counts: new Int32Array(MAX_OPAQUE_SPANS),
33
+ offsets: new Int32Array(MAX_OPAQUE_SPANS),
34
+ }
35
+ }
36
+
37
+ /** Grow scratch arrays when blend (uncapped) span count exceeds opaque cap. */
38
+ function ensureScratchCapacity (scratch: LegacyMultiDrawScratch, minLength: number): void {
39
+ if (scratch.counts.length >= minLength) return
40
+ let newLen = scratch.counts.length
41
+ while (newLen < minLength) newLen *= 2
42
+ scratch.counts = new Int32Array(newLen)
43
+ scratch.offsets = new Int32Array(newLen)
44
+ }
45
+
46
+ export function detectLegacyMultiDrawCaps (gl: WebGL2RenderingContext): LegacyMultiDrawCaps {
47
+ const ext = gl.getExtension('WEBGL_multi_draw')
48
+ if (ext) {
49
+ return { tier: 'A', ext: ext as MultiDrawElementsExt }
50
+ }
51
+ return { tier: 'B', ext: null }
52
+ }
53
+
54
+ let tierLogged = false
55
+
56
+ export function logLegacyMultiDrawTierOnce (tier: LegacyMultiDrawTier, debug: boolean): void {
57
+ if (tierLogged || !debug) return
58
+ tierLogged = true
59
+ console.info('[globalLegacyBuffer] legacy multi_draw tier', tier)
60
+ }
61
+
62
+ /**
63
+ * Issue indexed legacy draws for visible spans. Tier B/C loop drawElements.
64
+ */
65
+ export function drawLegacySpans (
66
+ gl: WebGL2RenderingContext,
67
+ caps: LegacyMultiDrawCaps,
68
+ spans: readonly LegacyDrawSpan[],
69
+ scratch: LegacyMultiDrawScratch,
70
+ ): void {
71
+ const drawCount = spans.length
72
+ if (drawCount === 0) return
73
+
74
+ ensureScratchCapacity(scratch, drawCount)
75
+
76
+ const mode = gl.TRIANGLES
77
+ const type = gl.UNSIGNED_INT
78
+
79
+ for (let i = 0; i < drawCount; i++) {
80
+ const span = spans[i]!
81
+ scratch.counts[i] = span.indexCount
82
+ scratch.offsets[i] = span.indexStart * 4
83
+ }
84
+
85
+ if (caps.tier === 'A' && caps.ext) {
86
+ caps.ext.multiDrawElementsWEBGL(
87
+ mode,
88
+ scratch.counts, 0,
89
+ type,
90
+ scratch.offsets, 0,
91
+ drawCount,
92
+ )
93
+ return
94
+ }
95
+
96
+ for (let i = 0; i < drawCount; i++) {
97
+ gl.drawElements(mode, scratch.counts[i]!, type, scratch.offsets[i]!)
98
+ }
99
+ }
@@ -9,6 +9,7 @@ vi.mock('../entity/EntityMesh', () => ({
9
9
  import { ChunkMeshManager } from '../chunkMeshManager'
10
10
  import type { WorldRendererThree } from '../worldRendererThree'
11
11
  import type { MesherGeometryOutput } from '../../mesher-shared/shared'
12
+ import { renderWasmOutputToGeometry } from '../../wasm-mesher/bridge/render-from-wasm'
12
13
 
13
14
  function makeQuadArrays () {
14
15
  const positions = new Float32Array([
@@ -123,6 +124,64 @@ function makeInvalidBlendGeometry (): MesherGeometryOutput {
123
124
 
124
125
  type ManagerOptions = {
125
126
  revealDefer?: boolean
127
+ shaderCubes?: boolean
128
+ finishedChunks?: Record<string, boolean>
129
+ }
130
+
131
+ function makeShaderCubeOnlyGeometry (): MesherGeometryOutput {
132
+ const block = {
133
+ position: [0, 0, 0] as [number, number, number],
134
+ block_state_id: 1,
135
+ visible_faces: (1 << 0) | (1 << 1) | (1 << 2) | (1 << 3) | (1 << 4) | (1 << 5),
136
+ ao_data: Array.from({ length: 6 }, () => [3, 3, 3, 3]),
137
+ light_data: Array.from({ length: 6 }, () => [1, 1, 1, 1]),
138
+ light_combined: Array.from({ length: 6 }, () => [255, 255, 255, 255]),
139
+ }
140
+ const out = renderWasmOutputToGeometry(
141
+ { blocks: [block], block_count: 1, block_iterations: 0 },
142
+ '1.16.5',
143
+ '0,0,0',
144
+ { x: 8, y: 8, z: 8 },
145
+ undefined,
146
+ { shaderCubes: true },
147
+ )
148
+ return {
149
+ sectionYNumber: 0,
150
+ chunkKey: '0,0',
151
+ sectionStartY: 0,
152
+ sectionEndY: 16,
153
+ sectionStartX: 0,
154
+ sectionEndX: 16,
155
+ sectionStartZ: 0,
156
+ sectionEndZ: 16,
157
+ sx: 8,
158
+ sy: 8,
159
+ sz: 8,
160
+ positions: new Float32Array(0),
161
+ normals: new Float32Array(0),
162
+ colors: new Float32Array(0),
163
+ skyLights: new Float32Array(0),
164
+ blockLights: new Float32Array(0),
165
+ uvs: new Float32Array(0),
166
+ indices: new Uint32Array(0),
167
+ indicesCount: 0,
168
+ using32Array: true,
169
+ tiles: {},
170
+ heads: {},
171
+ signs: {},
172
+ banners: {},
173
+ hadErrors: false,
174
+ blocksCount: 1,
175
+ shaderCubes: out.shaderCubes,
176
+ }
177
+ }
178
+
179
+ function makeCullCamera (): THREE.PerspectiveCamera {
180
+ const camera = new THREE.PerspectiveCamera(60, 1, 0.1, 1000)
181
+ camera.position.set(8, 8, 20)
182
+ camera.lookAt(8, 8, 8)
183
+ camera.updateMatrixWorld()
184
+ return camera
126
185
  }
127
186
 
128
187
  function createManager (opts: ManagerOptions = {}): ChunkMeshManager {
@@ -134,7 +193,7 @@ function createManager (opts: ManagerOptions = {}): ChunkMeshManager {
134
193
  }
135
194
  : undefined
136
195
  const worldRenderer = {
137
- shaderCubeBlocksEnabled: () => false,
196
+ shaderCubeBlocksEnabled: () => opts.shaderCubes ?? false,
138
197
  getModule: (name: string) => (name === 'futuristicReveal' ? revealModule : undefined),
139
198
  sceneOrigin: {
140
199
  track: () => {},
@@ -142,7 +201,9 @@ function createManager (opts: ManagerOptions = {}): ChunkMeshManager {
142
201
  removeAndUntrackAll: () => {},
143
202
  },
144
203
  blockEntities: {},
145
- worldRendererConfig: {},
204
+ worldRendererConfig: opts.shaderCubes ? { wasmMesher: true } : {},
205
+ displayOptions: { inWorldRenderingConfig: {} },
206
+ finishedChunks: opts.finishedChunks ?? (opts.shaderCubes ? { '0,0': true } : {}),
146
207
  } as unknown as WorldRendererThree
147
208
  return new ChunkMeshManager(worldRenderer, scene, material, 256, 1)
148
209
  }
@@ -187,11 +248,11 @@ test('ChunkMeshManager: hidden section excluded from draw spans', () => {
187
248
  camera.updateMatrixWorld()
188
249
 
189
250
  manager.updateSectionCullAndSort(camera, 8, 8, 20)
190
- expect(manager.globalLegacyBlendBuffer?.mesh.geometry.groups.length).toBe(0)
251
+ expect(manager.globalLegacyBlendBuffer?.getVisibleIndexSpans().length).toBe(0)
191
252
 
192
253
  section.visible = true
193
254
  manager.updateSectionCullAndSort(camera, 8, 8, 20)
194
- expect(manager.globalLegacyBlendBuffer?.mesh.geometry.groups.length).toBeGreaterThan(0)
255
+ expect(manager.globalLegacyBlendBuffer?.getVisibleIndexSpans().length).toBeGreaterThan(0)
195
256
 
196
257
  manager.cleanupSection(key)
197
258
  manager.dispose()
@@ -284,3 +345,50 @@ test('ChunkMeshManager: mixed opaque and blend route to separate global buffers'
284
345
 
285
346
  manager.dispose()
286
347
  })
348
+
349
+ test('ChunkMeshManager: hidden cube section excluded from draw spans', () => {
350
+ const manager = createManager({ shaderCubes: true })
351
+ const key = '0,0,0'
352
+ manager.updateSection(key, makeShaderCubeOnlyGeometry())
353
+ const section = manager.sectionObjects[key]!
354
+ expect(manager.globalBlockBuffer?.hasSection(key)).toBe(true)
355
+
356
+ const camera = makeCullCamera()
357
+ manager.updateSectionCullAndSort(camera, 8, 8, 20)
358
+ expect(manager.globalBlockBuffer?.getVisibleSpans().length).toBeGreaterThan(0)
359
+
360
+ section.visible = false
361
+ manager.updateSectionCullAndSort(camera, 8, 8, 20)
362
+ expect(manager.globalBlockBuffer?.getVisibleSpans().length).toBe(0)
363
+
364
+ section.visible = true
365
+ manager.updateSectionCullAndSort(camera, 8, 8, 20)
366
+ expect(manager.globalBlockBuffer?.getVisibleSpans().length).toBeGreaterThan(0)
367
+
368
+ manager.cleanupSection(key)
369
+ manager.dispose()
370
+ })
371
+
372
+ test('ChunkMeshManager: finishChunkDisplay reveals cube spans and marks cull dirty', () => {
373
+ const manager = createManager({ shaderCubes: true, finishedChunks: {} })
374
+ const key = '0,0,0'
375
+ manager.updateSection(key, makeShaderCubeOnlyGeometry())
376
+ expect(manager.sectionObjects[key]?.visible).toBe(false)
377
+ expect(manager.globalBlockBuffer?.hasSection(key)).toBe(true)
378
+
379
+ const camera = makeCullCamera()
380
+ manager.updateSectionCullAndSort(camera, 8, 8, 20)
381
+ expect(manager.globalBlockBuffer?.getVisibleSpans().length).toBe(0)
382
+
383
+ const markCullDirtySpy = vi.spyOn(manager, 'markCullDirty')
384
+ manager.finishChunkDisplay('0,0')
385
+ expect(manager.sectionObjects[key]?.visible).toBe(true)
386
+ expect(markCullDirtySpy).toHaveBeenCalled()
387
+
388
+ manager.updateSectionCullAndSort(camera, 8, 8, 20)
389
+ expect(manager.globalBlockBuffer?.getVisibleSpans().length).toBeGreaterThan(0)
390
+
391
+ markCullDirtySpy.mockRestore()
392
+ manager.cleanupSection(key)
393
+ manager.dispose()
394
+ })
@@ -39,6 +39,8 @@ function makeQuadGeometry (): {
39
39
 
40
40
  type BufferInternals = {
41
41
  pendingRanges: Array<{ start: number, end: number }>
42
+ pendingMove: { key: string, oldStart: number, newStart: number, count: number } | null
43
+ growCapacity: (minQuads: number) => void
42
44
  }
43
45
 
44
46
  function getInternals (buffer: GlobalLegacyBuffer): BufferInternals {
@@ -49,6 +51,20 @@ function drainUploads (buffer: GlobalLegacyBuffer): void {
49
51
  while (getInternals(buffer).pendingRanges.length) buffer.uploadDirtyRange()
50
52
  }
51
53
 
54
+ function finishCurrentMove (buffer: GlobalLegacyBuffer): void {
55
+ drainUploads(buffer)
56
+ buffer.compactStep()
57
+ }
58
+
59
+ function readSectionIndices (buffer: GlobalLegacyBuffer, key: string): number[] {
60
+ const slot = buffer.getSectionSlot(key)
61
+ if (!slot) throw new Error(`missing section ${key}`)
62
+ const indexAttr = buffer.mesh.geometry.index!.array as Uint32Array
63
+ const base = slot.start * 6
64
+ const len = slot.count * 6
65
+ return Array.from(indexAttr.slice(base, base + len))
66
+ }
67
+
52
68
  test('GlobalLegacyBuffer: slot reuse and a_origin fill', () => {
53
69
  const scene = new THREE.Scene()
54
70
  const mat = createGlobalLegacyBlockMaterial()
@@ -193,9 +209,9 @@ test('GlobalLegacyBuffer: updateDrawSpans opaque merges nearby spans', () => {
193
209
  buffer.addSection('b', geo, 16, 0, 0)
194
210
  buffer.updateDrawSpans([{ key: 'a', distSq: 1 }, { key: 'b', distSq: 4 }], 'opaque')
195
211
 
196
- const groups = buffer.mesh.geometry.groups
197
- expect(groups.length).toBe(1)
198
- expect(groups[0]!.count).toBe(12)
212
+ const spans = buffer.getVisibleIndexSpans()
213
+ expect(spans.length).toBe(1)
214
+ expect(spans[0]!.indexCount).toBe(12)
199
215
 
200
216
  buffer.dispose()
201
217
  mat.dispose()
@@ -212,10 +228,10 @@ test('GlobalLegacyBuffer: updateDrawSpans opaque full draw when most quads visib
212
228
  buffer.addSection('c', geo, 32, 0, 0)
213
229
  buffer.updateDrawSpans([{ key: 'a', distSq: 1 }, { key: 'b', distSq: 2 }, { key: 'c', distSq: 3 }], 'opaque')
214
230
 
215
- const groups = buffer.mesh.geometry.groups
216
- expect(groups.length).toBe(1)
217
- expect(groups[0]!.start).toBe(0)
218
- expect(groups[0]!.count).toBe(18)
231
+ const spans = buffer.getVisibleIndexSpans()
232
+ expect(spans.length).toBe(1)
233
+ expect(spans[0]!.indexStart).toBe(0)
234
+ expect(spans[0]!.indexCount).toBe(18)
219
235
 
220
236
  buffer.dispose()
221
237
  mat.dispose()
@@ -234,10 +250,10 @@ test('GlobalLegacyBuffer: updateDrawSpans sortedBlend orders back-to-front', ()
234
250
  { key: 'far', distSq: 100 },
235
251
  ], 'sortedBlend')
236
252
 
237
- const groups = buffer.mesh.geometry.groups
238
- expect(groups.length).toBe(2)
239
- expect(groups[0]!.start).toBe(6)
240
- expect(groups[1]!.start).toBe(0)
253
+ const spans = buffer.getVisibleIndexSpans()
254
+ expect(spans.length).toBe(2)
255
+ expect(spans[0]!.indexStart).toBe(6)
256
+ expect(spans[1]!.indexStart).toBe(0)
241
257
 
242
258
  buffer.dispose()
243
259
  mat.dispose()
@@ -252,14 +268,13 @@ test('GlobalLegacyBuffer: updateDrawSpans skips missing keys', () => {
252
268
  buffer.addSection('a', geo, 0, 0, 0)
253
269
  buffer.updateDrawSpans([{ key: 'missing', distSq: 1 }], 'opaque')
254
270
 
255
- expect(buffer.mesh.geometry.groups.length).toBe(0)
256
- expect(buffer.mesh.geometry.drawRange.count).toBe(0)
271
+ expect(buffer.getVisibleIndexSpans().length).toBe(0)
257
272
 
258
273
  buffer.dispose()
259
274
  mat.dispose()
260
275
  })
261
276
 
262
- test('GlobalLegacyBuffer: reset clears groups', () => {
277
+ test('GlobalLegacyBuffer: reset clears visible spans', () => {
263
278
  const scene = new THREE.Scene()
264
279
  const mat = createGlobalLegacyBlockMaterial()
265
280
  const buffer = new GlobalLegacyBuffer(mat, scene)
@@ -267,10 +282,10 @@ test('GlobalLegacyBuffer: reset clears groups', () => {
267
282
 
268
283
  buffer.addSection('a', geo, 0, 0, 0)
269
284
  buffer.updateDrawSpans([{ key: 'a', distSq: 1 }], 'opaque')
270
- expect(buffer.mesh.geometry.groups.length).toBeGreaterThan(0)
285
+ expect(buffer.getVisibleIndexSpans().length).toBeGreaterThan(0)
271
286
 
272
287
  buffer.reset()
273
- expect(buffer.mesh.geometry.groups.length).toBe(0)
288
+ expect(buffer.getVisibleIndexSpans().length).toBe(0)
274
289
 
275
290
  buffer.dispose()
276
291
  mat.dispose()
@@ -318,12 +333,12 @@ test('GlobalLegacyBuffer: updateDrawSpans opaque caps at MAX_OPAQUE_SPANS with f
318
333
 
319
334
  buffer.updateDrawSpans(visible, 'opaque')
320
335
 
321
- const groups = buffer.mesh.geometry.groups
322
- expect(groups.length).toBe(MAX_OPAQUE_SPANS)
336
+ const spans = buffer.getVisibleIndexSpans()
337
+ expect(spans.length).toBe(MAX_OPAQUE_SPANS)
323
338
 
324
339
  const covered = new Set<number>()
325
- for (const group of groups) {
326
- for (let idx = group.start; idx < group.start + group.count; idx++) {
340
+ for (const span of spans) {
341
+ for (let idx = span.indexStart; idx < span.indexStart + span.indexCount; idx++) {
327
342
  covered.add(idx)
328
343
  }
329
344
  }
@@ -358,3 +373,131 @@ test('GlobalLegacyBuffer: addSection rejects non-quad geometry', () => {
358
373
  buffer.dispose()
359
374
  mat.dispose()
360
375
  })
376
+
377
+ test('GlobalLegacyBuffer: compaction lowers watermark after interior-hole churn', () => {
378
+ const scene = new THREE.Scene()
379
+ const mat = createGlobalLegacyBlockMaterial()
380
+ const buffer = new GlobalLegacyBuffer(mat, scene)
381
+ const geo = makeQuadGeometry()
382
+
383
+ buffer.addSection('a', geo, 0, 0, 0)
384
+ buffer.addSection('b', geo, 16, 0, 0)
385
+ buffer.addSection('c', geo, 32, 0, 0)
386
+ expect(buffer.getHighWatermark()).toBe(3)
387
+
388
+ buffer.removeSection('b')
389
+ drainUploads(buffer)
390
+ expect(buffer.getHighWatermark()).toBe(3)
391
+
392
+ buffer.compactStep()
393
+ finishCurrentMove(buffer)
394
+
395
+ expect(buffer.getHighWatermark()).toBe(2)
396
+ expect(buffer.getSectionSlot('c')).toEqual({ start: 1, count: 1 })
397
+ const slotC = buffer.getSectionSlot('c')!
398
+ const indices = readSectionIndices(buffer, 'c')
399
+ expect(indices[0]).toBe(slotC.start * 4)
400
+
401
+ buffer.dispose()
402
+ mat.dispose()
403
+ })
404
+
405
+ test('GlobalLegacyBuffer: grow during in-flight move preserves section data', () => {
406
+ const scene = new THREE.Scene()
407
+ const mat = createGlobalLegacyBlockMaterial()
408
+ const buffer = new GlobalLegacyBuffer(mat, scene, { initialCapacityQuads: 4, growthIncrementQuads: 8 })
409
+ const geo = makeQuadGeometry()
410
+
411
+ buffer.addSection('a', geo, 0, 0, 0)
412
+ buffer.addSection('b', geo, 16, 0, 0)
413
+ buffer.addSection('c', geo, 32, 0, 0)
414
+ buffer.removeSection('b')
415
+ drainUploads(buffer)
416
+
417
+ buffer.compactStep()
418
+ expect(buffer.getPendingMove()).not.toBeNull()
419
+ expect(buffer.hasSection('c')).toBe(true)
420
+
421
+ getInternals(buffer).growCapacity(16)
422
+
423
+ expect(buffer.getPendingMove()).toBeNull()
424
+ expect(buffer.hasSection('c')).toBe(true)
425
+ const slotC = buffer.getSectionSlot('c')!
426
+ const indices = readSectionIndices(buffer, 'c')
427
+ expect(indices[0]).toBe(slotC.start * 4)
428
+ expect(buffer.getCapacityQuads()).toBeGreaterThanOrEqual(16)
429
+
430
+ buffer.dispose()
431
+ mat.dispose()
432
+ })
433
+
434
+ test('GlobalLegacyBuffer: finalize move bumps layoutVersion and updates draw spans', () => {
435
+ const scene = new THREE.Scene()
436
+ const mat = createGlobalLegacyBlockMaterial()
437
+ const buffer = new GlobalLegacyBuffer(mat, scene)
438
+ const geo = makeQuadGeometry()
439
+
440
+ buffer.addSection('a', geo, 0, 0, 0)
441
+ buffer.addSection('b', geo, 16, 0, 0)
442
+ buffer.addSection('c', geo, 32, 0, 0)
443
+ buffer.removeSection('b')
444
+ drainUploads(buffer)
445
+
446
+ buffer.compactStep()
447
+ expect(buffer.getPendingMove()).not.toBeNull()
448
+ const versionAfterMove = buffer.getLayoutVersion()
449
+
450
+ const visible = [{ key: 'c', distSq: 1 }]
451
+ buffer.updateDrawSpans(visible, 'opaque')
452
+ expect(buffer.getVisibleIndexSpans()[0]!.indexStart).toBe(2 * 6)
453
+
454
+ drainUploads(buffer)
455
+ buffer.compactStep()
456
+ expect(buffer.getPendingMove()).toBeNull()
457
+ expect(buffer.getLayoutVersion()).toBe(versionAfterMove + 1)
458
+
459
+ buffer.updateDrawSpans(visible, 'opaque')
460
+ expect(buffer.getVisibleIndexSpans()[0]!.indexStart).toBe(1 * 6)
461
+
462
+ buffer.dispose()
463
+ mat.dispose()
464
+ })
465
+
466
+ test('GlobalLegacyBuffer: sortedBlend accepts more than MAX_OPAQUE_SPANS sections', () => {
467
+ const scene = new THREE.Scene()
468
+ const mat = createGlobalLegacyBlockMaterial()
469
+ const sectionCount = MAX_OPAQUE_SPANS + 6
470
+ const buffer = new GlobalLegacyBuffer(mat, scene, {
471
+ initialCapacityQuads: sectionCount + 4,
472
+ growthIncrementQuads: 64,
473
+ })
474
+ const geo = makeQuadGeometry()
475
+ const visible: Array<{ key: string, distSq: number }> = []
476
+
477
+ for (let i = 0; i < sectionCount; i++) {
478
+ const key = `s${i}`
479
+ buffer.addSection(key, geo, i * 16, 0, 0)
480
+ visible.push({ key, distSq: sectionCount - i })
481
+ }
482
+
483
+ buffer.updateDrawSpans(visible, 'sortedBlend')
484
+ expect(buffer.getVisibleIndexSpans().length).toBe(sectionCount)
485
+
486
+ buffer.dispose()
487
+ mat.dispose()
488
+ })
489
+
490
+ test('GlobalLegacyBuffer: suppressThreeDraw uses minimal non-zero draw range', () => {
491
+ const scene = new THREE.Scene()
492
+ const mat = createGlobalLegacyBlockMaterial()
493
+ const buffer = new GlobalLegacyBuffer(mat, scene)
494
+ const geo = makeQuadGeometry()
495
+
496
+ buffer.addSection('a', geo, 0, 0, 0)
497
+ buffer.suppressThreeDraw()
498
+ expect(buffer.mesh.geometry.drawRange.start).toBe(0)
499
+ expect(buffer.mesh.geometry.drawRange.count).toBe(3)
500
+
501
+ buffer.dispose()
502
+ mat.dispose()
503
+ })
@@ -0,0 +1,109 @@
1
+ //@ts-nocheck
2
+ import { expect, test, vi } from 'vitest'
3
+ import {
4
+ createLegacyMultiDrawScratch,
5
+ detectLegacyMultiDrawCaps,
6
+ drawLegacySpans,
7
+ } from '../legacyMultiDraw'
8
+
9
+ function makeMockGl (opts?: { multiDraw?: boolean }): WebGL2RenderingContext {
10
+ const multiDrawElementsWEBGL = vi.fn()
11
+ const drawElements = vi.fn()
12
+ return {
13
+ TRIANGLES: 0x0004,
14
+ UNSIGNED_INT: 0x1405,
15
+ getExtension: (name: string) => {
16
+ if (opts?.multiDraw && name === 'WEBGL_multi_draw') {
17
+ return { multiDrawElementsWEBGL }
18
+ }
19
+ return null
20
+ },
21
+ drawElements,
22
+ } as unknown as WebGL2RenderingContext
23
+ }
24
+
25
+ test('detectLegacyMultiDrawCaps: tier A when WEBGL_multi_draw is available', () => {
26
+ const gl = makeMockGl({ multiDraw: true })
27
+ const caps = detectLegacyMultiDrawCaps(gl)
28
+ expect(caps.tier).toBe('A')
29
+ expect(caps.ext).not.toBeNull()
30
+ })
31
+
32
+ test('detectLegacyMultiDrawCaps: tier B when extension is missing', () => {
33
+ const gl = makeMockGl()
34
+ const caps = detectLegacyMultiDrawCaps(gl)
35
+ expect(caps.tier).toBe('B')
36
+ expect(caps.ext).toBeNull()
37
+ })
38
+
39
+ test('drawLegacySpans: tier A uses multiDrawElementsWEBGL with byte offsets', () => {
40
+ const gl = makeMockGl({ multiDraw: true })
41
+ const caps = detectLegacyMultiDrawCaps(gl)
42
+ const scratch = createLegacyMultiDrawScratch()
43
+ const spans = [
44
+ { indexStart: 12, indexCount: 6 },
45
+ { indexStart: 30, indexCount: 12 },
46
+ ]
47
+
48
+ drawLegacySpans(gl, caps, spans, scratch)
49
+
50
+ expect(scratch.counts[0]).toBe(6)
51
+ expect(scratch.counts[1]).toBe(12)
52
+ expect(scratch.offsets[0]).toBe(48)
53
+ expect(scratch.offsets[1]).toBe(120)
54
+
55
+ const ext = caps.ext as { multiDrawElementsWEBGL: ReturnType<typeof vi.fn> }
56
+ expect(ext.multiDrawElementsWEBGL).toHaveBeenCalledTimes(1)
57
+ expect(ext.multiDrawElementsWEBGL).toHaveBeenCalledWith(
58
+ gl.TRIANGLES,
59
+ scratch.counts, 0,
60
+ gl.UNSIGNED_INT,
61
+ scratch.offsets, 0,
62
+ 2,
63
+ )
64
+ })
65
+
66
+ test('drawLegacySpans: tier B loops drawElements per span', () => {
67
+ const gl = makeMockGl()
68
+ const caps = detectLegacyMultiDrawCaps(gl)
69
+ const scratch = createLegacyMultiDrawScratch()
70
+ const spans = [{ indexStart: 4, indexCount: 6 }]
71
+
72
+ drawLegacySpans(gl, caps, spans, scratch)
73
+
74
+ expect((gl as unknown as { drawElements: ReturnType<typeof vi.fn> }).drawElements).toHaveBeenCalledWith(
75
+ gl.TRIANGLES,
76
+ 6,
77
+ gl.UNSIGNED_INT,
78
+ 16,
79
+ )
80
+ })
81
+
82
+ test('drawLegacySpans: grows scratch past MAX_OPAQUE_SPANS for uncapped blend sets', () => {
83
+ const gl = makeMockGl({ multiDraw: true })
84
+ const caps = detectLegacyMultiDrawCaps(gl)
85
+ const scratch = createLegacyMultiDrawScratch()
86
+ expect(scratch.counts.length).toBe(64)
87
+
88
+ const spanCount = 70
89
+ const spans = Array.from({ length: spanCount }, (_, i) => ({
90
+ indexStart: i * 6,
91
+ indexCount: 6,
92
+ }))
93
+
94
+ drawLegacySpans(gl, caps, spans, scratch)
95
+
96
+ expect(scratch.counts.length).toBeGreaterThanOrEqual(spanCount)
97
+ expect(scratch.offsets.length).toBeGreaterThanOrEqual(spanCount)
98
+ expect(scratch.counts[69]).toBe(6)
99
+ expect(scratch.offsets[69]).toBe(69 * 6 * 4)
100
+
101
+ const ext = caps.ext as { multiDrawElementsWEBGL: ReturnType<typeof vi.fn> }
102
+ expect(ext.multiDrawElementsWEBGL).toHaveBeenCalledWith(
103
+ gl.TRIANGLES,
104
+ scratch.counts, 0,
105
+ gl.UNSIGNED_INT,
106
+ scratch.offsets, 0,
107
+ spanCount,
108
+ )
109
+ })