diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
commit | 26a029d407be480d791972afb5975cf62c9360a6 (patch) | |
tree | f435a8308119effd964b339f76abb83a57c29483 /dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/canvas/getCurrentTexture.spec.ts | |
parent | Initial commit. (diff) | |
download | firefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz firefox-26a029d407be480d791972afb5975cf62c9360a6.zip |
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/canvas/getCurrentTexture.spec.ts')
-rw-r--r-- | dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/canvas/getCurrentTexture.spec.ts | 383 |
1 files changed, 383 insertions, 0 deletions
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/canvas/getCurrentTexture.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/canvas/getCurrentTexture.spec.ts new file mode 100644 index 0000000000..609dacb907 --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/web_platform/canvas/getCurrentTexture.spec.ts @@ -0,0 +1,383 @@ +export const description = ` +Tests for GPUCanvasContext.getCurrentTexture. +`; + +import { SkipTestCase } from '../../../common/framework/fixture.js'; +import { makeTestGroup } from '../../../common/framework/test_group.js'; +import { timeout } from '../../../common/util/timeout.js'; +import { assert, unreachable } from '../../../common/util/util.js'; +import { GPUTest } from '../../gpu_test.js'; +import { kAllCanvasTypes, createCanvas, CanvasType } from '../../util/create_elements.js'; + +const kFormat = 'bgra8unorm'; + +class GPUContextTest extends GPUTest { + initCanvasContext(canvasType: CanvasType = 'onscreen'): GPUCanvasContext { + const canvas = createCanvas(this, canvasType, 2, 2); + if (canvasType === 'onscreen') { + // To make sure onscreen canvas are visible + const onscreencanvas = canvas as HTMLCanvasElement; + onscreencanvas.style.position = 'fixed'; + onscreencanvas.style.top = '0'; + onscreencanvas.style.left = '0'; + // Set it to transparent so that if multiple canvas are created, they are still visible. + onscreencanvas.style.opacity = '50%'; + document.body.appendChild(onscreencanvas); + this.trackForCleanup({ + close() { + document.body.removeChild(onscreencanvas); + }, + }); + } + const ctx = canvas.getContext('webgpu'); + assert(ctx instanceof GPUCanvasContext, 'Failed to get WebGPU context from canvas'); + + ctx.configure({ + device: this.device, + format: kFormat, + usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_SRC, + }); + + return ctx; + } +} + +export const g = makeTestGroup(GPUContextTest); + +g.test('configured') + .desc( + `Checks that calling getCurrentTexture requires the context to be configured first, and + that each call to configure causes getCurrentTexture to return a new texture.` + ) + .params(u => + u // + .combine('canvasType', kAllCanvasTypes) + ) + .fn(t => { + const canvas = createCanvas(t, t.params.canvasType, 2, 2); + const ctx = canvas.getContext('webgpu'); + assert(ctx instanceof GPUCanvasContext, 'Failed to get WebGPU context from canvas'); + + // Calling getCurrentTexture prior to configuration should throw an InvalidStateError exception. + t.shouldThrow('InvalidStateError', () => { + ctx.getCurrentTexture(); + }); + + // Once the context has been configured getCurrentTexture can be called. + ctx.configure({ + device: t.device, + format: kFormat, + }); + + let prevTexture = ctx.getCurrentTexture(); + + // Calling configure again with different values will change the texture returned. + ctx.configure({ + device: t.device, + format: 'bgra8unorm', + }); + + let currentTexture = ctx.getCurrentTexture(); + t.expect(prevTexture !== currentTexture); + prevTexture = currentTexture; + + // Calling configure again with the same values will still change the texture returned. + ctx.configure({ + device: t.device, + format: 'bgra8unorm', + }); + + currentTexture = ctx.getCurrentTexture(); + t.expect(prevTexture !== currentTexture); + prevTexture = currentTexture; + + // Calling getCurrentTexture after calling unconfigure should throw an InvalidStateError exception. + ctx.unconfigure(); + + t.shouldThrow('InvalidStateError', () => { + ctx.getCurrentTexture(); + }); + }); + +g.test('single_frames') + .desc(`Checks that the value of getCurrentTexture is consistent within a single frame.`) + .params(u => + u // + .combine('canvasType', kAllCanvasTypes) + ) + .fn(t => { + const ctx = t.initCanvasContext(t.params.canvasType); + const frameTexture = ctx.getCurrentTexture(); + + // Calling getCurrentTexture a second time returns the same texture. + t.expect(frameTexture === ctx.getCurrentTexture()); + + const encoder = t.device.createCommandEncoder(); + const pass = encoder.beginRenderPass({ + colorAttachments: [ + { + view: frameTexture.createView(), + clearValue: [1.0, 0.0, 0.0, 1.0], + loadOp: 'clear', + storeOp: 'store', + }, + ], + }); + pass.end(); + t.device.queue.submit([encoder.finish()]); + + // Calling getCurrentTexture after performing some work on the texture returns the same texture. + t.expect(frameTexture === ctx.getCurrentTexture()); + + // Ensure that getCurrentTexture does not clear the texture. + t.expectSingleColor(frameTexture, frameTexture.format, { + size: [frameTexture.width, frameTexture.height, 1], + exp: { R: 1, G: 0, B: 0, A: 1 }, + }); + + frameTexture.destroy(); + + // Calling getCurrentTexture after destroying the texture still returns the same texture. + t.expect(frameTexture === ctx.getCurrentTexture()); + }); + +g.test('multiple_frames') + .desc(`Checks that the value of getCurrentTexture differs across multiple frames.`) + .params(u => + u // + .combine('canvasType', kAllCanvasTypes) + .beginSubcases() + .combine('clearTexture', [true, false]) + ) + .beforeAllSubcases(t => { + const { canvasType } = t.params; + if (canvasType === 'offscreen' && !('transferToImageBitmap' in OffscreenCanvas.prototype)) { + throw new SkipTestCase('transferToImageBitmap not supported'); + } + }) + .fn(t => { + const { canvasType, clearTexture } = t.params; + + return new Promise(resolve => { + const ctx = t.initCanvasContext(canvasType); + let prevTexture: GPUTexture | undefined; + let frameCount = 0; + + function frameCheck() { + const currentTexture = ctx.getCurrentTexture(); + + if (prevTexture) { + // Ensure that each frame a new texture object is returned. + t.expect(currentTexture !== prevTexture); + + // Ensure that texture contents are transparent black. + t.expectSingleColor(currentTexture, currentTexture.format, { + size: [currentTexture.width, currentTexture.height, 1], + exp: { R: 0, G: 0, B: 0, A: 0 }, + }); + } + + if (clearTexture) { + // Clear the texture to test that texture contents don't carry over from frame to frame. + const encoder = t.device.createCommandEncoder(); + const pass = encoder.beginRenderPass({ + colorAttachments: [ + { + view: currentTexture.createView(), + clearValue: [1.0, 0.0, 0.0, 1.0], + loadOp: 'clear', + storeOp: 'store', + }, + ], + }); + pass.end(); + t.device.queue.submit([encoder.finish()]); + } + + prevTexture = currentTexture; + + if (frameCount++ < 5) { + // Which method will be used to begin a new "frame"? + switch (canvasType) { + case 'onscreen': + requestAnimationFrame(frameCheck); + break; + case 'offscreen': { + (ctx.canvas as OffscreenCanvas).transferToImageBitmap(); + frameCheck(); + break; + } + default: + unreachable(); + } + } else { + resolve(); + } + } + + // Call frameCheck for the first time from requestAnimationFrame + // To make sure two frameChecks are run in different frames for onscreen canvas. + // offscreen canvas doesn't care. + requestAnimationFrame(frameCheck); + }); + }); + +g.test('resize') + .desc(`Checks the value of getCurrentTexture differs when the canvas is resized.`) + .params(u => + u // + .combine('canvasType', kAllCanvasTypes) + ) + .fn(t => { + const ctx = t.initCanvasContext(t.params.canvasType); + let prevTexture = ctx.getCurrentTexture(); + + // Trigger a resize by changing the width. + ctx.canvas.width = 4; + + // When the canvas resizes the texture returned by getCurrentTexture should immediately begin + // returning a new texture matching the update dimensions. + let currentTexture = ctx.getCurrentTexture(); + t.expect(prevTexture !== currentTexture); + t.expect(currentTexture.width === ctx.canvas.width); + t.expect(currentTexture.height === ctx.canvas.height); + + // The width and height of the previous texture should remain unchanged. + t.expect(prevTexture.width === 2); + t.expect(prevTexture.height === 2); + prevTexture = currentTexture; + + // Ensure that texture contents are transparent black. + t.expectSingleColor(currentTexture, currentTexture.format, { + size: [currentTexture.width, currentTexture.height, 1], + exp: { R: 0, G: 0, B: 0, A: 0 }, + }); + + // Trigger a resize by changing the height. + ctx.canvas.height = 4; + + // Check to ensure the texture is resized again. + currentTexture = ctx.getCurrentTexture(); + t.expect(prevTexture !== currentTexture); + t.expect(currentTexture.width === ctx.canvas.width); + t.expect(currentTexture.height === ctx.canvas.height); + t.expect(prevTexture.width === 4); + t.expect(prevTexture.height === 2); + prevTexture = currentTexture; + + // Ensure that texture contents are transparent black. + t.expectSingleColor(currentTexture, currentTexture.format, { + size: [currentTexture.width, currentTexture.height, 1], + exp: { R: 0, G: 0, B: 0, A: 0 }, + }); + + // Simply setting the canvas width and height values to their current values should not trigger + // a change in the texture. + ctx.canvas.width = 4; + ctx.canvas.height = 4; + + currentTexture = ctx.getCurrentTexture(); + t.expect(prevTexture === currentTexture); + }); + +g.test('expiry') + .desc( + ` +Test automatic WebGPU canvas texture expiry on all canvas types with the following requirements: +- getCurrentTexture returns the same texture object until the next task: + - after previous frame update the rendering + - before current frame update the rendering + - in a microtask off the current frame task +- getCurrentTexture returns a new texture object and the old texture object becomes invalid + as soon as possible after HTML update the rendering. + +TODO: test more canvas types, and ways to update the rendering +- if on a different thread, expiry happens when the worker updates its rendering (worker "rPAF") OR transferToImageBitmap is called +- [draw, transferControlToOffscreen, then canvas is displayed] on either {main thread, or transferred to worker} +- [draw, canvas is displayed, then transferControlToOffscreen] on either {main thread, or transferred to worker} +- reftests for the above 2 (what gets displayed when the canvas is displayed) +- with canvas element added to DOM or not (applies to other canvas tests as well) + - canvas is added to DOM after being rendered + - canvas is already in DOM but becomes visible after being rendered + ` + ) + .params(u => + u // + .combine('canvasType', kAllCanvasTypes) + .combine('prevFrameCallsite', ['runInNewCanvasFrame', 'requestAnimationFrame'] as const) + .combine('getCurrentTextureAgain', [true, false] as const) + ) + .fn(t => { + const { canvasType, prevFrameCallsite, getCurrentTextureAgain } = t.params; + const ctx = t.initCanvasContext(t.params.canvasType); + // Create a bindGroupLayout to test invalid texture view usage later. + const bgl = t.device.createBindGroupLayout({ + entries: [ + { + binding: 0, + visibility: GPUShaderStage.COMPUTE, + texture: {}, + }, + ], + }); + + // The fn is called immediately after previous frame updating the rendering. + // Polyfill by calling the callback by setTimeout, in the requestAnimationFrame callback (for onscreen canvas) + // or after transferToImageBitmap (for offscreen canvas). + function runInNewCanvasFrame(fn: () => void) { + switch (canvasType) { + case 'onscreen': + requestAnimationFrame(() => timeout(fn)); + break; + case 'offscreen': + // for offscreen canvas, after calling transferToImageBitmap, we are in a new frame immediately + (ctx.canvas as OffscreenCanvas).transferToImageBitmap(); + fn(); + break; + default: + unreachable(); + } + } + + function checkGetCurrentTexture() { + // Call getCurrentTexture on previous frame. + const prevTexture = ctx.getCurrentTexture(); + + // Call getCurrentTexture immediately after the frame, the texture object should stay the same. + queueMicrotask(() => { + if (getCurrentTextureAgain) { + t.expect(prevTexture === ctx.getCurrentTexture()); + } + + // Call getCurrentTexture in a new frame. + // It should expire the previous texture object return a new texture object by the next frame by then. + // Call runInNewCanvasFrame in the micro task to make sure the new frame run after the getCurrentTexture in the micro task for offscreen canvas. + runInNewCanvasFrame(() => { + if (getCurrentTextureAgain) { + t.expect(prevTexture !== ctx.getCurrentTexture()); + } + + // Event when prevTexture expired, createView should still succeed anyway. + const prevTextureView = prevTexture.createView(); + // Using the invalid view should fail if it expires. + t.expectValidationError(() => { + t.device.createBindGroup({ + layout: bgl, + entries: [{ binding: 0, resource: prevTextureView }], + }); + }); + }); + }); + } + + switch (prevFrameCallsite) { + case 'runInNewCanvasFrame': + runInNewCanvasFrame(checkGetCurrentTexture); + break; + case 'requestAnimationFrame': + requestAnimationFrame(checkGetCurrentTexture); + break; + default: + break; + } + }); |