Skip to content

Commit

Permalink
WebGPU renderer handles canvas size change (#5148)
Browse files Browse the repository at this point in the history
* WebGPU renderer handles canvas size change

* making wgpu private again

* EVENT_SIZE static

---------

Co-authored-by: Martin Valigursky <mvaligursky@snapchat.com>
  • Loading branch information
mvaligursky and Martin Valigursky committed Mar 14, 2023
1 parent e85ca41 commit fefc153
Show file tree
Hide file tree
Showing 11 changed files with 206 additions and 88 deletions.
5 changes: 4 additions & 1 deletion rollup.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -260,7 +260,6 @@ const stripFunctions = [
'Debug.warnOnce',
'Debug.error',
'Debug.errorOnce',
'Debug.gpuError',
'Debug.log',
'Debug.logOnce',
'Debug.trace',
Expand All @@ -270,6 +269,10 @@ const stripFunctions = [
'DebugGraphics.clearGpuMarkers',
'DebugGraphics.pushGpuMarker',
'DebugGraphics.popGpuMarker',
'WebgpuDebug.validate',
'WebgpuDebug.memory',
'WebgpuDebug.internal',
'WebgpuDebug.end',
'WorldClustersDebug.render'
];

Expand Down
13 changes: 0 additions & 13 deletions src/core/debug.js
Original file line number Diff line number Diff line change
Expand Up @@ -123,19 +123,6 @@ class Debug {
}
}

/**
* Error in validation of GPU commands, logged no more than once.
*
* @param {...*} args - The values to be written to the log. Uniqueness of the first parameter
* is used to determine if the message was already logged out.
*/
static gpuError(...args) {
if (!Debug._loggedMessages.has(args[0])) {
Debug._loggedMessages.add(args[0]);
console.error(`GPU VALIDATION ERROR: `, ...args);
}
}

/**
* Trace message, which is logged to the console if the tracing for the channel is enabled
*
Expand Down
7 changes: 7 additions & 0 deletions src/framework/app-base.js
Original file line number Diff line number Diff line change
Expand Up @@ -1188,10 +1188,16 @@ class AppBase extends EventHandler {
// #endif
}

frameStart() {
this.graphicsDevice.frameStart();
}

/**
* Render the application's scene. More specifically, the scene's {@link LayerComposition} is
* rendered. This function is called internally in the application's main loop and does not
* need to be called explicitly.
*
* @ignore
*/
render() {
// #if _PROFILER
Expand Down Expand Up @@ -2204,6 +2210,7 @@ const makeTick = function (_app) {
Debug.trace(TRACEID_RENDER_FRAME_TIME, `-- RenderStart ${now().toFixed(2)}ms`);

application.updateCanvasSize();
application.frameStart();
application.render();
application.renderNextFrame = false;

Expand Down
33 changes: 16 additions & 17 deletions src/platform/graphics/graphics-device.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,6 @@ import { ScopeSpace } from './scope-space.js';
import { VertexBuffer } from './vertex-buffer.js';
import { VertexFormat } from './vertex-format.js';

const EVENT_RESIZE = 'resizecanvas';

/**
* The graphics device manages the underlying graphics context. It is responsible for submitting
* render state changes and graphics primitives to the hardware. A graphics device is tied to a
Expand Down Expand Up @@ -151,6 +149,8 @@ class GraphicsDevice extends EventHandler {
flags: CLEARFLAG_COLOR | CLEARFLAG_DEPTH
};

static EVENT_RESIZE = 'resizecanvas';

constructor(canvas) {
super();

Expand Down Expand Up @@ -407,18 +407,6 @@ class GraphicsDevice extends EventHandler {
* @ignore
*/
resizeCanvas(width, height) {
this._width = width;
this._height = height;

const ratio = Math.min(this._maxPixelRatio, platform.browser ? window.devicePixelRatio : 1);
width = Math.floor(width * ratio);
height = Math.floor(height * ratio);

if (this.canvas.width !== width || this.canvas.height !== height) {
this.canvas.width = width;
this.canvas.height = height;
this.fire(EVENT_RESIZE, width, height);
}
}

/**
Expand All @@ -434,7 +422,7 @@ class GraphicsDevice extends EventHandler {
this._height = height;
this.canvas.width = width;
this.canvas.height = height;
this.fire(EVENT_RESIZE, width, height);
this.fire(GraphicsDevice.EVENT_RESIZE, width, height);
}

updateClientRect() {
Expand Down Expand Up @@ -481,8 +469,10 @@ class GraphicsDevice extends EventHandler {
* @type {number}
*/
set maxPixelRatio(ratio) {
this._maxPixelRatio = ratio;
this.resizeCanvas(this._width, this._height);
if (this._maxPixelRatio !== ratio) {
this._maxPixelRatio = ratio;
this.resizeCanvas(this._width, this._height);
}
}

get maxPixelRatio() {
Expand Down Expand Up @@ -514,6 +504,15 @@ class GraphicsDevice extends EventHandler {
setBoneLimit(maxBones) {
this.boneLimit = maxBones;
}

/**
* Function which executes at the start of the frame. This should not be called manually, as
* it is handled by the AppBase instance.
*
* @ignore
*/
frameStart() {
}
}

export { GraphicsDevice };
16 changes: 16 additions & 0 deletions src/platform/graphics/webgl/webgl-graphics-device.js
Original file line number Diff line number Diff line change
Expand Up @@ -2714,6 +2714,22 @@ class WebglGraphicsDevice extends GraphicsDevice {
this._vaoMap.clear();
}

resizeCanvas(width, height) {

this._width = width;
this._height = height;

const ratio = Math.min(this._maxPixelRatio, platform.browser ? window.devicePixelRatio : 1);
width = Math.floor(width * ratio);
height = Math.floor(height * ratio);

if (this.canvas.width !== width || this.canvas.height !== height) {
this.canvas.width = width;
this.canvas.height = height;
this.fire(GraphicsDevice.EVENT_RESIZE, width, height);
}
}

/**
* Width of the back buffer in pixels.
*
Expand Down
21 changes: 7 additions & 14 deletions src/platform/graphics/webgpu/webgpu-bind-group.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Debug, DebugHelper } from '../../../core/debug.js';
import { WebgpuDebug } from './webgpu-debug.js';

/**
* A WebGPU implementation of the BindGroup, which is a wrapper over GPUBindGroup.
Expand All @@ -20,23 +21,15 @@ class WebgpuBindGroup {
/** @type {GPUBindGroupDescriptor} */
const descr = this.createDescriptor(device, bindGroup);

Debug.call(() => {
device.wgpu.pushErrorScope('validation');
});
WebgpuDebug.validate(device);

this.bindGroup = device.wgpu.createBindGroup(descr);

Debug.call(() => {
device.wgpu.popErrorScope().then((error) => {
if (error) {
Debug.gpuError(error.message, {
debugFormat: this.debugFormat,
descr: descr,
format: bindGroup.format,
bindGroup: bindGroup
});
}
});
WebgpuDebug.end(device, {
debugFormat: this.debugFormat,
descr: descr,
format: bindGroup.format,
bindGroup: bindGroup
});
}

Expand Down
75 changes: 75 additions & 0 deletions src/platform/graphics/webgpu/webgpu-debug.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { Debug } from "../../../core/debug.js";

// Maximum number of times a duplicate error message is logged.
const MAX_DUPLICATES = 5;

/**
* Internal WebGPU debug system. Note that the functions only execute in the debug build, and are
* stripped out in other builds.
*
* @ignore
*/
class WebgpuDebug {
static _scopes = [];

/** @type {Map<string,number>} */
static _loggedMessages = new Map();

/**
* Start a validation error scope.
*
* @param {import('./webgpu-graphics-device.js').WebgpuGraphicsDevice} device - The graphics
* device.
*/
static validate(device) {
device.wgpu.pushErrorScope('validation');
WebgpuDebug._scopes.push('validation');
}

/**
* Start an out-of-memory error scope.
*
* @param {import('./webgpu-graphics-device.js').WebgpuGraphicsDevice} device - The graphics
* device.
*/
static memory(device) {
device.wgpu.pushErrorScope('out-of-memory');
WebgpuDebug._scopes.push('out-of-memory');
}

/**
* Start an internal error scope.
*
* @param {import('./webgpu-graphics-device.js').WebgpuGraphicsDevice} device - The graphics
* device.
*/
static internal(device) {
device.wgpu.pushErrorScope('internal');
WebgpuDebug._scopes.push('internal');
}

/**
* End the previous error scope, and print errors if any.
*
* @param {import('./webgpu-graphics-device.js').WebgpuGraphicsDevice} device - The graphics
* device.
* @param {...any} args - Additional parameters that form the error message.
*/
static end(device, ...args) {
const header = WebgpuDebug._scopes.pop();
Debug.assert(header, 'Non matching end.');

device.wgpu.popErrorScope().then((error) => {
if (error) {
const count = WebgpuDebug._loggedMessages.get(error.message) ?? 0;
if (count < MAX_DUPLICATES) {
const tooMany = count === MAX_DUPLICATES - 1 ? ' (Too many errors, ignoring this one from now)' : '';
WebgpuDebug._loggedMessages.set(error.message, count + 1);
console.error(`WebGPU ${header} error: ${error.message}`, tooMany, ...args);
}
}
});
}
}

export { WebgpuDebug };
78 changes: 61 additions & 17 deletions src/platform/graphics/webgpu/webgpu-graphics-device.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Debug, DebugHelper } from '../../../core/debug.js';
import { Vec2 } from '../../../core/math/vec2.js';

import {
PIXELFORMAT_RGBA32F, PIXELFORMAT_RGBA8, PIXELFORMAT_BGRA8, CULLFACE_BACK
Expand All @@ -17,6 +18,7 @@ import { WebgpuUniformBuffer } from './webgpu-uniform-buffer.js';
import { WebgpuVertexBuffer } from './webgpu-vertex-buffer.js';
import { WebgpuClearRenderer } from './webgpu-clear-renderer.js';
import { DebugGraphics } from '../debug-graphics.js';
import { WebgpuDebug } from './webgpu-debug.js';

class WebgpuGraphicsDevice extends GraphicsDevice {
/**
Expand Down Expand Up @@ -173,7 +175,7 @@ class WebgpuGraphicsDevice extends GraphicsDevice {
format: preferredCanvasFormat,

// RENDER_ATTACHMENT is required, COPY_SRC allows scene grab to copy out from it
usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_SRC,
usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_SRC | GPUTextureUsage.COPY_DST,

// formats that views created from textures returned by getCurrentTexture may use
viewFormats: []
Expand All @@ -190,6 +192,7 @@ class WebgpuGraphicsDevice extends GraphicsDevice {
}

createFramebuffer() {
this.frameBufferDimensions = new Vec2();
this.frameBuffer = new RenderTarget({
name: 'WebgpuFramebuffer',
graphicsDevice: this,
Expand All @@ -198,6 +201,55 @@ class WebgpuGraphicsDevice extends GraphicsDevice {
});
}

resizeCanvas(width, height) {

this._width = width;
this._height = height;

if (this.canvas.width !== width || this.canvas.height !== height) {
this.canvas.width = width;
this.canvas.height = height;
this.fire(GraphicsDevice.EVENT_RESIZE, width, height);
}
}

frameStart() {

WebgpuDebug.memory(this);
WebgpuDebug.validate(this);

// current frame color output buffer
const outColorBuffer = this.gpuContext.getCurrentTexture();
DebugHelper.setLabel(outColorBuffer, `${this.frameBuffer.name}`);

// reallocate framebuffer if dimensions change, to match the output texture
if (this.frameBufferDimensions.x !== outColorBuffer.width || this.frameBufferDimensions.y !== outColorBuffer.height) {

this.frameBufferDimensions.set(outColorBuffer.width, outColorBuffer.height);

this.frameBuffer.destroy();
this.frameBuffer = null;

this.createFramebuffer();
}

const rt = this.frameBuffer;
const wrt = rt.impl;

// assign the format, allowing following init call to use it to allocate matching multisampled buffer
wrt.colorFormat = outColorBuffer.format;

this.initRenderTarget(rt);

// assign current frame's render texture
if (outColorBuffer) {
wrt.assignColorTexture(outColorBuffer);
}

WebgpuDebug.end(this);
WebgpuDebug.end(this);
}

createUniformBufferImpl(uniformBuffer) {
return new WebgpuUniformBuffer(uniformBuffer);
}
Expand Down Expand Up @@ -364,23 +416,12 @@ class WebgpuGraphicsDevice extends GraphicsDevice {
this.renderTarget = rt;
const wrt = rt.impl;

// current frame color buffer
let outColorBuffer;
if (rt === this.frameBuffer) {
outColorBuffer = this.gpuContext.getCurrentTexture();
DebugHelper.setLabel(outColorBuffer, rt.name);
WebgpuDebug.internal(this);
WebgpuDebug.validate(this);

// assign the format, allowing following init call to use it to allocate matching multisampled buffer
wrt.colorFormat = outColorBuffer.format;
}

this.initRenderTarget(rt);

// assign current frame's render texture if rendering to the main frame buffer
// TODO: this should probably be done at the start of the frame, so that it can be used
// as a destination of the copy operation
if (outColorBuffer) {
wrt.assignColorTexture(outColorBuffer);
// framebuffer is initialized at the start of the frame
if (rt !== this.frameBuffer) {
this.initRenderTarget(rt);
}

// set up clear / store / load settings
Expand Down Expand Up @@ -422,6 +463,9 @@ class WebgpuGraphicsDevice extends GraphicsDevice {
this.wgpu.queue.submit([this.commandEncoder.finish()]);
this.commandEncoder = null;

WebgpuDebug.end(this, { renderPass });
WebgpuDebug.end(this, { renderPass });

// each render pass can use different number of bind groups
this.bindGroupFormats.length = 0;

Expand Down

0 comments on commit fefc153

Please sign in to comment.