diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 48f612c52100..28224fddbe0f 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,2 +1,3 @@ * @kt3k /streams @kt3k @crowlKats +/webgpu @kt3k @crowlKats diff --git a/README.md b/README.md index ed5f76711944..7cf9c2a319b8 100644 --- a/README.md +++ b/README.md @@ -103,6 +103,7 @@ Check out the documentation [here](https://deno.land/std?doc). | ulid | Unstable | | url | Unstable | | uuid | Stable | +| webgpu | Unstable | | yaml | Stable | > For background and discussions regarding the stability of the following diff --git a/_tools/check_circular_submodule_dependencies.ts b/_tools/check_circular_submodule_dependencies.ts index ef75eb389f51..e4171370b6eb 100644 --- a/_tools/check_circular_submodule_dependencies.ts +++ b/_tools/check_circular_submodule_dependencies.ts @@ -126,6 +126,7 @@ deps["toml"] = await check("toml", "ready"); deps["ulid"] = await check("ulid", "not ready"); deps["url"] = await check("url", "not ready"); deps["uuid"] = await check("uuid", "ready"); +deps["webgpu"] = await check("webgpu", "not ready"); deps["yaml"] = await check("yaml", "ready"); /** Checks circular deps between sub modules */ diff --git a/webgpu/_test_util.ts b/webgpu/_test_util.ts new file mode 100644 index 000000000000..00c556e69377 --- /dev/null +++ b/webgpu/_test_util.ts @@ -0,0 +1,40 @@ +// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. + +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; + } + } +} + +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 CI because these do not have virtual GPUs. +const isLinuxOrMacCI = + (Deno.build.os === "linux" || Deno.build.os === "darwin") && isCI; +// Skip these tests in WSL because it doesn't have good GPU support. +const isWsl = await checkIsWsl(); + +export const ignore = isWsl || isLinuxOrMacCI; + +export function cleanUp(device: GPUDevice) { + device.destroy(); + + // TODO(lucacasonato): webgpu spec should add a explicit destroy method for + // adapters. + const resources = Object.keys(Deno.resources()); + Deno.close(Number(resources[resources.length - 1])); +} diff --git a/webgpu/create_capture.ts b/webgpu/create_capture.ts new file mode 100644 index 000000000000..dfa58b7fc847 --- /dev/null +++ b/webgpu/create_capture.ts @@ -0,0 +1,90 @@ +// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. + +import { getRowPadding } from "./row_padding.ts"; + +/** Return value for {@linkcode createCapture}. */ +export interface CreateCapture { + /** + * Texture to be used as view to render to. + */ + texture: GPUTexture; + + /** + * Represents the output buffer of the rendered texture. + * Can then be used to access and retrieve raw image data. + */ + outputBuffer: GPUBuffer; +} + +/** + * Creates a texture and buffer to use as a capture. + * + * @example + * ```ts + * import { createCapture } from "https://deno.land/std@$STD_VERSION/webgpu/create_capture.ts"; + * import { getRowPadding } from "https://deno.land/std@$STD_VERSION/webgpu/row_padding.ts"; + * + * const adapter = await navigator.gpu.requestAdapter(); + * const device = await adapter?.requestDevice()!; + * + * const dimensions = { + * width: 200, + * height: 200, + * }; + * + * const { texture, outputBuffer } = createCapture(device, dimensions.width, dimensions.height); + * + * const encoder = device.createCommandEncoder(); + * encoder.beginRenderPass({ + * colorAttachments: [ + * { + * view: texture.createView(), + * storeOp: "store", + * loadOp: "clear", + * clearValue: [1, 0, 0, 1], + * }, + * ], + * }).end(); + * + * const { padded } = getRowPadding(dimensions.width); + * + * encoder.copyTextureToBuffer( + * { + * texture, + * }, + * { + * buffer: outputBuffer, + * bytesPerRow: padded, + * }, + * dimensions, + * ); + * + * device.queue.submit([encoder.finish()]); + * + * // outputBuffer contains the raw image data, can then be used + * // to save as png or other formats. + * ``` + */ +export function createCapture( + device: GPUDevice, + width: number, + height: number, +): CreateCapture { + const { padded } = getRowPadding(width); + const outputBuffer = device.createBuffer({ + label: "Capture", + size: padded * height, + usage: GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST, + }); + const texture = device.createTexture({ + label: "Capture", + size: { + width, + height, + }, + format: "rgba8unorm-srgb", + usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_SRC, + }); + + return { texture, outputBuffer }; +} diff --git a/webgpu/create_capture_test.ts b/webgpu/create_capture_test.ts new file mode 100644 index 000000000000..b481643becb4 --- /dev/null +++ b/webgpu/create_capture_test.ts @@ -0,0 +1,35 @@ +// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. + +import { assert, assertEquals } from "../assert/mod.ts"; +import { cleanUp, ignore } from "./_test_util.ts"; +import { createCapture } from "./create_capture.ts"; + +Deno.test({ + ignore, + name: "createCapture()", + fn: async () => { + const adapter = await navigator.gpu.requestAdapter(); + assert(adapter); + const device = await adapter.requestDevice(); + assert(device); + + const { texture, outputBuffer } = createCapture(device, 2, 2); + + assertEquals(texture.width, 2); + assertEquals(texture.height, 2); + assertEquals(texture.depthOrArrayLayers, 1); + assertEquals(texture.dimension, "2d"); + assertEquals( + texture.usage, + GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_SRC, + ); + + assertEquals(outputBuffer.size, 512); + assertEquals( + outputBuffer.usage, + GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST, + ); + + cleanUp(device); + }, +}); diff --git a/webgpu/describe_texture_format.ts b/webgpu/describe_texture_format.ts new file mode 100644 index 000000000000..c38cb80684d1 --- /dev/null +++ b/webgpu/describe_texture_format.ts @@ -0,0 +1,359 @@ +// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. + +/** Return type for {@linkcode describeTextureFormat}. */ +export interface TextureFormatInfo { + /** The specific feature needed to use the format, if any. */ + requiredFeature?: GPUFeatureName; + /** Type of sampling that is valid for the texture. */ + sampleType: GPUTextureSampleType; + /** Valid bits of {@linkcode GPUTextureUsage}. */ + allowedUsages: number; + /** Dimension of a "block" of texels. This is always `[1, 1]` on + * uncompressed textures. */ + blockDimensions: [number, number]; + /** Size in bytes of a "block" of texels. This is the size per pixel on + * uncompressed textures. */ + blockSize: number; + /** Count of components in the texture. This determines which components + * there will be actual data in the shader for. */ + components: number; +} + +const basic = GPUTextureUsage.COPY_SRC | GPUTextureUsage.COPY_DST | + GPUTextureUsage.TEXTURE_BINDING; +const attachment = basic | GPUTextureUsage.RENDER_ATTACHMENT; +const storage = basic | GPUTextureUsage.STORAGE_BINDING; +const allFlags = GPUTextureUsage.COPY_SRC | GPUTextureUsage.COPY_DST | + GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.STORAGE_BINDING | + GPUTextureUsage.RENDER_ATTACHMENT; + +/** + * Get various information about a specific {@linkcode GPUTextureFormat}. + * + * @example + * ```ts + * import { describeTextureFormat } from "https://deno.land/std@$STD_VERSION/webgpu/describe_texture_format.ts"; + * + * describeTextureFormat("rgba8unorm-srgb"); + * ``` + */ +export function describeTextureFormat( + format: GPUTextureFormat, +): TextureFormatInfo { + let info: [ + requiredFeatures: GPUFeatureName | undefined, + sampleType: GPUTextureSampleType, + usage: number, + blockDimensions: [number, number], + blockSize: number, + components: number, + ]; + + switch (format) { + case "r8unorm": + info = [undefined, "float", attachment, [1, 1], 1, 1]; + break; + case "r8snorm": + info = [undefined, "float", basic, [1, 1], 1, 1]; + break; + case "r8uint": + info = [undefined, "uint", attachment, [1, 1], 1, 1]; + break; + case "r8sint": + info = [undefined, "sint", attachment, [1, 1], 1, 1]; + break; + + case "r16uint": + info = [undefined, "uint", attachment, [1, 1], 2, 1]; + break; + case "r16sint": + info = [undefined, "sint", attachment, [1, 1], 2, 1]; + break; + case "r16float": + info = [undefined, "float", attachment, [1, 1], 2, 1]; + break; + case "rg8unorm": + info = [undefined, "float", attachment, [1, 1], 2, 2]; + break; + case "rg8snorm": + info = [undefined, "float", attachment, [1, 1], 2, 2]; + break; + case "rg8uint": + info = [undefined, "uint", attachment, [1, 1], 2, 2]; + break; + case "rg8sint": + info = [undefined, "sint", basic, [1, 1], 2, 2]; + break; + + case "r32uint": + info = [undefined, "uint", allFlags, [1, 1], 4, 1]; + break; + case "r32sint": + info = [undefined, "sint", allFlags, [1, 1], 4, 1]; + break; + case "r32float": + info = [undefined, "unfilterable-float", allFlags, [1, 1], 4, 1]; + break; + case "rg16uint": + info = [undefined, "uint", attachment, [1, 1], 4, 2]; + break; + case "rg16sint": + info = [undefined, "sint", attachment, [1, 1], 4, 2]; + break; + case "rg16float": + info = [undefined, "float", attachment, [1, 1], 4, 2]; + break; + case "rgba8unorm": + info = [undefined, "float", allFlags, [1, 1], 4, 4]; + break; + case "rgba8unorm-srgb": + info = [undefined, "float", attachment, [1, 1], 4, 4]; + break; + case "rgba8snorm": + info = [undefined, "float", storage, [1, 1], 4, 4]; + break; + case "rgba8uint": + info = [undefined, "uint", allFlags, [1, 1], 4, 4]; + break; + case "rgba8sint": + info = [undefined, "sint", allFlags, [1, 1], 4, 4]; + break; + case "bgra8unorm": + info = [undefined, "float", attachment, [1, 1], 4, 4]; + break; + case "bgra8unorm-srgb": + info = [undefined, "float", attachment, [1, 1], 4, 4]; + break; + + case "rgb9e5ufloat": + info = [undefined, "float", basic, [1, 1], 4, 3]; + break; + + case "rgb10a2unorm": + info = [undefined, "float", attachment, [1, 1], 4, 4]; + break; + case "rg11b10ufloat": + info = [undefined, "float", basic, [1, 1], 4, 3]; + break; + + case "rg32uint": + info = [undefined, "uint", allFlags, [1, 1], 8, 2]; + break; + case "rg32sint": + info = [undefined, "sint", allFlags, [1, 1], 8, 2]; + break; + case "rg32float": + info = [undefined, "unfilterable-float", allFlags, [1, 1], 8, 2]; + break; + case "rgba16uint": + info = [undefined, "uint", allFlags, [1, 1], 8, 4]; + break; + case "rgba16sint": + info = [undefined, "sint", allFlags, [1, 1], 8, 4]; + break; + case "rgba16float": + info = [undefined, "float", allFlags, [1, 1], 8, 4]; + break; + + case "rgba32uint": + info = [undefined, "uint", allFlags, [1, 1], 16, 4]; + break; + case "rgba32sint": + info = [undefined, "sint", allFlags, [1, 1], 16, 4]; + break; + case "rgba32float": + info = [undefined, "float", allFlags, [1, 1], 16, 4]; + break; + + case "stencil8": + info = [undefined, "uint", attachment, [1, 1], 1, 1]; + break; + case "depth16unorm": + info = [undefined, "depth", attachment, [1, 1], 2, 1]; + break; + case "depth24plus": + info = [undefined, "depth", attachment, [1, 1], 4, 1]; + break; + case "depth24plus-stencil8": + info = [undefined, "depth", attachment, [1, 1], 4, 2]; + break; + case "depth32float": + info = [undefined, "depth", attachment, [1, 1], 4, 1]; + break; + + case "depth32float-stencil8": + info = ["depth32float-stencil8", "depth", attachment, [1, 1], 4, 2]; + break; + + case "bc1-rgba-unorm": + info = ["texture-compression-bc", "float", basic, [4, 4], 8, 4]; + break; + case "bc1-rgba-unorm-srgb": + info = ["texture-compression-bc", "float", basic, [4, 4], 8, 4]; + break; + case "bc2-rgba-unorm": + info = ["texture-compression-bc", "float", basic, [4, 4], 16, 4]; + break; + case "bc2-rgba-unorm-srgb": + info = ["texture-compression-bc", "float", basic, [4, 4], 16, 4]; + break; + case "bc3-rgba-unorm": + info = ["texture-compression-bc", "float", basic, [4, 4], 16, 4]; + break; + case "bc3-rgba-unorm-srgb": + info = ["texture-compression-bc", "float", basic, [4, 4], 16, 4]; + break; + case "bc4-r-unorm": + info = ["texture-compression-bc", "float", basic, [4, 4], 8, 1]; + break; + case "bc4-r-snorm": + info = ["texture-compression-bc", "float", basic, [4, 4], 8, 1]; + break; + case "bc5-rg-unorm": + info = ["texture-compression-bc", "float", basic, [4, 4], 16, 2]; + break; + case "bc5-rg-snorm": + info = ["texture-compression-bc", "float", basic, [4, 4], 16, 2]; + break; + case "bc6h-rgb-ufloat": + info = ["texture-compression-bc", "float", basic, [4, 4], 16, 3]; + break; + case "bc6h-rgb-float": + info = ["texture-compression-bc", "float", basic, [4, 4], 16, 3]; + break; + case "bc7-rgba-unorm": + info = ["texture-compression-bc", "float", basic, [4, 4], 16, 4]; + break; + case "bc7-rgba-unorm-srgb": + info = ["texture-compression-bc", "float", basic, [4, 4], 16, 4]; + break; + + case "etc2-rgb8unorm": + info = ["texture-compression-etc2", "float", basic, [4, 4], 8, 3]; + break; + case "etc2-rgb8unorm-srgb": + info = ["texture-compression-etc2", "float", basic, [4, 4], 8, 3]; + break; + case "etc2-rgb8a1unorm": + info = ["texture-compression-etc2", "float", basic, [4, 4], 8, 4]; + break; + case "etc2-rgb8a1unorm-srgb": + info = ["texture-compression-etc2", "float", basic, [4, 4], 8, 4]; + break; + case "etc2-rgba8unorm": + info = ["texture-compression-etc2", "float", basic, [4, 4], 16, 4]; + break; + case "etc2-rgba8unorm-srgb": + info = ["texture-compression-etc2", "float", basic, [4, 4], 16, 4]; + break; + case "eac-r11unorm": + info = ["texture-compression-etc2", "float", basic, [4, 4], 8, 1]; + break; + case "eac-r11snorm": + info = ["texture-compression-etc2", "float", basic, [4, 4], 8, 1]; + break; + case "eac-rg11unorm": + info = ["texture-compression-etc2", "float", basic, [4, 4], 16, 2]; + break; + case "eac-rg11snorm": + info = ["texture-compression-etc2", "float", basic, [4, 4], 16, 2]; + break; + + case "astc-4x4-unorm": + info = ["texture-compression-astc", "float", basic, [4, 4], 16, 4]; + break; + case "astc-4x4-unorm-srgb": + info = ["texture-compression-astc", "float", basic, [4, 4], 16, 4]; + break; + case "astc-5x4-unorm": + info = ["texture-compression-astc", "float", basic, [5, 4], 16, 4]; + break; + case "astc-5x4-unorm-srgb": + info = ["texture-compression-astc", "float", basic, [5, 4], 16, 4]; + break; + case "astc-5x5-unorm": + info = ["texture-compression-astc", "float", basic, [5, 5], 16, 4]; + break; + case "astc-5x5-unorm-srgb": + info = ["texture-compression-astc", "float", basic, [5, 5], 16, 4]; + break; + case "astc-6x5-unorm": + info = ["texture-compression-astc", "float", basic, [6, 5], 16, 4]; + break; + case "astc-6x5-unorm-srgb": + info = ["texture-compression-astc", "float", basic, [6, 5], 16, 4]; + break; + case "astc-6x6-unorm": + info = ["texture-compression-astc", "float", basic, [6, 6], 16, 4]; + break; + case "astc-6x6-unorm-srgb": + info = ["texture-compression-astc", "float", basic, [6, 6], 16, 4]; + break; + case "astc-8x5-unorm": + info = ["texture-compression-astc", "float", basic, [8, 5], 16, 4]; + break; + case "astc-8x5-unorm-srgb": + info = ["texture-compression-astc", "float", basic, [8, 5], 16, 4]; + break; + case "astc-8x6-unorm": + info = ["texture-compression-astc", "float", basic, [8, 6], 16, 4]; + break; + case "astc-8x6-unorm-srgb": + info = ["texture-compression-astc", "float", basic, [8, 6], 16, 4]; + break; + case "astc-8x8-unorm": + info = ["texture-compression-astc", "float", basic, [8, 8], 16, 4]; + break; + case "astc-8x8-unorm-srgb": + info = ["texture-compression-astc", "float", basic, [8, 8], 16, 4]; + break; + case "astc-10x5-unorm": + info = ["texture-compression-astc", "float", basic, [10, 5], 16, 4]; + break; + case "astc-10x5-unorm-srgb": + info = ["texture-compression-astc", "float", basic, [10, 5], 16, 4]; + break; + case "astc-10x6-unorm": + info = ["texture-compression-astc", "float", basic, [10, 6], 16, 4]; + break; + case "astc-10x6-unorm-srgb": + info = ["texture-compression-astc", "float", basic, [10, 6], 16, 4]; + break; + case "astc-10x8-unorm": + info = ["texture-compression-astc", "float", basic, [10, 8], 16, 4]; + break; + case "astc-10x8-unorm-srgb": + info = ["texture-compression-astc", "float", basic, [10, 8], 16, 4]; + break; + case "astc-10x10-unorm": + info = ["texture-compression-astc", "float", basic, [10, 10], 16, 4]; + break; + case "astc-10x10-unorm-srgb": + info = ["texture-compression-astc", "float", basic, [10, 10], 16, 4]; + break; + case "astc-12x10-unorm": + info = ["texture-compression-astc", "float", basic, [12, 10], 16, 4]; + break; + case "astc-12x10-unorm-srgb": + info = ["texture-compression-astc", "float", basic, [12, 10], 16, 4]; + break; + case "astc-12x12-unorm": + info = ["texture-compression-astc", "float", basic, [12, 12], 16, 4]; + break; + case "astc-12x12-unorm-srgb": + info = ["texture-compression-astc", "float", basic, [12, 12], 16, 4]; + break; + + default: + throw new TypeError(`Unsupported GPUTextureFormat '${format}'`); + } + + return { + requiredFeature: info[0], + sampleType: info[1], + allowedUsages: info[2], + blockDimensions: info[3], + blockSize: info[4], + components: info[5], + }; +} diff --git a/webgpu/describe_texture_format_test.ts b/webgpu/describe_texture_format_test.ts new file mode 100644 index 000000000000..b68547de764f --- /dev/null +++ b/webgpu/describe_texture_format_test.ts @@ -0,0 +1,109 @@ +// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. + +import { describeTextureFormat } from "./describe_texture_format.ts"; + +// From GPUTextureFormat webidl definition in https://github.com/denoland/deno/blob/main/ext/webgpu/01_webgpu.js +const FORMATS: GPUTextureFormat[] = [ + "r8unorm", + "r8snorm", + "r8uint", + "r8sint", + "r16uint", + "r16sint", + "r16float", + "rg8unorm", + "rg8snorm", + "rg8uint", + "rg8sint", + "r32uint", + "r32sint", + "r32float", + "rg16uint", + "rg16sint", + "rg16float", + "rgba8unorm", + "rgba8unorm-srgb", + "rgba8snorm", + "rgba8uint", + "rgba8sint", + "bgra8unorm", + "bgra8unorm-srgb", + "rgb9e5ufloat", + "rgb10a2unorm", + "rg11b10ufloat", + "rg32uint", + "rg32sint", + "rg32float", + "rgba16uint", + "rgba16sint", + "rgba16float", + "rgba32uint", + "rgba32sint", + "rgba32float", + "stencil8", + "depth16unorm", + "depth24plus", + "depth24plus-stencil8", + "depth32float", + "depth32float-stencil8", + "bc1-rgba-unorm", + "bc1-rgba-unorm-srgb", + "bc2-rgba-unorm", + "bc2-rgba-unorm-srgb", + "bc3-rgba-unorm", + "bc3-rgba-unorm-srgb", + "bc4-r-unorm", + "bc4-r-snorm", + "bc5-rg-unorm", + "bc5-rg-snorm", + "bc6h-rgb-ufloat", + "bc6h-rgb-float", + "bc7-rgba-unorm", + "bc7-rgba-unorm-srgb", + "etc2-rgb8unorm", + "etc2-rgb8unorm-srgb", + "etc2-rgb8a1unorm", + "etc2-rgb8a1unorm-srgb", + "etc2-rgba8unorm", + "etc2-rgba8unorm-srgb", + "eac-r11unorm", + "eac-r11snorm", + "eac-rg11unorm", + "eac-rg11snorm", + "astc-4x4-unorm", + "astc-4x4-unorm-srgb", + "astc-5x4-unorm", + "astc-5x4-unorm-srgb", + "astc-5x5-unorm", + "astc-5x5-unorm-srgb", + "astc-6x5-unorm", + "astc-6x5-unorm-srgb", + "astc-6x6-unorm", + "astc-6x6-unorm-srgb", + "astc-8x5-unorm", + "astc-8x5-unorm-srgb", + "astc-8x6-unorm", + "astc-8x6-unorm-srgb", + "astc-8x8-unorm", + "astc-8x8-unorm-srgb", + "astc-10x5-unorm", + "astc-10x5-unorm-srgb", + "astc-10x6-unorm", + "astc-10x6-unorm-srgb", + "astc-10x8-unorm", + "astc-10x8-unorm-srgb", + "astc-10x10-unorm", + "astc-10x10-unorm-srgb", + "astc-12x10-unorm", + "astc-12x10-unorm-srgb", + "astc-12x12-unorm", + "astc-12x12-unorm-srgb", +] as const; + +Deno.test("descripbeTextureFormat()", async (t) => { + for (const format of FORMATS) { + await t.step(format, () => { + describeTextureFormat(format); + }); + } +}); diff --git a/webgpu/mod.ts b/webgpu/mod.ts new file mode 100644 index 000000000000..940705ff01f1 --- /dev/null +++ b/webgpu/mod.ts @@ -0,0 +1,11 @@ +// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. + +/** + * Utilities for interacting with the + * [WebGPU API]{@link https://developer.mozilla.org/en-US/docs/Web/API/WebGPU_API}. + */ + +export * from "./create_capture.ts"; +export * from "./describe_texture_format.ts"; +export * from "./row_padding.ts"; +export * from "./texture_with_data.ts"; diff --git a/webgpu/row_padding.ts b/webgpu/row_padding.ts new file mode 100644 index 000000000000..549674db852a --- /dev/null +++ b/webgpu/row_padding.ts @@ -0,0 +1,76 @@ +// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. + +/** Return value for {@linkcode getRowPadding}. */ +export interface Padding { + /** The number of bytes per row without padding calculated. */ + unpadded: number; + /** The number of bytes per row with padding calculated. */ + padded: number; +} + +/** Buffer-Texture copies must have [`bytes_per_row`] aligned to this number. */ +export const COPY_BYTES_PER_ROW_ALIGNMENT = 256; + +/** Number of bytes per pixel. */ +export const BYTES_PER_PIXEL = 4; + +/** + * Calculates the number of bytes including necessary padding when passing a {@linkcode GPUImageCopyBuffer}. + * + * Ref: https://en.wikipedia.org/wiki/Data_structure_alignment#Computing_padding + * + * @example + * ```ts + * import { getRowPadding } from "https://deno.land/std@$STD_VERSION/webgpu/row_padding.ts"; + * + * getRowPadding(2); // { unpadded: 8, padded: 256 } + * ``` + */ +export function getRowPadding(width: number): Padding { + // It is a WebGPU requirement that + // GPUImageCopyBuffer.layout.bytesPerRow % COPY_BYTES_PER_ROW_ALIGNMENT == 0 + // So we calculate paddedBytesPerRow by rounding unpaddedBytesPerRow + // up to the next multiple of COPY_BYTES_PER_ROW_ALIGNMENT. + + const unpaddedBytesPerRow = width * BYTES_PER_PIXEL; + const paddedBytesPerRowPadding = (COPY_BYTES_PER_ROW_ALIGNMENT - + (unpaddedBytesPerRow % COPY_BYTES_PER_ROW_ALIGNMENT)) % + COPY_BYTES_PER_ROW_ALIGNMENT; + const paddedBytesPerRow = unpaddedBytesPerRow + paddedBytesPerRowPadding; + + return { + unpadded: unpaddedBytesPerRow, + padded: paddedBytesPerRow, + }; +} + +/** + * Creates a new buffer while removing any unnecessary empty bytes. + * Useful for when wanting to save an image as a specific format. + * + * @example + * ```ts + * import { resliceBufferWithPadding } from "https://deno.land/std@$STD_VERSION/webgpu/row_padding.ts"; + * + * const input = new Uint8Array([0, 255, 0, 255, 120, 120, 120]); + * resliceBufferWithPadding(input, 1, 1); // Uint8Array(4) [ 0, 255, 0, 255 ] + * ``` + */ +export function resliceBufferWithPadding( + buffer: Uint8Array, + width: number, + height: number, +): Uint8Array { + const { padded, unpadded } = getRowPadding(width); + const outputBuffer = new Uint8Array(unpadded * height); + + for (let i = 0; i < height; i++) { + const slice = buffer + .slice(i * padded, (i + 1) * padded) + .slice(0, unpadded); + + outputBuffer.set(slice, i * unpadded); + } + + return outputBuffer; +} diff --git a/webgpu/row_padding_test.ts b/webgpu/row_padding_test.ts new file mode 100644 index 000000000000..64d0c227cb62 --- /dev/null +++ b/webgpu/row_padding_test.ts @@ -0,0 +1,106 @@ +// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. + +import { getRowPadding, resliceBufferWithPadding } from "./row_padding.ts"; +import { assertEquals } from "../assert/assert_equals.ts"; + +Deno.test("getRowPadding()", () => { + const { unpadded, padded } = getRowPadding(64); + assertEquals(unpadded, 256); + assertEquals(padded, 256); +}); + +Deno.test("getRowPadding() works for odd number smaller than COPY_BYTES_PER_ROW_ALIGNMENT", () => { + const { unpadded, padded } = getRowPadding(5); + assertEquals(unpadded, 20); + assertEquals(padded, 256); +}); + +Deno.test("getRowPadding() works for odd number larger than COPY_BYTES_PER_ROW_ALIGNMENT", () => { + const { unpadded, padded } = getRowPadding(329); + assertEquals(unpadded, 1316); + assertEquals(padded, 1536); +}); + +Deno.test("getRowPadding() works for even number smaller than COPY_BYTES_PER_ROW_ALIGNMENT", () => { + const { unpadded, padded } = getRowPadding(4); + assertEquals(unpadded, 16); + assertEquals(padded, 256); +}); + +Deno.test("getRowPadding() works for even number larger than COPY_BYTES_PER_ROW_ALIGNMENT", () => { + const { unpadded, padded } = getRowPadding(4024); + assertEquals(unpadded, 16096); + assertEquals(padded, 16128); +}); + +Deno.test("resliceBufferWithPadding()", () => { + // deno-fmt-ignore + const input = new Uint8Array([ + 0, 255, 0, 255, 255, 0, 0, 255, 0, 255, 0, 255, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 255, 0, 0, 255, 255, 0, 0, 255, 255, 0, 0, 255, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, + 0]); + + const buf = resliceBufferWithPadding(input, 3, 2); + assertEquals( + buf, + new Uint8Array([ + 0, + 255, + 0, + 255, + 255, + 0, + 0, + 255, + 0, + 255, + 0, + 255, + 255, + 0, + 0, + 255, + 255, + 0, + 0, + 255, + 255, + 0, + 0, + 255, + ]), + ); +}); diff --git a/webgpu/texture_with_data.ts b/webgpu/texture_with_data.ts new file mode 100644 index 000000000000..9fd54d08a913 --- /dev/null +++ b/webgpu/texture_with_data.ts @@ -0,0 +1,156 @@ +// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. + +import { describeTextureFormat } from "./describe_texture_format.ts"; + +function textureDimensionArrayLayerCount( + texture: GPUTextureDescriptor, +): number { + switch (texture.dimension) { + case "1d": + case "3d": + return 1; + case undefined: + case "2d": + return normalizeExtent3D(texture.size).depthOrArrayLayers ?? 1; + } +} + +function normalizeExtent3D(size: GPUExtent3D): GPUExtent3DDict { + if (Array.isArray(size)) { + return { + width: size[0], + height: size[1], + depthOrArrayLayers: size[2], + }; + } else { + return size; + } +} + +function extent3DPhysicalSize( + size: GPUExtent3D, + format: GPUTextureFormat, +): GPUExtent3DDict { + const [blockWidth, blockHeight] = + describeTextureFormat(format).blockDimensions; + const nSize = normalizeExtent3D(size); + + const width = Math.floor((nSize.width + blockWidth - 1) / blockWidth) * + blockWidth; + const height = + Math.floor(((nSize.height ?? 1) + blockHeight - 1) / blockHeight) * + blockHeight; + + return { + width, + height, + depthOrArrayLayers: nSize.depthOrArrayLayers, + }; +} + +function extent3DMipLevelSize( + size: GPUExtent3D, + level: number, + is3D: boolean, +): GPUExtent3DDict { + const nSize = normalizeExtent3D(size); + return { + height: Math.max(1, nSize.width >> level), + width: Math.max(1, (nSize.height ?? 1) >> level), + depthOrArrayLayers: is3D + ? Math.max(1, (nSize.depthOrArrayLayers ?? 1) >> level) + : (nSize.depthOrArrayLayers ?? 1), + }; +} + +function textureMipLevelSize( + descriptor: GPUTextureDescriptor, + level: number, +): GPUExtent3DDict | undefined { + if (level >= (descriptor.mipLevelCount ?? 1)) { + return undefined; + } + + return extent3DMipLevelSize( + descriptor.size, + level, + descriptor.dimension === "3d", + ); +} + +/** + * Create a {@linkcode GPUTexture} with data. + * + * @example + * ```ts + * import { createTextureWithData } from "https://deno.land/std@$STD_VERSION/webgpu/texture_with_data.ts"; + * + * const adapter = await navigator.gpu.requestAdapter(); + * const device = await adapter?.requestDevice()!; + * + * createTextureWithData(device, { + * format: "bgra8unorm-srgb", + * size: { + * width: 3, + * height: 2, + * }, + * usage: GPUTextureUsage.COPY_SRC, + * }, new Uint8Array([1, 1, 1, 1, 1, 1, 1])); + * ``` + */ +export function createTextureWithData( + device: GPUDevice, + descriptor: GPUTextureDescriptor, + data: Uint8Array, +): GPUTexture { + descriptor.usage |= GPUTextureUsage.COPY_DST; + + const texture = device.createTexture(descriptor); + const layerIterations = textureDimensionArrayLayerCount(descriptor); + const formatInfo = describeTextureFormat(descriptor.format); + + let binaryOffset = 0; + for (let layer = 0; layer < layerIterations; layer++) { + for (let mip = 0; mip < (descriptor.mipLevelCount ?? 1); mip++) { + const mipSize = textureMipLevelSize(descriptor, mip)!; + if (descriptor.dimension !== "3d") { + mipSize.depthOrArrayLayers = 1; + } + + const mipPhysical = extent3DPhysicalSize(mipSize, descriptor.format); + const widthBlocks = Math.floor( + mipPhysical.width / formatInfo.blockDimensions[0], + ); + const heightBlocks = Math.floor( + mipPhysical.height! / formatInfo.blockDimensions[1], + ); + + const bytesPerRow = widthBlocks * formatInfo.blockSize; + const dataSize = bytesPerRow * heightBlocks * mipSize.depthOrArrayLayers!; + + const endOffset = binaryOffset + dataSize; + + device.queue.writeTexture( + { + texture, + mipLevel: mip, + origin: { + x: 0, + y: 0, + z: layer, + }, + }, + data.subarray(binaryOffset, endOffset), + { + bytesPerRow, + rowsPerImage: heightBlocks, + }, + mipPhysical, + ); + + binaryOffset = endOffset; + } + } + + return texture; +} diff --git a/webgpu/texture_with_data_test.ts b/webgpu/texture_with_data_test.ts new file mode 100644 index 000000000000..d87bd51b4914 --- /dev/null +++ b/webgpu/texture_with_data_test.ts @@ -0,0 +1,27 @@ +// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. + +import { assert } from "../assert/assert.ts"; +import { createTextureWithData } from "./texture_with_data.ts"; +import { cleanUp, ignore } from "./_test_util.ts"; + +Deno.test({ + ignore, + name: "createTextureWithData()", + fn: async () => { + const adapter = await navigator.gpu.requestAdapter(); + assert(adapter); + const device = await adapter.requestDevice(); + assert(device); + + createTextureWithData(device, { + format: "bgra8unorm-srgb", + size: { + width: 3, + height: 2, + }, + usage: GPUTextureUsage.COPY_SRC, + }, new Uint8Array([1, 1, 1, 1, 1, 1, 1])); + + cleanUp(device); + }, +});