diff options
Diffstat (limited to '')
-rw-r--r-- | dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/sampling/anisotropy.spec.ts | 320 |
1 files changed, 320 insertions, 0 deletions
diff --git a/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/sampling/anisotropy.spec.ts b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/sampling/anisotropy.spec.ts new file mode 100644 index 0000000000..705f317f5b --- /dev/null +++ b/dom/webgpu/tests/cts/checkout/src/webgpu/api/operation/sampling/anisotropy.spec.ts @@ -0,0 +1,320 @@ +export const description = ` +Tests the behavior of anisotropic filtering. + +TODO: +Note that anisotropic filtering is never guaranteed to occur, but we might be able to test some +things. If there are no guarantees we can issue warnings instead of failures. Ideas: + - No *more* than the provided maxAnisotropy samples are used, by testing how many unique + sample values come out of the sample operation. + - Check anisotropy is done in the correct direction (by having a 2D gradient and checking we get + more of the color in the correct direction). +`; + +import { makeTestGroup } from '../../../../common/framework/test_group.js'; +import { assert } from '../../../../common/util/util.js'; +import { GPUTest } from '../../../gpu_test.js'; +import { checkElementsEqual } from '../../../util/check_contents.js'; + +const kRTSize = 16; +const kBytesPerRow = 256; +const xMiddle = kRTSize / 2; // we check the pixel value in the middle of the render target +const kColorAttachmentFormat = 'rgba8unorm'; +const kTextureFormat = 'rgba8unorm'; +const colors = [ + new Uint8Array([0xff, 0x00, 0x00, 0xff]), // miplevel = 0 + new Uint8Array([0x00, 0xff, 0x00, 0xff]), // miplevel = 1 + new Uint8Array([0x00, 0x00, 0xff, 0xff]), // miplevel = 2 +]; +const checkerColors = [ + new Uint8Array([0xff, 0x00, 0x00, 0xff]), + new Uint8Array([0x00, 0xff, 0x00, 0xff]), +]; + +// renders texture a slanted plane placed in a specific way +class SamplerAnisotropicFilteringSlantedPlaneTest extends GPUTest { + copyRenderTargetToBuffer(rt: GPUTexture): GPUBuffer { + const byteLength = kRTSize * kBytesPerRow; + const buffer = this.device.createBuffer({ + size: byteLength, + usage: GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST, + }); + + const commandEncoder = this.device.createCommandEncoder(); + commandEncoder.copyTextureToBuffer( + { texture: rt, mipLevel: 0, origin: [0, 0, 0] }, + { buffer, bytesPerRow: kBytesPerRow, rowsPerImage: kRTSize }, + { width: kRTSize, height: kRTSize, depthOrArrayLayers: 1 } + ); + this.queue.submit([commandEncoder.finish()]); + + return buffer; + } + + private pipeline: GPURenderPipeline | undefined; + async init(): Promise<void> { + await super.init(); + + this.pipeline = this.device.createRenderPipeline({ + layout: 'auto', + vertex: { + module: this.device.createShaderModule({ + code: ` + struct Outputs { + @builtin(position) Position : vec4<f32>, + @location(0) fragUV : vec2<f32>, + }; + + @vertex fn main( + @builtin(vertex_index) VertexIndex : u32) -> Outputs { + var position : array<vec3<f32>, 6> = array<vec3<f32>, 6>( + vec3<f32>(-0.5, 0.5, -0.5), + vec3<f32>(0.5, 0.5, -0.5), + vec3<f32>(-0.5, 0.5, 0.5), + vec3<f32>(-0.5, 0.5, 0.5), + vec3<f32>(0.5, 0.5, -0.5), + vec3<f32>(0.5, 0.5, 0.5)); + // uv is pre-scaled to mimic repeating tiled texture + var uv : array<vec2<f32>, 6> = array<vec2<f32>, 6>( + vec2<f32>(0.0, 0.0), + vec2<f32>(1.0, 0.0), + vec2<f32>(0.0, 50.0), + vec2<f32>(0.0, 50.0), + vec2<f32>(1.0, 0.0), + vec2<f32>(1.0, 50.0)); + // draw a slanted plane in a specific way + let matrix : mat4x4<f32> = mat4x4<f32>( + vec4<f32>(-1.7320507764816284, 1.8322050568049563e-16, -6.176817699518044e-17, -6.170640314703498e-17), + vec4<f32>(-2.1211504944260596e-16, -1.496108889579773, 0.5043753981590271, 0.5038710236549377), + vec4<f32>(0.0, -43.63650894165039, -43.232173919677734, -43.18894577026367), + vec4<f32>(0.0, 21.693578720092773, 21.789791107177734, 21.86800193786621)); + + var output : Outputs; + output.fragUV = uv[VertexIndex]; + output.Position = matrix * vec4<f32>(position[VertexIndex], 1.0); + return output; + } + `, + }), + entryPoint: 'main', + }, + fragment: { + module: this.device.createShaderModule({ + code: ` + @group(0) @binding(0) var sampler0 : sampler; + @group(0) @binding(1) var texture0 : texture_2d<f32>; + + @fragment fn main( + @builtin(position) FragCoord : vec4<f32>, + @location(0) fragUV: vec2<f32>) + -> @location(0) vec4<f32> { + return textureSample(texture0, sampler0, fragUV); + } + `, + }), + entryPoint: 'main', + targets: [{ format: 'rgba8unorm' }], + }, + primitive: { topology: 'triangle-list' }, + }); + } + + // return the render target texture object + drawSlantedPlane(textureView: GPUTextureView, sampler: GPUSampler): GPUTexture { + // make sure it's already initialized + assert(this.pipeline !== undefined); + + const bindGroup = this.device.createBindGroup({ + entries: [ + { binding: 0, resource: sampler }, + { binding: 1, resource: textureView }, + ], + layout: this.pipeline.getBindGroupLayout(0), + }); + + const colorAttachment = this.device.createTexture({ + format: kColorAttachmentFormat, + size: { width: kRTSize, height: kRTSize, depthOrArrayLayers: 1 }, + usage: GPUTextureUsage.COPY_SRC | GPUTextureUsage.RENDER_ATTACHMENT, + }); + const colorAttachmentView = colorAttachment.createView(); + + const encoder = this.device.createCommandEncoder(); + const pass = encoder.beginRenderPass({ + colorAttachments: [ + { + view: colorAttachmentView, + storeOp: 'store', + clearValue: { r: 0.0, g: 0.0, b: 0.0, a: 1.0 }, + loadOp: 'clear', + }, + ], + }); + pass.setPipeline(this.pipeline); + pass.setBindGroup(0, bindGroup); + pass.draw(6); + pass.end(); + this.device.queue.submit([encoder.finish()]); + + return colorAttachment; + } +} + +export const g = makeTestGroup(SamplerAnisotropicFilteringSlantedPlaneTest); + +g.test('anisotropic_filter_checkerboard') + .desc( + `Anisotropic filter rendering tests that draws a slanted plane and samples from a texture + that only has a top level mipmap, the content of which is like a checkerboard. + We will check the rendering result using sampler with maxAnisotropy values to be + different from each other, as the sampling rate is different. + We will also check if those large maxAnisotropy values are clamped so that rendering is the + same as the supported upper limit say 16. + A similar webgl demo is at https://jsfiddle.net/yqnbez24` + ) + .fn(async t => { + // init texture with only a top level mipmap + const textureSize = 32; + const texture = t.device.createTexture({ + mipLevelCount: 1, + size: { width: textureSize, height: textureSize, depthOrArrayLayers: 1 }, + format: kTextureFormat, + usage: GPUTextureUsage.COPY_DST | GPUTextureUsage.TEXTURE_BINDING, + }); + + const textureEncoder = t.device.createCommandEncoder(); + + const bufferSize = kBytesPerRow * textureSize; // RGBA8 for each pixel (256 > 16 * 4) + + // init checkerboard texture data + const data: Uint8Array = new Uint8Array(bufferSize); + for (let r = 0; r < textureSize; r++) { + const o = r * kBytesPerRow; + for (let c = o, end = o + textureSize * 4; c < end; c += 4) { + const cid = (r + (c - o) / 4) % 2; + const color = checkerColors[cid]; + data[c] = color[0]; + data[c + 1] = color[1]; + data[c + 2] = color[2]; + data[c + 3] = color[3]; + } + } + const buffer = t.makeBufferWithContents( + data, + GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST + ); + const bytesPerRow = kBytesPerRow; + const rowsPerImage = textureSize; + + textureEncoder.copyBufferToTexture( + { + buffer, + bytesPerRow, + rowsPerImage, + }, + { + texture, + mipLevel: 0, + origin: [0, 0, 0], + }, + [textureSize, textureSize, 1] + ); + + t.device.queue.submit([textureEncoder.finish()]); + + const textureView = texture.createView(); + const byteLength = kRTSize * kBytesPerRow; + const results = []; + + for (const maxAnisotropy of [1, 16, 1024]) { + const sampler = t.device.createSampler({ + magFilter: 'linear', + minFilter: 'linear', + mipmapFilter: 'linear', + maxAnisotropy, + }); + const result = await t.readGPUBufferRangeTyped( + t.copyRenderTargetToBuffer(t.drawSlantedPlane(textureView, sampler)), + { type: Uint8Array, typedLength: byteLength } + ); + results.push(result); + } + + const check0 = checkElementsEqual(results[0].data, results[1].data); + if (check0 === undefined) { + t.warn('Render results with sampler.maxAnisotropy being 1 and 16 should be different.'); + } + const check1 = checkElementsEqual(results[1].data, results[2].data); + if (check1 !== undefined) { + t.expect( + false, + 'Render results with sampler.maxAnisotropy being 16 and 1024 should be the same.' + ); + } + + for (const result of results) { + result.cleanup(); + } + }); + +g.test('anisotropic_filter_mipmap_color') + .desc( + `Anisotropic filter rendering tests that draws a slanted plane and samples from a texture + containing mipmaps of different colors. Given the same fragment with dFdx and dFdy for uv being different, + sampler with bigger maxAnisotropy value tends to bigger mip levels to provide better details. + We can then look at the color of the fragment to know which mip level is being sampled from and to see + if it fits expectations. + A similar webgl demo is at https://jsfiddle.net/t8k7c95o/5/` + ) + .paramsSimple([ + { + maxAnisotropy: 1, + _results: [ + { coord: { x: xMiddle, y: 2 }, expected: colors[2] }, + { coord: { x: xMiddle, y: 6 }, expected: [colors[0], colors[1]] }, + ], + _generateWarningOnly: false, + }, + { + maxAnisotropy: 4, + _results: [ + { coord: { x: xMiddle, y: 2 }, expected: [colors[0], colors[1]] }, + { coord: { x: xMiddle, y: 6 }, expected: colors[0] }, + ], + _generateWarningOnly: true, + }, + ]) + .fn(async t => { + const texture = t.createTexture2DWithMipmaps(colors); + + const textureView = texture.createView(); + + const sampler = t.device.createSampler({ + magFilter: 'linear', + minFilter: 'linear', + mipmapFilter: 'linear', + maxAnisotropy: t.params.maxAnisotropy, + }); + + const colorAttachment = t.drawSlantedPlane(textureView, sampler); + + for (const entry of t.params._results) { + if (entry.expected instanceof Uint8Array) { + // equal exactly one color + t.expectSinglePixelIn2DTexture(colorAttachment, kColorAttachmentFormat, entry.coord, { + exp: entry.expected, + generateWarningOnly: t.params._generateWarningOnly, + }); + } else { + // a lerp between two colors + t.expectSinglePixelBetweenTwoValuesIn2DTexture( + colorAttachment, + kColorAttachmentFormat, + entry.coord, + { + exp: entry.expected as [Uint8Array, Uint8Array], + generateWarningOnly: t.params._generateWarningOnly, + } + ); + } + } + }); |