minecraft-renderer 0.1.106 → 0.1.108
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/lightOwnerWorker.js +9 -0
- package/dist/lightOwnerWorker.js.map +7 -0
- package/dist/mesher.js +1 -1
- package/dist/mesher.js.map +2 -2
- package/dist/mesherWasm.js +222 -222
- package/dist/minecraft-renderer.js +105 -76
- package/dist/minecraft-renderer.js.meta.json +1 -1
- package/dist/threeWorker.js +537 -508
- package/package.json +1 -1
- package/src/bundler/bundlePrepare.test.ts +25 -0
- package/src/bundler/bundlePrepare.ts +1 -1
- package/src/graphicsBackend/appViewer.ts +15 -0
- package/src/graphicsBackend/config.ts +8 -0
- package/src/graphicsBackend/rendererDefaultOptions.test.ts +51 -0
- package/src/graphicsBackend/rendererDefaultOptions.ts +18 -1
- package/src/graphicsBackend/rendererOptionsSync.ts +4 -3
- package/src/lib/blockEntityLightRegistry.ts +4 -0
- package/src/lib/skyLight.ts +17 -0
- package/src/lib/worldrendererCommon.lightOwner.test.ts +916 -0
- package/src/lib/worldrendererCommon.removeColumn.test.ts +16 -0
- package/src/lib/worldrendererCommon.ts +340 -67
- package/src/lighting/cache/rendererLightCache.test.ts +158 -0
- package/src/lighting/cache/rendererLightCache.ts +248 -0
- package/src/lighting/coordinator/clientLightCoordinator.test.ts +102 -0
- package/src/lighting/coordinator/clientLightCoordinator.ts +574 -0
- package/src/lighting/debug/clientLightProbe.test.ts +24 -0
- package/src/lighting/debug/clientLightProbe.ts +20 -0
- package/src/lighting/debug/clientLightTrace.test.ts +271 -0
- package/src/lighting/debug/clientLightTrace.ts +335 -0
- package/src/lighting/entity/entityLightController.test.ts +216 -0
- package/src/lighting/entity/entityLightController.ts +322 -0
- package/src/lighting/owner/clientLightOwner.test.ts +467 -0
- package/src/lighting/owner/clientLightOwner.ts +523 -0
- package/src/lighting/owner/lightOwnerHost.test.ts +78 -0
- package/src/lighting/owner/lightOwnerHost.ts +202 -0
- package/src/lighting/owner/lightOwnerWorker.ts +154 -0
- package/src/lighting/owner/updateLight1171OwnerPath.test.ts +68 -0
- package/src/lighting/owner/updateLightToOwnerEvents.ts +49 -0
- package/src/lighting/tables/lightTables1171.generated.json +10 -0
- package/src/lighting/tables/lightTables1171.test.ts +168 -0
- package/src/lighting/tables/lightTables1171.ts +59 -0
- package/src/lighting/versioning/clientLightVersions.test.ts +241 -0
- package/src/lighting/versioning/clientLightVersions.ts +203 -0
- package/src/lighting/versioning/pendingSectionFlush.test.ts +214 -0
- package/src/lighting/versioning/pendingSectionFlush.ts +123 -0
- package/src/mesher-shared/lightNibblePack.ts +26 -0
- package/src/mesher-shared/mcDataWorkerShim.cjs +9 -7
- package/src/mesher-shared/modelsGeometryCommon.test.ts +6 -1
- package/src/mesher-shared/shared.ts +48 -1
- package/src/three/chunkMeshManager.ts +26 -0
- package/src/three/entities.ts +97 -8
- package/src/three/entity/EntityMesh.ts +1 -0
- package/src/three/entity/legWalkAnimation.test.ts +154 -0
- package/src/three/entity/legWalkAnimation.ts +193 -0
- package/src/three/globalBlockBuffer.ts +94 -2
- package/src/three/globalLegacyBuffer.ts +94 -2
- package/src/three/graphicsBackendBase.ts +2 -9
- package/src/three/modules/clouds.ts +191 -0
- package/src/three/modules/index.ts +4 -0
- package/src/three/modules/sunMoon.ts +151 -0
- package/src/three/skyboxRenderer.ts +15 -8
- package/src/three/tests/clouds.test.ts +59 -0
- package/src/three/tests/globalBlockBuffer.test.ts +98 -0
- package/src/three/tests/globalLegacyBuffer.test.ts +108 -0
- package/src/three/tests/waterFog.test.ts +25 -0
- package/src/three/waterFog.ts +28 -0
- package/src/three/worldRendererThree.ts +84 -39
- package/src/wasm-mesher/bridge/render-from-wasm.ts +46 -1
- package/src/wasm-mesher/runtime-build/wasm_mesher.d.ts +31 -5
- package/src/wasm-mesher/runtime-build/wasm_mesher.js +180 -5
- package/src/wasm-mesher/runtime-build/wasm_mesher_bg.wasm +0 -0
- package/src/wasm-mesher/runtime-build/wasm_mesher_bg.wasm.d.ts +11 -0
- package/src/wasm-mesher/tests/mesherLightDependencies.test.ts +83 -0
- package/src/wasm-mesher/tests/mesherWasmColumnTickSelect.test.ts +39 -0
- package/src/wasm-mesher/tests/mesherWasmConversionCache.test.ts +30 -1
- package/src/wasm-mesher/tests/mesherWasmTickParseCache.test.ts +73 -0
- package/src/wasm-mesher/tests/mesherWasmTickSchedule.test.ts +51 -0
- package/src/wasm-mesher/tests/mesherWasmTopologyPostCache.test.ts +78 -0
- package/src/wasm-mesher/worker/clientLightDarkPublication.probe.test.ts +32 -0
- package/src/wasm-mesher/worker/mesherLightDependencies.ts +43 -0
- package/src/wasm-mesher/worker/mesherWasm.ownerLightSync.test.ts +198 -0
- package/src/wasm-mesher/worker/mesherWasm.ts +506 -96
- package/src/wasm-mesher/worker/mesherWasmChunkBuffer.ts +1 -0
- package/src/wasm-mesher/worker/mesherWasmColumnTickSelect.ts +47 -0
- package/src/wasm-mesher/worker/mesherWasmConversionCache.ts +16 -5
- package/src/wasm-mesher/worker/mesherWasmLightDirty.test.ts +17 -1
- package/src/wasm-mesher/worker/mesherWasmLightDirty.ts +10 -0
- package/src/wasm-mesher/worker/mesherWasmLightMerge.test.ts +208 -0
- package/src/wasm-mesher/worker/mesherWasmLightMerge.ts +228 -0
- package/src/wasm-mesher/worker/mesherWasmOwnerLight.test.ts +161 -0
- package/src/wasm-mesher/worker/mesherWasmOwnerLight.ts +112 -0
- package/src/wasm-mesher/worker/mesherWasmTickParseCache.ts +45 -0
- package/src/wasm-mesher/worker/mesherWasmTickSchedule.ts +44 -0
- package/src/wasm-mesher/worker/mesherWasmTopologyPostCache.ts +63 -0
- package/src/worldView/types.ts +3 -0
- package/src/worldView/worldView.geometryReload.test.ts +68 -0
- package/src/worldView/worldView.spiral.test.ts +1 -1
- package/src/worldView/worldView.ts +49 -1
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
//@ts-nocheck
|
|
2
|
+
import { describe, expect, it } from 'vitest'
|
|
3
|
+
import Chunks from 'prismarine-chunk'
|
|
4
|
+
import MinecraftData from 'minecraft-data'
|
|
5
|
+
import { Vec3 } from 'vec3'
|
|
6
|
+
import { RENDERER_LIGHT_BYTES_PER_SECTION, RendererLightCache } from './rendererLightCache'
|
|
7
|
+
|
|
8
|
+
const VERSION = '1.16.5'
|
|
9
|
+
|
|
10
|
+
function makeChunk() {
|
|
11
|
+
const Chunk = Chunks(VERSION) as any
|
|
12
|
+
return new Chunk(undefined as any)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function makePresentZeroSection(y = 64) {
|
|
16
|
+
const mcData = MinecraftData(VERSION)
|
|
17
|
+
const chunk = makeChunk()
|
|
18
|
+
const stoneId = mcData.blocksByName.stone.defaultState
|
|
19
|
+
chunk.setBlockStateId(new Vec3(8, y, 8), stoneId)
|
|
20
|
+
// Allocate light sections, then zero them. All-zero light is otherwise omitted
|
|
21
|
+
// and must stay the fullbright "absent section" fallback.
|
|
22
|
+
chunk.setBlockLight(new Vec3(8, y, 8), 1)
|
|
23
|
+
chunk.setSkyLight(new Vec3(8, y, 8), 1)
|
|
24
|
+
chunk.setBlockLight(new Vec3(8, y, 8), 0)
|
|
25
|
+
chunk.setSkyLight(new Vec3(8, y, 8), 0)
|
|
26
|
+
return chunk
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function makeLitBlock(blockLight: number, skyLight: number, x = 8, y = 64, z = 8) {
|
|
30
|
+
const mcData = MinecraftData(VERSION)
|
|
31
|
+
const chunk = makeChunk()
|
|
32
|
+
const stoneId = mcData.blocksByName.stone.defaultState
|
|
33
|
+
chunk.setBlockStateId(new Vec3(x, y, z), stoneId)
|
|
34
|
+
chunk.setBlockLight(new Vec3(x, y, z), blockLight)
|
|
35
|
+
chunk.setSkyLight(new Vec3(x, y, z), skyLight)
|
|
36
|
+
return chunk
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
describe('RendererLightCache', () => {
|
|
40
|
+
it('returns fullbright for a missing column or absent section', () => {
|
|
41
|
+
const cache = new RendererLightCache(VERSION)
|
|
42
|
+
cache.setWorldBounds(0, 256)
|
|
43
|
+
expect(cache.getLight(8, 64, 8)).toEqual({ block: 0, sky: 1 })
|
|
44
|
+
|
|
45
|
+
cache.ingestColumn(0, 0, makeChunk().toJson())
|
|
46
|
+
expect(cache.getLight(8, 64, 8)).toEqual({ block: 0, sky: 1 })
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it('applies +2 floor for a present raw-zero section and clamps at 15', () => {
|
|
50
|
+
const cache = new RendererLightCache(VERSION)
|
|
51
|
+
cache.setWorldBounds(0, 256)
|
|
52
|
+
cache.ingestColumn(0, 0, makePresentZeroSection().toJson())
|
|
53
|
+
expect(cache.getLight(8, 64, 8).block).toBeCloseTo(2 / 15, 5)
|
|
54
|
+
expect(cache.getLight(8, 64, 8).sky).toBeCloseTo(2 / 15, 5)
|
|
55
|
+
|
|
56
|
+
cache.ingestColumn(0, 0, makeLitBlock(14, 14).toJson())
|
|
57
|
+
expect(cache.getLight(8, 64, 8)).toEqual({ block: 1, sky: 1 })
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it('reads packed channels at negative chunk coordinates', () => {
|
|
61
|
+
const cache = new RendererLightCache(VERSION)
|
|
62
|
+
cache.setWorldBounds(0, 256)
|
|
63
|
+
cache.ingestColumn(-16, -16, makeLitBlock(13, 4, 15, 64, 15).toJson())
|
|
64
|
+
const light = cache.getLight(-1, 64, -1)
|
|
65
|
+
expect(light.block).toBeCloseTo(15 / 15, 5)
|
|
66
|
+
expect(light.sky).toBeCloseTo(6 / 15, 5)
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
it('does not bump column revision on identical re-ingest', () => {
|
|
70
|
+
const cache = new RendererLightCache(VERSION)
|
|
71
|
+
cache.setWorldBounds(0, 256)
|
|
72
|
+
const json = makeLitBlock(10, 5).toJson()
|
|
73
|
+
const first = cache.ingestColumn(0, 0, json)
|
|
74
|
+
const second = cache.ingestColumn(0, 0, json)
|
|
75
|
+
expect(first.changed).toBe(true)
|
|
76
|
+
expect(second.changed).toBe(false)
|
|
77
|
+
expect(second.revision).toBe(first.revision)
|
|
78
|
+
expect(cache.getColumnRevision(0, 0)).toBe(first.revision)
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
it('bumps revision when light changes and forgets removed columns', () => {
|
|
82
|
+
const cache = new RendererLightCache(VERSION)
|
|
83
|
+
cache.setWorldBounds(0, 256)
|
|
84
|
+
const first = cache.ingestColumn(0, 0, makeLitBlock(3, 0).toJson())
|
|
85
|
+
const second = cache.ingestColumn(0, 0, makeLitBlock(12, 0).toJson())
|
|
86
|
+
expect(second.changed).toBe(true)
|
|
87
|
+
expect(second.revision).toBeGreaterThan(first.revision)
|
|
88
|
+
expect(cache.getLight(8, 64, 8).block).toBeCloseTo(14 / 15, 5)
|
|
89
|
+
|
|
90
|
+
const globalBefore = cache.getGlobalRevision()
|
|
91
|
+
cache.removeColumn(0, 0)
|
|
92
|
+
expect(cache.getLight(8, 64, 8)).toEqual({ block: 0, sky: 1 })
|
|
93
|
+
expect(cache.getGlobalRevision()).toBe(globalBefore)
|
|
94
|
+
expect(cache.getAllocatedSectionCount()).toBe(0)
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
it('does not bump global revision on ingest of another column', () => {
|
|
98
|
+
const cache = new RendererLightCache(VERSION)
|
|
99
|
+
cache.setWorldBounds(0, 256)
|
|
100
|
+
cache.ingestColumn(0, 0, makeLitBlock(3, 0).toJson())
|
|
101
|
+
const globalBefore = cache.getGlobalRevision()
|
|
102
|
+
cache.ingestColumn(16, 0, makeLitBlock(12, 0).toJson())
|
|
103
|
+
expect(cache.getGlobalRevision()).toBe(globalBefore)
|
|
104
|
+
expect(cache.getColumnRevision(16, 0)).toBeGreaterThan(0)
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
it('keeps prior column state when ingest fails', () => {
|
|
108
|
+
const cache = new RendererLightCache(VERSION)
|
|
109
|
+
cache.setWorldBounds(0, 256)
|
|
110
|
+
const ok = cache.ingestColumn(0, 0, makeLitBlock(8, 0).toJson())
|
|
111
|
+
const failed = cache.ingestColumn(0, 0, { not: 'a-chunk' })
|
|
112
|
+
expect(failed.changed).toBe(false)
|
|
113
|
+
expect(failed.revision).toBe(ok.revision)
|
|
114
|
+
expect(cache.getLight(8, 64, 8).block).toBeCloseTo(10 / 15, 5)
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
it('clear and bounds change drop light and bump the global revision', () => {
|
|
118
|
+
const cache = new RendererLightCache(VERSION)
|
|
119
|
+
cache.setWorldBounds(0, 256)
|
|
120
|
+
cache.ingestColumn(16, 32, makeLitBlock(15, 15).toJson())
|
|
121
|
+
expect(cache.getAllocatedBytes()).toBeGreaterThanOrEqual(RENDERER_LIGHT_BYTES_PER_SECTION)
|
|
122
|
+
const globalBefore = cache.getGlobalRevision()
|
|
123
|
+
cache.clear()
|
|
124
|
+
expect(cache.getLight(24, 64, 40)).toEqual({ block: 0, sky: 1 })
|
|
125
|
+
expect(cache.getGlobalRevision()).toBeGreaterThan(globalBefore)
|
|
126
|
+
})
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
describe('RendererLightCache 1.18+', () => {
|
|
130
|
+
const VERSION_118 = '1.18.2'
|
|
131
|
+
const MIN_Y = -64
|
|
132
|
+
const WORLD_HEIGHT = 384
|
|
133
|
+
|
|
134
|
+
function make118Chunk() {
|
|
135
|
+
const Chunk = Chunks(VERSION_118) as any
|
|
136
|
+
return new Chunk({ minY: MIN_Y, worldHeight: WORLD_HEIGHT })
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function make118LitBlock(blockLight: number, skyLight: number, x = 8, y = -60, z = 8) {
|
|
140
|
+
const mcData = MinecraftData(VERSION_118)
|
|
141
|
+
const chunk = make118Chunk()
|
|
142
|
+
const stoneId = mcData.blocksByName.stone.defaultState
|
|
143
|
+
chunk.setBlockStateId(new Vec3(x, y, z), stoneId)
|
|
144
|
+
chunk.setBlockLight(new Vec3(x, y, z), blockLight)
|
|
145
|
+
chunk.setSkyLight(new Vec3(x, y, z), skyLight)
|
|
146
|
+
return chunk
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
it('packs and reads light in a negative-Y section', () => {
|
|
150
|
+
const cache = new RendererLightCache(VERSION_118)
|
|
151
|
+
cache.setWorldBounds(MIN_Y, WORLD_HEIGHT)
|
|
152
|
+
cache.ingestColumn(0, 0, make118LitBlock(11, 3).toJson())
|
|
153
|
+
const light = cache.getLight(8, -60, 8)
|
|
154
|
+
expect(light.block).toBeCloseTo(13 / 15, 5)
|
|
155
|
+
expect(light.sky).toBeCloseTo(5 / 15, 5)
|
|
156
|
+
expect(cache.getLight(8, -64, 8).block).toBeGreaterThan(0)
|
|
157
|
+
})
|
|
158
|
+
})
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
//@ts-nocheck
|
|
2
|
+
import Chunks from 'prismarine-chunk'
|
|
3
|
+
import { Vec3 } from 'vec3'
|
|
4
|
+
|
|
5
|
+
/** 16³ blocks, one packed byte (block<<4 | sky) per block. */
|
|
6
|
+
export const RENDERER_LIGHT_BYTES_PER_SECTION = 4096
|
|
7
|
+
|
|
8
|
+
const FULLBRIGHT = { block: 0, sky: 1 } as const
|
|
9
|
+
|
|
10
|
+
const sectionKey = (sx: number, sy: number, sz: number) => `${sx},${sy},${sz}`
|
|
11
|
+
const columnKey = (sx: number, sz: number) => `${sx},${sz}`
|
|
12
|
+
const alignSection = (coord: number) => Math.floor(coord / 16) * 16
|
|
13
|
+
const localCoord = (coord: number) => ((coord % 16) + 16) % 16
|
|
14
|
+
const blockIndex = (lx: number, ly: number, lz: number) => lx + lz * 16 + ly * 256
|
|
15
|
+
|
|
16
|
+
const channelNorm = (raw: number) => Math.min(15, raw + 2) / 15
|
|
17
|
+
|
|
18
|
+
const getLightSectionIndex = (y: number, minY = 0) => Math.floor((y - minY) / 16) + 1
|
|
19
|
+
|
|
20
|
+
const hasChunkSection = (column: any, pos: Vec3) => {
|
|
21
|
+
if (column._getSection) return column._getSection(pos)
|
|
22
|
+
if (column.skyLightSections) {
|
|
23
|
+
return column.skyLightSections[getLightSectionIndex(pos.y, column.minY)] || column.blockLightSections[getLightSectionIndex(pos.y, column.minY)]
|
|
24
|
+
}
|
|
25
|
+
if (column.sections) return column.sections[pos.y >> 4]
|
|
26
|
+
return false
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const unpackNibble = (packed: Uint8Array, lx: number, ly: number, lz: number) => {
|
|
30
|
+
const local = (ly << 8) | (lz << 4) | lx
|
|
31
|
+
const byte = packed[local >> 1] ?? 0
|
|
32
|
+
return local & 1 ? byte >> 4 : byte & 0x0f
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const hashBytes = (sections: Map<string, Uint8Array>) => {
|
|
36
|
+
let hash = 2166136261
|
|
37
|
+
const keys = [...sections.keys()].sort()
|
|
38
|
+
for (const key of keys) {
|
|
39
|
+
const data = sections.get(key)!
|
|
40
|
+
for (let i = 0; i < key.length; i++) {
|
|
41
|
+
hash ^= key.charCodeAt(i)
|
|
42
|
+
hash = Math.imul(hash, 16777619)
|
|
43
|
+
}
|
|
44
|
+
for (let i = 0; i < data.length; i++) {
|
|
45
|
+
hash ^= data[i]!
|
|
46
|
+
hash = Math.imul(hash, 16777619)
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return hash >>> 0
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
type LightChunk = {
|
|
53
|
+
getBlockLight: (pos: Vec3) => number
|
|
54
|
+
getSkyLight: (pos: Vec3) => number
|
|
55
|
+
minY?: number
|
|
56
|
+
_getSection?: (pos: Vec3) => unknown
|
|
57
|
+
skyLightSections?: unknown[]
|
|
58
|
+
blockLightSections?: unknown[]
|
|
59
|
+
sections?: unknown
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export type RendererLightSample = { block: number; sky: number }
|
|
63
|
+
|
|
64
|
+
export type RendererLightIngestResult = { changed: boolean; revision: number }
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Main-thread packed block/sky light for entity sampling.
|
|
68
|
+
* Absent sections stay fullbright; present raw-zero gets the project +2 floor.
|
|
69
|
+
*/
|
|
70
|
+
export class RendererLightCache {
|
|
71
|
+
private readonly sections = new Map<string, Uint8Array>()
|
|
72
|
+
private readonly columnHashes = new Map<string, number>()
|
|
73
|
+
private readonly columnRevisions = new Map<string, number>()
|
|
74
|
+
private Chunk: ReturnType<typeof Chunks>
|
|
75
|
+
private worldMinY = 0
|
|
76
|
+
private worldMaxY = 256
|
|
77
|
+
private globalRevision = 0
|
|
78
|
+
private nextColumnRevision = 1
|
|
79
|
+
|
|
80
|
+
constructor(version: string) {
|
|
81
|
+
this.Chunk = Chunks(version) as ReturnType<typeof Chunks>
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
setVersion(version: string) {
|
|
85
|
+
this.Chunk = Chunks(version) as ReturnType<typeof Chunks>
|
|
86
|
+
this.clear()
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
setWorldBounds(minY: number, worldHeight: number) {
|
|
90
|
+
if (this.worldMinY === minY && this.worldMaxY === worldHeight) return
|
|
91
|
+
this.worldMinY = minY
|
|
92
|
+
this.worldMaxY = worldHeight
|
|
93
|
+
this.clear()
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
clear() {
|
|
97
|
+
if (this.sections.size === 0 && this.columnRevisions.size === 0) {
|
|
98
|
+
this.globalRevision++
|
|
99
|
+
return
|
|
100
|
+
}
|
|
101
|
+
this.sections.clear()
|
|
102
|
+
this.columnHashes.clear()
|
|
103
|
+
this.columnRevisions.clear()
|
|
104
|
+
this.globalRevision++
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
getAllocatedSectionCount(): number {
|
|
108
|
+
return this.sections.size
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
getAllocatedBytes(): number {
|
|
112
|
+
return this.sections.size * RENDERER_LIGHT_BYTES_PER_SECTION
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
getGlobalRevision(): number {
|
|
116
|
+
return this.globalRevision
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
getColumnRevision(chunkX: number, chunkZ: number): number | undefined {
|
|
120
|
+
return this.columnRevisions.get(columnKey(alignSection(chunkX), alignSection(chunkZ)))
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
removeColumn(chunkX: number, chunkZ: number) {
|
|
124
|
+
const sx = alignSection(chunkX)
|
|
125
|
+
const sz = alignSection(chunkZ)
|
|
126
|
+
const col = columnKey(sx, sz)
|
|
127
|
+
const syStart = alignSection(this.worldMinY)
|
|
128
|
+
for (let sy = syStart; sy < this.worldMaxY; sy += 16) {
|
|
129
|
+
this.sections.delete(sectionKey(sx, sy, sz))
|
|
130
|
+
}
|
|
131
|
+
this.columnHashes.delete(col)
|
|
132
|
+
this.columnRevisions.delete(col)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Direct publication write: packed 2048-byte nibble channels.
|
|
137
|
+
* Block-only sections keep the current sky nibble; missing sky is 0, never 15.
|
|
138
|
+
* `sx/sy/sz` are world-aligned section origins.
|
|
139
|
+
*/
|
|
140
|
+
ingestPackedSections(sections: Array<{ sx: number; sy: number; sz: number; blockLight: Uint8Array; skyLight?: Uint8Array }>): RendererLightIngestResult {
|
|
141
|
+
const touchedColumns = new Set<string>()
|
|
142
|
+
for (const section of sections) {
|
|
143
|
+
const sx = alignSection(section.sx)
|
|
144
|
+
const sy = alignSection(section.sy)
|
|
145
|
+
const sz = alignSection(section.sz)
|
|
146
|
+
if (section.blockLight.length !== 2048) continue
|
|
147
|
+
if (section.skyLight && section.skyLight.length !== 2048) continue
|
|
148
|
+
const key = sectionKey(sx, sy, sz)
|
|
149
|
+
const prev = this.sections.get(key)
|
|
150
|
+
const data = prev ? new Uint8Array(prev) : new Uint8Array(RENDERER_LIGHT_BYTES_PER_SECTION)
|
|
151
|
+
for (let ly = 0; ly < 16; ly++) {
|
|
152
|
+
for (let lz = 0; lz < 16; lz++) {
|
|
153
|
+
for (let lx = 0; lx < 16; lx++) {
|
|
154
|
+
const idx = blockIndex(lx, ly, lz)
|
|
155
|
+
const block = unpackNibble(section.blockLight, lx, ly, lz)
|
|
156
|
+
const sky = section.skyLight ? unpackNibble(section.skyLight, lx, ly, lz) : data[idx]! & 0x0f
|
|
157
|
+
data[idx] = (block << 4) | sky
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
this.sections.set(key, data)
|
|
162
|
+
touchedColumns.add(columnKey(sx, sz))
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
let revision = 0
|
|
166
|
+
for (const col of touchedColumns) {
|
|
167
|
+
revision = this.nextColumnRevision++
|
|
168
|
+
this.columnRevisions.set(col, revision)
|
|
169
|
+
const packed = new Map<string, Uint8Array>()
|
|
170
|
+
for (const [key, data] of this.sections) {
|
|
171
|
+
const [colSx, , colSz] = key.split(',').map(Number)
|
|
172
|
+
if (`${colSx},${colSz}` === col) packed.set(key, data)
|
|
173
|
+
}
|
|
174
|
+
this.columnHashes.set(col, hashBytes(packed))
|
|
175
|
+
}
|
|
176
|
+
return { changed: touchedColumns.size > 0, revision }
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
ingestColumn(chunkX: number, chunkZ: number, chunkJson: unknown): RendererLightIngestResult {
|
|
180
|
+
const sx = alignSection(chunkX)
|
|
181
|
+
const sz = alignSection(chunkZ)
|
|
182
|
+
const col = columnKey(sx, sz)
|
|
183
|
+
const previousRevision = this.columnRevisions.get(col) ?? 0
|
|
184
|
+
|
|
185
|
+
let chunk: LightChunk
|
|
186
|
+
try {
|
|
187
|
+
chunk = this.Chunk.fromJson(chunkJson as Parameters<ReturnType<typeof Chunks>['fromJson']>[0]) as unknown as LightChunk
|
|
188
|
+
if (typeof chunk?.getBlockLight !== 'function' || typeof chunk?.getSkyLight !== 'function') {
|
|
189
|
+
throw new Error('invalid chunk light API')
|
|
190
|
+
}
|
|
191
|
+
} catch {
|
|
192
|
+
return { changed: false, revision: previousRevision }
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const packed = new Map<string, Uint8Array>()
|
|
196
|
+
const pos = new Vec3(0, 0, 0)
|
|
197
|
+
const syStart = alignSection(this.worldMinY)
|
|
198
|
+
for (let sy = syStart; sy < this.worldMaxY; sy += 16) {
|
|
199
|
+
pos.set(0, sy, 0)
|
|
200
|
+
if (!hasChunkSection(chunk, pos)) continue
|
|
201
|
+
const data = new Uint8Array(RENDERER_LIGHT_BYTES_PER_SECTION)
|
|
202
|
+
for (let ly = 0; ly < 16; ly++) {
|
|
203
|
+
pos.y = sy + ly
|
|
204
|
+
for (let lz = 0; lz < 16; lz++) {
|
|
205
|
+
pos.z = lz
|
|
206
|
+
for (let lx = 0; lx < 16; lx++) {
|
|
207
|
+
pos.x = lx
|
|
208
|
+
const block = Math.max(0, Math.min(15, chunk.getBlockLight(pos) | 0))
|
|
209
|
+
const sky = Math.max(0, Math.min(15, chunk.getSkyLight(pos) | 0))
|
|
210
|
+
data[blockIndex(lx, ly, lz)] = (block << 4) | sky
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
packed.set(sectionKey(sx, sy, sz), data)
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const hash = hashBytes(packed)
|
|
218
|
+
if (this.columnHashes.get(col) === hash && this.columnRevisions.has(col)) {
|
|
219
|
+
return { changed: false, revision: previousRevision }
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const syStartRemove = alignSection(this.worldMinY)
|
|
223
|
+
for (let sy = syStartRemove; sy < this.worldMaxY; sy += 16) {
|
|
224
|
+
this.sections.delete(sectionKey(sx, sy, sz))
|
|
225
|
+
}
|
|
226
|
+
for (const [key, data] of packed) {
|
|
227
|
+
this.sections.set(key, data)
|
|
228
|
+
}
|
|
229
|
+
this.columnHashes.set(col, hash)
|
|
230
|
+
const revision = this.nextColumnRevision++
|
|
231
|
+
this.columnRevisions.set(col, revision)
|
|
232
|
+
return { changed: true, revision }
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
getLight(x: number, y: number, z: number): RendererLightSample {
|
|
236
|
+
if (y < this.worldMinY || y >= this.worldMaxY) return { ...FULLBRIGHT }
|
|
237
|
+
const sx = alignSection(x)
|
|
238
|
+
const sy = alignSection(y)
|
|
239
|
+
const sz = alignSection(z)
|
|
240
|
+
const data = this.sections.get(sectionKey(sx, sy, sz))
|
|
241
|
+
if (!data) return { ...FULLBRIGHT }
|
|
242
|
+
const packed = data[blockIndex(localCoord(x), y - sy, localCoord(z))] ?? 0
|
|
243
|
+
return {
|
|
244
|
+
block: channelNorm(packed >> 4),
|
|
245
|
+
sky: channelNorm(packed & 0x0f)
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
//@ts-nocheck
|
|
2
|
+
import { describe, expect, test, vi } from 'vitest'
|
|
3
|
+
import { Vec3 } from 'vec3'
|
|
4
|
+
import { defaultWorldRendererConfig } from '../../graphicsBackend/config'
|
|
5
|
+
import { ClientLightCoordinator, type ClientLightHost } from './clientLightCoordinator'
|
|
6
|
+
|
|
7
|
+
function createHost(overrides: Partial<ClientLightHost> = {}) {
|
|
8
|
+
const dispatched: Array<{ pos: Vec3; value: boolean; useChangeWorker: boolean }> = []
|
|
9
|
+
const host: ClientLightHost = {
|
|
10
|
+
version: '1.17.1',
|
|
11
|
+
workers: [],
|
|
12
|
+
rendererLightCache: {} as ClientLightHost['rendererLightCache'],
|
|
13
|
+
worldRendererConfig: { ...defaultWorldRendererConfig },
|
|
14
|
+
worldSizeParams: { minY: 0, worldHeight: 256 },
|
|
15
|
+
playerStateReactive: {} as ClientLightHost['playerStateReactive'],
|
|
16
|
+
sectionsWaiting: new Map(),
|
|
17
|
+
loadedChunks: {},
|
|
18
|
+
sectionDirtyLightMeta: new Map(),
|
|
19
|
+
getSectionHeight: () => 16,
|
|
20
|
+
getWorkerNumber: () => 0,
|
|
21
|
+
setSectionDirty: vi.fn(),
|
|
22
|
+
dispatchDirtyImmediate: (pos, value, useChangeWorker) => dispatched.push({ pos, value, useChangeWorker }),
|
|
23
|
+
createLightOwnerWorker: vi.fn(),
|
|
24
|
+
...overrides
|
|
25
|
+
}
|
|
26
|
+
return { host, dispatched, coordinator: new ClientLightCoordinator(host) }
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
describe('ClientLightCoordinator', () => {
|
|
30
|
+
test('stays inert and accepts geometry while the owner is disabled', () => {
|
|
31
|
+
const { coordinator } = createHost()
|
|
32
|
+
coordinator.maybeSpawn()
|
|
33
|
+
expect(coordinator.isSessionLive()).toBe(false)
|
|
34
|
+
expect(coordinator.hasOwner()).toBe(false)
|
|
35
|
+
expect(coordinator.evaluateOwnerGeometry({ type: 'geometry', key: '0,64,0' })).toEqual({ accepted: true, keepCovering: false })
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
test('explains why the owner did not start when the version gate refuses it', () => {
|
|
39
|
+
const { coordinator } = createHost({ version: '1.20.4', worldRendererConfig: { ...defaultWorldRendererConfig, enableClientLightOwner: true } })
|
|
40
|
+
coordinator.maybeSpawn()
|
|
41
|
+
expect(coordinator.isSessionLive()).toBe(false)
|
|
42
|
+
expect(coordinator.getFailureReason()).toContain('1.17.1')
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
test('a block change bumps the touched section and an edge neighbour, and tracks the edit', () => {
|
|
46
|
+
const { coordinator } = createHost()
|
|
47
|
+
coordinator.noteBlockChange(new Vec3(0, 64, 5), 1, true)
|
|
48
|
+
expect(coordinator.topologyRevisionBySection.has('0,64,0')).toBe(true)
|
|
49
|
+
expect(coordinator.topologyRevisionBySection.has('-16,64,0')).toBe(true)
|
|
50
|
+
expect(coordinator.topologyRevisionBySection.has('16,64,0')).toBe(false)
|
|
51
|
+
expect(coordinator.interactiveSections.has('0,64,0')).toBe(true)
|
|
52
|
+
// neighbours of an interactive section count as interactive work too
|
|
53
|
+
expect(coordinator.sectionTouchesInteractiveWork('16,64,0')).toBe(true)
|
|
54
|
+
expect(coordinator.sectionTouchesInteractiveWork('48,64,0')).toBe(false)
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
test('neighbour topology is not bumped when neighbour updates are off', () => {
|
|
58
|
+
const { coordinator } = createHost()
|
|
59
|
+
coordinator.noteBlockChange(new Vec3(0, 64, 5), 1, false)
|
|
60
|
+
expect([...coordinator.topologyRevisionBySection.keys()]).toEqual(['0,64,0'])
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
test('removing a column bumps its incarnation and forgets only that column', () => {
|
|
64
|
+
const { coordinator } = createHost()
|
|
65
|
+
coordinator.topologyRevisionBySection.set('0,64,0', 3)
|
|
66
|
+
coordinator.topologyRevisionBySection.set('16,64,0', 3)
|
|
67
|
+
coordinator.pendingCoveringBySection.set('0,32,0', { requiredVersion: 1, worldGeneration: 1 })
|
|
68
|
+
coordinator.pendingTopologyBySection.set('0,32,0', 2)
|
|
69
|
+
coordinator.rejectedFinishedBySection.set('0,32,0', 1)
|
|
70
|
+
const before = coordinator.columnIncarnationFor('0,0')
|
|
71
|
+
|
|
72
|
+
coordinator.onColumnRemoved(0, 0)
|
|
73
|
+
|
|
74
|
+
expect(coordinator.columnIncarnationFor('0,0')).toBe(before + 1)
|
|
75
|
+
expect(coordinator.columnIncarnationFor('16,0')).toBe(1)
|
|
76
|
+
expect([...coordinator.topologyRevisionBySection.keys()]).toEqual(['16,64,0'])
|
|
77
|
+
expect(coordinator.pendingCoveringBySection.size).toBe(0)
|
|
78
|
+
expect(coordinator.pendingTopologyBySection.size).toBe(0)
|
|
79
|
+
expect(coordinator.rejectedFinishedBySection.size).toBe(0)
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
test('rejected finishes are consumed one by one', () => {
|
|
83
|
+
const { coordinator } = createHost()
|
|
84
|
+
coordinator.noteRejectedOwnerGeometry('0,64,0', undefined, { expectLaterFinished: true })
|
|
85
|
+
coordinator.noteRejectedOwnerGeometry('0,64,0', undefined, { expectLaterFinished: true })
|
|
86
|
+
expect(coordinator.consumeRejectedFinish('0,64,0')).toBe(true)
|
|
87
|
+
expect(coordinator.consumeRejectedFinish('0,64,0')).toBe(true)
|
|
88
|
+
expect(coordinator.consumeRejectedFinish('0,64,0')).toBe(false)
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
test('dirty stamps carry no owner versions while the owner is off, and urgency is restored', () => {
|
|
92
|
+
const { coordinator } = createHost()
|
|
93
|
+
const { lightMeta, ownerVersions, traceIds } = coordinator.describeDirty(new Vec3(0, 64, 0), '0,64,0')
|
|
94
|
+
expect(lightMeta).toBeUndefined()
|
|
95
|
+
expect(ownerVersions).toEqual({})
|
|
96
|
+
expect(traceIds).toBeUndefined()
|
|
97
|
+
|
|
98
|
+
coordinator.nextDirtyUrgent = true
|
|
99
|
+
coordinator.withDirtyUrgency(false, () => expect(coordinator.nextDirtyUrgent).toBe(false))
|
|
100
|
+
expect(coordinator.nextDirtyUrgent).toBe(true)
|
|
101
|
+
})
|
|
102
|
+
})
|