1
0
Fork 0
mirror of https://github.com/denoland/deno.git synced 2024-12-20 22:34:46 -05:00
denoland-deno/tests/unit/webgpu_test.ts
2024-11-26 10:45:18 -08:00

619 lines
17 KiB
TypeScript

// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
import { assert, assertEquals, assertThrows } from "./test_util.ts";
let isCI: boolean;
try {
isCI = (Deno.env.get("CI")?.length ?? 0) > 0;
} catch {
isCI = true;
}
// Skip these tests on linux CI, because the vulkan emulator is not good enough
// yet, and skip on macOS x86 CI because these do not have virtual GPUs.
const isCIWithoutGPU = (Deno.build.os === "linux" ||
(Deno.build.os === "darwin" && Deno.build.arch === "x86_64")) && isCI;
// Skip these tests in WSL because it doesn't have good GPU support.
const isWsl = await checkIsWsl();
Deno.test({
permissions: { read: true, env: true },
ignore: isWsl || isCIWithoutGPU,
}, async function webgpuComputePass() {
const adapter = await navigator.gpu.requestAdapter();
assert(adapter);
const numbers = [1, 4, 3, 295];
const device = await adapter.requestDevice();
assert(device);
const shaderCode = await Deno.readTextFile(
"tests/testdata/webgpu/computepass_shader.wgsl",
);
const shaderModule = device.createShaderModule({
code: shaderCode,
});
const size = new Uint32Array(numbers).byteLength;
const stagingBuffer = device.createBuffer({
size: size,
usage: GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST,
});
const storageBuffer = device.createBuffer({
label: "Storage Buffer",
size: size,
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST |
GPUBufferUsage.COPY_SRC,
mappedAtCreation: true,
});
const buf = new Uint32Array(storageBuffer.getMappedRange());
buf.set(numbers);
storageBuffer.unmap();
const computePipeline = device.createComputePipeline({
layout: "auto",
compute: {
module: shaderModule,
entryPoint: "main",
},
});
const bindGroupLayout = computePipeline.getBindGroupLayout(0);
const bindGroup = device.createBindGroup({
layout: bindGroupLayout,
entries: [
{
binding: 0,
resource: {
buffer: storageBuffer,
},
},
],
});
const encoder = device.createCommandEncoder();
const computePass = encoder.beginComputePass();
computePass.setPipeline(computePipeline);
computePass.setBindGroup(0, bindGroup);
computePass.insertDebugMarker("compute collatz iterations");
computePass.dispatchWorkgroups(numbers.length);
computePass.end();
encoder.copyBufferToBuffer(storageBuffer, 0, stagingBuffer, 0, size);
device.queue.submit([encoder.finish()]);
await stagingBuffer.mapAsync(1);
const data = stagingBuffer.getMappedRange();
assertEquals(new Uint32Array(data), new Uint32Array([0, 2, 7, 55]));
stagingBuffer.unmap();
device.destroy();
});
Deno.test({
permissions: { read: true, env: true },
ignore: isWsl || isCIWithoutGPU,
}, async function webgpuHelloTriangle() {
const adapter = await navigator.gpu.requestAdapter();
assert(adapter);
const device = await adapter.requestDevice();
assert(device);
const shaderCode = await Deno.readTextFile(
"tests/testdata/webgpu/hellotriangle_shader.wgsl",
);
const shaderModule = device.createShaderModule({
code: shaderCode,
});
const pipelineLayout = device.createPipelineLayout({
bindGroupLayouts: [],
});
const renderPipeline = device.createRenderPipeline({
layout: pipelineLayout,
vertex: {
module: shaderModule,
entryPoint: "vs_main",
// only test purpose
constants: {
value: 0.5,
},
},
fragment: {
module: shaderModule,
entryPoint: "fs_main",
// only test purpose
constants: {
value: 0.5,
},
targets: [
{
format: "rgba8unorm-srgb",
},
],
},
});
const dimensions = {
width: 200,
height: 200,
};
const unpaddedBytesPerRow = dimensions.width * 4;
const align = 256;
const paddedBytesPerRowPadding = (align - unpaddedBytesPerRow % align) %
align;
const paddedBytesPerRow = unpaddedBytesPerRow + paddedBytesPerRowPadding;
const outputBuffer = device.createBuffer({
label: "Capture",
size: paddedBytesPerRow * dimensions.height,
usage: GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST,
});
const texture = device.createTexture({
label: "Capture",
size: dimensions,
format: "rgba8unorm-srgb",
usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_SRC,
});
const encoder = device.createCommandEncoder();
const view = texture.createView();
const renderPass = encoder.beginRenderPass({
colorAttachments: [
{
view,
storeOp: "store",
loadOp: "clear",
clearValue: [0, 1, 0, 1],
},
],
});
renderPass.setPipeline(renderPipeline);
renderPass.draw(3, 1);
renderPass.end();
encoder.copyTextureToBuffer(
{
texture,
},
{
buffer: outputBuffer,
bytesPerRow: paddedBytesPerRow,
rowsPerImage: 0,
},
dimensions,
);
const bundle = encoder.finish();
device.queue.submit([bundle]);
await outputBuffer.mapAsync(1);
const data = new Uint8Array(outputBuffer.getMappedRange());
assertEquals(
data,
await Deno.readFile("tests/testdata/webgpu/hellotriangle.out"),
);
outputBuffer.unmap();
device.destroy();
});
Deno.test({
ignore: isWsl || isCIWithoutGPU,
}, async function webgpuAdapterHasFeatures() {
const adapter = await navigator.gpu.requestAdapter();
assert(adapter);
assert(adapter.features);
const device = await adapter.requestDevice();
device.destroy();
});
Deno.test({
ignore: isWsl || isCIWithoutGPU,
}, async function webgpuNullWindowSurfaceThrows() {
const adapter = await navigator.gpu.requestAdapter();
assert(adapter);
const device = await adapter.requestDevice();
assert(device);
assertThrows(
() => {
new Deno.UnsafeWindowSurface({
system: "cocoa",
windowHandle: null,
displayHandle: null,
width: 0,
height: 0,
});
},
);
device.destroy();
});
Deno.test(function webgpuWindowSurfaceNoWidthHeight() {
assertThrows(
() => {
// @ts-expect-error width and height are required
new Deno.UnsafeWindowSurface({
system: "x11",
windowHandle: null,
displayHandle: null,
});
},
);
});
Deno.test(function getPreferredCanvasFormat() {
const preferredFormat = navigator.gpu.getPreferredCanvasFormat();
assert(preferredFormat === "bgra8unorm" || preferredFormat === "rgba8unorm");
});
Deno.test({
ignore: isWsl || isCIWithoutGPU,
}, async function validateGPUColor() {
const adapter = await navigator.gpu.requestAdapter();
assert(adapter);
const device = await adapter.requestDevice();
assert(device);
const format = "rgba8unorm-srgb";
const encoder = device.createCommandEncoder();
const texture = device.createTexture({
size: [256, 256],
format,
usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_SRC,
});
const view = texture.createView();
const storeOp = "store";
const loadOp = "clear";
// values for validating GPUColor
const invalidSize = [0, 0, 0];
const msgIncludes =
"A sequence of number used as a GPUColor must have exactly 4 elements, received 3 elements";
// validate the argument of descriptor.colorAttachments[@@iterator].clearValue property's length of GPUCommandEncoder.beginRenderPass when its a sequence
// https://www.w3.org/TR/2024/WD-webgpu-20240409/#dom-gpucommandencoder-beginrenderpass
assertThrows(
() =>
encoder.beginRenderPass({
colorAttachments: [
{
view,
storeOp,
loadOp,
clearValue: invalidSize,
},
],
}),
TypeError,
msgIncludes,
);
const renderPass = encoder.beginRenderPass({
colorAttachments: [
{
view,
storeOp,
loadOp,
clearValue: [0, 0, 0, 1],
},
],
});
// validate the argument of color length of GPURenderPassEncoder.setBlendConstant when its a sequence
// https://www.w3.org/TR/2024/WD-webgpu-20240409/#dom-gpurenderpassencoder-setblendconstant
assertThrows(
() => renderPass.setBlendConstant(invalidSize),
TypeError,
msgIncludes,
);
device.destroy();
});
Deno.test({
ignore: isWsl || isCIWithoutGPU,
}, async function validateGPUExtent3D() {
const adapter = await navigator.gpu.requestAdapter();
assert(adapter);
const device = await adapter.requestDevice();
assert(device);
const format = "rgba8unorm-srgb";
const encoder = device.createCommandEncoder();
const buffer = device.createBuffer({
size: new Uint32Array([1, 4, 3, 295]).byteLength,
usage: GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST,
});
const usage = GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_SRC;
const texture = device.createTexture({
size: [256, 256],
format,
usage,
});
// values for validating GPUExtent3D
const belowSize: Array<number> = [];
const overSize = [256, 256, 1, 1];
const msgIncludes =
"A sequence of number used as a GPUExtent3D must have between 1 and 3 elements";
// validate the argument of descriptor.size property's length of GPUDevice.createTexture when its a sequence
// https://www.w3.org/TR/2024/WD-webgpu-20240409/#dom-gpudevice-createtexture
assertThrows(
() => device.createTexture({ size: belowSize, format, usage }),
TypeError,
msgIncludes,
);
assertThrows(
() => device.createTexture({ size: overSize, format, usage }),
TypeError,
msgIncludes,
);
// validate the argument of copySize property's length of GPUCommandEncoder.copyBufferToTexture when its a sequence
// https://www.w3.org/TR/2024/WD-webgpu-20240409/#dom-gpucommandencoder-copybuffertotexture
assertThrows(
() => encoder.copyBufferToTexture({ buffer }, { texture }, belowSize),
TypeError,
msgIncludes,
);
assertThrows(
() => encoder.copyBufferToTexture({ buffer }, { texture }, overSize),
TypeError,
msgIncludes,
);
// validate the argument of copySize property's length of GPUCommandEncoder.copyTextureToBuffer when its a sequence
// https://www.w3.org/TR/2024/WD-webgpu-20240409/#dom-gpucommandencoder-copytexturetobuffer
assertThrows(
() => encoder.copyTextureToBuffer({ texture }, { buffer }, belowSize),
TypeError,
msgIncludes,
);
assertThrows(
() => encoder.copyTextureToBuffer({ texture }, { buffer }, overSize),
TypeError,
msgIncludes,
);
// validate the argument of copySize property's length of GPUCommandEncoder.copyTextureToTexture when its a sequence
// https://www.w3.org/TR/2024/WD-webgpu-20240409/#dom-gpucommandencoder-copytexturetotexture
assertThrows(
() => encoder.copyTextureToTexture({ texture }, { texture }, belowSize),
TypeError,
msgIncludes,
);
assertThrows(
() => encoder.copyTextureToTexture({ texture }, { texture }, overSize),
TypeError,
msgIncludes,
);
const data = new Uint8Array([1 * 255, 1 * 255, 1 * 255, 1 * 255]);
// validate the argument of size property's length of GPUQueue.writeTexture when its a sequence
// https://www.w3.org/TR/2024/WD-webgpu-20240409/#dom-gpuqueue-writetexture
assertThrows(
() => device.queue.writeTexture({ texture }, data, {}, belowSize),
TypeError,
msgIncludes,
);
assertThrows(
() => device.queue.writeTexture({ texture }, data, {}, overSize),
TypeError,
msgIncludes,
);
// NOTE: GPUQueue.copyExternalImageToTexture needs to be validated the argument of copySize property's length when its a sequence, but it is not implemented yet
device.destroy();
});
Deno.test({
ignore: true,
}, async function validateGPUOrigin2D() {
// NOTE: GPUQueue.copyExternalImageToTexture needs to be validated the argument of source.origin property's length when its a sequence, but it is not implemented yet
});
Deno.test({
ignore: isWsl || isCIWithoutGPU,
}, async function validateGPUOrigin3D() {
const adapter = await navigator.gpu.requestAdapter();
assert(adapter);
const device = await adapter.requestDevice();
assert(device);
const format = "rgba8unorm-srgb";
const encoder = device.createCommandEncoder();
const buffer = device.createBuffer({
size: new Uint32Array([1, 4, 3, 295]).byteLength,
usage: GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST,
});
const usage = GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_SRC;
const size = [256, 256, 1];
const texture = device.createTexture({
size,
format,
usage,
});
// value for validating GPUOrigin3D
const overSize = [256, 256, 1, 1];
const msgIncludes =
"A sequence of number used as a GPUOrigin3D must have at most 3 elements, received 4 elements";
// validate the argument of destination.origin property's length of GPUCommandEncoder.copyBufferToTexture when its a sequence
// https://www.w3.org/TR/2024/WD-webgpu-20240409/#dom-gpucommandencoder-copybuffertotexture
assertThrows(
() =>
encoder.copyBufferToTexture(
{ buffer },
{ texture, origin: overSize },
size,
),
TypeError,
msgIncludes,
);
// validate the argument of source.origin property's length of GPUCommandEncoder.copyTextureToBuffer when its a sequence
// https://www.w3.org/TR/2024/WD-webgpu-20240409/#dom-gpucommandencoder-copytexturetobuffer
assertThrows(
() =>
encoder.copyTextureToBuffer(
{ texture, origin: overSize },
{ buffer },
size,
),
TypeError,
msgIncludes,
);
// validate the argument of source.origin property's length of GPUCommandEncoder.copyTextureToTexture when its a sequence
// https://www.w3.org/TR/2024/WD-webgpu-20240409/#dom-gpucommandencoder-copytexturetotexture
assertThrows(
() =>
encoder.copyTextureToTexture(
{ texture, origin: overSize },
{ texture },
size,
),
TypeError,
msgIncludes,
);
// validate the argument of destination.origin property's length of GPUCommandEncoder.copyTextureToTexture when its a sequence
assertThrows(
() =>
encoder.copyTextureToTexture(
{ texture },
{ texture, origin: overSize },
size,
),
TypeError,
msgIncludes,
);
// validate the argument of destination.origin property's length of GPUQueue.writeTexture when its a sequence
// https://www.w3.org/TR/2024/WD-webgpu-20240409/#dom-gpuqueue-writetexture
assertThrows(
() =>
device.queue.writeTexture(
{ texture, origin: overSize },
new Uint8Array([1 * 255, 1 * 255, 1 * 255, 1 * 255]),
{},
size,
),
TypeError,
msgIncludes,
);
// NOTE: GPUQueue.copyExternalImageToTexture needs to be validated the argument of destination.origin property's length when its a sequence, but it is not implemented yet
device.destroy();
});
Deno.test({
ignore: isWsl || isCIWithoutGPU,
}, async function beginRenderPassWithoutDepthClearValue() {
const adapter = await navigator.gpu.requestAdapter();
assert(adapter);
const device = await adapter.requestDevice();
assert(device);
const encoder = device.createCommandEncoder();
const depthTexture = device.createTexture({
size: [256, 256],
format: "depth32float",
usage: GPUTextureUsage.RENDER_ATTACHMENT,
});
const depthView = depthTexture.createView();
const renderPass = encoder.beginRenderPass({
colorAttachments: [],
depthStencilAttachment: {
view: depthView,
depthLoadOp: "load",
},
});
assert(renderPass);
device.destroy();
});
Deno.test({
ignore: isWsl || isCIWithoutGPU,
}, async function adapterLimitsAreNumbers() {
const limitNames = [
"maxTextureDimension1D",
"maxTextureDimension2D",
"maxTextureDimension3D",
"maxTextureArrayLayers",
"maxBindGroups",
"maxDynamicUniformBuffersPerPipelineLayout",
"maxDynamicStorageBuffersPerPipelineLayout",
"maxSampledTexturesPerShaderStage",
"maxSamplersPerShaderStage",
"maxStorageBuffersPerShaderStage",
"maxStorageTexturesPerShaderStage",
"maxUniformBuffersPerShaderStage",
"maxUniformBufferBindingSize",
"maxStorageBufferBindingSize",
"minUniformBufferOffsetAlignment",
"minStorageBufferOffsetAlignment",
"maxVertexBuffers",
"maxVertexAttributes",
"maxVertexBufferArrayStride",
"maxInterStageShaderComponents",
"maxComputeWorkgroupStorageSize",
"maxComputeInvocationsPerWorkgroup",
"maxComputeWorkgroupSizeX",
"maxComputeWorkgroupSizeY",
"maxComputeWorkgroupSizeZ",
"maxComputeWorkgroupsPerDimension",
];
const adapter = await navigator.gpu.requestAdapter();
assert(adapter);
for (const limitName of limitNames) {
// deno-lint-ignore ban-ts-comment
// @ts-ignore
assertEquals(typeof adapter.limits[limitName], "number");
}
const device = await adapter.requestDevice({
// deno-lint-ignore ban-ts-comment
// @ts-ignore
requiredLimits: adapter.limits,
});
assert(device);
device.destroy();
});
async function checkIsWsl() {
return Deno.build.os === "linux" && await hasMicrosoftProcVersion();
async function hasMicrosoftProcVersion() {
// https://github.com/microsoft/WSL/issues/423#issuecomment-221627364
try {
const procVersion = await Deno.readTextFile("/proc/version");
return /microsoft/i.test(procVersion);
} catch {
return false;
}
}
}