// Copyright 2020 Brandon Jones // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. import * as WebGPUConstants from './webgpuConstants'; import { Scalar } from '../../Maths/math.scalar'; import { WebGPUBufferManager } from './webgpuBufferManager'; import { Constants } from '../constants'; import { Nullable } from '../../types'; // TODO WEBGPU improve mipmap generation by not using the OutputAttachment flag // see https://github.com/toji/web-texture-tool/tree/main/src const mipmapVertexSource = ` #version 450 const vec2 pos[4] = vec2[4](vec2(-1.0f, 1.0f), vec2(1.0f, 1.0f), vec2(-1.0f, -1.0f), vec2(1.0f, -1.0f)); const vec2 tex[4] = vec2[4](vec2(0.0f, 0.0f), vec2(1.0f, 0.0f), vec2(0.0f, 1.0f), vec2(1.0f, 1.0f)); layout(location = 0) out vec2 vTex; void main() { vTex = tex[gl_VertexIndex]; gl_Position = vec4(pos[gl_VertexIndex], 0.0, 1.0); } `; const mipmapFragmentSource = ` #version 450 layout(set = 0, binding = 0) uniform sampler imgSampler; layout(set = 0, binding = 1) uniform texture2D img; layout(location = 0) in vec2 vTex; layout(location = 0) out vec4 outColor; void main() { outColor = texture(sampler2D(img, imgSampler), vTex); } `; export class WebGPUTextureHelper { private _device: GPUDevice; private _glslang: any; private _bufferManager: WebGPUBufferManager; private _mipmapSampler: GPUSampler; private _mipmapPipelines: { [format: string]: GPURenderPipeline } = {}; public static computeNumMipmapLevels(width: number, height: number) { return Scalar.ILog2(Math.max(width, height)) + 1; } constructor(device: GPUDevice, glslang: any, bufferManager: WebGPUBufferManager) { this._device = device; this._glslang = glslang; this._bufferManager = bufferManager; this._mipmapSampler = device.createSampler({ minFilter: WebGPUConstants.FilterMode.Linear }); this._getPipeline(WebGPUConstants.TextureFormat.RGBA8Unorm); } private _getPipeline(format: GPUTextureFormat): GPURenderPipeline { let pipeline = this._mipmapPipelines[format]; if (!pipeline) { pipeline = this._mipmapPipelines[format] = this._device.createRenderPipeline({ vertexStage: { module: this._device.createShaderModule({ code: this._glslang.compileGLSL(mipmapVertexSource, 'vertex') }), entryPoint: 'main' }, fragmentStage: { module: this._device.createShaderModule({ code: this._glslang.compileGLSL(mipmapFragmentSource, 'fragment') }), entryPoint: 'main' }, primitiveTopology: WebGPUConstants.PrimitiveTopology.TriangleStrip, vertexState: { indexFormat: WebGPUConstants.IndexFormat.Uint16 }, colorStates: [{ format: format, }] }); } return pipeline; } public isImageBitmap(imageBitmap: ImageBitmap | { width: number, height: number }): imageBitmap is ImageBitmap { return (imageBitmap as ImageBitmap).close !== undefined; } public isImageBitmapArray(imageBitmap: ImageBitmap[] | { width: number, height: number }): imageBitmap is ImageBitmap[] { return Array.isArray(imageBitmap as ImageBitmap[]) && (imageBitmap as ImageBitmap[])[0].close !== undefined; } public isCompressedFormat(format: GPUTextureFormat): boolean { switch (format) { case WebGPUConstants.TextureFormat.BC7RGBAUnormSRGB: case WebGPUConstants.TextureFormat.BC7RGBAUnorm: case WebGPUConstants.TextureFormat.BC6HRGBSFloat: case WebGPUConstants.TextureFormat.BC6HRGBUFloat: case WebGPUConstants.TextureFormat.BC5RGSnorm: case WebGPUConstants.TextureFormat.BC5RGUnorm: case WebGPUConstants.TextureFormat.BC4RSnorm: case WebGPUConstants.TextureFormat.BC4RUnorm: case WebGPUConstants.TextureFormat.BC3RGBAUnormSRGB: case WebGPUConstants.TextureFormat.BC3RGBAUnorm: case WebGPUConstants.TextureFormat.BC2RGBAUnormSRGB: case WebGPUConstants.TextureFormat.BC2RGBAUnorm: case WebGPUConstants.TextureFormat.BC1RGBAUnormSRGB: case WebGPUConstants.TextureFormat.BC1RGBAUNorm: return true; } return false; } public createTexture(imageBitmap: ImageBitmap | { width: number, height: number }, hasMipmaps = false, generateMipmaps = false, invertY = false, premultiplyAlpha = false, format: GPUTextureFormat = WebGPUConstants.TextureFormat.RGBA8Unorm, sampleCount = 1, commandEncoder?: GPUCommandEncoder, usage = -1): GPUTexture { let textureSize = { width: imageBitmap.width, height: imageBitmap.height, depth: 1, }; const mipLevelCount = hasMipmaps ? WebGPUTextureHelper.computeNumMipmapLevels(imageBitmap.width, imageBitmap.height) : 1; const usages = usage >= 0 ? usage : WebGPUConstants.TextureUsage.CopySrc | WebGPUConstants.TextureUsage.CopyDst | WebGPUConstants.TextureUsage.Sampled; const additionalUsages = hasMipmaps && !this.isCompressedFormat(format) ? WebGPUConstants.TextureUsage.CopySrc | WebGPUConstants.TextureUsage.OutputAttachment : 0; const gpuTexture = this._device.createTexture({ size: textureSize, dimension: WebGPUConstants.TextureDimension.E2d, format, usage: usages | additionalUsages, sampleCount, mipLevelCount }); if (this.isImageBitmap(imageBitmap)) { this.updateTexture(imageBitmap, gpuTexture, imageBitmap.width, imageBitmap.height, format, 0, 0, invertY, premultiplyAlpha, 0, 0, commandEncoder); if (hasMipmaps && generateMipmaps) { this.generateMipmaps(gpuTexture, format, mipLevelCount, 0, commandEncoder); } } return gpuTexture; } public createCubeTexture(imageBitmaps: ImageBitmap[] | { width: number, height: number }, hasMipmaps = false, generateMipmaps = false, invertY = false, premultiplyAlpha = false, format: GPUTextureFormat = WebGPUConstants.TextureFormat.RGBA8Unorm, sampleCount = 1, commandEncoder?: GPUCommandEncoder, usage = -1): GPUTexture { const width = this.isImageBitmapArray(imageBitmaps) ? imageBitmaps[0].width : imageBitmaps.width; const height = this.isImageBitmapArray(imageBitmaps) ? imageBitmaps[0].height : imageBitmaps.height; const mipLevelCount = hasMipmaps ? WebGPUTextureHelper.computeNumMipmapLevels(width, height) : 1; const usages = usage >= 0 ? usage : WebGPUConstants.TextureUsage.CopySrc | WebGPUConstants.TextureUsage.CopyDst | WebGPUConstants.TextureUsage.Sampled; const additionalUsages = hasMipmaps && !this.isCompressedFormat(format) ? WebGPUConstants.TextureUsage.CopySrc | WebGPUConstants.TextureUsage.OutputAttachment : 0; const gpuTexture = this._device.createTexture({ size: { width, height, depth: 6, }, dimension: WebGPUConstants.TextureDimension.E2d, format, usage: usages | additionalUsages, sampleCount, mipLevelCount }); if (this.isImageBitmapArray(imageBitmaps)) { this.updateCubeTextures(imageBitmaps, gpuTexture, width, height, format, invertY, premultiplyAlpha, 0, 0, commandEncoder); if (hasMipmaps && generateMipmaps) { this.generateCubeMipmaps(gpuTexture, format, mipLevelCount, commandEncoder); } } return gpuTexture; } public generateCubeMipmaps(gpuTexture: GPUTexture, format: GPUTextureFormat, mipLevelCount: number, commandEncoder?: GPUCommandEncoder): void { for (let f = 0; f < 6; ++f) { this.generateMipmaps(gpuTexture, format, mipLevelCount, f, commandEncoder); } } public generateMipmaps(gpuTexture: GPUTexture, format: GPUTextureFormat, mipLevelCount: number, faceIndex= 0, commandEncoder?: GPUCommandEncoder): void { const useOwnCommandEncoder = commandEncoder === undefined; const pipeline = this._getPipeline(format); const bindGroupLayout = pipeline.getBindGroupLayout(0); if (useOwnCommandEncoder) { commandEncoder = this._device.createCommandEncoder({}); } commandEncoder!.pushDebugGroup("start create mipmaps"); for (let i = 1; i < mipLevelCount; ++i) { const passEncoder = commandEncoder!.beginRenderPass({ colorAttachments: [{ attachment: gpuTexture.createView({ dimension: WebGPUConstants.TextureViewDimension.E2d, baseMipLevel: i, mipLevelCount: 1, arrayLayerCount: 1, baseArrayLayer: faceIndex, }), loadValue: { r: 0.0, g: 0.0, b: 0.0, a: 0.0 }, }], }); const bindGroup = this._device.createBindGroup({ layout: bindGroupLayout, entries: [{ binding: 0, resource: this._mipmapSampler, }, { binding: 1, resource: gpuTexture.createView({ dimension: WebGPUConstants.TextureViewDimension.E2d, baseMipLevel: i - 1, mipLevelCount: 1, arrayLayerCount: 1, baseArrayLayer: faceIndex, }), }], }); passEncoder.setPipeline(pipeline); passEncoder.setBindGroup(0, bindGroup); passEncoder.draw(4, 1, 0, 0); passEncoder.endPass(); } commandEncoder!.popDebugGroup(); if (useOwnCommandEncoder) { this._device.defaultQueue.submit([commandEncoder!.finish()]); commandEncoder = null as any; } } private _getTextureTypeFromFormat(format: GPUTextureFormat): number { switch (format) { // One Component = 8 bits case WebGPUConstants.TextureFormat.R8Unorm: case WebGPUConstants.TextureFormat.R8Snorm: case WebGPUConstants.TextureFormat.R8Uint: case WebGPUConstants.TextureFormat.R8Sint: case WebGPUConstants.TextureFormat.RG8Unorm: case WebGPUConstants.TextureFormat.RG8Snorm: case WebGPUConstants.TextureFormat.RG8Uint: case WebGPUConstants.TextureFormat.RG8Sint: case WebGPUConstants.TextureFormat.RGBA8Unorm: case WebGPUConstants.TextureFormat.RGBA8UnormSRGB: case WebGPUConstants.TextureFormat.RGBA8Snorm: case WebGPUConstants.TextureFormat.RGBA8Uint: case WebGPUConstants.TextureFormat.RGBA8Sint: case WebGPUConstants.TextureFormat.BGRA8Unorm: case WebGPUConstants.TextureFormat.BGRA8UnormSRGB: case WebGPUConstants.TextureFormat.RGB10A2Unorm: // composite format - let's say it's byte... case WebGPUConstants.TextureFormat.RGB9E5UFloat: // composite format - let's say it's byte... case WebGPUConstants.TextureFormat.RG11B10UFloat: // composite format - let's say it's byte... case WebGPUConstants.TextureFormat.Depth24UnormStencil8: // composite format - let's say it's byte... case WebGPUConstants.TextureFormat.Depth32FloatStencil8: // composite format - let's say it's byte... case WebGPUConstants.TextureFormat.BC7RGBAUnorm: case WebGPUConstants.TextureFormat.BC7RGBAUnormSRGB: case WebGPUConstants.TextureFormat.BC6HRGBUFloat: case WebGPUConstants.TextureFormat.BC6HRGBSFloat: case WebGPUConstants.TextureFormat.BC5RGUnorm: case WebGPUConstants.TextureFormat.BC5RGSnorm: case WebGPUConstants.TextureFormat.BC3RGBAUnorm: case WebGPUConstants.TextureFormat.BC3RGBAUnormSRGB: case WebGPUConstants.TextureFormat.BC2RGBAUnorm: case WebGPUConstants.TextureFormat.BC2RGBAUnormSRGB: case WebGPUConstants.TextureFormat.BC4RUnorm: case WebGPUConstants.TextureFormat.BC4RSnorm: case WebGPUConstants.TextureFormat.BC1RGBAUNorm: case WebGPUConstants.TextureFormat.BC1RGBAUnormSRGB: return Constants.TEXTURETYPE_UNSIGNED_BYTE; // One component = 16 bits case WebGPUConstants.TextureFormat.R16Uint: case WebGPUConstants.TextureFormat.R16Sint: case WebGPUConstants.TextureFormat.RG16Uint: case WebGPUConstants.TextureFormat.RG16Sint: case WebGPUConstants.TextureFormat.RGBA16Uint: case WebGPUConstants.TextureFormat.RGBA16Sint: case WebGPUConstants.TextureFormat.Depth16Unorm: return Constants.TEXTURETYPE_UNSIGNED_SHORT; case WebGPUConstants.TextureFormat.R16Float: case WebGPUConstants.TextureFormat.RG16Float: case WebGPUConstants.TextureFormat.RGBA16Float: return Constants.TEXTURETYPE_HALF_FLOAT; // One component = 32 bits case WebGPUConstants.TextureFormat.R32Uint: case WebGPUConstants.TextureFormat.R32Sint: case WebGPUConstants.TextureFormat.RG32Uint: case WebGPUConstants.TextureFormat.RG32Sint: case WebGPUConstants.TextureFormat.RGBA32Uint: case WebGPUConstants.TextureFormat.RGBA32Sint: return Constants.TEXTURETYPE_UNSIGNED_INTEGER; case WebGPUConstants.TextureFormat.R32Float: case WebGPUConstants.TextureFormat.RG32Float: case WebGPUConstants.TextureFormat.RGBA32Float: case WebGPUConstants.TextureFormat.Depth32Float: return Constants.TEXTURETYPE_FLOAT; case WebGPUConstants.TextureFormat.Stencil8: throw "No fixed size for Stencil8 format!"; case WebGPUConstants.TextureFormat.Depth24Plus: throw "No fixed size for Depth24Plus format!"; case WebGPUConstants.TextureFormat.Depth24PlusStencil8: throw "No fixed size for Depth24PlusStencil8 format!"; } return Constants.TEXTURETYPE_UNSIGNED_BYTE; } private _getBlockInformationFromFormat(format: GPUTextureFormat): { width: number, height: number, length: number } { switch (format) { // 8 bits formats case WebGPUConstants.TextureFormat.R8Unorm: case WebGPUConstants.TextureFormat.R8Snorm: case WebGPUConstants.TextureFormat.R8Uint: case WebGPUConstants.TextureFormat.R8Sint: return { width: 1, height: 1, length: 1 }; // 16 bits formats case WebGPUConstants.TextureFormat.R16Uint: case WebGPUConstants.TextureFormat.R16Sint: case WebGPUConstants.TextureFormat.R16Float: case WebGPUConstants.TextureFormat.RG8Unorm: case WebGPUConstants.TextureFormat.RG8Snorm: case WebGPUConstants.TextureFormat.RG8Uint: case WebGPUConstants.TextureFormat.RG8Sint: return { width: 1, height: 1, length: 2 }; // 32 bits formats case WebGPUConstants.TextureFormat.R32Uint: case WebGPUConstants.TextureFormat.R32Sint: case WebGPUConstants.TextureFormat.R32Float: case WebGPUConstants.TextureFormat.RG16Uint: case WebGPUConstants.TextureFormat.RG16Sint: case WebGPUConstants.TextureFormat.RG16Float: case WebGPUConstants.TextureFormat.RGBA8Unorm: case WebGPUConstants.TextureFormat.RGBA8UnormSRGB: case WebGPUConstants.TextureFormat.RGBA8Snorm: case WebGPUConstants.TextureFormat.RGBA8Uint: case WebGPUConstants.TextureFormat.RGBA8Sint: case WebGPUConstants.TextureFormat.BGRA8Unorm: case WebGPUConstants.TextureFormat.BGRA8UnormSRGB: case WebGPUConstants.TextureFormat.RGB9E5UFloat: case WebGPUConstants.TextureFormat.RGB10A2Unorm: case WebGPUConstants.TextureFormat.RG11B10UFloat: return { width: 1, height: 1, length: 4 }; // 64 bits formats case WebGPUConstants.TextureFormat.RG32Uint: case WebGPUConstants.TextureFormat.RG32Sint: case WebGPUConstants.TextureFormat.RG32Float: case WebGPUConstants.TextureFormat.RGBA16Uint: case WebGPUConstants.TextureFormat.RGBA16Sint: case WebGPUConstants.TextureFormat.RGBA16Float: return { width: 1, height: 1, length: 8 }; // 128 bits formats case WebGPUConstants.TextureFormat.RGBA32Uint: case WebGPUConstants.TextureFormat.RGBA32Sint: case WebGPUConstants.TextureFormat.RGBA32Float: return { width: 1, height: 1, length: 16 }; // Depth and stencil formats case WebGPUConstants.TextureFormat.Stencil8: throw "No fixed size for Stencil8 format!"; case WebGPUConstants.TextureFormat.Depth16Unorm: return { width: 1, height: 1, length: 2 }; case WebGPUConstants.TextureFormat.Depth24Plus: throw "No fixed size for Depth24Plus format!"; case WebGPUConstants.TextureFormat.Depth24PlusStencil8: throw "No fixed size for Depth24PlusStencil8 format!"; case WebGPUConstants.TextureFormat.Depth32Float: return { width: 1, height: 1, length: 4 }; case WebGPUConstants.TextureFormat.Depth24UnormStencil8: return { width: 1, height: 1, length: 4 }; case WebGPUConstants.TextureFormat.Depth32FloatStencil8: return { width: 1, height: 1, length: 5 }; // BC compressed formats usable if "texture-compression-bc" is both // supported by the device/user agent and enabled in requestDevice. case WebGPUConstants.TextureFormat.BC7RGBAUnorm: case WebGPUConstants.TextureFormat.BC7RGBAUnormSRGB: case WebGPUConstants.TextureFormat.BC6HRGBUFloat: case WebGPUConstants.TextureFormat.BC6HRGBSFloat: case WebGPUConstants.TextureFormat.BC5RGUnorm: case WebGPUConstants.TextureFormat.BC5RGSnorm: case WebGPUConstants.TextureFormat.BC3RGBAUnorm: case WebGPUConstants.TextureFormat.BC3RGBAUnormSRGB: case WebGPUConstants.TextureFormat.BC2RGBAUnorm: case WebGPUConstants.TextureFormat.BC2RGBAUnormSRGB: return { width: 4, height: 4, length: 16 }; case WebGPUConstants.TextureFormat.BC4RUnorm: case WebGPUConstants.TextureFormat.BC4RSnorm: case WebGPUConstants.TextureFormat.BC1RGBAUNorm: case WebGPUConstants.TextureFormat.BC1RGBAUnormSRGB: return { width: 4, height: 4, length: 8 }; } return { width: 1, height: 1, length: 4 }; } public updateCubeTextures(imageBitmaps: ImageBitmap[] | Uint8Array[], gpuTexture: GPUTexture, width: number, height: number, format: GPUTextureFormat, invertY = false, premultiplyAlpha = false, offsetX = 0, offsetY = 0, commandEncoder?: GPUCommandEncoder): void { const faces = [0, 3, 1, 4, 2, 5]; for (let f = 0; f < faces.length; ++f) { let imageBitmap = imageBitmaps[faces[f]]; this.updateTexture(imageBitmap, gpuTexture, width, height, format, f, 0, invertY, premultiplyAlpha, offsetX, offsetY, commandEncoder); } } public updateTexture(imageBitmap: ImageBitmap | Uint8Array, gpuTexture: GPUTexture, width: number, height: number, format: GPUTextureFormat, faceIndex: number = 0, mipLevel: number = 0, invertY = false, premultiplyAlpha = false, offsetX = 0, offsetY = 0, commandEncoder?: GPUCommandEncoder): void { const useOwnCommandEncoder = commandEncoder === undefined; if (useOwnCommandEncoder) { commandEncoder = this._device.createCommandEncoder({}); } const blockInformation = this._getBlockInformationFromFormat(format); const textureCopyView: GPUTextureCopyView = { texture: gpuTexture, origin: { x: offsetX, y: offsetY, z: Math.max(faceIndex, 0) }, mipLevel: mipLevel }; const textureExtent = { width: Math.ceil(width / blockInformation.width) * blockInformation.width, height: Math.ceil(height / blockInformation.height) * blockInformation.height, depth: 1 }; if ((imageBitmap as Uint8Array).byteLength !== undefined) { // Note that we can't honor the invertY / premultiplyAlpha flags as we don't know the format of the array imageBitmap = imageBitmap as Uint8Array; const bytesPerRow = Math.ceil(width / blockInformation.width) * blockInformation.length; const aligned = Math.ceil(bytesPerRow / 256) * 256 === bytesPerRow; if (aligned) { const buffer = this._bufferManager.createRawBuffer(imageBitmap.byteLength, GPUBufferUsage.MAP_WRITE | GPUBufferUsage.COPY_SRC, true); const arrayBuffer = buffer.getMappedRange(); new Uint8Array(arrayBuffer).set(imageBitmap); buffer.unmap(); commandEncoder!.copyBufferToTexture({ buffer: buffer, offset: 0, bytesPerRow }, textureCopyView, textureExtent); if (useOwnCommandEncoder) { this._device.defaultQueue.submit([commandEncoder!.finish()]); commandEncoder = null as any; } this._bufferManager.releaseBuffer(buffer); } else { this._device.defaultQueue.writeTexture(textureCopyView, imageBitmap, { offset: 0, bytesPerRow }, textureExtent); } } else { imageBitmap = imageBitmap as ImageBitmap; if (invertY || premultiplyAlpha) { createImageBitmap(imageBitmap, { imageOrientation: invertY ? "flipY" : "none", premultiplyAlpha: premultiplyAlpha ? "premultiply" : "none" }).then((imageBitmap) => { this._device.defaultQueue.copyImageBitmapToTexture({ imageBitmap }, textureCopyView, textureExtent); }); } else { this._device.defaultQueue.copyImageBitmapToTexture({ imageBitmap }, textureCopyView, textureExtent); } } } public readPixels(texture: GPUTexture, x: number, y: number, width: number, height: number, format: GPUTextureFormat, faceIndex: number = 0, mipLevel: number = 0, buffer: Nullable = null): Promise { const blockInformation = this._getBlockInformationFromFormat(format); const bytesPerRow = Math.ceil(width / blockInformation.width) * blockInformation.length; const size = bytesPerRow * height; const gpuBuffer = this._bufferManager.createRawBuffer(size, GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST); const commandEncoder = this._device.createCommandEncoder({}); commandEncoder.copyTextureToBuffer({ texture, mipLevel, origin: { x, y, z: Math.max(faceIndex, 0) } }, { buffer: gpuBuffer, offset: 0, bytesPerRow }, { width, height, depth: 1 }); this._device.defaultQueue.submit([commandEncoder!.finish()]); const type = this._getTextureTypeFromFormat(format); const floatFormat = type === Constants.TEXTURETYPE_FLOAT ? 2 : type === Constants.TEXTURETYPE_HALF_FLOAT ? 1 : 0; return this._bufferManager.readDataFromBuffer(gpuBuffer, size, width, height, floatFormat, 0, buffer); } }