diff --git a/prismarine-viewer/examples/chunksStorage.test.ts b/prismarine-viewer/examples/chunksStorage.test.ts index e39c920a4..344ad3345 100644 --- a/prismarine-viewer/examples/chunksStorage.test.ts +++ b/prismarine-viewer/examples/chunksStorage.test.ts @@ -4,21 +4,46 @@ import { ChunksStorage } from './chunksStorage' globalThis.reportError = err => { throw err } -const testTile = {} as any test('Free areas', () => { const storage = new ChunksStorage() - const blocks100 = Object.fromEntries(Array.from({ length: 100 }).map((_, i) => { - return [`${i},0,0`, testTile] + storage.chunkSizeDisplay = 1 + const blocksWith1 = Object.fromEntries(Array.from({ length: 100 }).map((_, i) => { + return [`${i},0,0`, 1 as any] })) - const blocks100Test = Object.fromEntries(Array.from({ length: 100 }).map((_, i) => { - return [`${i},0,0`, 10 as any] + const blocksWith2 = Object.fromEntries(Array.from({ length: 100 }).map((_, i) => { + return [`${i},0,0`, 2 as any] })) - const blocks10 = Object.fromEntries(Array.from({ length: 10 }).map((_, i) => { - return [`${i},0,0`, testTile] + const blocksWith3 = Object.fromEntries(Array.from({ length: 10 }).map((_, i) => { + return [`${i},0,0`, 3 as any] })) + const blocksWith4 = Object.fromEntries(Array.from({ length: 10 }).map((_, i) => { + return [`${i},0,0`, 4 as any] + })) + + const getRangeString = () => { + const ranges = {} + let lastNum = storage.allBlocks[0]?.[3] + let lastNumI = 0 + for (let i = 0; i < storage.allBlocks.length; i++) { + const num = storage.allBlocks[i]?.[3] + if (lastNum !== num || i === storage.allBlocks.length - 1) { + const inclusive = i === storage.allBlocks.length - 1 + ranges[`[${lastNumI}-${i}${inclusive ? ']' : ')'}`] = lastNum + lastNum = num + lastNumI = i + } + } + return ranges + } + + const testRange = (start, end, number) => { + for (let i = start; i < end; i++) { + expect(storage.allBlocks[i]?.[3], `allblocks ${i} (range ${start}-${end})`).toBe(number) + } + } - storage.addChunk(blocks100, '0,0,0') - storage.addChunk(blocks100Test, '1,0,0') + storage.addChunk(blocksWith1, '0,0,0') + storage.addChunk(blocksWith2, '1,0,0') expect(storage.chunksMap).toMatchInlineSnapshot(` Map { "0,0,0" => 0, @@ -36,7 +61,7 @@ test('Free areas', () => { { "free": false, "length": 100, - "x": 0.0625, + "x": 1, "z": 0, }, ] @@ -46,22 +71,31 @@ test('Free areas', () => { "chunk": { "free": false, "length": 100, - "x": 0.0625, + "x": 1, "z": 0, }, "index": 1, } `) - expect(storage.allBlocks[99]?.[3]).not.toBe(10) - for (let i = 100; i < 200; i++) { - expect(storage.allBlocks[i]?.[3]).toBe(10) - } + expect(getRangeString()).toMatchInlineSnapshot(` + { + "[0-100)": 1, + "[100-199]": 2, + } + `) storage.removeChunk('0,0,0') expect(storage.chunks[0].free).toBe(true) expect(storage.chunks[0].length).toBe(100) - storage.addChunk(blocks10, `0,0,2`) + expect(getRangeString()).toMatchInlineSnapshot(` + { + "[0-100)": undefined, + "[100-199]": 2, + } + `) + + storage.addChunk(blocksWith3, `0,0,2`) expect(storage.chunksMap).toMatchInlineSnapshot(` Map { "1,0,0" => 1, @@ -74,19 +108,26 @@ test('Free areas', () => { "free": false, "length": 100, "x": 0, - "z": 0.125, + "z": 2, }, { "free": false, "length": 100, - "x": 0.0625, + "x": 1, "z": 0, }, ] `) + expect(getRangeString()).toMatchInlineSnapshot(` + { + "[0-10)": 3, + "[10-100)": undefined, + "[100-199]": 2, + } + `) // update (no map changes) - storage.addChunk(blocks10, `0,0,2`) + storage.addChunk(blocksWith4, `0,0,2`) expect(storage.chunksMap).toMatchInlineSnapshot(` Map { "1,0,0" => 1, @@ -99,18 +140,25 @@ test('Free areas', () => { "free": false, "length": 100, "x": 0, - "z": 0.125, + "z": 2, }, { "free": false, "length": 100, - "x": 0.0625, + "x": 1, "z": 0, }, ] `) + expect(getRangeString()).toMatchInlineSnapshot(` + { + "[0-10)": 4, + "[10-100)": undefined, + "[100-199]": 2, + } + `) - storage.addChunk(blocks10, `0,0,3`) + storage.addChunk(blocksWith3, `0,0,3`) expect(storage.chunksMap).toMatchInlineSnapshot(` Map { "1,0,0" => 1, @@ -124,20 +172,68 @@ test('Free areas', () => { "free": false, "length": 100, "x": 0, - "z": 0.125, + "z": 2, + }, + { + "free": false, + "length": 100, + "x": 1, + "z": 0, + }, + { + "free": false, + "length": 10, + "x": 0, + "z": 3, + }, + ] + `) + expect(getRangeString()).toMatchInlineSnapshot(` + { + "[0-10)": 4, + "[10-100)": undefined, + "[100-200)": 2, + "[200-209]": 3, + } + `) + expect(storage.allBlocks.length).toBe(210) + + // update 0,0,2 + storage.addChunk(blocksWith1, `0,0,2`) + expect(storage.chunksMap).toMatchInlineSnapshot(` + Map { + "1,0,0" => 1, + "0,0,3" => 2, + "0,0,2" => 0, + } + `) + expect(storage.chunks).toMatchInlineSnapshot(` + [ + { + "free": false, + "length": 100, + "x": 0, + "z": 2, }, { "free": false, "length": 100, - "x": 0.0625, + "x": 1, "z": 0, }, { "free": false, "length": 10, "x": 0, - "z": 0.1875, + "z": 3, }, ] `) + expect(getRangeString()).toMatchInlineSnapshot(` + { + "[0-100)": 1, + "[100-200)": 2, + "[200-209]": 3, + } + `) }) diff --git a/prismarine-viewer/examples/chunksStorage.ts b/prismarine-viewer/examples/chunksStorage.ts index 0adb8370f..c616f2e7f 100644 --- a/prismarine-viewer/examples/chunksStorage.ts +++ b/prismarine-viewer/examples/chunksStorage.ts @@ -12,6 +12,7 @@ export class ChunksStorage { awaitingUpdateEnd: number | undefined // dataSize = 0 lastFetchedSize = 0 + chunkSizeDisplay = 16 get dataSize () { return this.allBlocks.length @@ -48,6 +49,11 @@ export class ChunksStorage { } } + setAwaitingUpdate ({ awaitingUpdateStart, awaitingUpdateSize }: { awaitingUpdateStart: number, awaitingUpdateSize: number }) { + this.awaitingUpdateStart = awaitingUpdateStart + this.awaitingUpdateEnd = awaitingUpdateStart + awaitingUpdateSize + } + clearData () { this.chunks = [] this.allBlocks = [] @@ -55,12 +61,11 @@ export class ChunksStorage { this.awaitingUpdateEnd = undefined } - addBlocksData (start: number, newData: typeof this.allBlocks) { - let i = 0 - while (i < newData.length) { - this.allBlocks.splice(start + i, 0, ...newData.slice(i, i + 1024)) - i += 1024 + replaceBlocksData (start: number, newData: typeof this.allBlocks) { + if (newData.length > 16 * 16 * 16) { + throw new Error(`Chunk cant be that big: ${newData.length}`) } + this.allBlocks.splice(start, newData.length, ...newData) } getAvailableChunk (size: number) { @@ -73,6 +78,7 @@ export class ChunksStorage { if (chunkLength >= size) { usingChunk = chunk usingChunk.free = false + currentStart -= chunkLength break } } @@ -97,31 +103,33 @@ export class ChunksStorage { removeChunk (chunkPosKey: string) { if (!this.chunksMap.has(chunkPosKey)) return let currentStart = 0 - const chunkIndex = this.chunksMap.get(chunkPosKey)! + let chunkIndex = this.chunksMap.get(chunkPosKey)! const chunk = this.chunks[chunkIndex] - for (let i = 0; i <= chunkIndex; i++) { + for (let i = 0; i < chunkIndex; i++) { const chunk = this.chunks[i]! currentStart += chunk.length } - this.addBlocksData(currentStart, Array.from({ length: chunk.length }).map(() => undefined)) // empty data, will be filled with 0 + this.replaceBlocksData(currentStart, Array.from({ length: chunk.length }).map(() => undefined)) // empty data, will be filled with 0 + this.requestRangeUpdate(currentStart, currentStart + chunk.length) chunk.free = true this.chunksMap.delete(chunkPosKey) // try merge backwards - for (let i = chunkIndex - 1; i >= 0; i--) { - const chunk = this.chunks[i]! - if (!chunk.free) break - chunk.length += this.chunks[i]!.length - this.chunks.splice(i, 1) - } - // try merge forwards - for (let i = chunkIndex + 1; i < this.chunks.length; i++) { - const chunk = this.chunks[i]! - if (!chunk.free) break - chunk.length += this.chunks[i]!.length - this.chunks.splice(i, 1) - i-- - } + // for (let i = chunkIndex - 1; i >= 0; i--) { + // const chunk = this.chunks[i]! + // if (!chunk.free) break + // chunk.length += this.chunks[i]!.length + // this.chunks.splice(i, 1) + // chunkIndex-- + // } + // // try merge forwards + // for (let i = chunkIndex + 1; i < this.chunks.length; i++) { + // const chunk = this.chunks[i]! + // if (!chunk.free) break + // chunk.length += this.chunks[i]!.length + // this.chunks.splice(i, 1) + // i-- + // } } addChunk (blocks: Record, rawPosKey: string) { @@ -160,18 +168,27 @@ export class ChunksStorage { // } const { chunk, start } = this.getAvailableChunk(newData.length) - chunk.x = xSection / 16 - chunk.z = zSection / 16 + chunk.x = xSection / this.chunkSizeDisplay + chunk.z = zSection / this.chunkSizeDisplay const chunkIndex = this.chunks.indexOf(chunk) this.chunksMap.set(rawPosKey, chunkIndex) - for (const newDatum of newData) { - //@ts-expect-error - newDatum[3].chunk = chunkIndex + for (const b of newData) { + if (b[3] && typeof b[3] === 'object') { + b[3].chunk = chunkIndex + } } - this.addBlocksData(start, newData) + this.replaceBlocksData(start, newData) + this.requestRangeUpdate(start, start + newData.length) + } + + requestRangeUpdate (start: number, end: number) { this.awaitingUpdateStart = Math.min(this.awaitingUpdateStart ?? Infinity, start) - this.awaitingUpdateEnd = Math.max(this.awaitingUpdateEnd ?? -Infinity, start + newData.length) + this.awaitingUpdateEnd = Math.max(this.awaitingUpdateEnd ?? -Infinity, end) + } + + clearRange (start: number, end: number) { + // this.replaceBlocksData(start, Array.from({ length: end - start }).map(() => undefined)) } } diff --git a/prismarine-viewer/examples/shared.ts b/prismarine-viewer/examples/shared.ts index 15259e18a..cf4c97282 100644 --- a/prismarine-viewer/examples/shared.ts +++ b/prismarine-viewer/examples/shared.ts @@ -21,6 +21,7 @@ export type BlockType = { tint?: [number, number, number] // for testing + chunk?: number block: string } diff --git a/prismarine-viewer/examples/webgpuRenderer.ts b/prismarine-viewer/examples/webgpuRenderer.ts index 5e4b42cff..85ef88595 100644 --- a/prismarine-viewer/examples/webgpuRenderer.ts +++ b/prismarine-viewer/examples/webgpuRenderer.ts @@ -68,7 +68,7 @@ export class WebgpuRenderer { // eslint-disable-next-line max-params constructor (public canvas: HTMLCanvasElement, public imageBlob: ImageBitmapSource, public isPlayground: boolean, public camera: THREE.PerspectiveCamera, public localStorage: any, public NUMBER_OF_CUBES: number, public blocksDataModel: Record) { - this.NUMBER_OF_CUBES = 3_000_000 + this.NUMBER_OF_CUBES = 500_000 void this.init().catch((err) => { console.error(err) postMessage({ type: 'rendererProblem', isContextLost: false, message: err.message }) @@ -573,7 +573,7 @@ export class WebgpuRenderer { const oldCubesBuffer = this.cubesBuffer const oldVisibleCubesBuffer = this.visibleCubesBuffer - this.cubesBuffer = this.createVertexStorage(this.NUMBER_OF_CUBES * 12, 'cubesBuffer') + this.cubesBuffer = this.createVertexStorage(this.NUMBER_OF_CUBES * cubeByteLength, 'cubesBuffer') this.chunksBuffer = this.createVertexStorage(65_535 * 12, 'cubesBuffer') this.visibleCubesBuffer = this.createVertexStorage(this.NUMBER_OF_CUBES * 12, 'visibleCubesBuffer') @@ -598,6 +598,8 @@ export class WebgpuRenderer { } updateCubesBuffersDataFromLoop () { + const DEBUG_DATA = false + const dataForBuffers = chunksStorage.getDataForBuffers() if (!dataForBuffers) return const { allBlocks, chunks, awaitingUpdateSize: updateSize, awaitingUpdateStart: updateOffset } = dataForBuffers @@ -612,6 +614,9 @@ export class WebgpuRenderer { console.time('recreate buffers') this.createNewDataBuffers() console.timeEnd('recreate buffers') + chunksStorage.setAwaitingUpdate(dataForBuffers) + console.timeEnd('updateBlocks') + return } this.realNumberOfCubes = NUMBER_OF_CUBES_NEEDED @@ -626,7 +631,6 @@ export class WebgpuRenderer { const cubeFlatData = new Uint32Array(updateSize * 3) const blocksToUpdate = allBlocks.slice(updateOffset, updateOffset + updateSize) - const actualCount = updateOffset + blocksToUpdate.length // let chunk = chunksStorage.findBelongingChunk(updateOffset)! // let remaining = chunk.chunk.length @@ -660,14 +664,15 @@ export class WebgpuRenderer { (visibility[4] << 4) | (visibility[5] << 5) second = ((visibilityCombined << 8 | colors[2]) << 8 | colors[1]) << 8 | colors[0] - //@ts-expect-error - third = block.chunk + third = block.chunk! } cubeFlatData[i * 3] = first cubeFlatData[i * 3 + 1] = second cubeFlatData[i * 3 + 2] = third - // debugCheckDuplicate(first, second, third) + if (DEBUG_DATA) { + debugCheckDuplicate(first, second, third) + } } const chunksCount = chunks.length @@ -681,8 +686,11 @@ export class WebgpuRenderer { const cubesCount = length totalFromChunks += cubesCount } - if (totalFromChunks !== actualCount) { - reportError?.(new Error(`Buffers length mismatch: from chunks: ${totalFromChunks}, flat data: ${actualCount}`)) + if (DEBUG_DATA) { + const actualCount = allBlocks.filter(Boolean).length + if (totalFromChunks !== actualCount) { + reportError?.(new Error(`Buffers length mismatch: from chunks: ${totalFromChunks}, flat data: ${actualCount}`)) + } } this.device.queue.writeBuffer(this.cubesBuffer, updateOffset * cubeByteLength, cubeFlatData) @@ -691,7 +699,9 @@ export class WebgpuRenderer { this.notRenderedBlockChanges++ console.timeEnd('updateBlocks') this.realNumberOfCubes = this.NUMBER_OF_CUBES - // chunksStorage.clearRange(updateOffset, updateOffset + updateSize) + if (!DEBUG_DATA) { + chunksStorage.clearRange(updateOffset, updateOffset + updateSize) + } } lastCall = performance.now()