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

Update to WebGL shader compilation and linking #4964

Merged
merged 3 commits into from
Jan 16, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/platform/graphics/device-cache.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ class DeviceCache {
* @param {import('./graphics-device.js').GraphicsDevice} device - The graphics device.
*/
remove(device) {
this._cache.get(device)?.destroy(device);
this._cache.get(device)?.destroy?.(device);
this._cache.delete(device);
}
}
Expand Down
11 changes: 10 additions & 1 deletion src/platform/graphics/webgl/webgl-graphics-device.js
Original file line number Diff line number Diff line change
Expand Up @@ -1155,6 +1155,15 @@ class WebglGraphicsDevice extends GraphicsDevice {
}
}

/**
* Called after a batch of shaders was created, to guide in their optimal preparation for rendering.
*
* @ignore
*/
endShaderBatch() {
WebglShader.endShaderBatch(this);
}

/**
* Set the active rectangle for rendering on the specified device.
*
Expand Down Expand Up @@ -2714,7 +2723,7 @@ class WebglGraphicsDevice extends GraphicsDevice {
if (shader !== this.shader) {
if (shader.failed) {
return false;
} else if (!shader.ready && !shader.impl.postLink(this, shader)) {
} else if (!shader.ready && !shader.impl.finalize(this, shader)) {
shader.failed = true;
return false;
}
Expand Down
98 changes: 74 additions & 24 deletions src/platform/graphics/webgl/webgl-shader.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,18 @@ class CompiledShaderCache {
}
}

// class used to hold a list of recently created shaders forming a batch, to allow their more optimized compilation
class ShaderBatchCache {
shaders = [];

loseContext(device) {
this.shaders = [];
}
}

const _vertexShaderCache = new DeviceCache();
const _fragmentShaderCache = new DeviceCache();
const _shaderBatchCache = new DeviceCache();

/**
* A WebGL implementation of the Shader.
Expand All @@ -47,7 +57,15 @@ class WebglShader {

constructor(shader) {
this.init();
this.compileAndLink(shader.device, shader);

// kick off vertex and fragment shader compilation, but not linking here, as that would
// make it blocking.
this.compile(shader.device, shader);

// add the shader to recently created list
WebglShader.getBatchShaders(shader.device).push(shader);

// add it to a device list of all shaders
shader.device.shaders.push(shader);
}

Expand All @@ -73,6 +91,22 @@ class WebglShader {
this.glFragmentShader = null;
}

static getBatchShaders(device) {
const batchCache = _shaderBatchCache.get(device, () => {
return new ShaderBatchCache();
});
return batchCache.shaders;
}

static endShaderBatch(device) {

// Trigger link step for all recently created shaders. This allows linking to be done in parallel, before
// the blocking wait on the linking result is triggered in finalize function
const shaders = WebglShader.getBatchShaders(device);
shaders.forEach(shader => shader.impl.link(device, shader));
shaders.length = 0;
}

/**
* Dispose the shader when the context has been lost.
*/
Expand All @@ -87,33 +121,48 @@ class WebglShader {
* @param {import('../shader.js').Shader} shader - The shader to restore.
*/
restoreContext(device, shader) {
this.compileAndLink(device, shader);
this.compile(device, shader);
}

/**
* Compile and link a shader program.
* Compile shader programs.
*
* @param {import('./webgl-graphics-device.js').WebglGraphicsDevice} device - The graphics device.
* @param {import('../shader.js').Shader} shader - The shader to compile.
*/
compileAndLink(device, shader) {
compile(device, shader) {

const definition = shader.definition;
this.glVertexShader = this._compileShaderSource(device, definition.vshader, true);
this.glFragmentShader = this._compileShaderSource(device, definition.fshader, false);
}

/**
* Link shader programs. This is called at a later stage, to allow many shaders to compile in parallel.
*
* @param {import('./webgl-graphics-device.js').WebglGraphicsDevice} device - The graphics device.
* @param {import('../shader.js').Shader} shader - The shader to compile.
*/
link(device, shader) {

// if the shader was already linked
if (this.glProgram)
return;

let startTime = 0;
Debug.call(() => {
this.compileDuration = 0;
startTime = now();
});

const definition = shader.definition;
const glVertexShader = this._compileShaderSource(device, definition.vshader, true);
const glFragmentShader = this._compileShaderSource(device, definition.fshader, false);

const gl = device.gl;
const glProgram = gl.createProgram();
this.glProgram = glProgram;

gl.attachShader(glProgram, glVertexShader);
gl.attachShader(glProgram, glFragmentShader);
gl.attachShader(glProgram, this.glVertexShader);
gl.attachShader(glProgram, this.glFragmentShader);

const definition = shader.definition;
const attrs = definition.attributes;
if (device.webgl2 && definition.useTransformFeedback) {
// Collect all "out_" attributes and use them for output
Expand Down Expand Up @@ -141,11 +190,6 @@ class WebglShader {

gl.linkProgram(glProgram);

// Cache the WebGL objects on the shader
this.glVertexShader = glVertexShader;
this.glFragmentShader = glFragmentShader;
this.glProgram = glProgram;

Debug.call(() => {
this.compileDuration = now() - startTime;
});
Expand Down Expand Up @@ -216,15 +260,19 @@ class WebglShader {
}

/**
* Extract attribute and uniform information from a successfully linked shader.
* Link the shader, and extract its attributes and uniform information.
*
* @param {import('./webgl-graphics-device.js').WebglGraphicsDevice} device - The graphics device.
* @param {import('../shader.js').Shader} shader - The shader to query.
* @returns {boolean} True if the shader was successfully queried and false otherwise.
*/
postLink(device, shader) {
const gl = device.gl;
finalize(device, shader) {

// if the program wasn't linked yet (shader was not created in batch)
if (!this.glProgram)
this.link(device, shader);

const gl = device.gl;
const glProgram = this.glProgram;
const definition = shader.definition;

Expand All @@ -236,19 +284,21 @@ class WebglShader {
});
// #endif

// this is the main thead blocking part of the shader compilation, time it
let linkStartTime = 0;
Debug.call(() => {
linkStartTime = now();
});

// Check for compilation errors
if (!this._isCompiled(device, shader, this.glVertexShader, definition.vshader, "vertex"))
return false;
const linkStatus = gl.getProgramParameter(glProgram, gl.LINK_STATUS);
if (!linkStatus) {

if (!this._isCompiled(device, shader, this.glFragmentShader, definition.fshader, "fragment"))
return false;
// Check for compilation errors
if (!this._isCompiled(device, shader, this.glVertexShader, definition.vshader, "vertex"))
return false;

if (!gl.getProgramParameter(glProgram, gl.LINK_STATUS)) {
if (!this._isCompiled(device, shader, this.glFragmentShader, definition.fshader, "fragment"))
return false;

const message = "Failed to link shader program. Error: " + gl.getProgramInfoLog(glProgram);

Expand Down
8 changes: 8 additions & 0 deletions src/scene/renderer/forward-renderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -513,6 +513,9 @@ class ForwardRenderer extends Renderer {

if (!drawCall._shader[pass] || drawCall._shaderDefs !== objDefs || drawCall._lightHash !== lightHash) {

// marker to allow us to see the source node for shader alloc
DebugGraphics.pushGpuMarker(device, drawCall.node.name);

// draw calls not using static lights use variants cache on material to quickly find the shader, as they are all
// the same for the same pass, using all lights of the scene
if (!drawCall.isStatic) {
Expand All @@ -529,6 +532,8 @@ class ForwardRenderer extends Renderer {
drawCall.updatePassShader(scene, pass, drawCall._staticLightList, sortedLights, this.viewUniformFormat, this.viewBindGroupFormat);
}
drawCall._lightHash = lightHash;

DebugGraphics.popGpuMarker(device);
}

Debug.assert(drawCall._shader[pass], "no shader for pass", material);
Expand All @@ -542,6 +547,9 @@ class ForwardRenderer extends Renderer {
}
}

// process any catch of shaders created here
device.endShaderBatch();

return _drawCallList;
}

Expand Down
3 changes: 3 additions & 0 deletions src/scene/renderer/shadow-renderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,9 @@ class ShadowRenderer {
// Sort shadow casters
const shadowPass = ShaderPass.getShadow(light._type, light._shadowType);

// TODO: Similarly to forward renderer, a shader creation part of this loop should be split into a separate loop,
// and endShaderBatch should be called at its end

// Render
const count = visibleCasters.length;
for (let i = 0; i < count; i++) {
Expand Down