Skip to content

Commit

Permalink
wip
Browse files Browse the repository at this point in the history
  • Loading branch information
ibgreen committed Sep 1, 2024
1 parent 975a162 commit a447955
Show file tree
Hide file tree
Showing 13 changed files with 138 additions and 29 deletions.
10 changes: 8 additions & 2 deletions modules/core/src/adapter/device.ts
Original file line number Diff line number Diff line change
Expand Up @@ -233,8 +233,12 @@ export type DeviceProps = {
_disabledFeatures?: Partial<Record<DeviceFeature, boolean>>;
/** WebGL specific - Initialize all features on startup */
_initializeFeatures?: boolean;
/** Enable shader caching (via ShaderFactory) */
_cacheShaders?: boolean;
/** Enable shader caching (via PipelineFactory) */
_cachePipelines?: boolean;
/** Never destroy cached shaders and pipelines */
_factoryDestroyPolicy?: 'unused' | 'never';
_cacheDestroyPolicy?: 'unused' | 'never';
/** Resource default overrides */
_resourceDefaults?: {
texture?: Partial<TextureProps>;
Expand Down Expand Up @@ -283,7 +287,9 @@ export abstract class Device {
onError: (error: Error) => log.error(error.message)(),

_requestMaxLimits: true,
_factoryDestroyPolicy: 'unused',
_cacheShaders: false,
_cachePipelines: false,
_cacheDestroyPolicy: 'unused',
// TODO - Change these after confirming things work as expected
_initializeFeatures: true,
_disabledFeatures: {
Expand Down
17 changes: 16 additions & 1 deletion modules/engine/src/factories/pipeline-factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export class PipelineFactory {
}

readonly device: Device;
readonly cachingEnabled: boolean;
readonly destroyPolicy: 'unused' | 'never';
readonly debug: boolean;

Expand All @@ -43,12 +44,17 @@ export class PipelineFactory {

constructor(device: Device) {
this.device = device;
this.destroyPolicy = device.props._factoryDestroyPolicy;
this.cachingEnabled = device.props._cachePipelines;
this.destroyPolicy = device.props._cacheDestroyPolicy;
this.debug = device.props.debugFactories;
}

/** Return a RenderPipeline matching supplied props. Reuses an equivalent pipeline if already created. */
createRenderPipeline(props: RenderPipelineProps): RenderPipeline {
if (!this.cachingEnabled) {
return this.device.createRenderPipeline(props);
}

const allProps: Required<RenderPipelineProps> = {...RenderPipeline.defaultProps, ...props};

const cache = this._renderPipelineCache;
Expand Down Expand Up @@ -79,6 +85,10 @@ export class PipelineFactory {

/** Return a ComputePipeline matching supplied props. Reuses an equivalent pipeline if already created. */
createComputePipeline(props: ComputePipelineProps): ComputePipeline {
if (!this.cachingEnabled) {
return this.device.createComputePipeline(props);
}

const allProps: Required<ComputePipelineProps> = {...ComputePipeline.defaultProps, ...props};

const cache = this._computePipelineCache;
Expand Down Expand Up @@ -108,6 +118,11 @@ export class PipelineFactory {
}

release(pipeline: RenderPipeline | ComputePipeline): void {
if (!this.cachingEnabled) {
pipeline.destroy();
return;
}

const cache = this._getCache(pipeline);
const hash = pipeline.hash;

Expand Down
46 changes: 41 additions & 5 deletions modules/engine/src/factories/shader-factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// SPDX-License-Identifier: MIT
// Copyright (c) vis.gl contributors

import {Device, Shader, ShaderProps} from '@luma.gl/core';
import {Device, Shader, ShaderProps, log} from '@luma.gl/core';

/** Manages a cached pool of Shaders for reuse. */
export class ShaderFactory {
Expand All @@ -15,17 +15,34 @@ export class ShaderFactory {
}

public readonly device: Device;
readonly cachingEnabled: boolean;
readonly destroyPolicy: 'unused' | 'never';
readonly debug: boolean;

private readonly _cache: Record<string, {shader: Shader; useCount: number}> = {};

get [Symbol.toStringTag](): string {
return 'ShaderFactory';
}

toString(): string {
return `${this[Symbol.toStringTag]}(${this.device.id})`;
}

/** @internal */
constructor(device: Device) {
this.device = device;
this.destroyPolicy = device.props._factoryDestroyPolicy;
this.cachingEnabled = device.props._cacheShaders;
this.destroyPolicy = device.props._cacheDestroyPolicy;
this.debug = true; // device.props.debugFactories;
}

/** Requests a {@link Shader} from the cache, creating a new Shader only if necessary. */
createShader(props: ShaderProps): Shader {
if (!this.cachingEnabled) {
return this.device.createShader(props);
}

const key = this._hashShader(props);

let cacheEntry = this._cache[key];
Expand All @@ -34,15 +51,27 @@ export class ShaderFactory {
...props,
id: props.id ? `${props.id}-cached` : undefined
});
this._cache[key] = cacheEntry = {shader, useCount: 0};
this._cache[key] = cacheEntry = {shader, useCount: 1};
if (this.debug) {
log.warn(`${this}: Created new shader ${shader.id}`)();
}
} else {
cacheEntry.useCount++;
if (this.debug) {
log.warn(`${this}: Reusing shader ${cacheEntry.shader.id} count=${cacheEntry.useCount}`)();
}
}

cacheEntry.useCount++;
return cacheEntry.shader;
}

/** Releases a previously-requested {@link Shader}, destroying it if no users remain. */
release(shader: Shader): void {
if (!this.cachingEnabled) {
shader.destroy();
return;
}

const key = this._hashShader(shader);
const cacheEntry = this._cache[key];
if (cacheEntry) {
Expand All @@ -51,14 +80,21 @@ export class ShaderFactory {
if (this.destroyPolicy === 'unused') {
delete this._cache[key];
cacheEntry.shader.destroy();
if (this.debug) {
log.warn(`${this}: Releasing shader ${shader.id}, destroyed`)();
}
}
} else if (cacheEntry.useCount < 0) {
throw new Error(`ShaderFactory: Shader ${shader.id} released too many times`);
} else if (this.debug) {
log.warn(`${this}: Releasing shader ${shader.id} count=${cacheEntry.useCount}`)();
}
}
}

// PRIVATE

private _hashShader(value: Shader | ShaderProps): string {
protected _hashShader(value: Shader | ShaderProps): string {
return `${value.stage}:${value.source}`;
}
}
4 changes: 3 additions & 1 deletion modules/engine/src/model/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -352,11 +352,13 @@ export class Model {

destroy(): void {
if (!this._destroyed) {
// Release pipeline before we destroy the shaders used by the pipeline
this.pipelineFactory.release(this.pipeline);
// Release the shaders
this.shaderFactory.release(this.pipeline.vs);
if (this.pipeline.fs) {
this.shaderFactory.release(this.pipeline.fs);
}
this.pipelineFactory.release(this.pipeline);
this._uniformStore.destroy();
// TODO - mark resource as managed and destroyIfManaged() ?
this._gpuGeometry?.destroy();
Expand Down
10 changes: 10 additions & 0 deletions modules/engine/test/lib/model.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,11 @@ test('Model#topology', async t => {

test('Model#pipeline caching', async t => {
const webglDevice = await getWebGLTestDevice();
if (!webglDevice.props._cachePipelines) {
t.comment('Pipeline caching is disabled');
t.end();
return;
}

const pipelineFactory = new PipelineFactory(webglDevice);
const shaderFactory = new ShaderFactory(webglDevice);
Expand Down Expand Up @@ -289,6 +294,11 @@ test('Model#pipeline caching', async t => {

test('Model#pipeline caching with defines and modules', async t => {
const webglDevice = await getWebGLTestDevice();
if (!webglDevice.props._cachePipelines) {
t.comment('Pipeline caching is disabled');
t.end();
return;
}

const pipelineFactory = PipelineFactory.getDefaultPipelineFactory(webglDevice);
const shaderFactory = ShaderFactory.getDefaultShaderFactory(webglDevice);
Expand Down
5 changes: 5 additions & 0 deletions modules/engine/test/lib/pipeline-factory.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,11 @@ test('PipelineFactory#getDefaultPipelineFactory', async t => {

test('PipelineFactory#release', async t => {
const webglDevice = await getWebGLTestDevice();
if (!webglDevice.props._cachePipelines) {
t.comment('Pipeline caching not enabled');
t.end();
return;
}

const pipelineFactory = new PipelineFactory(webglDevice);

Expand Down
10 changes: 10 additions & 0 deletions modules/engine/test/lib/shader-factory.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,11 @@ test('ShaderFactory#getDefaultShaderFactory', async t => {

test('ShaderFactory#createShader', async t => {
const webglDevice = await getWebGLTestDevice();
if (!webglDevice.props._cacheShaders) {
t.comment('Shader caching not enabled');
t.end();
return;
}

const factory = ShaderFactory.getDefaultShaderFactory(webglDevice);
const shader1 = factory.createShader({id: '1', stage: 'vertex', source: vs1});
Expand All @@ -62,6 +67,11 @@ test('ShaderFactory#createShader', async t => {

test('ShaderFactory#release', async t => {
const webglDevice = await getWebGLTestDevice();
if (!webglDevice.props._cacheShaders) {
t.comment('Shader caching not enabled');
t.end();
return;
}

const factory = new ShaderFactory(webglDevice);
const shader1 = factory.createShader({id: '1', stage: 'vertex', source: vs1});
Expand Down
4 changes: 3 additions & 1 deletion modules/webgl/src/adapter/resources/webgl-buffer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,9 @@ export class WEBGLBuffer extends Buffer {

const handle = typeof props === 'object' ? props.handle : undefined;
this.handle = handle || this.gl.createBuffer();
device.setSpectorMetadata(this.handle, {...this.props, data: typeof this.props.data});
device._setWebGLDebugMetadata(this.handle, this, {
spector: {...this.props, data: typeof this.props.data}
});

// - In WebGL1, need to make sure we use GL.ELEMENT_ARRAY_BUFFER when initializing element buffers
// otherwise buffer type will lock to generic (non-element) buffer
Expand Down
4 changes: 2 additions & 2 deletions modules/webgl/src/adapter/resources/webgl-framebuffer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,8 @@ export class WEBGLFramebuffer extends Framebuffer {
this.props.handle || isDefaultFramebuffer ? this.props.handle : this.gl.createFramebuffer();

if (!isDefaultFramebuffer) {
// default framebuffer handle is null, so we can't set spector metadata...
device.setSpectorMetadata(this.handle, {id: this.props.id, props: this.props});
// default framebuffer handle is null, so we can't set debug metadata...
device._setWebGLDebugMetadata(this.handle, this, {spector: this.props});

// Auto create textures for attachments if needed
this.autoCreateAttachmentTextures();
Expand Down
9 changes: 5 additions & 4 deletions modules/webgl/src/adapter/resources/webgl-render-pipeline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,9 +65,7 @@ export class WEBGLRenderPipeline extends RenderPipeline {
super(device, props);
this.device = device;
this.handle = this.props.handle || this.device.gl.createProgram();
this.device.setSpectorMetadata(this.handle, {id: this.props.id});
// @ts-expect-error
this.handle.luma = this;
this.device._setWebGLDebugMetadata(this.handle, this, {spector: {id: this.props.id}});

// Create shaders if needed
this.vs = props.vs as WEBGLShader;
Expand Down Expand Up @@ -95,9 +93,12 @@ export class WEBGLRenderPipeline extends RenderPipeline {
override destroy(): void {
if (this.handle) {
// log.error(`Deleting program ${this.id}`)();
this.device.gl.useProgram(null);
this.device.gl.deleteProgram(this.handle);
this.handle = null;
this.destroyed = true;
// @ts-expect-error
this.handle.destroyed = true;
this.handle = null;
}
}

Expand Down
8 changes: 7 additions & 1 deletion modules/webgl/src/adapter/resources/webgl-shader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,15 +27,21 @@ export class WEBGLShader extends Shader {
default:
throw new Error(this.props.stage);
}

// default framebuffer handle is null, so we can't set spector metadata...
device._setWebGLDebugMetadata(this.handle, this, {spector: this.props});

this._compile(this.source);
}

override destroy(): void {
if (this.handle) {
this.removeStats();
this.device.gl.deleteShader(this.handle);
// this.handle = null;
this.destroyed = true;
// @ts-expect-error
this.handle.destroyed = true;
// this.handle = null;
}
}

Expand Down
7 changes: 6 additions & 1 deletion modules/webgl/src/adapter/resources/webgl-texture.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,12 @@ export class WEBGLTexture extends Texture {
// eslint-disable-next-line max-statements
_initialize(propsWithData: TextureProps): void {
this.handle = this.props.handle || this.gl.createTexture();
this.device.setSpectorMetadata(this.handle, {...this.props, data: propsWithData.data});
this.device._setWebGLDebugMetadata(this.handle, this, {
spector: {
...this.props,
data: propsWithData.data
}
});

let {width, height} = propsWithData;

Expand Down
33 changes: 22 additions & 11 deletions modules/webgl/src/adapter/webgl-device.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ import type {
// CommandEncoder,
CommandEncoderProps,
TransformFeedbackProps,
QuerySetProps
QuerySetProps,
Resource
} from '@luma.gl/core';
import {Device, CanvasContext, log} from '@luma.gl/core';
import type {GLExtensions} from '@luma.gl/constants';
Expand Down Expand Up @@ -411,16 +412,6 @@ export class WebGLDevice extends Device {
webglState.pop();
}

/**
* Storing data on a special field on WebGLObjects makes that data visible in SPECTOR chrome debug extension
* luma.gl ids and props can be inspected
*/
setSpectorMetadata(handle: unknown, props: Record<string, unknown>) {
// @ts-expect-error
// eslint-disable-next-line camelcase
handle.__SPECTOR_Metadata = props;
}

/**
* Returns the GL.<KEY> constant that corresponds to a numeric value of a GL constant
* Be aware that there are some duplicates especially for constants that are 0,
Expand Down Expand Up @@ -491,6 +482,26 @@ export class WebGLDevice extends Device {
getWebGLExtension(this.gl, name, this._extensions);
return this._extensions;
}

// INTERNAL SUPPORT METHODS FOR WEBGL RESOURCES

/**
* Storing data on a special field on WebGLObjects makes that data visible in SPECTOR chrome debug extension
* luma.gl ids and props can be inspected
*/
_setWebGLDebugMetadata(
handle: unknown,
resource: Resource<any>,
options: {spector: Record<string, unknown>}
): void {
// @ts-expect-error
handle.luma = resource;

const spectorMetadata = {props: options.spector, id: options.spector.id};
// @ts-expect-error
// eslint-disable-next-line camelcase
handle.__SPECTOR_Metadata = spectorMetadata;
}
}

/** Set constant float array attribute */
Expand Down

0 comments on commit a447955

Please sign in to comment.