// 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; } } }