/
githubmirror
/
deno
Обзор
Документация
Войти
/
githubmirror
/
deno
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
tests/unit/webgpu_test.ts
1 217 строк
34 KB
Nathan Whitaker
fix(webgpu): own mapped range backing stores (#36257)
27 июл 2026, 23:45
Не верифицирован
27 июл 2026, 23:45
094e471
Код
Авторство
О чём код?
// Copyright 2018-2026 the Deno authors. 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(); // Skip the window surface tests when there is no windowing system available // (e.g. headless or Wayland-only). Checked up front so those tests are // reported as ignored rather than silently passing. const hasWindowingSystem = checkWindowingSystem(); function checkWindowingSystem(): boolean { if (Deno.build.os === "windows") { // Window creation is asserted in the test itself. return true; } if (Deno.build.os !== "linux") { return false; } try { const x11 = Deno.dlopen( "libX11.so.6", { XOpenDisplay: { parameters: ["pointer"], result: "pointer" }, XCloseDisplay: { parameters: ["pointer"], result: "i32" }, } as const, ); try { const display = x11.symbols.XOpenDisplay(null); if (display === null) { return false; } // Safe to close: this probe connection never backs a wgpu surface. x11.symbols.XCloseDisplay(display); return true; } finally { x11.close(); } } catch { // libX11 is not present return false; } } 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 Float32Array(numbers).byteLength; const inputDataBuffer = device.createBuffer({ label: "Input Data Buffer", size: size, usage: GPUBufferUsage.STORAGE, mappedAtCreation: true, }); const buf = new Float32Array(inputDataBuffer.getMappedRange()); buf.set(numbers); inputDataBuffer.unmap(); const outputDataBuffer = device.createBuffer({ label: "Output Data Buffer", size: size, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC, }); const downloadBuffer = device.createBuffer({ label: "Download Buffer", size: size, usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ, }); const bindGroupLayout = device.createBindGroupLayout({ entries: [ // input buffer { binding: 0, visibility: GPUShaderStage.COMPUTE, buffer: { type: "read-only-storage", minBindingSize: 4, }, }, // output buffer { binding: 1, visibility: GPUShaderStage.COMPUTE, buffer: { type: "storage", minBindingSize: 4, }, }, ], }); const bindGroup = device.createBindGroup({ layout: bindGroupLayout, entries: [ { binding: 0, resource: { buffer: inputDataBuffer, }, }, { binding: 1, resource: { buffer: outputDataBuffer, }, }, ], }); const pipelineLayout = device.createPipelineLayout({ bindGroupLayouts: [bindGroupLayout], }); const computePipeline = device.createComputePipeline({ layout: pipelineLayout, compute: { module: shaderModule, entryPoint: "doubleMe", }, }); const encoder = device.createCommandEncoder(); const computePass = encoder.beginComputePass(); computePass.setPipeline(computePipeline); computePass.setBindGroup(0, bindGroup); computePass.dispatchWorkgroups(numbers.length); computePass.end(); encoder.copyBufferToBuffer(outputDataBuffer, 0, downloadBuffer, 0, size); device.queue.submit([encoder.finish()]); await downloadBuffer.mapAsync(GPUMapMode.READ); const data = downloadBuffer.getMappedRange(); assertEquals(new Float32Array(data), new Float32Array([2, 8, 6, 590])); downloadBuffer.unmap(); device.destroy(); }); Deno.test({ ignore: isWsl || isCIWithoutGPU, }, async function webgpuBufferUnmapWritesMappedRangesBack() { const adapter = await navigator.gpu.requestAdapter(); assert(adapter); const device = await adapter.requestDevice(); assert(device); const size = 16; const writeBuffer = device.createBuffer({ size, usage: GPUBufferUsage.MAP_WRITE | GPUBufferUsage.COPY_SRC, }); const readBuffer = device.createBuffer({ size, usage: GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST, }); await writeBuffer.mapAsync(GPUMapMode.WRITE); const firstRange = writeBuffer.getMappedRange(0, 8); const secondRange = writeBuffer.getMappedRange(8, 8); new Uint8Array(firstRange).set([1, 2, 3, 4, 5, 6, 7, 8]); new Uint8Array(secondRange).set([9, 10, 11, 12, 13, 14, 15, 16]); writeBuffer.unmap(); assertEquals(firstRange.byteLength, 0); assertEquals(secondRange.byteLength, 0); const encoder = device.createCommandEncoder(); encoder.copyBufferToBuffer(writeBuffer, 0, readBuffer, 0, size); device.queue.submit([encoder.finish()]); await readBuffer.mapAsync(GPUMapMode.READ); const readRange = readBuffer.getMappedRange(); assertEquals( new Uint8Array(readRange), new Uint8Array([ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, ]), ); readBuffer.unmap(); assertEquals(readRange.byteLength, 0); device.destroy(); }); Deno.test({ ignore: isWsl || isCIWithoutGPU, }, async function webgpuBufferDestroyDetachesMappedRange() { const adapter = await navigator.gpu.requestAdapter(); assert(adapter); const device = await adapter.requestDevice(); assert(device); const buffer = device.createBuffer({ size: 4096, usage: GPUBufferUsage.MAP_WRITE | GPUBufferUsage.COPY_SRC, }); await buffer.mapAsync(GPUMapMode.WRITE); const mappedRange = buffer.getMappedRange(); const mappedView = new Uint8Array(mappedRange); mappedView[0] = 0x41; buffer.destroy(); assertEquals(mappedRange.byteLength, 0); assertEquals(mappedView.byteLength, 0); assertEquals(mappedView[0], undefined); 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, ); device.queue.submit([encoder.finish()]); 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); const msgIncludes = "Invalid parameters"; assertThrows( () => { new Deno.UnsafeWindowSurface({ system: "cocoa", windowHandle: null, displayHandle: null, width: 0, height: 0, }); }, TypeError, msgIncludes, ); device.destroy(); }); Deno.test(function webgpuWindowSurfaceNoWidthHeight() { const msgIncludes = "expected type `v8::data::Number`, got `v8::data::Value`"; assertThrows( () => { // @ts-expect-error width and height are required new Deno.UnsafeWindowSurface({ system: "x11", windowHandle: null, displayHandle: null, }); }, TypeError, msgIncludes, ); }); Deno.test( { permissions: { ffi: false } }, function webgpuWindowSurfaceFfiPermission() { const response = new Response("ok", { status: 201 }); const nativeResponseSymbol = Object.getOwnPropertySymbols(response).find( (symbol) => symbol.description === "serve native response", ); assert(nativeResponseSymbol !== undefined); const nativeResponse = (response as unknown as Record<symbol, Deno.PointerValue>)[ nativeResponseSymbol ]; assert(nativeResponse); const system = Deno.build.os === "darwin" ? "x11" : "cocoa"; assertThrows( () => { new Deno.UnsafeWindowSurface({ system, windowHandle: nativeResponse, displayHandle: nativeResponse, width: 1, height: 1, }); }, Deno.errors.NotCapable, "Requires ffi access", ); }, ); Deno.test({ permissions: { ffi: true }, ignore: isWsl || isCIWithoutGPU || !hasWindowingSystem, }, async function webgpuWindowSurfaceResizeAfterConfigure() { // Regression test for the RefCell double-borrow panic where the // UnsafeWindowSurface width/height setters held the SurfaceData borrow // across the canvas context resize. const nativeWindow = Deno.build.os === "windows" ? createWin32Window() : createX11Window(); try { const adapter = await navigator.gpu.requestAdapter(); assert(adapter); const device = await adapter.requestDevice(); assert(device); try { const surface = new Deno.UnsafeWindowSurface({ system: nativeWindow.system, windowHandle: nativeWindow.windowHandle, displayHandle: nativeWindow.displayHandle, width: 320, height: 240, }); const context = surface.getContext("webgpu") as GPUCanvasContext; context.configure({ device, format: navigator.gpu.getPreferredCanvasFormat(), }); // Each assignment triggers a resize of the configured context, which // reads the same SurfaceData the setter just mutated. The getter reads // prove the setters released their borrows. surface.width = 640; surface.height = 480; assertEquals(surface.width, 640); assertEquals(surface.height, 480); } finally { device.destroy(); } } finally { nativeWindow.close(); } }); // The native window and display are deliberately leaked: the wgpu surface // held by UnsafeWindowSurface is a GC-managed object that is dropped at // isolate teardown, after the test body has finished. Destroying the window // or display connection earlier makes that surface drop a use-after-free in // the driver. close() only releases the dlopen handle. interface NativeWindow { system: "win32" | "x11"; windowHandle: Deno.PointerValue; displayHandle: Deno.PointerValue; close(): void; } function createWin32Window(): NativeWindow { const user32 = Deno.dlopen( "user32.dll", { CreateWindowExW: { parameters: [ "u32", // dwExStyle "buffer", // lpClassName "buffer", // lpWindowName "u32", // dwStyle "i32", // X "i32", // Y "i32", // nWidth "i32", // nHeight "pointer", // hWndParent "pointer", // hMenu "pointer", // hInstance "pointer", // lpParam ], result: "pointer", }, } as const, ); function wide(s: string): Uint16Array { const buf = new Uint16Array(s.length + 1); for (let i = 0; i < s.length; i++) { buf[i] = s.charCodeAt(i); } return buf; } const WS_OVERLAPPEDWINDOW = 0x00CF0000; // The predefined "STATIC" window class avoids needing RegisterClassW. The // window is deliberately not WS_VISIBLE; surface creation works on hidden // windows. const hwnd = user32.symbols.CreateWindowExW( 0, wide("STATIC"), wide("deno webgpu test"), WS_OVERLAPPEDWINDOW, 0, 0, 320, 240, null, null, null, null, ); assert(hwnd !== null, "CreateWindowExW failed"); return { system: "win32", windowHandle: hwnd, displayHandle: null, close() { user32.close(); }, }; } function createX11Window(): NativeWindow { const symbols = { XOpenDisplay: { parameters: ["pointer"], result: "pointer" }, XDefaultRootWindow: { parameters: ["pointer"], result: "u64" }, XCreateSimpleWindow: { // (display, parent, x, y, width, height, border_width, border, // background) parameters: [ "pointer", "u64", "i32", "i32", "u32", "u32", "u32", "u64", "u64", ], result: "u64", }, XFlush: { parameters: ["pointer"], result: "i32" }, } as const; // The top-level windowing system check already dlopened libX11 and opened a // display, so any failure here is a real error rather than a reason to skip. const x11 = Deno.dlopen("libX11.so.6", symbols); const display = x11.symbols.XOpenDisplay(null); assert(display !== null, "XOpenDisplay failed"); const root = x11.symbols.XDefaultRootWindow(display); const win = x11.symbols.XCreateSimpleWindow( display, root, 0, 0, 320, 240, 0, 0n, 0n, ); x11.symbols.XFlush(display); return { system: "x11", // The windowHandle external's value is the X11 window XID itself. windowHandle: Deno.UnsafePointer.create(BigInt(win)), displayHandle: display, close() { x11.close(); }, }; } 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(); }); // Regression test for https://github.com/denoland/deno/issues/24821. // // Using a `"2d-array"` view of a single-layered 2D texture as a color // attachment used to invalidate the command encoder, causing every command // recorded after the render pass (including a compute pass writing to an // unrelated storage buffer) to silently no-op. The storage buffer was returned // in its zero-initialized state. Deno.test({ ignore: isWsl || isCIWithoutGPU, }, async function renderPass2dArrayColorAttachmentDoesNotCorruptComputePass() { const adapter = await navigator.gpu.requestAdapter(); assert(adapter); const device = await adapter.requestDevice(); assert(device); const texture = device.createTexture({ format: "bgra8unorm", size: { width: 4, height: 4, depthOrArrayLayers: 1 }, usage: GPUTextureUsage.RENDER_ATTACHMENT, }); const view = texture.createView({ dimension: "2d-array" }); const encoder = device.createCommandEncoder(); const renderPass = encoder.beginRenderPass({ colorAttachments: [{ view, loadOp: "load", storeOp: "discard", }], }); renderPass.end(); const shaderModule = device.createShaderModule({ code: ` @group(0) @binding(0) var<storage, read_write> output: array<u32, 4>; @compute @workgroup_size(1) fn main() { output[0] = 0xdeadbeefu; output[1] = 0xcafebabeu; output[2] = 0xfeedfaceu; output[3] = 0x12345678u; } `, }); const pipeline = device.createComputePipeline({ layout: "auto", compute: { module: shaderModule, entryPoint: "main" }, }); const storageBuffer = device.createBuffer({ size: 16, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC, }); const readBuffer = device.createBuffer({ size: 16, usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ, }); const bindGroup = device.createBindGroup({ layout: pipeline.getBindGroupLayout(0), entries: [{ binding: 0, resource: { buffer: storageBuffer } }], }); const computePass = encoder.beginComputePass(); computePass.setPipeline(pipeline); computePass.setBindGroup(0, bindGroup); computePass.dispatchWorkgroups(1); computePass.end(); encoder.copyBufferToBuffer(storageBuffer, 0, readBuffer, 0, 16); device.queue.submit([encoder.finish()]); await readBuffer.mapAsync(GPUMapMode.READ); const result = new Uint32Array(readBuffer.getMappedRange().slice(0)); readBuffer.unmap(); assertEquals( result, new Uint32Array([0xdeadbeef, 0xcafebabe, 0xfeedface, 0x12345678]), ); 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", "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", limitName); } const device = await adapter.requestDevice({ // deno-lint-ignore ban-ts-comment // @ts-ignore requiredLimits: adapter.limits, }); assert(device); device.destroy(); }); Deno.test({ ignore: isWsl || isCIWithoutGPU, }, async function testOnSubmittedWorkDone() { const adapter = await navigator.gpu.requestAdapter(); assert(adapter); const device = await adapter.requestDevice(); assert(device); const encoder = device.createCommandEncoder(); const fut = device.queue.onSubmittedWorkDone(); device.queue.submit([encoder.finish()]); await fut; device.destroy(); }); Deno.test({ permissions: { read: true, env: true }, ignore: isWsl || isCIWithoutGPU, }, async function webgpuWriteBufferTypedArrayElementOffsets() { const adapter = await navigator.gpu.requestAdapter(); assert(adapter); const device = await adapter.requestDevice(); assert(device); const source = new Float32Array([1.0, 2.0, 3.0, 4.0]); const gpuBuffer = device.createBuffer({ size: 2 * Float32Array.BYTES_PER_ELEMENT, usage: GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST, }); // dataOffset=1 and size=2 should be in elements for a Float32Array device.queue.writeBuffer(gpuBuffer, 0, source, 1, 2); const readBuffer = device.createBuffer({ size: 2 * Float32Array.BYTES_PER_ELEMENT, usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ, }); const encoder = device.createCommandEncoder(); encoder.copyBufferToBuffer( gpuBuffer, 0, readBuffer, 0, 2 * Float32Array.BYTES_PER_ELEMENT, ); device.queue.submit([encoder.finish()]); await readBuffer.mapAsync(GPUMapMode.READ); const result = new Float32Array(readBuffer.getMappedRange()); assertEquals(result, new Float32Array([2.0, 3.0])); readBuffer.unmap(); device.destroy(); }); Deno.test({ permissions: { env: true }, ignore: isWsl || isCIWithoutGPU, }, async function writeBufferAcceptsArrayBuffer() { const adapter = await navigator.gpu.requestAdapter(); assert(adapter); const device = await adapter.requestDevice(); assert(device); const source = new ArrayBuffer(4 * Float32Array.BYTES_PER_ELEMENT); new Float32Array(source).set([1.0, 2.0, 3.0, 4.0]); const gpuBuffer = device.createBuffer({ size: source.byteLength, usage: GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST, }); device.queue.writeBuffer(gpuBuffer, 0, source); const readBuffer = device.createBuffer({ size: source.byteLength, usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ, }); const encoder = device.createCommandEncoder(); encoder.copyBufferToBuffer(gpuBuffer, 0, readBuffer, 0, source.byteLength); device.queue.submit([encoder.finish()]); await readBuffer.mapAsync(GPUMapMode.READ); const result = new Float32Array(readBuffer.getMappedRange()); assertEquals(result, new Float32Array([1.0, 2.0, 3.0, 4.0])); readBuffer.unmap(); device.destroy(); }); // Regression test for https://github.com/denoland/deno/issues/33956. // Before the fix, the Uint32Array fast path of setBindGroup sliced the // backing ArrayBuffer with an unchecked `&data[start..start+len]`. An // out-of-range length panicked inside the op's `extern "C"` callback, // which crosses the C ABI as a process abort. After the fix, the // bounds-check surfaces a GPUValidationError instead. Deno.test({ permissions: { read: true, env: true }, ignore: isWsl || isCIWithoutGPU, }, async function webgpuSetBindGroupBoundsCheck() { const adapter = await navigator.gpu.requestAdapter(); assert(adapter); const device = await adapter.requestDevice(); const layout = device.createBindGroupLayout({ entries: [] }); const bg = device.createBindGroup({ layout, entries: [] }); // ComputePass.setBindGroup: out-of-range len pushes a validation // error instead of aborting the process. { const encoder = device.createCommandEncoder(); const pass = encoder.beginComputePass(); device.pushErrorScope("validation"); pass.setBindGroup(0, bg, new Uint32Array(4), 0, 1_000_000); pass.end(); const err = await device.popErrorScope(); assert(err, "expected GPUValidationError on out-of-range setBindGroup"); } // RenderBundleEncoder.setBindGroup: same input shape, different code // path (returns the validation as a thrown JS error, since there is // no error_handler.push_error at this site). { const bundleEncoder = device.createRenderBundleEncoder({ colorFormats: ["rgba8unorm"], }); let threw = false; try { bundleEncoder.setBindGroup(0, bg, new Uint32Array(4), 0, 1_000_000); } catch (_) { threw = true; } assert( threw, "expected setBindGroup on a RenderBundleEncoder to throw on out-of-range args", ); } // Sanity: valid args still work (no regression on the happy path). { const encoder = device.createCommandEncoder(); const pass = encoder.beginComputePass(); pass.setBindGroup(0, bg, new Uint32Array(4), 0, 0); pass.end(); } 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; } } }