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
package/package.json
CHANGED
|
@@ -140,6 +140,9 @@ export class ChunkMeshManager {
|
|
|
140
140
|
private readonly _legacyCullBoxMin = new THREE.Vector3()
|
|
141
141
|
private readonly _legacyCullBoxMax = new THREE.Vector3()
|
|
142
142
|
private readonly _visibleSectionSpans: Array<{ key: string, distSq: number }> = []
|
|
143
|
+
/** Sections with geometry in global legacy opaque and/or blend buffers — cull/raycast scan only these. */
|
|
144
|
+
private readonly legacyCullSections = new Map<string, { worldX: number, worldY: number, worldZ: number }>()
|
|
145
|
+
private _lastCullFingerprint = ''
|
|
143
146
|
/** Drives per-frame cull + span rebuild; cleared after updateSectionCullAndSort. */
|
|
144
147
|
cullDirty = true
|
|
145
148
|
private readonly _lastCullCamPos = new THREE.Vector3()
|
|
@@ -384,6 +387,45 @@ export class ChunkMeshManager {
|
|
|
384
387
|
return this.activeSections.has(sectionKey)
|
|
385
388
|
}
|
|
386
389
|
|
|
390
|
+
private registerLegacyCullSection (sectionKey: string, worldX: number, worldY: number, worldZ: number): void {
|
|
391
|
+
this.legacyCullSections.set(sectionKey, { worldX, worldY, worldZ })
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
private maybeUnregisterLegacyCullSection (sectionKey: string): void {
|
|
395
|
+
const inOpaque = this.globalLegacyBuffer?.hasSection(sectionKey) ?? false
|
|
396
|
+
const inBlend = this.globalLegacyBlendBuffer?.hasSection(sectionKey) ?? false
|
|
397
|
+
if (!inOpaque && !inBlend) {
|
|
398
|
+
this.legacyCullSections.delete(sectionKey)
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
private updatePooledLegacyCullState (
|
|
403
|
+
cameraWorldX: number,
|
|
404
|
+
cameraWorldY: number,
|
|
405
|
+
cameraWorldZ: number,
|
|
406
|
+
): void {
|
|
407
|
+
for (const poolEntry of this.activeSections.values()) {
|
|
408
|
+
const sectionKey = poolEntry.sectionKey
|
|
409
|
+
if (!sectionKey) continue
|
|
410
|
+
const sectionObject = this.sectionObjects[sectionKey]
|
|
411
|
+
if (!sectionObject) continue
|
|
412
|
+
|
|
413
|
+
updateLegacySectionCullState(
|
|
414
|
+
poolEntry.mesh,
|
|
415
|
+
sectionObject.worldX ?? 0,
|
|
416
|
+
sectionObject.worldY ?? 0,
|
|
417
|
+
sectionObject.worldZ ?? 0,
|
|
418
|
+
cameraWorldX,
|
|
419
|
+
cameraWorldY,
|
|
420
|
+
cameraWorldZ,
|
|
421
|
+
this._legacyCullFrustum,
|
|
422
|
+
this._legacyCullBox,
|
|
423
|
+
this._legacyCullBoxMin,
|
|
424
|
+
this._legacyCullBoxMax,
|
|
425
|
+
)
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
387
429
|
/**
|
|
388
430
|
* Shared section visibility + span groups for global legacy and cube buffers.
|
|
389
431
|
*/
|
|
@@ -394,8 +436,9 @@ export class ChunkMeshManager {
|
|
|
394
436
|
const visible = this._visibleSectionSpans
|
|
395
437
|
visible.length = 0
|
|
396
438
|
|
|
397
|
-
for (const [sectionKey
|
|
398
|
-
|
|
439
|
+
for (const [sectionKey] of this.legacyCullSections) {
|
|
440
|
+
const sectionObject = this.sectionObjects[sectionKey]
|
|
441
|
+
if (!sectionObject?.visible || sectionObject.worldX === undefined) continue
|
|
399
442
|
const { visible: inFrustum, distSq } = sectionIntersectsFrustum(
|
|
400
443
|
sectionObject.worldX,
|
|
401
444
|
sectionObject.worldY ?? 0,
|
|
@@ -413,13 +456,27 @@ export class ChunkMeshManager {
|
|
|
413
456
|
}
|
|
414
457
|
}
|
|
415
458
|
|
|
416
|
-
this.globalLegacyBuffer
|
|
417
|
-
this.globalLegacyBlendBuffer
|
|
418
|
-
|
|
459
|
+
const opaqueBuf = this.globalLegacyBuffer
|
|
460
|
+
const blendBuf = this.globalLegacyBlendBuffer
|
|
419
461
|
const gb = this.globalBlockBuffer
|
|
462
|
+
|
|
463
|
+
const opaqueKeys: string[] = []
|
|
464
|
+
for (const entry of visible) {
|
|
465
|
+
if (opaqueBuf?.hasSection(entry.key)) opaqueKeys.push(entry.key)
|
|
466
|
+
}
|
|
467
|
+
opaqueKeys.sort()
|
|
468
|
+
|
|
469
|
+
const blendVisible = visible.filter(v => blendBuf?.hasSection(v.key))
|
|
470
|
+
blendVisible.sort((a, b) => b.distSq - a.distSq)
|
|
471
|
+
const blendKeys = blendVisible.map(v => v.key)
|
|
472
|
+
|
|
473
|
+
const cubeVisibleKeys: string[] = []
|
|
474
|
+
const visibleSlots: Array<{ start: number, count: number }> = []
|
|
420
475
|
if (gb) {
|
|
421
|
-
const visibleSlots: Array<{ start: number, count: number }> = []
|
|
422
476
|
gb.forEachSectionSlot((key, slot) => {
|
|
477
|
+
const sectionObject = this.sectionObjects[key]
|
|
478
|
+
// Keep in sync with legacy gate (line 440).
|
|
479
|
+
if (!sectionObject?.visible) return
|
|
423
480
|
const entry = this.shaderSectionRaycastBoxes.get(key)
|
|
424
481
|
if (!entry) {
|
|
425
482
|
return
|
|
@@ -439,35 +496,39 @@ export class ChunkMeshManager {
|
|
|
439
496
|
if (!inFrustum) {
|
|
440
497
|
return
|
|
441
498
|
}
|
|
499
|
+
cubeVisibleKeys.push(key)
|
|
442
500
|
const drawStart = gb.getSectionDrawStart(key)
|
|
443
501
|
if (drawStart !== undefined) {
|
|
444
502
|
visibleSlots.push({ start: drawStart, count: slot.count })
|
|
445
503
|
}
|
|
446
504
|
})
|
|
447
|
-
const spans = buildVisibleCubeSpans(visibleSlots, gb.getHighWatermark())
|
|
448
|
-
gb.setVisibleSpans(spans)
|
|
449
505
|
}
|
|
506
|
+
cubeVisibleKeys.sort()
|
|
450
507
|
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
508
|
+
const fingerprint = [
|
|
509
|
+
opaqueKeys.join(','),
|
|
510
|
+
blendKeys.join(','),
|
|
511
|
+
cubeVisibleKeys.join(','),
|
|
512
|
+
opaqueBuf?.getLayoutVersion() ?? 0,
|
|
513
|
+
blendBuf?.getLayoutVersion() ?? 0,
|
|
514
|
+
gb?.getLayoutVersion() ?? 0,
|
|
515
|
+
].join('|')
|
|
456
516
|
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
sectionObject.worldY ?? 0,
|
|
461
|
-
sectionObject.worldZ ?? 0,
|
|
462
|
-
cameraWorldX,
|
|
463
|
-
cameraWorldY,
|
|
464
|
-
cameraWorldZ,
|
|
465
|
-
this._legacyCullFrustum,
|
|
466
|
-
this._legacyCullBox,
|
|
467
|
-
this._legacyCullBoxMin,
|
|
468
|
-
this._legacyCullBoxMax,
|
|
469
|
-
)
|
|
517
|
+
if (fingerprint === this._lastCullFingerprint) {
|
|
518
|
+
this.updatePooledLegacyCullState(cameraWorldX, cameraWorldY, cameraWorldZ)
|
|
519
|
+
return
|
|
470
520
|
}
|
|
521
|
+
this._lastCullFingerprint = fingerprint
|
|
522
|
+
|
|
523
|
+
opaqueBuf?.updateDrawSpans(visible, 'opaque')
|
|
524
|
+
blendBuf?.updateDrawSpans(visible, 'sortedBlend')
|
|
525
|
+
|
|
526
|
+
if (gb) {
|
|
527
|
+
const spans = buildVisibleCubeSpans(visibleSlots, gb.getHighWatermark())
|
|
528
|
+
gb.setVisibleSpans(spans)
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
this.updatePooledLegacyCullState(cameraWorldX, cameraWorldY, cameraWorldZ)
|
|
471
532
|
}
|
|
472
533
|
|
|
473
534
|
markCullDirty (): void {
|
|
@@ -609,13 +670,16 @@ export class ChunkMeshManager {
|
|
|
609
670
|
const wy = section.worldY
|
|
610
671
|
const wz = section.worldZ
|
|
611
672
|
if (wx !== undefined && wy !== undefined && wz !== undefined) {
|
|
612
|
-
this.getGlobalLegacyBuffer().addSection(
|
|
673
|
+
const added = this.getGlobalLegacyBuffer().addSection(
|
|
613
674
|
sectionKey,
|
|
614
675
|
{ positions, colors, skyLights, blockLights, uvs, indices },
|
|
615
676
|
wx,
|
|
616
677
|
wy,
|
|
617
678
|
wz,
|
|
618
679
|
)
|
|
680
|
+
if (added) {
|
|
681
|
+
this.registerLegacyCullSection(sectionKey, wx, wy, wz)
|
|
682
|
+
}
|
|
619
683
|
}
|
|
620
684
|
delete section.deferredLegacyOpaque
|
|
621
685
|
}
|
|
@@ -626,13 +690,16 @@ export class ChunkMeshManager {
|
|
|
626
690
|
const wy = section.worldY
|
|
627
691
|
const wz = section.worldZ
|
|
628
692
|
if (wx !== undefined && wy !== undefined && wz !== undefined) {
|
|
629
|
-
this.getGlobalLegacyBlendBuffer().addSection(
|
|
693
|
+
const added = this.getGlobalLegacyBlendBuffer().addSection(
|
|
630
694
|
sectionKey,
|
|
631
695
|
{ positions, colors, skyLights, blockLights, uvs, indices },
|
|
632
696
|
wx,
|
|
633
697
|
wy,
|
|
634
698
|
wz,
|
|
635
699
|
)
|
|
700
|
+
if (added) {
|
|
701
|
+
this.registerLegacyCullSection(sectionKey, wx, wy, wz)
|
|
702
|
+
}
|
|
636
703
|
}
|
|
637
704
|
delete section.deferredLegacyBlend
|
|
638
705
|
section.hasBlendMesh = false
|
|
@@ -660,15 +727,13 @@ export class ChunkMeshManager {
|
|
|
660
727
|
const far = raycaster.far
|
|
661
728
|
const halfExtent = LEGACY_SECTION_HALF_EXTENT + 0.01
|
|
662
729
|
const candidates: string[] = []
|
|
663
|
-
for (const [key
|
|
664
|
-
|
|
730
|
+
for (const [key] of this.legacyCullSections) {
|
|
731
|
+
const section = this.sectionObjects[key]
|
|
732
|
+
if (!section || section.worldX === undefined) continue
|
|
665
733
|
const dx = section.worldX - origin.x
|
|
666
734
|
const dy = (section.worldY ?? 0) - origin.y
|
|
667
735
|
const dz = (section.worldZ ?? 0) - origin.z
|
|
668
736
|
if (dx * dx + dy * dy + dz * dz > maxDistSq) continue
|
|
669
|
-
const inOpaque = this.globalLegacyBuffer?.hasSection(key) ?? false
|
|
670
|
-
const inBlend = this.globalLegacyBlendBuffer?.hasSection(key) ?? false
|
|
671
|
-
if (!inOpaque && !inBlend) continue
|
|
672
737
|
if (!sectionAabbIntersectsRay(
|
|
673
738
|
section.worldX,
|
|
674
739
|
section.worldY ?? 0,
|
|
@@ -912,7 +977,9 @@ export class ChunkMeshManager {
|
|
|
912
977
|
geometryData.sy,
|
|
913
978
|
geometryData.sz,
|
|
914
979
|
)
|
|
915
|
-
if (
|
|
980
|
+
if (added) {
|
|
981
|
+
this.registerLegacyCullSection(sectionKey, geometryData.sx, geometryData.sy, geometryData.sz)
|
|
982
|
+
} else {
|
|
916
983
|
const poolEntry = this.acquirePooledSectionMesh(sectionKey)
|
|
917
984
|
if (!poolEntry) return null
|
|
918
985
|
legacyMesh = this.uploadLegacyPooledMesh(poolEntry, geometryData, geometryData.sx, geometryData.sy, geometryData.sz)
|
|
@@ -960,7 +1027,9 @@ export class ChunkMeshManager {
|
|
|
960
1027
|
geometryData.sy,
|
|
961
1028
|
geometryData.sz,
|
|
962
1029
|
)
|
|
963
|
-
if (
|
|
1030
|
+
if (added) {
|
|
1031
|
+
this.registerLegacyCullSection(sectionKey, geometryData.sx, geometryData.sy, geometryData.sz)
|
|
1032
|
+
} else {
|
|
964
1033
|
console.warn(`ChunkMeshManager: blend invariant violation for section ${sectionKey}, using pooled mesh fallback`)
|
|
965
1034
|
const poolEntry = this.acquirePooledSectionMesh(sectionKey)
|
|
966
1035
|
if (!poolEntry) return null
|
|
@@ -1197,6 +1266,7 @@ export class ChunkMeshManager {
|
|
|
1197
1266
|
sectionObject.visible = true
|
|
1198
1267
|
}
|
|
1199
1268
|
delete this.waitingChunksToDisplay[chunkKey]
|
|
1269
|
+
this.markCullDirty()
|
|
1200
1270
|
}
|
|
1201
1271
|
|
|
1202
1272
|
// Re-check every parked entry; each has its own grace window via `ageMs`.
|
|
@@ -1359,6 +1429,7 @@ export class ChunkMeshManager {
|
|
|
1359
1429
|
this.globalBlockBuffer?.removeSection(sectionKey)
|
|
1360
1430
|
this.globalLegacyBuffer?.removeSection(sectionKey)
|
|
1361
1431
|
this.globalLegacyBlendBuffer?.removeSection(sectionKey)
|
|
1432
|
+
this.maybeUnregisterLegacyCullSection(sectionKey)
|
|
1362
1433
|
this.unregisterShaderSectionRaycastBox(sectionKey)
|
|
1363
1434
|
this.markCullDirty()
|
|
1364
1435
|
delete sectionObject.deferredLegacyOpaque
|
|
@@ -69,6 +69,7 @@ export class GlobalBlockBuffer {
|
|
|
69
69
|
private tierCAttrs: TierCAttrState | null = null
|
|
70
70
|
private tierCGl: WebGL2RenderingContext | null = null
|
|
71
71
|
private debugOverlay = false
|
|
72
|
+
private layoutVersion = 0
|
|
72
73
|
|
|
73
74
|
constructor (
|
|
74
75
|
material: THREE.ShaderMaterial,
|
|
@@ -225,6 +226,11 @@ export class GlobalBlockBuffer {
|
|
|
225
226
|
this.sectionSlots.set(sectionKey, slot)
|
|
226
227
|
this.markDirty(slot.start, slot.start + faceCount - 1)
|
|
227
228
|
this.mesh.geometry.instanceCount = this.highWatermark
|
|
229
|
+
this.layoutVersion++
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
getLayoutVersion (): number {
|
|
233
|
+
return this.layoutVersion
|
|
228
234
|
}
|
|
229
235
|
|
|
230
236
|
hasSection (sectionKey: string): boolean {
|
|
@@ -288,6 +294,7 @@ export class GlobalBlockBuffer {
|
|
|
288
294
|
this.insertFreeSlot(slot)
|
|
289
295
|
this.shrinkHighWatermark()
|
|
290
296
|
this.mesh.geometry.instanceCount = this.highWatermark
|
|
297
|
+
this.layoutVersion++
|
|
291
298
|
}
|
|
292
299
|
|
|
293
300
|
/** One interior-hole move per frame when fragmentation exceeds threshold; deferred shrink. */
|
|
@@ -318,6 +325,7 @@ export class GlobalBlockBuffer {
|
|
|
318
325
|
this.sectionSlots.set(section.key, { start: newStart, count: section.count })
|
|
319
326
|
this.markDirty(newStart, newStart + section.count - 1)
|
|
320
327
|
this.pendingMove = { key: section.key, oldStart, newStart, count: section.count }
|
|
328
|
+
this.layoutVersion++
|
|
321
329
|
}
|
|
322
330
|
|
|
323
331
|
uploadDirtyRange (): void {
|
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
//@ts-nocheck
|
|
2
2
|
import * as THREE from 'three'
|
|
3
|
+
import {
|
|
4
|
+
createLegacyMultiDrawScratch,
|
|
5
|
+
detectLegacyMultiDrawCaps,
|
|
6
|
+
drawLegacySpans,
|
|
7
|
+
logLegacyMultiDrawTierOnce,
|
|
8
|
+
type LegacyDrawSpan,
|
|
9
|
+
type LegacyMultiDrawCaps,
|
|
10
|
+
type LegacyMultiDrawScratch,
|
|
11
|
+
} from './legacyMultiDraw'
|
|
3
12
|
import { computeCameraRelativeUniforms, type RenderOrigin } from './shaders/legacyBlockShader'
|
|
4
13
|
|
|
5
14
|
const VERTS_PER_QUAD = 4
|
|
@@ -11,6 +20,9 @@ const FLOATS_PER_LIGHT_VERT = 1
|
|
|
11
20
|
const DEFAULT_INITIAL_CAPACITY_QUADS = 128_000
|
|
12
21
|
const DEFAULT_GROWTH_INCREMENT_QUADS = 128_000
|
|
13
22
|
const MAX_UPLOAD_QUADS_PER_FRAME = 5_000
|
|
23
|
+
const FRAGMENTATION_THRESHOLD = 0.25
|
|
24
|
+
|
|
25
|
+
type PendingMove = { key: string, oldStart: number, newStart: number, count: number }
|
|
14
26
|
|
|
15
27
|
/** CPU bytes per allocated quad slot (all legacy vertex/index attrs). */
|
|
16
28
|
export const LEGACY_BYTES_PER_QUAD =
|
|
@@ -44,6 +56,8 @@ export type LegacySectionGeometryData = LegacySectionGeometry & {
|
|
|
44
56
|
sz: number
|
|
45
57
|
}
|
|
46
58
|
|
|
59
|
+
export type { LegacyDrawSpan } from './legacyMultiDraw'
|
|
60
|
+
|
|
47
61
|
/**
|
|
48
62
|
* Single GPU mesh for legacy quads (opaque+cutout or transparent blend).
|
|
49
63
|
* Camera-relative via per-vertex a_origin (relative to render origin) + u_originDelta uniforms.
|
|
@@ -67,6 +81,12 @@ export class GlobalLegacyBuffer {
|
|
|
67
81
|
private pendingRanges: Array<{ start: number, end: number }> = []
|
|
68
82
|
private readonly _spanScratch: Array<{ start: number, count: number }> = []
|
|
69
83
|
private renderOrigin: RenderOrigin = { x: 0, y: 0, z: 0 }
|
|
84
|
+
private layoutVersion = 0
|
|
85
|
+
private pendingMove: PendingMove | null = null
|
|
86
|
+
private visibleIndexSpans: LegacyDrawSpan[] = []
|
|
87
|
+
private readonly _drawScratch: LegacyMultiDrawScratch = createLegacyMultiDrawScratch()
|
|
88
|
+
private multiDrawCaps: LegacyMultiDrawCaps | null = null
|
|
89
|
+
private debugOverlay = false
|
|
70
90
|
|
|
71
91
|
constructor (
|
|
72
92
|
material: THREE.ShaderMaterial,
|
|
@@ -114,6 +134,38 @@ export class GlobalLegacyBuffer {
|
|
|
114
134
|
this.mesh.position.set(0, 0, 0)
|
|
115
135
|
scene.add(this.mesh)
|
|
116
136
|
this.syncDefaultDrawGroups()
|
|
137
|
+
|
|
138
|
+
this.mesh.onAfterRender = (renderer, _scene, _camera, _geometry, material) => {
|
|
139
|
+
if (this.visibleIndexSpans.length === 0) return
|
|
140
|
+
const gl = renderer.getContext() as WebGL2RenderingContext
|
|
141
|
+
if (!this.multiDrawCaps) {
|
|
142
|
+
this.multiDrawCaps = detectLegacyMultiDrawCaps(gl)
|
|
143
|
+
logLegacyMultiDrawTierOnce(this.multiDrawCaps.tier, this.debugOverlay)
|
|
144
|
+
}
|
|
145
|
+
drawLegacySpans(gl, this.multiDrawCaps, this.visibleIndexSpans, this._drawScratch)
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
setDebugOverlay (enabled: boolean): void {
|
|
150
|
+
this.debugOverlay = enabled
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Suppress three's full-buffer indexed draw; onAfterRender issues visible spans only.
|
|
155
|
+
* setDrawRange(0,0) skips bindingStates.setup — use a minimal non-zero range so
|
|
156
|
+
* program/VAO/ELEMENT_ARRAY_BUFFER stay bound while three draws ~nothing.
|
|
157
|
+
* Draws one triangle from index 0 (usually harmless; if quad 0 is culled, one stray tri).
|
|
158
|
+
*/
|
|
159
|
+
suppressThreeDraw (): void {
|
|
160
|
+
this.mesh.geometry.setDrawRange(0, 3)
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
setVisibleIndexSpans (spans: LegacyDrawSpan[]): void {
|
|
164
|
+
this.visibleIndexSpans = spans
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
getVisibleIndexSpans (): readonly LegacyDrawSpan[] {
|
|
168
|
+
return this.visibleIndexSpans
|
|
117
169
|
}
|
|
118
170
|
|
|
119
171
|
private syncDefaultDrawGroups (): void {
|
|
@@ -171,14 +223,14 @@ export class GlobalLegacyBuffer {
|
|
|
171
223
|
this.uvs.set(geo.uvs, dstUvBase)
|
|
172
224
|
|
|
173
225
|
const originOff = dstFloatBase
|
|
174
|
-
const
|
|
175
|
-
const
|
|
176
|
-
const
|
|
226
|
+
const ox = sx - this.renderOrigin.x
|
|
227
|
+
const oy = sy - this.renderOrigin.y
|
|
228
|
+
const oz = sz - this.renderOrigin.z
|
|
177
229
|
for (let v = 0; v < vertCount; v++) {
|
|
178
230
|
const o = originOff + v * FLOATS_PER_VERT
|
|
179
|
-
this.aOrigin[o] =
|
|
180
|
-
this.aOrigin[o + 1] =
|
|
181
|
-
this.aOrigin[o + 2] =
|
|
231
|
+
this.aOrigin[o] = ox
|
|
232
|
+
this.aOrigin[o + 1] = oy
|
|
233
|
+
this.aOrigin[o + 2] = oz
|
|
182
234
|
}
|
|
183
235
|
|
|
184
236
|
const dstIndexBase = slot.start * INDICES_PER_QUAD
|
|
@@ -190,15 +242,60 @@ export class GlobalLegacyBuffer {
|
|
|
190
242
|
this.sectionSlots.set(sectionKey, slot)
|
|
191
243
|
this.markDirty(slot.start, slot.start + quadCount - 1)
|
|
192
244
|
this.syncDefaultDrawGroups()
|
|
245
|
+
this.layoutVersion++
|
|
193
246
|
return true
|
|
194
247
|
}
|
|
195
248
|
|
|
249
|
+
getLayoutVersion (): number {
|
|
250
|
+
return this.layoutVersion
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
getSectionDrawStart (sectionKey: string): number | undefined {
|
|
254
|
+
const slot = this.sectionSlots.get(sectionKey)
|
|
255
|
+
if (!slot) return undefined
|
|
256
|
+
if (this.pendingMove?.key === sectionKey) return this.pendingMove.oldStart
|
|
257
|
+
return slot.start
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
getPendingMove (): PendingMove | null {
|
|
261
|
+
return this.pendingMove
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/** One interior-hole move per frame when fragmentation exceeds threshold; deferred shrink. */
|
|
265
|
+
compactStep (): void {
|
|
266
|
+
if (this.pendingMove) {
|
|
267
|
+
const { newStart, count } = this.pendingMove
|
|
268
|
+
if (this.rangeFullyUploaded(newStart, newStart + count - 1)) {
|
|
269
|
+
this.finalizePendingMove()
|
|
270
|
+
}
|
|
271
|
+
return
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
if (this.highWatermark === 0) return
|
|
275
|
+
const interiorFree = this.interiorFreeQuads()
|
|
276
|
+
if (interiorFree / this.highWatermark <= FRAGMENTATION_THRESHOLD) return
|
|
277
|
+
|
|
278
|
+
const section = this.findMovableSection(MAX_UPLOAD_QUADS_PER_FRAME)
|
|
279
|
+
if (!section) return
|
|
280
|
+
|
|
281
|
+
const hole = this.findLowestInteriorHole(section.start, section.count)
|
|
282
|
+
if (!hole) return
|
|
283
|
+
|
|
284
|
+
const reserved = this.reserveFreeSlotAt(hole.index, section.count)
|
|
285
|
+
const oldStart = section.start
|
|
286
|
+
const newStart = reserved.start
|
|
287
|
+
|
|
288
|
+
this.copySectionRange(oldStart, newStart, section.count)
|
|
289
|
+
this.sectionSlots.set(section.key, { start: newStart, count: section.count })
|
|
290
|
+
this.markDirty(newStart, newStart + section.count - 1)
|
|
291
|
+
this.pendingMove = { key: section.key, oldStart, newStart, count: section.count }
|
|
292
|
+
this.layoutVersion++
|
|
293
|
+
}
|
|
294
|
+
|
|
196
295
|
updateDrawSpans (visible: VisibleSectionSpan[], mode: 'opaque' | 'sortedBlend'): void {
|
|
197
|
-
|
|
198
|
-
geometry.clearGroups()
|
|
296
|
+
this.visibleIndexSpans = []
|
|
199
297
|
|
|
200
298
|
if (this.highWatermark === 0) {
|
|
201
|
-
geometry.setDrawRange(0, 0)
|
|
202
299
|
return
|
|
203
300
|
}
|
|
204
301
|
|
|
@@ -207,21 +304,30 @@ export class GlobalLegacyBuffer {
|
|
|
207
304
|
let visibleQuadCount = 0
|
|
208
305
|
|
|
209
306
|
for (const entry of visible) {
|
|
307
|
+
const drawStart = this.getSectionDrawStart(entry.key)
|
|
210
308
|
const slot = this.sectionSlots.get(entry.key)
|
|
211
|
-
if (!slot) continue
|
|
212
|
-
spans.push({ start:
|
|
309
|
+
if (drawStart === undefined || !slot) continue
|
|
310
|
+
spans.push({ start: drawStart, count: slot.count })
|
|
213
311
|
visibleQuadCount += slot.count
|
|
214
312
|
}
|
|
215
313
|
|
|
216
314
|
if (spans.length === 0) {
|
|
217
|
-
geometry.setDrawRange(0, 0)
|
|
218
315
|
return
|
|
219
316
|
}
|
|
220
317
|
|
|
318
|
+
const pushIndexSpan = (quadStart: number, quadCount: number): void => {
|
|
319
|
+
this.visibleIndexSpans.push({
|
|
320
|
+
indexStart: quadStart * INDICES_PER_QUAD,
|
|
321
|
+
indexCount: quadCount * INDICES_PER_QUAD,
|
|
322
|
+
})
|
|
323
|
+
}
|
|
324
|
+
|
|
221
325
|
if (mode === 'opaque') {
|
|
222
326
|
if (visibleQuadCount >= this.highWatermark * FULL_DRAW_VISIBLE_FRACTION) {
|
|
223
|
-
|
|
224
|
-
|
|
327
|
+
this.visibleIndexSpans.push({
|
|
328
|
+
indexStart: 0,
|
|
329
|
+
indexCount: this.highWatermark * INDICES_PER_QUAD,
|
|
330
|
+
})
|
|
225
331
|
return
|
|
226
332
|
}
|
|
227
333
|
|
|
@@ -230,18 +336,17 @@ export class GlobalLegacyBuffer {
|
|
|
230
336
|
this.capOpaqueSpans(spans)
|
|
231
337
|
|
|
232
338
|
for (const span of spans) {
|
|
233
|
-
|
|
339
|
+
pushIndexSpan(span.start, span.count)
|
|
234
340
|
}
|
|
235
341
|
} else {
|
|
236
342
|
visible.sort((a, b) => b.distSq - a.distSq)
|
|
237
343
|
for (const entry of visible) {
|
|
344
|
+
const drawStart = this.getSectionDrawStart(entry.key)
|
|
238
345
|
const slot = this.sectionSlots.get(entry.key)
|
|
239
|
-
if (!slot) continue
|
|
240
|
-
|
|
346
|
+
if (drawStart === undefined || !slot) continue
|
|
347
|
+
pushIndexSpan(drawStart, slot.count)
|
|
241
348
|
}
|
|
242
349
|
}
|
|
243
|
-
|
|
244
|
-
geometry.setDrawRange(0, this.highWatermark * INDICES_PER_QUAD)
|
|
245
350
|
}
|
|
246
351
|
|
|
247
352
|
private mergeOpaqueSpans (spans: Array<{ start: number, count: number }>): void {
|
|
@@ -328,6 +433,18 @@ export class GlobalLegacyBuffer {
|
|
|
328
433
|
const slot = this.sectionSlots.get(sectionKey)
|
|
329
434
|
if (!slot) return
|
|
330
435
|
|
|
436
|
+
if (this.pendingMove?.key === sectionKey) {
|
|
437
|
+
const { oldStart, count } = this.pendingMove
|
|
438
|
+
const oldIndexBase = oldStart * INDICES_PER_QUAD
|
|
439
|
+
const oldIndexLen = count * INDICES_PER_QUAD
|
|
440
|
+
for (let i = 0; i < oldIndexLen; i++) {
|
|
441
|
+
this.indices[oldIndexBase + i] = 0
|
|
442
|
+
}
|
|
443
|
+
this.markDirty(oldStart, oldStart + count - 1)
|
|
444
|
+
this.insertFreeSlot({ start: oldStart, count })
|
|
445
|
+
this.pendingMove = null
|
|
446
|
+
}
|
|
447
|
+
|
|
331
448
|
const dstIndexBase = slot.start * INDICES_PER_QUAD
|
|
332
449
|
const indexLen = slot.count * INDICES_PER_QUAD
|
|
333
450
|
for (let i = 0; i < indexLen; i++) {
|
|
@@ -339,6 +456,7 @@ export class GlobalLegacyBuffer {
|
|
|
339
456
|
this.insertFreeSlot(slot)
|
|
340
457
|
this.shrinkHighWatermark()
|
|
341
458
|
this.syncDefaultDrawGroups()
|
|
459
|
+
this.layoutVersion++
|
|
342
460
|
}
|
|
343
461
|
|
|
344
462
|
hasPendingUploads (): boolean {
|
|
@@ -505,6 +623,8 @@ export class GlobalLegacyBuffer {
|
|
|
505
623
|
this.freeList.length = 0
|
|
506
624
|
this.highWatermark = 0
|
|
507
625
|
this.pendingRanges.length = 0
|
|
626
|
+
this.pendingMove = null
|
|
627
|
+
this.visibleIndexSpans = []
|
|
508
628
|
this.syncDefaultDrawGroups()
|
|
509
629
|
}
|
|
510
630
|
|
|
@@ -585,7 +705,125 @@ export class GlobalLegacyBuffer {
|
|
|
585
705
|
}
|
|
586
706
|
}
|
|
587
707
|
|
|
708
|
+
private interiorFreeQuads (): number {
|
|
709
|
+
let total = 0
|
|
710
|
+
for (const slot of this.freeList) {
|
|
711
|
+
if (slot.start < this.highWatermark) total += slot.count
|
|
712
|
+
}
|
|
713
|
+
return total
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
private findMovableSection (maxCount: number): { key: string, start: number, count: number } | undefined {
|
|
717
|
+
const sections: Array<{ key: string, start: number, count: number }> = []
|
|
718
|
+
for (const [key, slot] of this.sectionSlots) {
|
|
719
|
+
sections.push({ key, start: slot.start, count: slot.count })
|
|
720
|
+
}
|
|
721
|
+
if (sections.length === 0) return undefined
|
|
722
|
+
|
|
723
|
+
sections.sort((a, b) => {
|
|
724
|
+
if (b.start !== a.start) return b.start - a.start
|
|
725
|
+
return (b.start + b.count) - (a.start + a.count)
|
|
726
|
+
})
|
|
727
|
+
|
|
728
|
+
const tailmost = sections[0]!
|
|
729
|
+
if (tailmost.count <= maxCount && this.findLowestInteriorHole(tailmost.start, tailmost.count)) {
|
|
730
|
+
return tailmost
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
const candidates = sections
|
|
734
|
+
.filter(s => s.count <= maxCount)
|
|
735
|
+
.sort((a, b) => b.count - a.count)
|
|
736
|
+
|
|
737
|
+
for (const s of candidates) {
|
|
738
|
+
if (this.findLowestInteriorHole(s.start, s.count)) return s
|
|
739
|
+
}
|
|
740
|
+
return undefined
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
private findLowestInteriorHole (
|
|
744
|
+
sectionStart: number,
|
|
745
|
+
count: number,
|
|
746
|
+
): { start: number, count: number, index: number } | undefined {
|
|
747
|
+
for (let i = 0; i < this.freeList.length; i++) {
|
|
748
|
+
const slot = this.freeList[i]!
|
|
749
|
+
if (slot.start < sectionStart && slot.count >= count) {
|
|
750
|
+
return { start: slot.start, count: slot.count, index: i }
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
return undefined
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
private reserveFreeSlotAt (index: number, count: number): { start: number, count: number } {
|
|
757
|
+
const slot = this.freeList[index]!
|
|
758
|
+
this.freeList.splice(index, 1)
|
|
759
|
+
if (slot.count === count) return { start: slot.start, count }
|
|
760
|
+
const used = { start: slot.start, count }
|
|
761
|
+
this.insertFreeSlot({ start: slot.start + count, count: slot.count - count })
|
|
762
|
+
return used
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
private copySectionRange (oldStart: number, newStart: number, quadCount: number): void {
|
|
766
|
+
const oldVertBase = oldStart * VERTS_PER_QUAD
|
|
767
|
+
const newVertBase = newStart * VERTS_PER_QUAD
|
|
768
|
+
const vertCount = quadCount * VERTS_PER_QUAD
|
|
769
|
+
const vertDelta = newVertBase - oldVertBase
|
|
770
|
+
|
|
771
|
+
const oldFloatBase = oldVertBase * FLOATS_PER_VERT
|
|
772
|
+
const newFloatBase = newVertBase * FLOATS_PER_VERT
|
|
773
|
+
const floatLen = vertCount * FLOATS_PER_VERT
|
|
774
|
+
this.positions.copyWithin(newFloatBase, oldFloatBase, oldFloatBase + floatLen)
|
|
775
|
+
this.colors.copyWithin(newFloatBase, oldFloatBase, oldFloatBase + floatLen)
|
|
776
|
+
this.aOrigin.copyWithin(newFloatBase, oldFloatBase, oldFloatBase + floatLen)
|
|
777
|
+
|
|
778
|
+
const oldLightBase = oldVertBase * FLOATS_PER_LIGHT_VERT
|
|
779
|
+
const newLightBase = newVertBase * FLOATS_PER_LIGHT_VERT
|
|
780
|
+
const lightLen = vertCount * FLOATS_PER_LIGHT_VERT
|
|
781
|
+
this.skyLights.copyWithin(newLightBase, oldLightBase, oldLightBase + lightLen)
|
|
782
|
+
this.blockLights.copyWithin(newLightBase, oldLightBase, oldLightBase + lightLen)
|
|
783
|
+
|
|
784
|
+
const oldUvBase = oldVertBase * FLOATS_PER_UV_VERT
|
|
785
|
+
const newUvBase = newVertBase * FLOATS_PER_UV_VERT
|
|
786
|
+
const uvLen = vertCount * FLOATS_PER_UV_VERT
|
|
787
|
+
this.uvs.copyWithin(newUvBase, oldUvBase, oldUvBase + uvLen)
|
|
788
|
+
|
|
789
|
+
const oldIndexBase = oldStart * INDICES_PER_QUAD
|
|
790
|
+
const newIndexBase = newStart * INDICES_PER_QUAD
|
|
791
|
+
const indexLen = quadCount * INDICES_PER_QUAD
|
|
792
|
+
for (let i = 0; i < indexLen; i++) {
|
|
793
|
+
this.indices[newIndexBase + i] = this.indices[oldIndexBase + i]! + vertDelta
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
private rangeFullyUploaded (start: number, end: number): boolean {
|
|
798
|
+
for (const r of this.pendingRanges) {
|
|
799
|
+
if (r.start <= end && r.end >= start) return false
|
|
800
|
+
}
|
|
801
|
+
return true
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
private finalizePendingMove (): void {
|
|
805
|
+
const move = this.pendingMove
|
|
806
|
+
if (!move) return
|
|
807
|
+
|
|
808
|
+
const { oldStart, count } = move
|
|
809
|
+
const oldIndexBase = oldStart * INDICES_PER_QUAD
|
|
810
|
+
const oldIndexLen = count * INDICES_PER_QUAD
|
|
811
|
+
for (let i = 0; i < oldIndexLen; i++) {
|
|
812
|
+
this.indices[oldIndexBase + i] = 0
|
|
813
|
+
}
|
|
814
|
+
this.insertFreeSlot({ start: oldStart, count })
|
|
815
|
+
this.shrinkHighWatermark()
|
|
816
|
+
if (oldStart < this.highWatermark) {
|
|
817
|
+
this.markDirty(oldStart, oldStart + count - 1)
|
|
818
|
+
}
|
|
819
|
+
this.syncDefaultDrawGroups()
|
|
820
|
+
this.pendingMove = null
|
|
821
|
+
this.layoutVersion++
|
|
822
|
+
}
|
|
823
|
+
|
|
588
824
|
private growCapacity (minQuads: number): void {
|
|
825
|
+
if (this.pendingMove) this.finalizePendingMove()
|
|
826
|
+
|
|
589
827
|
let newCap = this.capacityQuads
|
|
590
828
|
while (newCap < minQuads) newCap += this.growthIncrementQuads
|
|
591
829
|
|