From 6894942f7badf6e8695ebdc83e15a5dab008e5ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Piellard?= Date: Mon, 25 Mar 2024 15:25:55 +0100 Subject: [PATCH 01/12] feat: new logger module --- src/lib/helpers/logger.ts | 42 +++++++++++++++++++ src/lib/helpers/webgpu/webgpu-device.ts | 4 +- src/lib/index.ts | 1 + .../split/gpu/patch-computer-gpu.ts | 4 +- .../split/gpu/patch-factory-gpu-optimized.ts | 10 ++--- src/lib/terrain/terrain.ts | 3 +- src/test/main.ts | 4 +- 7 files changed, 59 insertions(+), 9 deletions(-) create mode 100644 src/lib/helpers/logger.ts diff --git a/src/lib/helpers/logger.ts b/src/lib/helpers/logger.ts new file mode 100644 index 00000000..88988753 --- /dev/null +++ b/src/lib/helpers/logger.ts @@ -0,0 +1,42 @@ +enum EVerbosity { + WARN = 0, + INFO, + DEBUG, + DIAGNOSTIC, +} + +class Logger { + private static readonly prefix = 'aresrpg-engine: '; + public verbosity = EVerbosity.INFO; + + public warn(message: string): void { + if (this.verbosity >= EVerbosity.WARN) { + console.warn(Logger.prefix + message); + } + } + + public info(message: string): void { + if (this.verbosity >= EVerbosity.INFO) { + console.info(Logger.prefix + message); + } + } + + public debug(message: string): void { + if (this.verbosity >= EVerbosity.DEBUG) { + console.debug(Logger.prefix + message); + } + } + + public diagnostic(message: string): void { + if (this.verbosity >= EVerbosity.DIAGNOSTIC) { + console.debug(Logger.prefix + message); + } + } +} + +const logger = new Logger(); +function setVerbosity(verbosity: EVerbosity): void { + logger.verbosity = verbosity; +} + +export { EVerbosity, logger, setVerbosity }; diff --git a/src/lib/helpers/webgpu/webgpu-device.ts b/src/lib/helpers/webgpu/webgpu-device.ts index 56c9abcd..c4fe44e7 100644 --- a/src/lib/helpers/webgpu/webgpu-device.ts +++ b/src/lib/helpers/webgpu/webgpu-device.ts @@ -1,5 +1,7 @@ /// +import { logger } from '../logger'; + let devicePromise: Promise | null = null; async function getGpuDevice(): Promise { @@ -18,7 +20,7 @@ async function getGpuDevice(): Promise { } if (adapter.isFallbackAdapter) { - console.warn('The retrieved GPU adapter is fallback. The performance might be degraded.'); + logger.warn('The retrieved GPU adapter is fallback. The performance might be degraded.'); } devicePromise = adapter.requestDevice(); diff --git a/src/lib/index.ts b/src/lib/index.ts index 40521731..871140e2 100644 --- a/src/lib/index.ts +++ b/src/lib/index.ts @@ -1,2 +1,3 @@ +export { EVerbosity, setVerbosity } from './helpers/logger'; export type { IVoxel, IVoxelMap, IVoxelMaterial } from './terrain/i-voxel-map'; export { EPatchComputingMode, Terrain } from './terrain/terrain'; diff --git a/src/lib/terrain/patch/patch-factory/split/gpu/patch-computer-gpu.ts b/src/lib/terrain/patch/patch-factory/split/gpu/patch-computer-gpu.ts index 6b775a4a..384b6045 100644 --- a/src/lib/terrain/patch/patch-factory/split/gpu/patch-computer-gpu.ts +++ b/src/lib/terrain/patch/patch-factory/split/gpu/patch-computer-gpu.ts @@ -2,6 +2,7 @@ import * as THREE from 'three'; +import { logger } from '../../../../../helpers/logger'; import { getGpuDevice } from '../../../../../helpers/webgpu/webgpu-device'; import * as Cube from '../../cube'; import { type LocalMapCache } from '../../patch-factory-base'; @@ -16,6 +17,7 @@ type ComputationOutputs = Record; class PatchComputerGpu { public static async create(localCacheSize: THREE.Vector3, vertexDataEncoder: VertexDataEncoder): Promise { + logger.debug('Requesting WebGPU device...'); const device = await getGpuDevice(); return new PatchComputerGpu(device, localCacheSize, vertexDataEncoder); } @@ -205,7 +207,7 @@ class PatchComputerGpu { Object.values(this.faceBuffers).forEach( faceBuffer => (totalBuffersSize += faceBuffer.readableBuffer.size + faceBuffer.storageBuffer.size) ); - console.log(`Allocated ${(totalBuffersSize / 1024 / 1024).toFixed(1)} MB of webgpu buffers.`); + logger.info(`Allocated ${(totalBuffersSize / 1024 / 1024).toFixed(1)} MB of webgpu buffers.`); const bindgroupBuffers = [this.localCacheBuffer, ...Object.values(this.faceBuffers).map(faceBuffer => faceBuffer.storageBuffer)]; this.computePipelineBindgroup = this.device.createBindGroup({ diff --git a/src/lib/terrain/patch/patch-factory/split/gpu/patch-factory-gpu-optimized.ts b/src/lib/terrain/patch/patch-factory/split/gpu/patch-factory-gpu-optimized.ts index b4c1e88a..bacdc1f4 100644 --- a/src/lib/terrain/patch/patch-factory/split/gpu/patch-factory-gpu-optimized.ts +++ b/src/lib/terrain/patch/patch-factory/split/gpu/patch-factory-gpu-optimized.ts @@ -30,14 +30,14 @@ class PatchFactoryGpuOptimized extends PatchFactoryGpu { return new Promise(resolve => { const patchId = this.nextPatchId++; - // console.log(`Asking for patch ${patchId}`); + // logger.diagnostic(`Asking for patch ${patchId}`); this.pendingJobs.push({ patchId, cpuTask: () => { - // console.log(`CPU ${patchId} start`); + // logger.diagnostic(`CPU ${patchId} start`); const result = this.buildLocalMapCache(patchStart, patchEnd); - // console.log(`CPU ${patchId} end`); + // logger.diagnostic(`CPU ${patchId} end`); return result; }, resolve, @@ -59,10 +59,10 @@ class PatchFactoryGpuOptimized extends PatchFactoryGpu { const localMapCache = currentJob.cpuTaskOutput; currentJob.gpuTaskPromise = (async () => { - // console.log(`GPU ${currentJob.patchId} start`); + // logger.diagnostic(`GPU ${currentJob.patchId} start`); const patchComputerGpu = await this.getPatchComputerGpu(); const gpuTaskOutput = await patchComputerGpu.computeBuffers(localMapCache); - // console.log(`GPU ${currentJob.patchId} end`); + // logger.diagnostic(`GPU ${currentJob.patchId} end`); const result = this.assembleGeometryAndMaterials(gpuTaskOutput); currentJob.resolve(result); diff --git a/src/lib/terrain/terrain.ts b/src/lib/terrain/terrain.ts index 5881b019..73150bc5 100644 --- a/src/lib/terrain/terrain.ts +++ b/src/lib/terrain/terrain.ts @@ -1,3 +1,4 @@ +import { logger } from '../helpers/logger'; import * as THREE from '../three-usage'; import { AsyncPatch } from './async-patch'; @@ -73,7 +74,7 @@ class Terrain { } this.patchSize = this.patchFactory.maxPatchSize.clone(); - console.log(`Using max patch size ${this.patchSize.x}x${this.patchSize.y}x${this.patchSize.z}.`); + logger.info(`Using max patch size ${this.patchSize.x}x${this.patchSize.y}x${this.patchSize.z}.`); this.container = new THREE.Group(); } diff --git a/src/test/main.ts b/src/test/main.ts index 19a39ef6..c054aede 100644 --- a/src/test/main.ts +++ b/src/test/main.ts @@ -1,10 +1,12 @@ import * as THREE from 'three'; import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'; -import { Terrain } from '../lib/index'; +import { Terrain, setVerbosity, EVerbosity } from '../lib/index'; import { VoxelMap } from './voxel-map'; +setVerbosity(EVerbosity.DIAGNOSTIC); + const renderer = new THREE.WebGLRenderer(); document.body.appendChild(renderer.domElement); renderer.setClearColor(0x880000); From f007a86e8c77256dbfc7708d46e3f61849b6f939 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Piellard?= Date: Mon, 25 Mar 2024 15:28:22 +0100 Subject: [PATCH 02/12] fix: avoid webgpu memory leak --- .../patch/patch-factory/split/gpu/patch-computer-gpu.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/lib/terrain/patch/patch-factory/split/gpu/patch-computer-gpu.ts b/src/lib/terrain/patch/patch-factory/split/gpu/patch-computer-gpu.ts index 384b6045..5b128963 100644 --- a/src/lib/terrain/patch/patch-factory/split/gpu/patch-computer-gpu.ts +++ b/src/lib/terrain/patch/patch-factory/split/gpu/patch-computer-gpu.ts @@ -282,10 +282,13 @@ class PatchComputerGpu { } public dispose(): void { + this.localCacheBuffer.destroy(); for (const buffer of Object.values(this.faceBuffers)) { buffer.storageBuffer.destroy(); buffer.readableBuffer.destroy(); } + logger.debug('Destroying WebGPU device...'); + this.device.destroy(); } } From e4c3256cf187c709c27fdaad0fd3a09851afd321 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Piellard?= Date: Mon, 25 Mar 2024 15:49:39 +0100 Subject: [PATCH 03/12] perf: skip GPU computation for empty patches --- .../patch/patch-factory/patch-factory-base.ts | 8 ++++++ .../split/gpu/patch-factory-gpu-optimized.ts | 27 ++++++++++++------- .../split/gpu/patch-factory-gpu-sequential.ts | 3 +++ 3 files changed, 28 insertions(+), 10 deletions(-) diff --git a/src/lib/terrain/patch/patch-factory/patch-factory-base.ts b/src/lib/terrain/patch/patch-factory/patch-factory-base.ts index 8bc8ef8a..bb2843e3 100644 --- a/src/lib/terrain/patch/patch-factory/patch-factory-base.ts +++ b/src/lib/terrain/patch/patch-factory/patch-factory-base.ts @@ -29,6 +29,7 @@ type FaceData = { type LocalMapCache = { readonly data: Uint16Array; readonly size: THREE.Vector3; + readonly isEmpty: boolean; neighbourExists(voxelIndex: number, neighbourRelativePosition: THREE.Vector3): boolean; }; @@ -206,6 +207,10 @@ abstract class PatchFactoryBase { } private *iterateOnVisibleFacesWithCache(localMapCache: LocalMapCache): Generator { + if (localMapCache.isEmpty) { + return; + } + let cacheIndex = 0; const localPosition = new THREE.Vector3(); for (localPosition.z = 0; localPosition.z < localMapCache.size.z; localPosition.z++) { @@ -309,15 +314,18 @@ abstract class PatchFactoryBase { return neighbourData !== 0; }; + let isEmpty = true; for (const voxel of this.map.iterateOnVoxels(cacheStart, cacheEnd)) { const localPosition = new THREE.Vector3().subVectors(voxel.position, cacheStart); const cacheIndex = buildIndex(localPosition); cache[cacheIndex] = 1 + voxel.materialId; + isEmpty = false; } return { data: cache, size: cacheSize, + isEmpty, neighbourExists, }; } diff --git a/src/lib/terrain/patch/patch-factory/split/gpu/patch-factory-gpu-optimized.ts b/src/lib/terrain/patch/patch-factory/split/gpu/patch-factory-gpu-optimized.ts index bacdc1f4..ae1a317e 100644 --- a/src/lib/terrain/patch/patch-factory/split/gpu/patch-factory-gpu-optimized.ts +++ b/src/lib/terrain/patch/patch-factory/split/gpu/patch-factory-gpu-optimized.ts @@ -58,17 +58,24 @@ class PatchFactoryGpuOptimized extends PatchFactoryGpu { if (!currentJob.gpuTaskPromise) { const localMapCache = currentJob.cpuTaskOutput; - currentJob.gpuTaskPromise = (async () => { - // logger.diagnostic(`GPU ${currentJob.patchId} start`); - const patchComputerGpu = await this.getPatchComputerGpu(); - const gpuTaskOutput = await patchComputerGpu.computeBuffers(localMapCache); - // logger.diagnostic(`GPU ${currentJob.patchId} end`); - - const result = this.assembleGeometryAndMaterials(gpuTaskOutput); - currentJob.resolve(result); + if (localMapCache.isEmpty) { + currentJob.gpuTaskPromise = Promise.resolve(); this.pendingJobs.shift(); - this.runNextTask(); - })(); + currentJob.resolve([]); + setTimeout(() => this.runNextTask()); + } else { + currentJob.gpuTaskPromise = (async () => { + // logger.diagnostic(`GPU ${currentJob.patchId} start`); + const patchComputerGpu = await this.getPatchComputerGpu(); + const gpuTaskOutput = await patchComputerGpu.computeBuffers(localMapCache); + // logger.diagnostic(`GPU ${currentJob.patchId} end`); + + const result = this.assembleGeometryAndMaterials(gpuTaskOutput); + this.pendingJobs.shift(); + currentJob.resolve(result); + this.runNextTask(); + })(); + } } const nextJob = this.pendingJobs[1]; diff --git a/src/lib/terrain/patch/patch-factory/split/gpu/patch-factory-gpu-sequential.ts b/src/lib/terrain/patch/patch-factory/split/gpu/patch-factory-gpu-sequential.ts index 0b825932..80804d71 100644 --- a/src/lib/terrain/patch/patch-factory/split/gpu/patch-factory-gpu-sequential.ts +++ b/src/lib/terrain/patch/patch-factory/split/gpu/patch-factory-gpu-sequential.ts @@ -21,6 +21,9 @@ class PatchFactoryGpuSequential extends PatchFactoryGpu { } const localMapCache = this.buildLocalMapCache(patchStart, patchEnd); + if (localMapCache.isEmpty) { + return []; + } const patchComputerGpu = await this.getPatchComputerGpu(); const buffers = await patchComputerGpu.computeBuffers(localMapCache); From e31c908414f4b5f90ce6645a1f3e3f3963dc901a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Piellard?= Date: Mon, 25 Mar 2024 17:14:34 +0100 Subject: [PATCH 04/12] test: add code to test the Terrain.showMapAroundPosition() method --- src/test/main.ts | 30 +++++++++++++++++++++++++++--- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/src/test/main.ts b/src/test/main.ts index c054aede..203cd160 100644 --- a/src/test/main.ts +++ b/src/test/main.ts @@ -1,7 +1,8 @@ import * as THREE from 'three'; import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'; +import { TransformControls } from 'three/examples/jsm/controls/TransformControls.js'; -import { Terrain, setVerbosity, EVerbosity } from '../lib/index'; +import { EVerbosity, Terrain, setVerbosity } from '../lib/index'; import { VoxelMap } from './voxel-map'; @@ -24,7 +25,7 @@ udpateRendererSize(); const scene = new THREE.Scene(); -const voxelMap = new VoxelMap(256, 256, 16); +const voxelMap = new VoxelMap(512, 512, 16); const terrain = new Terrain(voxelMap); scene.add(terrain.container); @@ -34,7 +35,30 @@ camera.position.set(-50, 100, -50); const cameraControl = new OrbitControls(camera, renderer.domElement); cameraControl.target.set(voxelMap.size.x / 2, 0, voxelMap.size.z / 2); -terrain.showEntireMap(); +const playerViewRadius = 32; +const playerContainer = new THREE.Group(); +playerContainer.position.x = voxelMap.size.x / 2; +playerContainer.position.y = voxelMap.size.y + 1; +playerContainer.position.z = voxelMap.size.z / 2; +const player = new THREE.Mesh(new THREE.SphereGeometry(2), new THREE.MeshBasicMaterial({ color: '#FF0000' })); +const playerViewSphere = new THREE.Mesh( + new THREE.SphereGeometry(playerViewRadius, 16, 16), + new THREE.MeshBasicMaterial({ color: 0xffffff, wireframe: true }) +); +playerContainer.add(player); +playerContainer.add(playerViewSphere); +const playerControls = new TransformControls(camera, renderer.domElement); +playerControls.addEventListener('dragging-changed', event => { + cameraControl.enabled = !event.value; +}); +playerControls.attach(playerContainer); +scene.add(playerContainer); +scene.add(playerControls); +setInterval(() => { + terrain.showMapAroundPosition(playerContainer.position, playerViewRadius); +}, 200); + +// terrain.showEntireMap(); function render(): void { cameraControl.update(); terrain.updateUniforms(); From 056ad5cc9106f5f6e2a13b82018e86553ac1f37a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Piellard?= Date: Mon, 25 Mar 2024 17:19:22 +0100 Subject: [PATCH 05/12] test: add stats --- src/test/main.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/test/main.ts b/src/test/main.ts index 203cd160..f6ea3e30 100644 --- a/src/test/main.ts +++ b/src/test/main.ts @@ -1,6 +1,7 @@ import * as THREE from 'three'; import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'; import { TransformControls } from 'three/examples/jsm/controls/TransformControls.js'; +import Stats from 'three/examples/jsm/libs/stats.module.js'; import { EVerbosity, Terrain, setVerbosity } from '../lib/index'; @@ -8,6 +9,9 @@ import { VoxelMap } from './voxel-map'; setVerbosity(EVerbosity.DIAGNOSTIC); +const stats = new Stats(); +document.body.appendChild(stats.dom); + const renderer = new THREE.WebGLRenderer(); document.body.appendChild(renderer.domElement); renderer.setClearColor(0x880000); @@ -60,6 +64,8 @@ setInterval(() => { // terrain.showEntireMap(); function render(): void { + stats.update(); + cameraControl.update(); terrain.updateUniforms(); renderer.render(scene, camera); From cf5c47aa2843967dc054454ab99bf1d5ba63c1e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Piellard?= Date: Mon, 25 Mar 2024 17:24:22 +0100 Subject: [PATCH 06/12] refactor: patch, remove unused attributes --- src/lib/terrain/patch/patch-factory/patch-factory-base.ts | 2 -- src/lib/terrain/patch/patch.ts | 7 +------ 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/src/lib/terrain/patch/patch-factory/patch-factory-base.ts b/src/lib/terrain/patch/patch-factory/patch-factory-base.ts index bb2843e3..7ad44dd3 100644 --- a/src/lib/terrain/patch/patch-factory/patch-factory-base.ts +++ b/src/lib/terrain/patch/patch-factory/patch-factory-base.ts @@ -100,8 +100,6 @@ abstract class PatchFactoryBase { boundingBox.getBoundingSphere(boundingSphere); return new Patch( - patchStart, - patchSize, geometryAndMaterialsList.map(geometryAndMaterial => { const { geometry } = geometryAndMaterial; geometry.boundingBox = boundingBox.clone(); diff --git a/src/lib/terrain/patch/patch.ts b/src/lib/terrain/patch/patch.ts index 0eeadd34..c439fcf1 100644 --- a/src/lib/terrain/patch/patch.ts +++ b/src/lib/terrain/patch/patch.ts @@ -31,16 +31,11 @@ class Patch { }, }; - public readonly patchStart: THREE.Vector3; - public readonly patchSize: THREE.Vector3; - private gpuResources: { readonly patchMeshes: ReadonlyArray; } | null = null; - public constructor(patchStart: THREE.Vector3, patchSize: THREE.Vector3, patchMeshes: PatchMesh[]) { - this.patchStart = patchStart; - this.patchSize = patchSize; + public constructor(patchMeshes: PatchMesh[]) { this.gpuResources = { patchMeshes }; this.container = new THREE.Object3D(); From e359c9c2f30c4989f15fb2de7db6b5481da10558 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Piellard?= Date: Mon, 25 Mar 2024 17:34:36 +0100 Subject: [PATCH 07/12] perf: optimize Terrain.showMapAroundPosition() Only compute patches that are sure to be within the visibility sphere --- src/lib/terrain/terrain.ts | 11 ++++++++--- src/test/main.ts | 2 +- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/lib/terrain/terrain.ts b/src/lib/terrain/terrain.ts index 73150bc5..3c2b3a09 100644 --- a/src/lib/terrain/terrain.ts +++ b/src/lib/terrain/terrain.ts @@ -114,15 +114,20 @@ class Terrain { patch.visible = false; } + const visibilitySphere = new THREE.Sphere(position, radius); const promises: Promise[] = []; const patchId = new THREE.Vector3(); for (patchId.x = patchIdFrom.x; patchId.x < patchIdTo.x; patchId.x++) { for (patchId.y = patchIdFrom.y; patchId.y < patchIdTo.y; patchId.y++) { for (patchId.z = patchIdFrom.z; patchId.z < patchIdTo.z; patchId.z++) { const patchStart = new THREE.Vector3().multiplyVectors(patchId, this.patchSize); - const patch = this.getPatch(patchStart); - patch.visible = true; - promises.push(patch.ready()); + + const boundingBox = new THREE.Box3(patchStart, patchStart.clone().add(this.patchSize)); + if (visibilitySphere.intersectsBox(boundingBox)) { + const patch = this.getPatch(patchStart); + patch.visible = true; + promises.push(patch.ready()); + } } } } diff --git a/src/test/main.ts b/src/test/main.ts index f6ea3e30..ee149b31 100644 --- a/src/test/main.ts +++ b/src/test/main.ts @@ -39,7 +39,7 @@ camera.position.set(-50, 100, -50); const cameraControl = new OrbitControls(camera, renderer.domElement); cameraControl.target.set(voxelMap.size.x / 2, 0, voxelMap.size.z / 2); -const playerViewRadius = 32; +const playerViewRadius = 40; const playerContainer = new THREE.Group(); playerContainer.position.x = voxelMap.size.x / 2; playerContainer.position.y = voxelMap.size.y + 1; From cdf2e0fe5ae5df179816d2b90447910ec5e665e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Piellard?= Date: Mon, 25 Mar 2024 17:50:30 +0100 Subject: [PATCH 08/12] perf: optimize Terrain.showMapAroundPosition() Don't even ask for patches that are out of bounds --- src/lib/terrain/terrain.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lib/terrain/terrain.ts b/src/lib/terrain/terrain.ts index 3c2b3a09..c96b12cc 100644 --- a/src/lib/terrain/terrain.ts +++ b/src/lib/terrain/terrain.ts @@ -105,8 +105,8 @@ class Terrain { * @param radius The visibility radius, in voxels. */ public async showMapAroundPosition(position: THREE.Vector3, radius: number): Promise { - const voxelFrom = new THREE.Vector3().copy(position).subScalar(radius); - const voxelTo = new THREE.Vector3().copy(position).addScalar(radius); + const voxelFrom = new THREE.Vector3().copy(position).subScalar(radius).max({ x: 0, y: 0, z: 0 }); + const voxelTo = new THREE.Vector3().copy(position).addScalar(radius).min(this.map.size); const patchIdFrom = voxelFrom.divide(this.patchSize).floor(); const patchIdTo = voxelTo.divide(this.patchSize).ceil(); From 3768e72b14d88b66d838d4c80014d3dcd9730e4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Piellard?= Date: Mon, 25 Mar 2024 18:57:02 +0100 Subject: [PATCH 09/12] feat: GPU LRU cache of invisible patches This allows to free GPU memory when there are too any invisible patches. An invisible patch is a patch that was computed once and then went invisible because it is out of sight. --- src/lib/terrain/async-patch.ts | 37 +++++++++++++++++---- src/lib/terrain/terrain.ts | 60 ++++++++++++++++++++++++++++++---- 2 files changed, 84 insertions(+), 13 deletions(-) diff --git a/src/lib/terrain/async-patch.ts b/src/lib/terrain/async-patch.ts index 5937dfae..8e1e5855 100644 --- a/src/lib/terrain/async-patch.ts +++ b/src/lib/terrain/async-patch.ts @@ -6,22 +6,29 @@ class AsyncPatch { readonly state: 'pending'; readonly promise: Promise; visible: boolean; - deleted: boolean; + disposed: boolean; } | { readonly state: 'ready'; readonly patch: Patch | null; - deleted: boolean; + disposed: boolean; }; - public constructor(container: THREE.Object3D, promise: Promise) { + public readonly id: string; + public readonly boundingBox: THREE.Box3; + private invisibilityTimestamp = performance.now(); + + public constructor(container: THREE.Object3D, promise: Promise, id: string, boundingBox: THREE.Box3) { this.data = { state: 'pending', promise, visible: false, - deleted: false, + disposed: false, }; + this.id = id; + this.boundingBox = boundingBox; + promise.then((patch: Patch | null) => { if (this.data.state !== 'pending') { throw new Error(); @@ -35,8 +42,12 @@ class AsyncPatch { this.data = { state: 'ready', patch, - deleted: this.data.deleted, + disposed: this.data.disposed, }; + if (this.data.disposed) { + // disposal has been asked before the computation ended + this.patch?.dispose(); + } }); } @@ -50,6 +61,14 @@ class AsyncPatch { } public set visible(value: boolean) { + if (this.visible === value) { + return; // nothing to do + } + + if (!value) { + this.invisibilityTimestamp = performance.now(); + } + if (this.data.state === 'pending') { this.data.visible = value; } else if (this.data.patch) { @@ -64,9 +83,13 @@ class AsyncPatch { return null; } + public get invisibleSince(): number { + return this.invisibilityTimestamp; + } + public async dispose(): Promise { - if (!this.data.deleted) { - this.data.deleted = true; + if (!this.data.disposed) { + this.data.disposed = true; this.patch?.dispose(); } } diff --git a/src/lib/terrain/terrain.ts b/src/lib/terrain/terrain.ts index c96b12cc..6bc5249a 100644 --- a/src/lib/terrain/terrain.ts +++ b/src/lib/terrain/terrain.ts @@ -43,11 +43,13 @@ class Terrain { }, }; + private maxPatchesInCache = 200; + private readonly map: IVoxelMap; private readonly patchFactory: PatchFactoryBase; private readonly patchSize: THREE.Vector3; - private patches: Record = {}; + private readonly patches: Record = {}; /** * @@ -132,10 +134,14 @@ class Terrain { } } + this.garbageCollectPatches(); + await Promise.all(promises); } - /** Call this method before rendering. */ + /** + * Call this method before rendering. + * */ public updateUniforms(): void { for (const asyncPatch of Object.values(this.patches)) { const patch = asyncPatch.patch; @@ -163,11 +169,10 @@ class Terrain { * It will be recomputed if needed again. */ public clear(): void { - for (const patch of Object.values(this.patches)) { - patch.dispose(); + for (const patchId of Object.keys(this.patches)) { + this.disposePatch(patchId); } this.container.clear(); - this.patches = {}; } /** @@ -178,6 +183,48 @@ class Terrain { this.patchFactory.dispose(); } + /** + * Gets the maximum size of the GPU LRU cache of invisible patches. + */ + public get patchesCacheSize(): number { + return this.maxPatchesInCache; + } + + /** + * Sets the maximum size of the GPU LRU cache of invisible patches. + */ + public set patchesCacheSize(value: number) { + if (value <= 0) { + throw new Error(`Invalid patches cache size "${value}".`); + } + this.maxPatchesInCache = value; + this.garbageCollectPatches(); + } + + private garbageCollectPatches(): void { + const patches = Object.entries(this.patches); + const invisiblePatches = patches.filter(([, patch]) => !patch.visible); + invisiblePatches.sort(([, patch1], [, patch2]) => patch1.invisibleSince - patch2.invisibleSince); + + while (invisiblePatches.length > this.maxPatchesInCache) { + const nextPatchToDelete = invisiblePatches.shift(); + if (!nextPatchToDelete) { + break; + } + this.disposePatch(nextPatchToDelete[0]); + } + } + + private disposePatch(patchId: string): void { + const patch = this.patches[patchId]; + if (patch) { + patch.dispose(); + delete this.patches[patchId]; + } else { + logger.warn(`Patch ${patchId} does not exist.`); + } + } + private getPatch(patchStart: THREE.Vector3): AsyncPatch { const patchId = this.computePatchId(patchStart); @@ -185,8 +232,9 @@ class Terrain { if (typeof patch === 'undefined') { const patchEnd = new THREE.Vector3().addVectors(patchStart, this.patchSize); + const boundingBox = new THREE.Box3(patchStart.clone(), patchEnd.clone()); const promise = this.patchFactory.buildPatch(patchStart, patchEnd); - patch = new AsyncPatch(this.container, promise); + patch = new AsyncPatch(this.container, promise, patchId, boundingBox); this.patches[patchId] = patch; } From 8f61367107900179860cfca38dc7b773f64c8bd4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Piellard?= Date: Mon, 25 Mar 2024 23:18:11 +0100 Subject: [PATCH 10/12] fix: change console logs style to match dapp's --- src/lib/helpers/logger.ts | 59 +++++++++++++++++++++++---------------- src/lib/index.ts | 2 +- 2 files changed, 36 insertions(+), 25 deletions(-) diff --git a/src/lib/helpers/logger.ts b/src/lib/helpers/logger.ts index 88988753..f6bb7abd 100644 --- a/src/lib/helpers/logger.ts +++ b/src/lib/helpers/logger.ts @@ -1,42 +1,53 @@ -enum EVerbosity { +enum ELogLevel { WARN = 0, INFO, DEBUG, DIAGNOSTIC, } +type LevelStyle = { + readonly method: 'error' | 'warn' | 'log' | 'debug'; + readonly colors: { + readonly header: string; + readonly message: string; + }; +}; + class Logger { - private static readonly prefix = 'aresrpg-engine: '; - public verbosity = EVerbosity.INFO; + // eslint-disable-next-line no-useless-constructor + public constructor( + private readonly prefix: string, + private readonly logStyle: Record + ) { } - public warn(message: string): void { - if (this.verbosity >= EVerbosity.WARN) { - console.warn(Logger.prefix + message); - } - } + public verbosity = ELogLevel.INFO; - public info(message: string): void { - if (this.verbosity >= EVerbosity.INFO) { - console.info(Logger.prefix + message); - } - } + public readonly warn = this.log.bind(this, ELogLevel.WARN); + public readonly info = this.log.bind(this, ELogLevel.INFO); + public readonly debug = this.log.bind(this, ELogLevel.DEBUG); + public readonly diagnostic = this.log.bind(this, ELogLevel.DIAGNOSTIC); - public debug(message: string): void { - if (this.verbosity >= EVerbosity.DEBUG) { - console.debug(Logger.prefix + message); - } - } + private log(level: ELogLevel, message: string): void { + if (this.verbosity >= level) { + const logStyle = this.logStyle[level]; - public diagnostic(message: string): void { - if (this.verbosity >= EVerbosity.DIAGNOSTIC) { - console.debug(Logger.prefix + message); + console[logStyle.method]( + `%c${this.prefix}%c ${message}`, + `background: ${logStyle.colors.header}; color: white; padding: 2px 4px; border-radius: 2px`, + `font-weight: 800; color: ${logStyle.colors.message}` + ); } } } -const logger = new Logger(); -function setVerbosity(verbosity: EVerbosity): void { +const logger = new Logger('aresrpg-engine', [ + { method: 'warn', colors: { header: '#7B9E7B', message: '#FF6A00' } }, + { method: 'log', colors: { header: '#7B9E7B', message: '#0094FF' } }, + { method: 'debug', colors: { header: '#7B9E7B', message: '#808080' } }, + { method: 'debug', colors: { header: '#7B9E7B', message: '#A56148' } }, +]); +function setVerbosity(verbosity: ELogLevel): void { logger.verbosity = verbosity; } -export { EVerbosity, logger, setVerbosity }; +export { ELogLevel, logger, setVerbosity }; diff --git a/src/lib/index.ts b/src/lib/index.ts index 871140e2..2d6ce4bd 100644 --- a/src/lib/index.ts +++ b/src/lib/index.ts @@ -1,3 +1,3 @@ -export { EVerbosity, setVerbosity } from './helpers/logger'; +export { ELogLevel, setVerbosity } from './helpers/logger'; export type { IVoxel, IVoxelMap, IVoxelMaterial } from './terrain/i-voxel-map'; export { EPatchComputingMode, Terrain } from './terrain/terrain'; From 4c9ac6892c02c6f36d3dabd2e303deb74fa0210a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Piellard?= Date: Mon, 25 Mar 2024 23:18:32 +0100 Subject: [PATCH 11/12] fix: change default verbosity to only show warnings --- src/lib/helpers/logger.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/helpers/logger.ts b/src/lib/helpers/logger.ts index f6bb7abd..3f780b2c 100644 --- a/src/lib/helpers/logger.ts +++ b/src/lib/helpers/logger.ts @@ -20,7 +20,7 @@ class Logger { private readonly logStyle: Record ) { } - public verbosity = ELogLevel.INFO; + public verbosity = ELogLevel.WARN; public readonly warn = this.log.bind(this, ELogLevel.WARN); public readonly info = this.log.bind(this, ELogLevel.INFO); From c434da271550dcb2272cb6224e9a7e8c9a10453b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Piellard?= Date: Mon, 25 Mar 2024 23:21:01 +0100 Subject: [PATCH 12/12] style: prettier --- src/lib/helpers/logger.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/helpers/logger.ts b/src/lib/helpers/logger.ts index 3f780b2c..1b246694 100644 --- a/src/lib/helpers/logger.ts +++ b/src/lib/helpers/logger.ts @@ -18,7 +18,7 @@ class Logger { public constructor( private readonly prefix: string, private readonly logStyle: Record - ) { } + ) {} public verbosity = ELogLevel.WARN;