Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/optimizations #10

Merged
merged 12 commits into from
Mar 27, 2024
53 changes: 53 additions & 0 deletions src/lib/helpers/logger.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
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 {
Sceat marked this conversation as resolved.
Show resolved Hide resolved
// eslint-disable-next-line no-useless-constructor
public constructor(
private readonly prefix: string,
private readonly logStyle: Record<ELogLevel, LevelStyle>
) {}

public verbosity = ELogLevel.WARN;

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);

private log(level: ELogLevel, message: string): void {
if (this.verbosity >= level) {
const logStyle = this.logStyle[level];

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('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 { ELogLevel, logger, setVerbosity };
4 changes: 3 additions & 1 deletion src/lib/helpers/webgpu/webgpu-device.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
/// <reference types="@webgpu/types" />

import { logger } from '../logger';

let devicePromise: Promise<GPUDevice> | null = null;

async function getGpuDevice(): Promise<GPUDevice> {
Expand All @@ -18,7 +20,7 @@ async function getGpuDevice(): Promise<GPUDevice> {
}

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();
Expand Down
1 change: 1 addition & 0 deletions src/lib/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export { ELogLevel, setVerbosity } from './helpers/logger';
export type { IVoxel, IVoxelMap, IVoxelMaterial } from './terrain/i-voxel-map';
export { EPatchComputingMode, Terrain } from './terrain/terrain';
37 changes: 30 additions & 7 deletions src/lib/terrain/async-patch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,29 @@ class AsyncPatch {
readonly state: 'pending';
readonly promise: Promise<Patch | null>;
visible: boolean;
deleted: boolean;
disposed: boolean;
}
| {
readonly state: 'ready';
readonly patch: Patch | null;
deleted: boolean;
disposed: boolean;
};

public constructor(container: THREE.Object3D, promise: Promise<Patch | null>) {
public readonly id: string;
public readonly boundingBox: THREE.Box3;
private invisibilityTimestamp = performance.now();

public constructor(container: THREE.Object3D, promise: Promise<Patch | null>, 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();
Expand All @@ -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();
}
});
}

Expand All @@ -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) {
Expand All @@ -64,9 +83,13 @@ class AsyncPatch {
return null;
}

public get invisibleSince(): number {
return this.invisibilityTimestamp;
}

public async dispose(): Promise<void> {
if (!this.data.deleted) {
this.data.deleted = true;
if (!this.data.disposed) {
this.data.disposed = true;
this.patch?.dispose();
}
}
Expand Down
10 changes: 8 additions & 2 deletions src/lib/terrain/patch/patch-factory/patch-factory-base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};

Expand Down Expand Up @@ -99,8 +100,6 @@ abstract class PatchFactoryBase {
boundingBox.getBoundingSphere(boundingSphere);

return new Patch(
patchStart,
patchSize,
geometryAndMaterialsList.map(geometryAndMaterial => {
const { geometry } = geometryAndMaterial;
geometry.boundingBox = boundingBox.clone();
Expand Down Expand Up @@ -206,6 +205,10 @@ abstract class PatchFactoryBase {
}

private *iterateOnVisibleFacesWithCache(localMapCache: LocalMapCache): Generator<FaceData> {
if (localMapCache.isEmpty) {
return;
}

let cacheIndex = 0;
const localPosition = new THREE.Vector3();
for (localPosition.z = 0; localPosition.z < localMapCache.size.z; localPosition.z++) {
Expand Down Expand Up @@ -309,15 +312,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,
};
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -16,6 +17,7 @@ type ComputationOutputs = Record<Cube.FaceType, Uint32Array>;

class PatchComputerGpu {
public static async create(localCacheSize: THREE.Vector3, vertexDataEncoder: VertexDataEncoder): Promise<PatchComputerGpu> {
logger.debug('Requesting WebGPU device...');
const device = await getGpuDevice();
return new PatchComputerGpu(device, localCacheSize, vertexDataEncoder);
}
Expand Down Expand Up @@ -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({
Expand Down Expand Up @@ -280,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();
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,14 +30,14 @@ class PatchFactoryGpuOptimized extends PatchFactoryGpu {

return new Promise<GeometryAndMaterial[]>(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,
Expand All @@ -58,17 +58,24 @@ class PatchFactoryGpuOptimized extends PatchFactoryGpu {
if (!currentJob.gpuTaskPromise) {
const localMapCache = currentJob.cpuTaskOutput;

currentJob.gpuTaskPromise = (async () => {
// console.log(`GPU ${currentJob.patchId} start`);
const patchComputerGpu = await this.getPatchComputerGpu();
const gpuTaskOutput = await patchComputerGpu.computeBuffers(localMapCache);
// console.log(`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];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
7 changes: 1 addition & 6 deletions src/lib/terrain/patch/patch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,16 +31,11 @@ class Patch {
},
};

public readonly patchStart: THREE.Vector3;
public readonly patchSize: THREE.Vector3;

private gpuResources: {
readonly patchMeshes: ReadonlyArray<PatchMesh>;
} | 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();
Expand Down
Loading