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.
- package/dist/minecraft-renderer.js +28 -28
- package/dist/minecraft-renderer.js.meta.json +1 -1
- package/dist/threeWorker.js +395 -395
- package/package.json +1 -1
- package/src/three/chunkMeshManager.ts +106 -35
- package/src/three/globalBlockBuffer.ts +8 -0
- package/src/three/globalLegacyBuffer.ts +257 -19
- package/src/three/legacyMultiDraw.ts +99 -0
- package/src/three/tests/chunkMeshManagerLegacy.test.ts +112 -4
- package/src/three/tests/globalLegacyBuffer.test.ts +163 -20
- package/src/three/tests/legacyMultiDraw.test.ts +109 -0
- package/src/three/tests/pr3PerFrameCpuCuts.test.ts +293 -0
- package/src/three/worldRendererThree.ts +14 -4
|
@@ -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?.
|
|
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?.
|
|
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
|
|
197
|
-
expect(
|
|
198
|
-
expect(
|
|
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
|
|
216
|
-
expect(
|
|
217
|
-
expect(
|
|
218
|
-
expect(
|
|
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
|
|
238
|
-
expect(
|
|
239
|
-
expect(
|
|
240
|
-
expect(
|
|
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.
|
|
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
|
|
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.
|
|
285
|
+
expect(buffer.getVisibleIndexSpans().length).toBeGreaterThan(0)
|
|
271
286
|
|
|
272
287
|
buffer.reset()
|
|
273
|
-
expect(buffer.
|
|
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
|
|
322
|
-
expect(
|
|
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
|
|
326
|
-
for (let 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
|
+
})
|