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

WebGPU renderer handles canvas size change #5148

Merged
merged 3 commits into from
Mar 14, 2023
Merged
Show file tree
Hide file tree
Changes from 2 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
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
};

EVENT_RESIZE = 'resizecanvas';
mvaligursky marked this conversation as resolved.
Show resolved Hide resolved

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(this.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(this.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(this.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