import { assert, memcpy } from '../../../common/util/util.js'; import { resolvePerAspectFormat, SizedTextureFormat, EncodableTextureFormat, getBlockInfoForTextureFormat, getBlockInfoForEncodableTextureFormat, } from '../../format_info.js'; import { GPUTest } from '../../gpu_test.js'; import { align } from '../math.js'; import { reifyExtent3D } from '../unions.js'; import { physicalMipSize, virtualMipSize } from './base.js'; /** The minimum `bytesPerRow` alignment, per spec. */ export const kBytesPerRowAlignment = 256; /** The minimum buffer copy alignment, per spec. */ export const kBufferCopyAlignment = 4; /** * Overridable layout options for {@link getTextureCopyLayout}. */ export interface LayoutOptions { mipLevel: number; bytesPerRow?: number; rowsPerImage?: number; aspect?: GPUTextureAspect; } const kDefaultLayoutOptions = { mipLevel: 0, bytesPerRow: undefined, rowsPerImage: undefined, aspect: 'all' as const, }; /** The info returned by {@link getTextureSubCopyLayout}. */ export interface TextureSubCopyLayout { bytesPerBlock: number; byteLength: number; /** Number of bytes in each row, not accounting for {@link kBytesPerRowAlignment}. */ minBytesPerRow: number; /** * Actual value of bytesPerRow, defaulting to `align(minBytesPerRow, kBytesPerRowAlignment}` * if not overridden. */ bytesPerRow: number; /** Actual value of rowsPerImage, defaulting to `mipSize[1]` if not overridden. */ rowsPerImage: number; } /** The info returned by {@link getTextureCopyLayout}. */ export interface TextureCopyLayout extends TextureSubCopyLayout { mipSize: [number, number, number]; } /** * Computes layout information for a copy of the whole subresource at `mipLevel` of a GPUTexture * of size `baseSize` with the provided `format` and `dimension`. * * Computes default values for `bytesPerRow` and `rowsPerImage` if not specified. * * MAINTENANCE_TODO: Change input/output to Required for consistency. */ export function getTextureCopyLayout( format: GPUTextureFormat, dimension: GPUTextureDimension, baseSize: readonly [number, number, number], { mipLevel, bytesPerRow, rowsPerImage, aspect }: LayoutOptions = kDefaultLayoutOptions ): TextureCopyLayout { const mipSize = physicalMipSize( { width: baseSize[0], height: baseSize[1], depthOrArrayLayers: baseSize[2] }, format, dimension, mipLevel ); const layout = getTextureSubCopyLayout(format, mipSize, { bytesPerRow, rowsPerImage, aspect }); return { ...layout, mipSize: [mipSize.width, mipSize.height, mipSize.depthOrArrayLayers] }; } /** * Computes layout information for a copy of size `copySize` to/from a GPUTexture with the provided * `format`. * * Computes default values for `bytesPerRow` and `rowsPerImage` if not specified. */ export function getTextureSubCopyLayout( format: GPUTextureFormat, copySize: GPUExtent3D, { bytesPerRow, rowsPerImage, aspect = 'all' as const, }: { readonly bytesPerRow?: number; readonly rowsPerImage?: number; readonly aspect?: GPUTextureAspect; } = {} ): TextureSubCopyLayout { format = resolvePerAspectFormat(format, aspect); const { blockWidth, blockHeight, bytesPerBlock } = getBlockInfoForTextureFormat(format); assert(bytesPerBlock !== undefined); const copySize_ = reifyExtent3D(copySize); assert( copySize_.width > 0 && copySize_.height > 0 && copySize_.depthOrArrayLayers > 0, 'not implemented for empty copySize' ); assert( copySize_.width % blockWidth === 0 && copySize_.height % blockHeight === 0, () => `copySize (${copySize_.width},${copySize_.height}) must be a multiple of the block size (${blockWidth},${blockHeight})` ); const copySizeBlocks = { width: copySize_.width / blockWidth, height: copySize_.height / blockHeight, depthOrArrayLayers: copySize_.depthOrArrayLayers, }; const minBytesPerRow = copySizeBlocks.width * bytesPerBlock; const alignedMinBytesPerRow = align(minBytesPerRow, kBytesPerRowAlignment); if (bytesPerRow !== undefined) { assert(bytesPerRow >= alignedMinBytesPerRow); assert(bytesPerRow % kBytesPerRowAlignment === 0); } else { bytesPerRow = alignedMinBytesPerRow; } if (rowsPerImage !== undefined) { assert(rowsPerImage >= copySizeBlocks.height); } else { rowsPerImage = copySizeBlocks.height; } const bytesPerSlice = bytesPerRow * rowsPerImage; const sliceSize = bytesPerRow * (copySizeBlocks.height - 1) + bytesPerBlock * copySizeBlocks.width; const byteLength = bytesPerSlice * (copySizeBlocks.depthOrArrayLayers - 1) + sliceSize; return { bytesPerBlock, byteLength: align(byteLength, kBufferCopyAlignment), minBytesPerRow, bytesPerRow, rowsPerImage, }; } /** * Fill an ArrayBuffer with the linear-memory representation of a solid-color * texture where every texel has the byte value `texelValue`. * Preserves the contents of `outputBuffer` which are in "padding" space between image rows. * * Effectively emulates a copyTextureToBuffer from a solid-color texture to a buffer. */ export function fillTextureDataWithTexelValue( texelValue: ArrayBuffer, format: EncodableTextureFormat, dimension: GPUTextureDimension, outputBuffer: ArrayBuffer, size: [number, number, number], options: LayoutOptions = kDefaultLayoutOptions ): void { const { blockWidth, blockHeight, bytesPerBlock } = getBlockInfoForEncodableTextureFormat(format); // Block formats are not handled correctly below. assert(blockWidth === 1); assert(blockHeight === 1); assert(bytesPerBlock === texelValue.byteLength, 'texelValue must be of size bytesPerBlock'); const { byteLength, rowsPerImage, bytesPerRow } = getTextureCopyLayout( format, dimension, size, options ); assert(byteLength <= outputBuffer.byteLength); const mipSize = virtualMipSize(dimension, size, options.mipLevel); const outputTexelValueBytes = new Uint8Array(outputBuffer); for (let slice = 0; slice < mipSize[2]; ++slice) { for (let row = 0; row < mipSize[1]; row += blockHeight) { for (let col = 0; col < mipSize[0]; col += blockWidth) { const byteOffset = slice * rowsPerImage * bytesPerRow + row * bytesPerRow + col * texelValue.byteLength; memcpy({ src: texelValue }, { dst: outputTexelValueBytes, start: byteOffset }); } } } } /** * Create a `COPY_SRC` GPUBuffer containing the linear-memory representation of a solid-color * texture where every texel has the byte value `texelValue`. */ export function createTextureUploadBuffer( t: GPUTest, texelValue: ArrayBuffer, format: EncodableTextureFormat, dimension: GPUTextureDimension, size: [number, number, number], options: LayoutOptions = kDefaultLayoutOptions ): { buffer: GPUBuffer; bytesPerRow: number; rowsPerImage: number; } { const { byteLength, bytesPerRow, rowsPerImage, bytesPerBlock } = getTextureCopyLayout( format, dimension, size, options ); const buffer = t.createBufferTracked({ mappedAtCreation: true, size: byteLength, usage: GPUBufferUsage.COPY_SRC, }); const mapping = buffer.getMappedRange(); assert(texelValue.byteLength === bytesPerBlock); fillTextureDataWithTexelValue(texelValue, format, dimension, mapping, size, options); buffer.unmap(); return { buffer, bytesPerRow, rowsPerImage, }; } export type ImageCopyType = 'WriteTexture' | 'CopyB2T' | 'CopyT2B'; export const kImageCopyTypes: readonly ImageCopyType[] = [ 'WriteTexture', 'CopyB2T', 'CopyT2B', ] as const; /** * Computes `bytesInACompleteRow` (as defined by the WebGPU spec) for image copies (B2T/T2B/writeTexture). */ export function bytesInACompleteRow(copyWidth: number, format: SizedTextureFormat): number { const info = getBlockInfoForTextureFormat(format); assert(!!info.bytesPerBlock); assert(copyWidth % info.blockWidth === 0); return (info.bytesPerBlock * copyWidth) / info.blockWidth; } function validateBytesPerRow({ bytesPerRow, bytesInLastRow, sizeInBlocks, }: { bytesPerRow: number | undefined; bytesInLastRow: number; sizeInBlocks: Required; }) { // If specified, layout.bytesPerRow must be greater than or equal to bytesInLastRow. if (bytesPerRow !== undefined && bytesPerRow < bytesInLastRow) { return false; } // If heightInBlocks > 1, layout.bytesPerRow must be specified. // If copyExtent.depthOrArrayLayers > 1, layout.bytesPerRow and layout.rowsPerImage must be specified. if ( bytesPerRow === undefined && (sizeInBlocks.height > 1 || sizeInBlocks.depthOrArrayLayers > 1) ) { return false; } return true; } function validateRowsPerImage({ rowsPerImage, sizeInBlocks, }: { rowsPerImage: number | undefined; sizeInBlocks: Required; }) { // If specified, layout.rowsPerImage must be greater than or equal to heightInBlocks. if (rowsPerImage !== undefined && rowsPerImage < sizeInBlocks.height) { return false; } // If copyExtent.depthOrArrayLayers > 1, layout.bytesPerRow and layout.rowsPerImage must be specified. if (rowsPerImage === undefined && sizeInBlocks.depthOrArrayLayers > 1) { return false; } return true; } interface DataBytesForCopyArgs { layout: GPUTexelCopyBufferLayout; format: SizedTextureFormat; copySize: Readonly | readonly number[]; method: ImageCopyType; } /** * Validate a copy and compute the number of bytes it needs. Throws if the copy is invalid. */ export function dataBytesForCopyOrFail(args: DataBytesForCopyArgs): number { const { minDataSizeOrOverestimate, copyValid } = dataBytesForCopyOrOverestimate(args); assert(copyValid, 'copy was invalid'); return minDataSizeOrOverestimate; } /** * Validate a copy and compute the number of bytes it needs. If the copy is invalid, attempts to * "conservatively guess" (overestimate) the number of bytes that could be needed for a copy, even * if the copy parameters turn out to be invalid. This hopes to avoid "buffer too small" validation * errors when attempting to test other validation errors. */ export function dataBytesForCopyOrOverestimate({ layout, format, copySize: copySize_, method, }: DataBytesForCopyArgs): { minDataSizeOrOverestimate: number; copyValid: boolean } { const copyExtent = reifyExtent3D(copySize_); const info = getBlockInfoForTextureFormat(format); assert(!!info.bytesPerBlock); assert(copyExtent.width % info.blockWidth === 0); assert(copyExtent.height % info.blockHeight === 0); const sizeInBlocks = { width: copyExtent.width / info.blockWidth, height: copyExtent.height / info.blockHeight, depthOrArrayLayers: copyExtent.depthOrArrayLayers, } as const; const bytesInLastRow = sizeInBlocks.width * info.bytesPerBlock; let valid = true; const offset = layout.offset ?? 0; if (method !== 'WriteTexture') { if (offset % info.bytesPerBlock !== 0) valid = false; if (layout.bytesPerRow && layout.bytesPerRow % 256 !== 0) valid = false; } let requiredBytesInCopy = 0; { let { bytesPerRow, rowsPerImage } = layout; // If bytesPerRow or rowsPerImage is invalid, guess a value for the sake of various tests that // don't actually care about the exact value. // (In particular for validation tests that want to test invalid bytesPerRow or rowsPerImage but // need to make sure the total buffer size is still big enough.) if (!validateBytesPerRow({ bytesPerRow, bytesInLastRow, sizeInBlocks })) { bytesPerRow = undefined; valid = false; } if (!validateRowsPerImage({ rowsPerImage, sizeInBlocks })) { rowsPerImage = undefined; valid = false; } // Pick values for cases when (a) bpr/rpi was invalid or (b) they're validly undefined. bytesPerRow ??= align(info.bytesPerBlock * sizeInBlocks.width, 256); rowsPerImage ??= sizeInBlocks.height; if (copyExtent.depthOrArrayLayers > 1) { const bytesPerImage = bytesPerRow * rowsPerImage; const bytesBeforeLastImage = bytesPerImage * (copyExtent.depthOrArrayLayers - 1); requiredBytesInCopy += bytesBeforeLastImage; } if (copyExtent.depthOrArrayLayers > 0) { if (sizeInBlocks.height > 1) requiredBytesInCopy += bytesPerRow * (sizeInBlocks.height - 1); if (sizeInBlocks.height > 0) requiredBytesInCopy += bytesInLastRow; } } return { minDataSizeOrOverestimate: offset + requiredBytesInCopy, copyValid: valid }; }