minecraft-renderer 0.1.71 → 0.1.72
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/README.md +2 -2
- package/dist/minecraft-renderer.js +20 -20
- package/dist/minecraft-renderer.js.meta.json +1 -1
- package/dist/threeWorker.js +3 -3
- package/package.json +1 -1
- package/src/graphicsBackend/rendererDefaultOptions.ts +4 -4
- package/src/lib/worldrendererCommon.reconfigure.test.ts +202 -0
- package/src/lib/worldrendererCommon.ts +138 -16
- package/src/three/graphicsBackendOffThread.ts +16 -1
- package/src/worldView/worldView.ts +11 -0
package/package.json
CHANGED
|
@@ -166,8 +166,8 @@ export const RENDERER_OPTIONS_META: Partial<Record<RendererDefaultOptionKey, Ren
|
|
|
166
166
|
},
|
|
167
167
|
rendererWorldPerformance: {
|
|
168
168
|
text: 'World performance',
|
|
169
|
-
tooltip: 'Background workers for chunk geometry.
|
|
170
|
-
|
|
169
|
+
tooltip: 'Background workers for chunk geometry. Recreates mesher workers and reloads chunks.',
|
|
170
|
+
requiresChunksReload: true,
|
|
171
171
|
possibleValues: [
|
|
172
172
|
['low-energy', 'Low Energy'],
|
|
173
173
|
['normal', 'Normal'],
|
|
@@ -193,8 +193,8 @@ export const RENDERER_OPTIONS_META: Partial<Record<RendererDefaultOptionKey, Ren
|
|
|
193
193
|
rendererMesher: {
|
|
194
194
|
possibleValues: [['wasm', 'WASM'], ['legacy-js', 'Legacy JS']],
|
|
195
195
|
text: 'Mesher pipeline',
|
|
196
|
-
tooltip: '
|
|
197
|
-
|
|
196
|
+
tooltip: 'Browser technology for processing world geometry before render. WASM is the fastest; if you see a dead tab icon, reloads, or other errors, switch to Legacy JS.',
|
|
197
|
+
requiresChunksReload: true,
|
|
198
198
|
},
|
|
199
199
|
rendererShaderCubeBlocks: {
|
|
200
200
|
text: '(UNSTABLE) Instanced shader cubes',
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
//@ts-nocheck
|
|
2
|
+
import { EventEmitter } from 'events'
|
|
3
|
+
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'
|
|
4
|
+
import { Vec3 } from 'vec3'
|
|
5
|
+
import { proxy } from 'valtio'
|
|
6
|
+
import * as worldRendererModule from './worldrendererCommon'
|
|
7
|
+
import { WorldRendererCommon } from './worldrendererCommon'
|
|
8
|
+
import { defaultWorldRendererConfig } from '../graphicsBackend/config'
|
|
9
|
+
import { getInitialPlayerState } from '../playerState/playerState'
|
|
10
|
+
import type { DisplayWorldOptions, GraphicsInitOptions } from '../graphicsBackend/types'
|
|
11
|
+
|
|
12
|
+
vi.mock('./ui/newStats', () => ({
|
|
13
|
+
addNewStat: vi.fn(() => ({ updateText: vi.fn(), setVisibility: vi.fn() })),
|
|
14
|
+
updateStatText: vi.fn(),
|
|
15
|
+
removeAllStats: vi.fn(),
|
|
16
|
+
updatePanesVisibility: vi.fn(),
|
|
17
|
+
MC_RENDERER_DEBUG_OVERLAY_CLASS: 'mc-renderer-debug-overlay',
|
|
18
|
+
}))
|
|
19
|
+
|
|
20
|
+
vi.mock('./utils/skins', () => ({
|
|
21
|
+
setSkinsConfig: vi.fn(),
|
|
22
|
+
steveTexture: {},
|
|
23
|
+
stevePngUrl: '',
|
|
24
|
+
}))
|
|
25
|
+
|
|
26
|
+
function ensurePromiseWithResolvers() {
|
|
27
|
+
if (!Promise.withResolvers) {
|
|
28
|
+
Promise.withResolvers = function <T>() {
|
|
29
|
+
let resolve!: (value: T | PromiseLike<T>) => void
|
|
30
|
+
let reject!: (reason?: unknown) => void
|
|
31
|
+
const promise = new Promise<T>((res, rej) => {
|
|
32
|
+
resolve = res
|
|
33
|
+
reject = rej
|
|
34
|
+
})
|
|
35
|
+
return { promise, resolve, reject }
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
class TestWorldRenderer extends WorldRendererCommon {
|
|
41
|
+
outputFormat = 'threeJs' as const
|
|
42
|
+
|
|
43
|
+
changeBackgroundColor() {}
|
|
44
|
+
changeCardinalLight() {}
|
|
45
|
+
handleWorkerMessage() {}
|
|
46
|
+
updateCamera() {}
|
|
47
|
+
render() {}
|
|
48
|
+
updateShowChunksBorder() {}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function createRenderer(workerCount = 2, worldView?: DisplayWorldOptions['worldView']) {
|
|
52
|
+
const reloadLoadedChunks = vi.fn(async () => {})
|
|
53
|
+
const rendererState = proxy({
|
|
54
|
+
world: {
|
|
55
|
+
chunksLoaded: {} as Record<string, true>,
|
|
56
|
+
heightmaps: {} as Record<string, Int16Array>,
|
|
57
|
+
allChunksLoaded: false,
|
|
58
|
+
mesherWork: false,
|
|
59
|
+
instabilityFactors: {},
|
|
60
|
+
intersectMedia: null,
|
|
61
|
+
},
|
|
62
|
+
renderer: '',
|
|
63
|
+
preventEscapeMenu: false,
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
const displayOptions: DisplayWorldOptions = {
|
|
67
|
+
version: '1.21.1',
|
|
68
|
+
worldView: (worldView ?? Object.assign(new EventEmitter(), { reloadLoadedChunks })) as DisplayWorldOptions['worldView'],
|
|
69
|
+
inWorldRenderingConfig: proxy({ ...defaultWorldRendererConfig, mesherWorkers: workerCount }),
|
|
70
|
+
playerStateReactive: getInitialPlayerState(),
|
|
71
|
+
rendererState,
|
|
72
|
+
nonReactiveState: {
|
|
73
|
+
fps: 0,
|
|
74
|
+
worstRenderTime: 0,
|
|
75
|
+
avgRenderTime: 0,
|
|
76
|
+
world: {
|
|
77
|
+
chunksLoadedCount: 0,
|
|
78
|
+
chunksTotalNumber: 0,
|
|
79
|
+
chunksFullInfo: '',
|
|
80
|
+
},
|
|
81
|
+
renderer: {
|
|
82
|
+
timeline: { live: [], frozen: [], lastSecond: [] },
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
resourcesManager: {
|
|
86
|
+
currentResources: {
|
|
87
|
+
mcData: { version: {} },
|
|
88
|
+
blocksAtlasJson: {},
|
|
89
|
+
blockstatesModels: {},
|
|
90
|
+
},
|
|
91
|
+
} as DisplayWorldOptions['resourcesManager'],
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const initOptions: GraphicsInitOptions = {
|
|
95
|
+
config: { sceneBackground: '#000' },
|
|
96
|
+
rendererSpecificSettings: {},
|
|
97
|
+
callbacks: {
|
|
98
|
+
displayCriticalError: vi.fn(),
|
|
99
|
+
setRendererSpecificSettings: vi.fn(),
|
|
100
|
+
fireCustomEvent: vi.fn(),
|
|
101
|
+
},
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const renderer = new TestWorldRenderer(displayOptions.resourcesManager, displayOptions, initOptions)
|
|
105
|
+
renderer.active = true
|
|
106
|
+
renderer.workers = Array.from({ length: workerCount }, () => ({
|
|
107
|
+
postMessage: vi.fn(),
|
|
108
|
+
terminate: vi.fn(),
|
|
109
|
+
}))
|
|
110
|
+
renderer['syncMesherPoolSnapshot']()
|
|
111
|
+
renderer.viewDistance = 8
|
|
112
|
+
renderer.viewerChunkPosition = new Vec3(0, 64, 0)
|
|
113
|
+
renderer.worldSizeParams = { minY: 0, worldHeight: 256 }
|
|
114
|
+
return { renderer, reloadLoadedChunks }
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
describe('WorldRendererCommon.reconfigureMesherWorkers', () => {
|
|
118
|
+
beforeEach(() => {
|
|
119
|
+
ensurePromiseWithResolvers()
|
|
120
|
+
vi.stubGlobal('location', { href: 'http://localhost/' })
|
|
121
|
+
vi.stubGlobal('Worker', class MockWorker {
|
|
122
|
+
postMessage = vi.fn()
|
|
123
|
+
terminate = vi.fn()
|
|
124
|
+
addEventListener = vi.fn()
|
|
125
|
+
onmessage: ((event: MessageEvent) => void) | null = null
|
|
126
|
+
})
|
|
127
|
+
vi.spyOn(worldRendererModule, 'meshersSendMcData').mockImplementation(() => {})
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
afterEach(() => {
|
|
131
|
+
vi.restoreAllMocks()
|
|
132
|
+
vi.unstubAllGlobals()
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
test('recreates workers with new count and reloads chunks', async () => {
|
|
136
|
+
const { renderer, reloadLoadedChunks } = createRenderer(3)
|
|
137
|
+
const terminated = renderer.workers.map((worker) => worker.terminate)
|
|
138
|
+
renderer.worldRendererConfig.mesherWorkers = 1
|
|
139
|
+
|
|
140
|
+
await renderer.reconfigureMesherWorkers()
|
|
141
|
+
|
|
142
|
+
for (const terminate of terminated) {
|
|
143
|
+
expect(terminate).toHaveBeenCalled()
|
|
144
|
+
}
|
|
145
|
+
expect(renderer.workers).toHaveLength(1)
|
|
146
|
+
expect(reloadLoadedChunks).toHaveBeenCalledTimes(1)
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
test('recreates workers when mesher pipeline changes', async () => {
|
|
150
|
+
const { renderer, reloadLoadedChunks } = createRenderer(2)
|
|
151
|
+
const terminated = renderer.workers.map((worker) => worker.terminate)
|
|
152
|
+
renderer.worldRendererConfig.wasmMesher = false
|
|
153
|
+
|
|
154
|
+
await renderer.reconfigureMesherWorkers()
|
|
155
|
+
|
|
156
|
+
for (const terminate of terminated) {
|
|
157
|
+
expect(terminate).toHaveBeenCalled()
|
|
158
|
+
}
|
|
159
|
+
expect(renderer.workers).toHaveLength(2)
|
|
160
|
+
expect(reloadLoadedChunks).toHaveBeenCalledTimes(1)
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
test('watchMesherPoolConfig registers valtio unsubscribe functions', () => {
|
|
164
|
+
const { renderer } = createRenderer(2)
|
|
165
|
+
|
|
166
|
+
renderer['watchMesherPoolConfig']()
|
|
167
|
+
expect(renderer['valtioUnsubs']).toHaveLength(3)
|
|
168
|
+
for (const unsub of renderer['valtioUnsubs']) {
|
|
169
|
+
expect(typeof unsub).toBe('function')
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
expect(() => {
|
|
173
|
+
for (const unsub of renderer['valtioUnsubs']) unsub()
|
|
174
|
+
renderer.destroy()
|
|
175
|
+
}).not.toThrow()
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
test('config watcher enqueues reconfigure when mesherWorkers changes', async () => {
|
|
179
|
+
const enqueueSpy = vi.spyOn(TestWorldRenderer.prototype as any, 'enqueueMesherWorkersReconfigure')
|
|
180
|
+
const { renderer } = createRenderer(2)
|
|
181
|
+
|
|
182
|
+
renderer['watchMesherPoolConfig']()
|
|
183
|
+
renderer.worldRendererConfig.mesherWorkers = 4
|
|
184
|
+
|
|
185
|
+
await vi.waitFor(() => {
|
|
186
|
+
expect(enqueueSpy).toHaveBeenCalled()
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
enqueueSpy.mockRestore()
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
test('skips tail work when destroyed during bootstrap', async () => {
|
|
193
|
+
const { renderer, reloadLoadedChunks } = createRenderer(2)
|
|
194
|
+
vi.spyOn(renderer, 'updateAssetsData').mockImplementation(async () => {
|
|
195
|
+
renderer.active = false
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
await renderer.reconfigureMesherWorkers()
|
|
199
|
+
|
|
200
|
+
expect(reloadLoadedChunks).not.toHaveBeenCalled()
|
|
201
|
+
})
|
|
202
|
+
})
|
|
@@ -127,6 +127,12 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
|
|
|
127
127
|
debugStopGeometryUpdate = false
|
|
128
128
|
|
|
129
129
|
protocolCustomBlocks = new Map<string, CustomBlockModels>()
|
|
130
|
+
private mesherPoolSnapshot = {
|
|
131
|
+
mesherWorkers: -1,
|
|
132
|
+
wasmMesher: false,
|
|
133
|
+
dedicatedChangeWorker: false,
|
|
134
|
+
}
|
|
135
|
+
private mesherReconfigureQueue: Promise<void> = Promise.resolve()
|
|
130
136
|
private heightmapDebounceTimers = new Map<string, ReturnType<typeof setTimeout>>()
|
|
131
137
|
|
|
132
138
|
// Geometry throttle: first dirty per section is instant, subsequent within window are grouped
|
|
@@ -272,6 +278,7 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
|
|
|
272
278
|
|
|
273
279
|
this.watchReactivePlayerState()
|
|
274
280
|
this.watchReactiveConfig()
|
|
281
|
+
this.watchMesherPoolConfig()
|
|
275
282
|
this.worldReadyResolvers.resolve()
|
|
276
283
|
}
|
|
277
284
|
|
|
@@ -309,18 +316,127 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
|
|
|
309
316
|
}
|
|
310
317
|
}
|
|
311
318
|
|
|
319
|
+
private getMesherWorkerScript(): 'wasm' | 'legacy' {
|
|
320
|
+
return this.worldRendererConfig.wasmMesher ? 'wasm' : 'legacy'
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
private createMesherWorker() {
|
|
324
|
+
const script = this.getMesherWorkerScript()
|
|
325
|
+
return initMesherWorker((data) => {
|
|
326
|
+
if (Array.isArray(data)) {
|
|
327
|
+
this.messageQueue.push(...data)
|
|
328
|
+
} else {
|
|
329
|
+
this.messageQueue.push(data)
|
|
330
|
+
}
|
|
331
|
+
void this.processMessageQueue('worker')
|
|
332
|
+
}, script === 'wasm' ? 'mesherWasm.js' : 'mesher.js')
|
|
333
|
+
}
|
|
334
|
+
|
|
312
335
|
initWorkers(numWorkers = this.worldRendererConfig.mesherWorkers) {
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
336
|
+
for (let i = 0; i < numWorkers; i++) {
|
|
337
|
+
this.workers.push(this.createMesherWorker())
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
private syncMesherPoolSnapshot() {
|
|
342
|
+
this.mesherPoolSnapshot = {
|
|
343
|
+
mesherWorkers: this.worldRendererConfig.mesherWorkers,
|
|
344
|
+
wasmMesher: this.worldRendererConfig.wasmMesher,
|
|
345
|
+
dedicatedChangeWorker: this.worldRendererConfig.dedicatedChangeWorker,
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
private watchMesherPoolConfig() {
|
|
350
|
+
this.syncMesherPoolSnapshot()
|
|
351
|
+
|
|
352
|
+
const tryReconfigure = () => {
|
|
353
|
+
const cfg = this.worldRendererConfig
|
|
354
|
+
const snap = this.mesherPoolSnapshot
|
|
355
|
+
if (
|
|
356
|
+
cfg.mesherWorkers === snap.mesherWorkers &&
|
|
357
|
+
cfg.wasmMesher === snap.wasmMesher &&
|
|
358
|
+
cfg.dedicatedChangeWorker === snap.dedicatedChangeWorker
|
|
359
|
+
) {
|
|
360
|
+
return
|
|
361
|
+
}
|
|
362
|
+
this.syncMesherPoolSnapshot()
|
|
363
|
+
this.enqueueMesherWorkersReconfigure()
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
for (const key of ['mesherWorkers', 'wasmMesher', 'dedicatedChangeWorker'] as const) {
|
|
367
|
+
this.valtioUnsubs.push(
|
|
368
|
+
this.onReactiveConfigUpdated(key, tryReconfigure, false)
|
|
369
|
+
)
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
private enqueueMesherWorkersReconfigure() {
|
|
374
|
+
this.mesherReconfigureQueue = this.mesherReconfigureQueue
|
|
375
|
+
.then(() => this.reconfigureMesherWorkers())
|
|
376
|
+
.catch((err) => {
|
|
377
|
+
console.error('[Mesher] Failed to reconfigure workers:', err)
|
|
378
|
+
})
|
|
379
|
+
}
|
|
380
|
+
private clearMesherPendingState() {
|
|
381
|
+
this.sectionsWaiting.clear()
|
|
382
|
+
this.toWorkerMessagesQueue = {}
|
|
383
|
+
this.queueAwaited = false
|
|
384
|
+
this.messageQueue = []
|
|
385
|
+
this.isProcessingQueue = false
|
|
386
|
+
for (const timer of this.sectionDirtyTimers.values()) {
|
|
387
|
+
clearTimeout(timer)
|
|
388
|
+
}
|
|
389
|
+
this.sectionDirtyTimers.clear()
|
|
390
|
+
this.sectionDirtyCount.clear()
|
|
391
|
+
this.sectionDirtyPendingArgs.clear()
|
|
392
|
+
this.reactiveState.world.mesherWork = false
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
private terminateAllMesherWorkers() {
|
|
396
|
+
for (const worker of this.workers) {
|
|
397
|
+
worker.terminate()
|
|
398
|
+
}
|
|
399
|
+
this.workers = []
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
private async bootstrapMesherWorkers() {
|
|
403
|
+
if (this.workers.length === 0) return
|
|
404
|
+
|
|
405
|
+
this.sendMesherMcData()
|
|
406
|
+
await this.updateAssetsData()
|
|
407
|
+
this.logWorkerWork('# mesher workers bootstrapped')
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
async reconfigureMesherWorkers() {
|
|
411
|
+
if (!this.active) return
|
|
412
|
+
|
|
413
|
+
this.clearMesherPendingState()
|
|
414
|
+
this.terminateAllMesherWorkers()
|
|
415
|
+
this.initWorkers()
|
|
416
|
+
await this.bootstrapMesherWorkers()
|
|
417
|
+
if (!this.active) return
|
|
418
|
+
|
|
419
|
+
await this.requestLoadedChunksReload()
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
private async requestLoadedChunksReload() {
|
|
423
|
+
try {
|
|
424
|
+
const worldView = this.displayOptions.worldView as {
|
|
425
|
+
reloadLoadedChunks?: () => void | Promise<void>
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
if (typeof worldView.reloadLoadedChunks === 'function') {
|
|
429
|
+
await worldView.reloadLoadedChunks()
|
|
430
|
+
return
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
const workerScope = globalThis as typeof globalThis & { WorkerGlobalScope?: typeof WorkerGlobalScope }
|
|
434
|
+
if (typeof workerScope.WorkerGlobalScope !== 'undefined' && globalThis instanceof workerScope.WorkerGlobalScope) {
|
|
435
|
+
// eslint-disable-next-line no-restricted-globals
|
|
436
|
+
self.postMessage({ type: 'reloadLoadedChunks' })
|
|
437
|
+
}
|
|
438
|
+
} catch (err) {
|
|
439
|
+
console.error('[Mesher] Failed to reload chunks after worker reconfigure:', err)
|
|
324
440
|
}
|
|
325
441
|
}
|
|
326
442
|
|
|
@@ -331,13 +447,18 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
|
|
|
331
447
|
return subscribeKey(this.playerStateReactive, key, callback)
|
|
332
448
|
}
|
|
333
449
|
|
|
334
|
-
onReactiveConfigUpdated<T extends keyof typeof this.worldRendererConfig>(
|
|
335
|
-
|
|
450
|
+
onReactiveConfigUpdated<T extends keyof typeof this.worldRendererConfig>(
|
|
451
|
+
key: T,
|
|
452
|
+
callback: (value: typeof this.worldRendererConfig[T]) => void,
|
|
453
|
+
initial = true
|
|
454
|
+
) {
|
|
455
|
+
if (initial) {
|
|
456
|
+
callback(this.worldRendererConfig[key])
|
|
457
|
+
}
|
|
336
458
|
if ((key as any) === '*') {
|
|
337
|
-
subscribe(this.worldRendererConfig, callback as any)
|
|
338
|
-
} else {
|
|
339
|
-
subscribeKey(this.worldRendererConfig, key, callback)
|
|
459
|
+
return subscribe(this.worldRendererConfig, callback as any)
|
|
340
460
|
}
|
|
461
|
+
return subscribeKey(this.worldRendererConfig, key, callback)
|
|
341
462
|
}
|
|
342
463
|
|
|
343
464
|
onReactiveDebugUpdated<T extends keyof typeof this.reactiveDebugParams>(key: T, callback: (value: typeof this.reactiveDebugParams[T]) => void) {
|
|
@@ -637,6 +758,7 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
|
|
|
637
758
|
|
|
638
759
|
this.initWorkers()
|
|
639
760
|
this.active = true
|
|
761
|
+
this.syncMesherPoolSnapshot()
|
|
640
762
|
|
|
641
763
|
this.sendMesherMcData()
|
|
642
764
|
}
|
|
@@ -9,6 +9,7 @@ import type { MenuBackgroundOptions } from './menuBackground/types'
|
|
|
9
9
|
import { MENU_BACKGROUND_MC_VERSION } from './menuBackground/shared'
|
|
10
10
|
import { createGraphicsBackendBase, type ThreeJsBackendMethods } from './graphicsBackendBase'
|
|
11
11
|
import { addCanvasForWorker } from './documentRenderer'
|
|
12
|
+
import type { WorldView } from '../worldView'
|
|
12
13
|
|
|
13
14
|
function initThreeWorker(onGotMessage: (data: any) => void) {
|
|
14
15
|
// Node environment needs an absolute path, but browser needs the url of the file
|
|
@@ -31,7 +32,12 @@ function initThreeWorker(onGotMessage: (data: any) => void) {
|
|
|
31
32
|
}
|
|
32
33
|
|
|
33
34
|
export const createGraphicsBackendOffThread: GraphicsBackendLoader = async (initOptions) => {
|
|
34
|
-
const
|
|
35
|
+
const workerSideChannel = {
|
|
36
|
+
onMessage: (_data: unknown) => {},
|
|
37
|
+
}
|
|
38
|
+
const worker = initThreeWorker((data) => {
|
|
39
|
+
workerSideChannel.onMessage(data)
|
|
40
|
+
})
|
|
35
41
|
type WorkerType = ReturnType<ReturnType<typeof createGraphicsBackendBase>['workerProxy']>
|
|
36
42
|
|
|
37
43
|
const proxy = useWorkerProxy<WorkerType>(worker)
|
|
@@ -79,6 +85,15 @@ export const createGraphicsBackendOffThread: GraphicsBackendLoader = async (init
|
|
|
79
85
|
}
|
|
80
86
|
},
|
|
81
87
|
async startWorld(options) {
|
|
88
|
+
const worldView = options.worldView as unknown as WorldView
|
|
89
|
+
workerSideChannel.onMessage = (data: any) => {
|
|
90
|
+
if (data?.type === 'reloadLoadedChunks') {
|
|
91
|
+
void worldView.reloadLoadedChunks().catch((err) => {
|
|
92
|
+
console.error('[Renderer] Failed to reload chunks after mesher reconfigure:', err)
|
|
93
|
+
})
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
82
97
|
const workerThreeSendData = {
|
|
83
98
|
...dynamicMcDataFiles,
|
|
84
99
|
items: 'itemsArray',
|
|
@@ -320,6 +320,17 @@ export class WorldView extends (EventEmitter as new () => TypedEmitter<WorldView
|
|
|
320
320
|
}
|
|
321
321
|
}
|
|
322
322
|
|
|
323
|
+
/**
|
|
324
|
+
* Re-fetch and re-emit every loaded chunk (e.g. after mesher workers are recreated).
|
|
325
|
+
*/
|
|
326
|
+
async reloadLoadedChunks(): Promise<void> {
|
|
327
|
+
const coords = Object.keys(this.loadedChunks)
|
|
328
|
+
for (const key of coords) {
|
|
329
|
+
const [x, z] = key.split(',').map(Number)
|
|
330
|
+
await this.loadChunk({ x, z }, false, 'mesher-reconfigure')
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
323
334
|
/**
|
|
324
335
|
* Unload all chunks.
|
|
325
336
|
*/
|