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.
Files changed (98) hide show
  1. package/dist/lightOwnerWorker.js +9 -0
  2. package/dist/lightOwnerWorker.js.map +7 -0
  3. package/dist/mesher.js +1 -1
  4. package/dist/mesher.js.map +2 -2
  5. package/dist/mesherWasm.js +222 -222
  6. package/dist/minecraft-renderer.js +105 -76
  7. package/dist/minecraft-renderer.js.meta.json +1 -1
  8. package/dist/threeWorker.js +537 -508
  9. package/package.json +1 -1
  10. package/src/bundler/bundlePrepare.test.ts +25 -0
  11. package/src/bundler/bundlePrepare.ts +1 -1
  12. package/src/graphicsBackend/appViewer.ts +15 -0
  13. package/src/graphicsBackend/config.ts +8 -0
  14. package/src/graphicsBackend/rendererDefaultOptions.test.ts +51 -0
  15. package/src/graphicsBackend/rendererDefaultOptions.ts +18 -1
  16. package/src/graphicsBackend/rendererOptionsSync.ts +4 -3
  17. package/src/lib/blockEntityLightRegistry.ts +4 -0
  18. package/src/lib/skyLight.ts +17 -0
  19. package/src/lib/worldrendererCommon.lightOwner.test.ts +916 -0
  20. package/src/lib/worldrendererCommon.removeColumn.test.ts +16 -0
  21. package/src/lib/worldrendererCommon.ts +340 -67
  22. package/src/lighting/cache/rendererLightCache.test.ts +158 -0
  23. package/src/lighting/cache/rendererLightCache.ts +248 -0
  24. package/src/lighting/coordinator/clientLightCoordinator.test.ts +102 -0
  25. package/src/lighting/coordinator/clientLightCoordinator.ts +574 -0
  26. package/src/lighting/debug/clientLightProbe.test.ts +24 -0
  27. package/src/lighting/debug/clientLightProbe.ts +20 -0
  28. package/src/lighting/debug/clientLightTrace.test.ts +271 -0
  29. package/src/lighting/debug/clientLightTrace.ts +335 -0
  30. package/src/lighting/entity/entityLightController.test.ts +216 -0
  31. package/src/lighting/entity/entityLightController.ts +322 -0
  32. package/src/lighting/owner/clientLightOwner.test.ts +467 -0
  33. package/src/lighting/owner/clientLightOwner.ts +523 -0
  34. package/src/lighting/owner/lightOwnerHost.test.ts +78 -0
  35. package/src/lighting/owner/lightOwnerHost.ts +202 -0
  36. package/src/lighting/owner/lightOwnerWorker.ts +154 -0
  37. package/src/lighting/owner/updateLight1171OwnerPath.test.ts +68 -0
  38. package/src/lighting/owner/updateLightToOwnerEvents.ts +49 -0
  39. package/src/lighting/tables/lightTables1171.generated.json +10 -0
  40. package/src/lighting/tables/lightTables1171.test.ts +168 -0
  41. package/src/lighting/tables/lightTables1171.ts +59 -0
  42. package/src/lighting/versioning/clientLightVersions.test.ts +241 -0
  43. package/src/lighting/versioning/clientLightVersions.ts +203 -0
  44. package/src/lighting/versioning/pendingSectionFlush.test.ts +214 -0
  45. package/src/lighting/versioning/pendingSectionFlush.ts +123 -0
  46. package/src/mesher-shared/lightNibblePack.ts +26 -0
  47. package/src/mesher-shared/mcDataWorkerShim.cjs +9 -7
  48. package/src/mesher-shared/modelsGeometryCommon.test.ts +6 -1
  49. package/src/mesher-shared/shared.ts +48 -1
  50. package/src/three/chunkMeshManager.ts +26 -0
  51. package/src/three/entities.ts +97 -8
  52. package/src/three/entity/EntityMesh.ts +1 -0
  53. package/src/three/entity/legWalkAnimation.test.ts +154 -0
  54. package/src/three/entity/legWalkAnimation.ts +193 -0
  55. package/src/three/globalBlockBuffer.ts +94 -2
  56. package/src/three/globalLegacyBuffer.ts +94 -2
  57. package/src/three/graphicsBackendBase.ts +2 -9
  58. package/src/three/modules/clouds.ts +191 -0
  59. package/src/three/modules/index.ts +4 -0
  60. package/src/three/modules/sunMoon.ts +151 -0
  61. package/src/three/skyboxRenderer.ts +15 -8
  62. package/src/three/tests/clouds.test.ts +59 -0
  63. package/src/three/tests/globalBlockBuffer.test.ts +98 -0
  64. package/src/three/tests/globalLegacyBuffer.test.ts +108 -0
  65. package/src/three/tests/waterFog.test.ts +25 -0
  66. package/src/three/waterFog.ts +28 -0
  67. package/src/three/worldRendererThree.ts +84 -39
  68. package/src/wasm-mesher/bridge/render-from-wasm.ts +46 -1
  69. package/src/wasm-mesher/runtime-build/wasm_mesher.d.ts +31 -5
  70. package/src/wasm-mesher/runtime-build/wasm_mesher.js +180 -5
  71. package/src/wasm-mesher/runtime-build/wasm_mesher_bg.wasm +0 -0
  72. package/src/wasm-mesher/runtime-build/wasm_mesher_bg.wasm.d.ts +11 -0
  73. package/src/wasm-mesher/tests/mesherLightDependencies.test.ts +83 -0
  74. package/src/wasm-mesher/tests/mesherWasmColumnTickSelect.test.ts +39 -0
  75. package/src/wasm-mesher/tests/mesherWasmConversionCache.test.ts +30 -1
  76. package/src/wasm-mesher/tests/mesherWasmTickParseCache.test.ts +73 -0
  77. package/src/wasm-mesher/tests/mesherWasmTickSchedule.test.ts +51 -0
  78. package/src/wasm-mesher/tests/mesherWasmTopologyPostCache.test.ts +78 -0
  79. package/src/wasm-mesher/worker/clientLightDarkPublication.probe.test.ts +32 -0
  80. package/src/wasm-mesher/worker/mesherLightDependencies.ts +43 -0
  81. package/src/wasm-mesher/worker/mesherWasm.ownerLightSync.test.ts +198 -0
  82. package/src/wasm-mesher/worker/mesherWasm.ts +506 -96
  83. package/src/wasm-mesher/worker/mesherWasmChunkBuffer.ts +1 -0
  84. package/src/wasm-mesher/worker/mesherWasmColumnTickSelect.ts +47 -0
  85. package/src/wasm-mesher/worker/mesherWasmConversionCache.ts +16 -5
  86. package/src/wasm-mesher/worker/mesherWasmLightDirty.test.ts +17 -1
  87. package/src/wasm-mesher/worker/mesherWasmLightDirty.ts +10 -0
  88. package/src/wasm-mesher/worker/mesherWasmLightMerge.test.ts +208 -0
  89. package/src/wasm-mesher/worker/mesherWasmLightMerge.ts +228 -0
  90. package/src/wasm-mesher/worker/mesherWasmOwnerLight.test.ts +161 -0
  91. package/src/wasm-mesher/worker/mesherWasmOwnerLight.ts +112 -0
  92. package/src/wasm-mesher/worker/mesherWasmTickParseCache.ts +45 -0
  93. package/src/wasm-mesher/worker/mesherWasmTickSchedule.ts +44 -0
  94. package/src/wasm-mesher/worker/mesherWasmTopologyPostCache.ts +63 -0
  95. package/src/worldView/types.ts +3 -0
  96. package/src/worldView/worldView.geometryReload.test.ts +68 -0
  97. package/src/worldView/worldView.spiral.test.ts +1 -1
  98. package/src/worldView/worldView.ts +49 -1
