From 080f9b61e9d6f4e59d74bb3ea2a336190d0edaa8 Mon Sep 17 00:00:00 2001 From: Gregg Tavares Date: Tue, 19 Nov 2024 18:10:12 -0800 Subject: [PATCH] Test texture mix weights separately Before this change, the texture builtin utils would query how the local GPU mixes texture mip levels. It expected that to be close to a linear interpolation which is true for most GPUs. The known exceptions are Mac AMD and Mac Intel in a compute stage. It would check how far from linear iterpolation the weights are. If the weights were too far off linear interpolation it would assert. With this case, those asserts for linear interpolation are moved from all the tests that use these weights to a separate test. This allows the texture builtin tests to run further on devices that have bad mix weights with the new test at least documenting a particular GPU's failure to implement linear interpolation between mip levels. --- .../call/builtin/texture_utils.spec.ts | 125 ++++++++++++++++++ .../expression/call/builtin/texture_utils.ts | 93 +------------ 2 files changed, 131 insertions(+), 87 deletions(-) diff --git a/src/webgpu/shader/execution/expression/call/builtin/texture_utils.spec.ts b/src/webgpu/shader/execution/expression/call/builtin/texture_utils.spec.ts index caa544fac318..2e5d539f2cf1 100644 --- a/src/webgpu/shader/execution/expression/call/builtin/texture_utils.spec.ts +++ b/src/webgpu/shader/execution/expression/call/builtin/texture_utils.spec.ts @@ -12,12 +12,15 @@ import { PerTexelComponent, TexelRepresentationInfo, } from '../../../../../util/texture/texel_data.js'; +import { kShaderStages } from '../../../../validation/decl/util.js'; import { chooseTextureSize, createTextureWithRandomDataAndGetTexels, + graphWeights, isSupportedViewFormatCombo, makeRandomDepthComparisonTexelGenerator, + queryMipLevelMixWeightsForDevice, readTextureToTexelViews, texelsApproximatelyEqual, } from './texture_utils.js'; @@ -151,3 +154,125 @@ g.test('readTextureToTexelViews') assert(errors.length === 0, errors.join('\n')); } }); + +function validateWeights(stage: string, builtin: string, weights: number[]) { + const kNumMixSteps = weights.length - 1; + const showWeights = () => ` +${weights.map((v, i) => `${i.toString().padStart(2)}: ${v}`).join('\n')} + +e = expected +A = actual +${graphWeights(32, weights)} +`; + + assert( + weights[0] === 0, + `stage: ${stage}, ${builtin}, weight 0 expected 0 but was ${weights[0]}\n${showWeights()}` + ); + assert( + weights[kNumMixSteps] === 1, + `stage: ${stage}, ${builtin}, top weight expected 1 but was ${ + weights[kNumMixSteps] + }\n${showWeights()}` + ); + + const dx = 1 / kNumMixSteps; + for (let i = 0; i < kNumMixSteps; ++i) { + const dy = weights[i + 1] - weights[i]; + // dy / dx because dy might be 0 + const slope = dy / dx; + assert( + slope >= 0, + `stage: ${stage}, ${builtin}, weight[${i}] was not <= weight[${i + 1}]\n${showWeights()}` + ); + assert( + slope <= 2, + `stage: ${stage}, ${builtin}, slope from weight[${i}] to weight[${ + i + 1 + }] is > 2.\n${showWeights()}` + ); + } + + assert( + new Set(weights).size >= ((weights.length * 0.66) | 0), + `stage: ${stage}, ${builtin}, expected more unique weights\n${showWeights()}` + ); +} + +g.test('weights') + .desc( + ` +Test the mip level weights are linear. + +Given 2 mip levels, textureSampleLevel(....., mipLevel) should return +mix(colorFromLevel0, colorFromLevel1, mipLevel). + +Similarly, textureSampleGrad(...., ddx, ...) where ddx is +vec2(mix(1.0, 2.0, mipLevel) / textureWidth, 0) should so return +mix(colorFromLevel0, colorFromLevel1, mipLevel). + +If we put 0,0,0,0 in level 0 and 1,1,1,1 in level 1 then we should arguably +be able to assert + + for (mipLevel = 0; mipLevel <= 1, mipLevel += 0.01) { + assert(textureSampleLevel(t, s, vec2f(0.5), mipLevel) === mipLevel) + ddx = vec2(mix(1.0, 2.0, mipLevel) / textureWidth, 0) + assert(textureSampleGrad(t, s, vec2f(0.5), ddx, vec2f(0)) === mipLevel) + } + +Unfortunately, the GPUs do not do this. In particular: + +AMD Mac goes like this: Not great but we allow it + + +----------------+ + | ***| + | ** | + | * | + | ** | + | ** | + | * | + | ** | + |*** | + +----------------+ + + Intel Mac goes like this in a compute stage + + +----------------+ + | *******| + | * | + | * | + | * | + | * | + | * | + | * | + |******* | + +----------------+ + +Where as they should go like this + + +----------------+ + | **| + | ** | + | ** | + | ** | + | ** | + | ** | + | ** | + |** | + +----------------+ + +To make the texture builtin tests pass, they use the mix weights we query from the GPU +even if they are arguably bad. This test is to surface the failure of the GPU +to use mix weights the approximate a linear interpolation. + +We allow the AMD case as but disallow extreme Intel case. WebGPU implementations +are supposed to work around this issue by poly-filling on devices that fail this test. +` + ) + .params(u => u.combine('stage', kShaderStages)) + .fn(async t => { + const { stage } = t.params; + const weights = await queryMipLevelMixWeightsForDevice(t, t.params.stage); + validateWeights(stage, 'textureSampleLevel', weights.sampleLevelWeights); + validateWeights(stage, 'textureSampleGrad', weights.softwareMixToGPUMixGradWeights); + }); diff --git a/src/webgpu/shader/execution/expression/call/builtin/texture_utils.ts b/src/webgpu/shader/execution/expression/call/builtin/texture_utils.ts index 97eefa81467c..f76d49cc7030 100644 --- a/src/webgpu/shader/execution/expression/call/builtin/texture_utils.ts +++ b/src/webgpu/shader/execution/expression/call/builtin/texture_utils.ts @@ -246,7 +246,10 @@ function* linear0to1OverN(n: number) { } } -function graphWeights(height: number, weights: number[]) { +/** + * Generates an ascii graph of weights + */ +export function graphWeights(height: number, weights: number[]) { const graph = makeGraph(weights.length, height); graph.plotValues(linear0to1OverN(weights.length - 1), 1); graph.plotValues(weights, 2); @@ -276,92 +279,8 @@ ${graphWeights(32, weights)} weights[kMipLevelWeightSteps] }\n${showWeights()}` ); - - // Note: for 16 steps, these are the AMD weights - // - // standard - // step mipLevel gpu AMD - // ---- -------- -------- ---------- - // 0: 0 0 0 - // 1: 0.0625 0.0625 0 - // 2: 0.125 0.125 0.03125 - // 3: 0.1875 0.1875 0.109375 - // 4: 0.25 0.25 0.1875 - // 5: 0.3125 0.3125 0.265625 - // 6: 0.375 0.375 0.34375 - // 7: 0.4375 0.4375 0.421875 - // 8: 0.5 0.5 0.5 - // 9: 0.5625 0.5625 0.578125 - // 10: 0.625 0.625 0.65625 - // 11: 0.6875 0.6875 0.734375 - // 12: 0.75 0.75 0.8125 - // 13: 0.8125 0.8125 0.890625 - // 14: 0.875 0.875 0.96875 - // 15: 0.9375 0.9375 1 - // 16: 1 1 1 - // - // notice step 1 is 0 and step 15 is 1. - // so we only check the 1 through 14. - // - // Note: these 2 changes are effectively here to catch Intel Mac - // issues and require implementations to work around them. - // - // Ideally the weights should form a straight line - // - // +----------------+ - // | **| - // | ** | - // | ** | - // | ** | - // | ** | - // | ** | - // | ** | - // |** | - // +----------------+ - // - // AMD Mac goes like this: Not great but we allow it - // - // +----------------+ - // | ***| - // | ** | - // | * | - // | ** | - // | ** | - // | * | - // | ** | - // |*** | - // +----------------+ - // - // Intel Mac goes like this: Unacceptable - // - // +----------------+ - // | *******| - // | * | - // | * | - // | * | - // | * | - // | * | - // | * | - // |******* | - // +----------------+ - // - const dx = 1 / kMipLevelWeightSteps; - for (let i = 0; i < kMipLevelWeightSteps; ++i) { - const dy = weights[i + 1] - weights[i]; - // dy / dx because dy might be 0 - const slope = dy / dx; - assert( - slope >= 0, - `stage: ${stage}, weight[${i}] was not <= weight[${i + 1}]\n${showWeights()}` - ); - assert( - slope <= 2, - `stage: ${stage}, slope from weight[${i}] to weight[${i + 1}] is > 2.\n${showWeights()}` - ); - } - assert( - new Set(weights).size >= ((weights.length * 0.66) | 0), + new Set(weights).size >= ((weights.length * 0.25) | 0), `stage: ${stage}, expected more unique weights\n${showWeights()}` ); } @@ -467,7 +386,7 @@ ${graphWeights(32, weights)} * +--------+--------+--------+--------+ */ -async function queryMipLevelMixWeightsForDevice(t: GPUTest, stage: ShaderStage) { +export async function queryMipLevelMixWeightsForDevice(t: GPUTest, stage: ShaderStage) { const { device } = t; const kNumWeightTypes = 2; const module = device.createShaderModule({