@@ -4,6 +4,7 @@ export type PendingChunkMessage = {
4
4
  z: number
5
5
  chunk: any
6
6
  customBlockModels?: any
7
+ isLightUpdate?: boolean
7
8
  }
8
9
 
9
10
  export class PendingChunkBuffer {
@@ -0,0 +1,47 @@
1
+ //@ts-nocheck
2
+ /**
3
+ * Choose which dirty columns a mesher tick processes.
4
+ *
5
+ * Urgent (player-edit) columns go first with no cap. Bulk columns are limited
6
+ * so an incoming edit is not stuck behind a long FIFO of load-wave remeshes.
7
+ */
8
+
9
+ export const BULK_COLUMNS_PER_TICK = 4
10
+
11
+ export type ColumnTickSection = { key: string; x: number; y: number; z: number; count: number }
12
+
13
+ export type ColumnTickGroup = { x: number; z: number; sections: ColumnTickSection[] }
14
+
15
+ export function groupDirtySectionsByColumn(dirtySections: Iterable<[string, number]>): ColumnTickGroup[] {
16
+ const groups = new Map<string, ColumnTickGroup>()
17
+ for (const [key, count] of dirtySections) {
18
+ const [sx, sy, sz] = key.split(',').map(v => parseInt(v, 10))
19
+ const colKey = `${sx},${sz}`
20
+ let group = groups.get(colKey)
21
+ if (!group) {
22
+ group = { x: sx, z: sz, sections: [] }
23
+ groups.set(colKey, group)
24
+ }
25
+ group.sections.push({ key, x: sx, y: sy, z: sz, count })
26
+ }
27
+ return [...groups.values()]
28
+ }
29
+
30
+ export function selectColumnGroupsForTick(
31
+ dirtySections: Map<string, number>,
32
+ urgentKeys: Set<string>,
33
+ bulkLimit = BULK_COLUMNS_PER_TICK
34
+ ): { selected: ColumnTickGroup[]; remaining: Map<string, number> } {
35
+ const urgent: ColumnTickGroup[] = []
36
+ const bulk: ColumnTickGroup[] = []
37
+ for (const group of groupDirtySectionsByColumn(dirtySections)) {
38
+ if (group.sections.some(section => urgentKeys.has(section.key))) urgent.push(group)
39
+ else bulk.push(group)
40
+ }
41
+ const selected = [...urgent, ...bulk.slice(0, bulkLimit)]
42
+ const remaining = new Map<string, number>()
43
+ for (const group of bulk.slice(bulkLimit)) {
44
+ for (const section of group.sections) remaining.set(section.key, section.count)
45
+ }
46
+ return { selected, remaining }
47
+ }
@@ -8,9 +8,9 @@
8
8
  // redundant conversions.
9
9
  //
10
10
  // Correctness invariants:
11
- // - Cache value MUST NOT be mutated by consumers. The `ChunkConversionResult`
12
- // typed arrays are used as `set()` SOURCES (and as wasm INPUTS) in the
13
- // mesher tick, never as destinations, so the read-only contract holds.
11
+ // - Block arrays are patched only by patchConversionBlock. Light arrays may
12
+ // receive the current display overlay before meshing. Other consumers must
13
+ // not mutate cache values. WASM receives the resulting arrays as inputs.
14
14
  // - Identity-based key validation: a hit is served only when the stored
15
15
  // chunk reference is `===` the live `world.getColumn(x,z)` reference, so a
16
16
  // replaced column (via `world.addColumn`) can never accidentally serve
@@ -18,8 +18,8 @@
18
18
  // skipped (defense in depth).
19
19
  // - In-place mutation of an existing column object (e.g. via
20
20
  // `world.setBlockStateId` from a `blockUpdate` message) preserves identity
21
- // but changes content. The `blockUpdate` handler explicitly invalidates
22
- // the affected `(chunkX, chunkZ)` to handle this.
21
+ // but changes content. The `blockUpdate` handler patches the cached state
22
+ // and count, or invalidates when it cannot patch safely.
23
23
 
24
24
  import type { ChunkConversionResult } from '../bridge/convertChunk'
25
25
 
@@ -124,6 +124,17 @@ export function getConversionCacheLimit(): number {
124
124
  return activeLimit
125
125
  }
126
126
 
127
+ /** Patch a live column without discarding its converted topology or metadata. */
128
+ export function patchConversionBlock(x: number, z: number, chunkRef: any, blockX: number, blockY: number, blockZ: number, stateId: number): boolean {
129
+ const e = cache.get(keyOf(x, z))
130
+ if (!e || e.chunkRef !== chunkRef || blockY < e.worldMinY || blockY >= e.worldMaxY) return false
131
+ const index = blockX - x + (blockZ - z) * 16 + (blockY - e.worldMinY) * 256
132
+ const old = e.result.blockStates[index]!
133
+ e.result.blockStates[index] = stateId
134
+ e.result.blockCount += Number(stateId !== 0) - Number(old !== 0)
135
+ return true
136
+ }
137
+
127
138
  export function invalidateConversion(x: number, z: number): boolean {
128
139
  return cache.delete(keyOf(x, z))
129
140
  }
@@ -1,9 +1,25 @@
1
1
  //@ts-nocheck
2
2
  import { describe, expect, it } from 'vitest'
3
- import { sectionYsForLightColumnDirty } from './mesherWasmLightDirty'
3
+ import { dropRawMapChunkOnLightOnlyReload, sectionYsForLightColumnDirty } from './mesherWasmLightDirty'
4
4
 
5
5
  describe('sectionYsForLightColumnDirty', () => {
6
6
  it('covers every section in a 256-high overworld column', () => {
7
7
  expect(sectionYsForLightColumnDirty(0, 256)).toEqual(Array.from({ length: 16 }, (_, i) => i * 16))
8
8
  })
9
9
  })
10
+
11
+ describe('dropRawMapChunkOnLightOnlyReload', () => {
12
+ it('deletes only the fused raw entry when the chunk reload is light-only', () => {
13
+ const raw = new Map<string, number>([
14
+ ['0,0', 1],
15
+ ['16,0', 2]
16
+ ])
17
+ expect(dropRawMapChunkOnLightOnlyReload(false, raw, '0,0')).toBe(false)
18
+ expect(raw.has('0,0')).toBe(true)
19
+ expect(dropRawMapChunkOnLightOnlyReload(undefined, raw, '0,0')).toBe(false)
20
+ expect(raw.has('0,0')).toBe(true)
21
+ expect(dropRawMapChunkOnLightOnlyReload(true, raw, '0,0')).toBe(true)
22
+ expect(raw.has('0,0')).toBe(false)
23
+ expect(raw.has('16,0')).toBe(true)
24
+ })
25
+ })
@@ -9,3 +9,13 @@ export function sectionYsForLightColumnDirty(worldMinY: number, worldMaxY: numbe
9
9
  }
10
10
  return ys
11
11
  }
12
+
13
+ /**
14
+ * 1.18+ fused meshing prefers the original raw map_chunk. A later light-only
15
+ * column reload must drop that entry so the JSON column walk is used.
16
+ * Normal chunk loads must keep it (`setRawMapChunk` arrives first).
17
+ */
18
+ export function dropRawMapChunkOnLightOnlyReload(isLightUpdate: boolean | undefined, rawCache: { delete: (key: string) => boolean }, key: string): boolean {
19
+ if (!isLightUpdate) return false
20
+ return rawCache.delete(key)
21
+ }
@@ -0,0 +1,208 @@
1
+ //@ts-nocheck
2
+ import { describe, expect, it } from 'vitest'
3
+ import {
4
+ LIGHT_BLOCKS_PER_SECTION,
5
+ createEmptyLightCache,
6
+ displayLightColumn,
7
+ isLightSectionPresent,
8
+ mergeUpdateLight,
9
+ worldSectionMaskBit,
10
+ type ParsedUpdateLight,
11
+ type UpdateLightColumnCache
12
+ } from './mesherWasmLightMerge'
13
+
14
+ const NUM_SECTIONS = 16
15
+ const WORLD_BYTES = NUM_SECTIONS * LIGHT_BLOCKS_PER_SECTION
16
+
17
+ function maskWithBits(...bits: number[]): Uint32Array {
18
+ const out = new Uint32Array(2)
19
+ for (const bit of bits) {
20
+ const bitInLong = bit & 63
21
+ if (bitInLong < 32) out[0] |= 1 << bitInLong
22
+ else out[1] |= 1 << (bitInLong - 32)
23
+ }
24
+ return out
25
+ }
26
+
27
+ function emptyParsed(overrides: Partial<ParsedUpdateLight> = {}): ParsedUpdateLight {
28
+ return {
29
+ x: 0,
30
+ z: 0,
31
+ trustEdges: false,
32
+ numSections: NUM_SECTIONS,
33
+ skyLight: new Uint8Array(WORLD_BYTES),
34
+ blockLight: new Uint8Array(WORLD_BYTES),
35
+ skyLightMask: new Uint32Array(2),
36
+ emptySkyLightMask: new Uint32Array(2),
37
+ blockLightMask: new Uint32Array(2),
38
+ emptyBlockLightMask: new Uint32Array(2),
39
+ ...overrides
40
+ }
41
+ }
42
+
43
+ function writeWorldSection(column: Uint8Array, sectionIndex: number, value: number) {
44
+ const start = sectionIndex * LIGHT_BLOCKS_PER_SECTION
45
+ column.fill(value, start, start + LIGHT_BLOCKS_PER_SECTION)
46
+ }
47
+
48
+ function sectionSlice(column: Uint8Array, sectionIndex: number): Uint8Array {
49
+ const start = sectionIndex * LIGHT_BLOCKS_PER_SECTION
50
+ return column.subarray(start, start + LIGHT_BLOCKS_PER_SECTION)
51
+ }
52
+
53
+ function sectionAll(column: Uint8Array, sectionIndex: number, value: number): boolean {
54
+ return sectionSlice(column, sectionIndex).every(v => v === value)
55
+ }
56
+
57
+ describe('mergeUpdateLight', () => {
58
+ it('does not bake omitted sky as authoritative 15 on the first packet', () => {
59
+ const skyLight = new Uint8Array(WORLD_BYTES)
60
+ writeWorldSection(skyLight, 1, 3)
61
+ const cache = mergeUpdateLight(
62
+ undefined,
63
+ emptyParsed({
64
+ skyLight,
65
+ skyLightMask: maskWithBits(worldSectionMaskBit(1))
66
+ })
67
+ )
68
+
69
+ expect(isLightSectionPresent(cache.skyPresent, worldSectionMaskBit(1))).toBe(true)
70
+ expect(isLightSectionPresent(cache.skyPresent, worldSectionMaskBit(0))).toBe(false)
71
+ expect(sectionAll(cache.skyLight, 1, 3)).toBe(true)
72
+ expect(sectionAll(cache.skyLight, 0, 15)).toBe(false)
73
+ expect(displayLightColumn(cache, 'sky', 15)[0]).toBe(15)
74
+ expect(displayLightColumn(cache, 'sky')[LIGHT_BLOCKS_PER_SECTION]).toBe(3)
75
+ })
76
+
77
+ it('preserves an omitted neighbor section across a repeated partial delta', () => {
78
+ const firstSky = new Uint8Array(WORLD_BYTES)
79
+ writeWorldSection(firstSky, 0, 7)
80
+ let cache = mergeUpdateLight(
81
+ undefined,
82
+ emptyParsed({
83
+ skyLight: firstSky,
84
+ skyLightMask: maskWithBits(worldSectionMaskBit(0))
85
+ })
86
+ )
87
+
88
+ const secondSky = new Uint8Array(WORLD_BYTES)
89
+ writeWorldSection(secondSky, 1, 4)
90
+ cache = mergeUpdateLight(
91
+ cache,
92
+ emptyParsed({
93
+ skyLight: secondSky,
94
+ skyLightMask: maskWithBits(worldSectionMaskBit(1))
95
+ })
96
+ )
97
+
98
+ expect(sectionAll(cache.skyLight, 0, 7)).toBe(true)
99
+ expect(sectionAll(cache.skyLight, 1, 4)).toBe(true)
100
+ expect(isLightSectionPresent(cache.skyPresent, worldSectionMaskBit(0))).toBe(true)
101
+ expect(isLightSectionPresent(cache.skyPresent, worldSectionMaskBit(1))).toBe(true)
102
+ expect(isLightSectionPresent(cache.skyPresent, worldSectionMaskBit(2))).toBe(false)
103
+ })
104
+
105
+ it('zeros a section when the empty-bit is set', () => {
106
+ const firstSky = new Uint8Array(WORLD_BYTES)
107
+ writeWorldSection(firstSky, 0, 7)
108
+ let cache = mergeUpdateLight(
109
+ undefined,
110
+ emptyParsed({
111
+ skyLight: firstSky,
112
+ skyLightMask: maskWithBits(worldSectionMaskBit(0))
113
+ })
114
+ )
115
+
116
+ cache = mergeUpdateLight(
117
+ cache,
118
+ emptyParsed({
119
+ emptySkyLightMask: maskWithBits(worldSectionMaskBit(0))
120
+ })
121
+ )
122
+
123
+ expect(isLightSectionPresent(cache.skyPresent, worldSectionMaskBit(0))).toBe(true)
124
+ expect(sectionAll(cache.skyLight, 0, 0)).toBe(true)
125
+ expect(displayLightColumn(cache, 'sky', 15)[0]).toBe(0)
126
+ })
127
+
128
+ it('updates sky and block independently', () => {
129
+ const skyLight = new Uint8Array(WORLD_BYTES)
130
+ const blockLight = new Uint8Array(WORLD_BYTES)
131
+ writeWorldSection(skyLight, 0, 12)
132
+ writeWorldSection(blockLight, 0, 5)
133
+ let cache = mergeUpdateLight(
134
+ undefined,
135
+ emptyParsed({
136
+ skyLight,
137
+ blockLight,
138
+ skyLightMask: maskWithBits(worldSectionMaskBit(0)),
139
+ blockLightMask: maskWithBits(worldSectionMaskBit(0))
140
+ })
141
+ )
142
+
143
+ cache = mergeUpdateLight(
144
+ cache,
145
+ emptyParsed({
146
+ emptyBlockLightMask: maskWithBits(worldSectionMaskBit(0))
147
+ })
148
+ )
149
+
150
+ expect(sectionAll(cache.skyLight, 0, 12)).toBe(true)
151
+ expect(sectionAll(cache.blockLight, 0, 0)).toBe(true)
152
+ expect(isLightSectionPresent(cache.skyPresent, worldSectionMaskBit(0))).toBe(true)
153
+ expect(isLightSectionPresent(cache.blockPresent, worldSectionMaskBit(0))).toBe(true)
154
+ })
155
+
156
+ it('records padding bits without writing them into the world column', () => {
157
+ const skyBelow = new Uint8Array(LIGHT_BLOCKS_PER_SECTION).fill(9)
158
+ const cache = mergeUpdateLight(
159
+ undefined,
160
+ emptyParsed({
161
+ skyBelow,
162
+ skyLightMask: maskWithBits(0)
163
+ })
164
+ )
165
+
166
+ expect(isLightSectionPresent(cache.skyPresent, 0)).toBe(true)
167
+ expect(cache.skyBelow).toBeDefined()
168
+ expect(cache.skyBelow!.every(v => v === 9)).toBe(true)
169
+ expect(sectionAll(cache.skyLight, 0, 0)).toBe(true)
170
+ expect(isLightSectionPresent(cache.skyPresent, worldSectionMaskBit(0))).toBe(false)
171
+ })
172
+
173
+ it('applies captured 1.17.1 empty-block bits without seeding omitted sky as 15', () => {
174
+ const cache = mergeUpdateLight(
175
+ undefined,
176
+ emptyParsed({
177
+ trustEdges: true,
178
+ emptyBlockLightMask: maskWithBits(1, 2, 3, 4, 5)
179
+ })
180
+ )
181
+
182
+ expect(cache.trustEdges).toBe(true)
183
+ for (let s = 0; s <= 4; s++) {
184
+ expect(isLightSectionPresent(cache.blockPresent, worldSectionMaskBit(s))).toBe(true)
185
+ expect(sectionAll(cache.blockLight, s, 0)).toBe(true)
186
+ }
187
+ expect(isLightSectionPresent(cache.skyPresent, worldSectionMaskBit(0))).toBe(false)
188
+ expect(sectionAll(cache.skyLight, 0, 15)).toBe(false)
189
+ expect(displayLightColumn(cache, 'sky', 15)[0]).toBe(15)
190
+ })
191
+
192
+ it('keeps trustEdges from the latest packet without changing merge rules', () => {
193
+ const cache = mergeUpdateLight(createEmptyLightCache(NUM_SECTIONS), emptyParsed({ trustEdges: true }))
194
+ expect(cache.trustEdges).toBe(true)
195
+ })
196
+ })
197
+
198
+ describe('displayLightColumn', () => {
199
+ it('fills only absent world sections and leaves explicit zeros', () => {
200
+ const cache: UpdateLightColumnCache = createEmptyLightCache(NUM_SECTIONS)
201
+ writeWorldSection(cache.blockLight, 2, 0)
202
+ cache.blockPresent = maskWithBits(worldSectionMaskBit(2))
203
+
204
+ const displayed = displayLightColumn(cache, 'block', 0)
205
+ expect(sectionAll(displayed, 2, 0)).toBe(true)
206
+ expect(sectionAll(displayed, 1, 0)).toBe(true)
207
+ })
208
+ })
@@ -0,0 +1,228 @@
1
+ //@ts-nocheck
2
+ /** 16³ unpacked light values per section. */
3
+ export const LIGHT_BLOCKS_PER_SECTION = 4096
4
+
5
+ export type ParsedUpdateLight = {
6
+ x: number
7
+ z: number
8
+ trustEdges: boolean
9
+ numSections: number
10
+ skyLight: Uint8Array
11
+ blockLight: Uint8Array
12
+ skyLightMask: Uint32Array
13
+ emptySkyLightMask: Uint32Array
14
+ blockLightMask: Uint32Array
15
+ emptyBlockLightMask: Uint32Array
16
+ skyBelow?: Uint8Array
17
+ skyAbove?: Uint8Array
18
+ blockBelow?: Uint8Array
19
+ blockAbove?: Uint8Array
20
+ }
21
+
22
+ export type UpdateLightColumnCache = {
23
+ numSections: number
24
+ trustEdges: boolean
25
+ skyLight: Uint8Array
26
+ blockLight: Uint8Array
27
+ skyPresent: Uint32Array
28
+ blockPresent: Uint32Array
29
+ skyBelow?: Uint8Array
30
+ skyAbove?: Uint8Array
31
+ blockBelow?: Uint8Array
32
+ blockAbove?: Uint8Array
33
+ }
34
+
35
+ export function worldSectionMaskBit(sectionIndex: number): number {
36
+ return sectionIndex + 1
37
+ }
38
+
39
+ export function lightMaskWordCount(numSections: number): number {
40
+ const bits = numSections + 2
41
+ return Math.max(2, Math.ceil(bits / 64) * 2)
42
+ }
43
+
44
+ export function maskBitGet(mask: Uint32Array, i: number): boolean {
45
+ const longIdx = i >> 6
46
+ const bitInLong = i & 63
47
+ const pair = longIdx * 2
48
+ if (pair + 1 >= mask.length) return false
49
+ const val = bitInLong < 32 ? mask[pair]! : mask[pair + 1]!
50
+ const bit = bitInLong & 31
51
+ return ((val >>> bit) & 1) === 1
52
+ }
53
+
54
+ export function maskBitSet(mask: Uint32Array, i: number): void {
55
+ const longIdx = i >> 6
56
+ const bitInLong = i & 63
57
+ const pair = longIdx * 2
58
+ if (pair + 1 >= mask.length) return
59
+ if (bitInLong < 32) mask[pair] = (mask[pair]! | (1 << bitInLong)) >>> 0
60
+ else mask[pair + 1] = (mask[pair + 1]! | (1 << (bitInLong - 32))) >>> 0
61
+ }
62
+
63
+ export function isLightSectionPresent(present: Uint32Array, maskBit: number): boolean {
64
+ return maskBitGet(present, maskBit)
65
+ }
66
+
67
+ export function createEmptyLightCache(numSections: number): UpdateLightColumnCache {
68
+ const worldBytes = numSections * LIGHT_BLOCKS_PER_SECTION
69
+ const words = lightMaskWordCount(numSections)
70
+ return {
71
+ numSections,
72
+ trustEdges: false,
73
+ skyLight: new Uint8Array(worldBytes),
74
+ blockLight: new Uint8Array(worldBytes),
75
+ skyPresent: new Uint32Array(words),
76
+ blockPresent: new Uint32Array(words)
77
+ }
78
+ }
79
+
80
+ function copySection(dest: Uint8Array, src: Uint8Array, sectionIndex: number, value?: number) {
81
+ const start = sectionIndex * LIGHT_BLOCKS_PER_SECTION
82
+ const end = start + LIGHT_BLOCKS_PER_SECTION
83
+ if (value !== undefined) {
84
+ dest.fill(value, start, end)
85
+ return
86
+ }
87
+ if (src.length < end) return
88
+ dest.set(src.subarray(start, end), start)
89
+ }
90
+
91
+ function mergePadding(hasData: boolean, incoming?: Uint8Array): Uint8Array {
92
+ if (hasData && incoming && incoming.length === LIGHT_BLOCKS_PER_SECTION) {
93
+ return new Uint8Array(incoming)
94
+ }
95
+ return new Uint8Array(LIGHT_BLOCKS_PER_SECTION)
96
+ }
97
+
98
+ function mergeChannel(
99
+ destValues: Uint8Array,
100
+ destPresent: Uint32Array,
101
+ srcValues: Uint8Array,
102
+ dataMask: Uint32Array,
103
+ emptyMask: Uint32Array,
104
+ numSections: number,
105
+ destBelow: Uint8Array | undefined,
106
+ destAbove: Uint8Array | undefined,
107
+ incomingBelow: Uint8Array | undefined,
108
+ incomingAbove: Uint8Array | undefined
109
+ ): { below?: Uint8Array; above?: Uint8Array } {
110
+ let below = destBelow
111
+ let above = destAbove
112
+ const totalBits = numSections + 2
113
+ for (let bit = 0; bit < totalBits; bit++) {
114
+ const hasData = maskBitGet(dataMask, bit)
115
+ const hasEmpty = maskBitGet(emptyMask, bit)
116
+ if (!hasData && !hasEmpty) continue
117
+
118
+ if (bit === 0) {
119
+ below = mergePadding(hasData, incomingBelow)
120
+ maskBitSet(destPresent, 0)
121
+ continue
122
+ }
123
+ if (bit === numSections + 1) {
124
+ above = mergePadding(hasData, incomingAbove)
125
+ maskBitSet(destPresent, bit)
126
+ continue
127
+ }
128
+
129
+ const sectionIndex = bit - 1
130
+ if (hasData) copySection(destValues, srcValues, sectionIndex)
131
+ else copySection(destValues, srcValues, sectionIndex, 0)
132
+ maskBitSet(destPresent, bit)
133
+ }
134
+ return { below, above }
135
+ }
136
+
137
+ export function mergeUpdateLight(cache: UpdateLightColumnCache | undefined, parsed: ParsedUpdateLight): UpdateLightColumnCache {
138
+ const next = cache ? cloneLightCache(cache) : createEmptyLightCache(parsed.numSections)
139
+ next.trustEdges = parsed.trustEdges
140
+ next.numSections = parsed.numSections
141
+
142
+ const skyPad = mergeChannel(
143
+ next.skyLight,
144
+ next.skyPresent,
145
+ parsed.skyLight,
146
+ parsed.skyLightMask,
147
+ parsed.emptySkyLightMask,
148
+ parsed.numSections,
149
+ next.skyBelow,
150
+ next.skyAbove,
151
+ parsed.skyBelow,
152
+ parsed.skyAbove
153
+ )
154
+ next.skyBelow = skyPad.below
155
+ next.skyAbove = skyPad.above
156
+
157
+ const blockPad = mergeChannel(
158
+ next.blockLight,
159
+ next.blockPresent,
160
+ parsed.blockLight,
161
+ parsed.blockLightMask,
162
+ parsed.emptyBlockLightMask,
163
+ parsed.numSections,
164
+ next.blockBelow,
165
+ next.blockAbove,
166
+ parsed.blockBelow,
167
+ parsed.blockAbove
168
+ )
169
+ next.blockBelow = blockPad.below
170
+ next.blockAbove = blockPad.above
171
+
172
+ return next
173
+ }
174
+
175
+ export function cloneLightCache(cache: UpdateLightColumnCache): UpdateLightColumnCache {
176
+ return {
177
+ numSections: cache.numSections,
178
+ trustEdges: cache.trustEdges,
179
+ skyLight: new Uint8Array(cache.skyLight),
180
+ blockLight: new Uint8Array(cache.blockLight),
181
+ skyPresent: new Uint32Array(cache.skyPresent),
182
+ blockPresent: new Uint32Array(cache.blockPresent),
183
+ skyBelow: cache.skyBelow ? new Uint8Array(cache.skyBelow) : undefined,
184
+ skyAbove: cache.skyAbove ? new Uint8Array(cache.skyAbove) : undefined,
185
+ blockBelow: cache.blockBelow ? new Uint8Array(cache.blockBelow) : undefined,
186
+ blockAbove: cache.blockAbove ? new Uint8Array(cache.blockAbove) : undefined
187
+ }
188
+ }
189
+
190
+ export function displayLightColumn(cache: UpdateLightColumnCache, channel: 'sky' | 'block', fill = channel === 'sky' ? 15 : 0): Uint8Array {
191
+ const out = new Uint8Array(cache.numSections * LIGHT_BLOCKS_PER_SECTION)
192
+ overlayDisplayLight(cache, channel, out, fill)
193
+ return out
194
+ }
195
+
196
+ /** Overlay the current display into a reusable conversion buffer. */
197
+ export function overlayDisplayLight(cache: UpdateLightColumnCache, channel: 'sky' | 'block', out: Uint8Array, fill = channel === 'sky' ? 15 : 0): void {
198
+ const src = channel === 'sky' ? cache.skyLight : cache.blockLight
199
+ const present = channel === 'sky' ? cache.skyPresent : cache.blockPresent
200
+ for (let s = 0; s < cache.numSections; s++) {
201
+ const start = s * LIGHT_BLOCKS_PER_SECTION
202
+ const end = start + LIGHT_BLOCKS_PER_SECTION
203
+ if (maskBitGet(present, worldSectionMaskBit(s))) {
204
+ out.set(src.subarray(start, end), start)
205
+ } else {
206
+ out.fill(fill, start, end)
207
+ }
208
+ }
209
+ }
210
+
211
+ export function parsedUpdateLightFromWasm(parsed: any, numSections: number): ParsedUpdateLight {
212
+ return {
213
+ x: parsed.x as number,
214
+ z: parsed.z as number,
215
+ trustEdges: Boolean(parsed.trustEdges),
216
+ numSections,
217
+ skyLight: parsed.skyLight as Uint8Array,
218
+ blockLight: parsed.blockLight as Uint8Array,
219
+ skyLightMask: (parsed.skyLightMask as Uint32Array) ?? new Uint32Array(2),
220
+ emptySkyLightMask: (parsed.emptySkyLightMask as Uint32Array) ?? new Uint32Array(2),
221
+ blockLightMask: (parsed.blockLightMask as Uint32Array) ?? new Uint32Array(2),
222
+ emptyBlockLightMask: (parsed.emptyBlockLightMask as Uint32Array) ?? new Uint32Array(2),
223
+ skyBelow: parsed.skyBelow as Uint8Array | undefined,
224
+ skyAbove: parsed.skyAbove as Uint8Array | undefined,
225
+ blockBelow: parsed.blockBelow as Uint8Array | undefined,
226
+ blockAbove: parsed.blockAbove as Uint8Array | undefined
227
+ }
228
+ }