From bc88622f42d01a22d70126a189d07f8263408e53 Mon Sep 17 00:00:00 2001 From: Greggman Date: Tue, 19 Nov 2024 22:14:00 -0800 Subject: [PATCH] Test texture mix weights separately (#4051) * 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 | 138 ++++++++++++++++++ .../expression/call/builtin/texture_utils.ts | 99 ++----------- 2 files changed, 150 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..483b8f36a456 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,138 @@ g.test('readTextureToTexelViews') assert(errors.length === 0, errors.join('\n')); } }); + +function validateWeights(t: GPUTest, 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)} +`; + + t.expect( + weights[0] === 0, + `stage: ${stage}, ${builtin}, weight 0 expected 0 but was ${weights[0]}\n${showWeights()}` + ); + t.expect( + 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; + + // Validate the slope is not going down. + assert( + slope >= 0, + `stage: ${stage}, ${builtin}, weight[${i}] was not <= weight[${i + 1}]\n${showWeights()}` + ); + + // Validate the slope is not going up too steeply. + // The correct slope is 1 / kNumMixSteps but Mac AMD and Mac Intel + // have the wrong mix weights. 2 is enough to pass Mac AMD which we + // decided is ok but will fail on Mac Intel in compute stage which we + // decides is not ok. + assert( + slope <= 2, + `stage: ${stage}, ${builtin}, slope from weight[${i}] to weight[${ + i + 1 + }] is > 2.\n${showWeights()}` + ); + } + + // Test that we don't have a mostly flat set of weights. + // Note: Ideally every value is unique but 66% is enough to pass AMD Mac + // which we decided was ok but high enough to fail Intel Mac in a compute stage + // which we decided is not ok. + const kMinPercentUniqueWeights = 66; + t.expect( + new Set(weights).size >= ((weights.length * kMinPercentUniqueWeights * 0.01) | 0), + `stage: ${stage}, ${builtin}, expected at least ~${kMinPercentUniqueWeights}% 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(t, stage, 'textureSampleLevel', weights.sampleLevelWeights); + validateWeights(t, 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..b70630fdd07e 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); @@ -277,92 +280,14 @@ ${graphWeights(32, weights)} }\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()}` - ); - } - + // Test that we don't have a mostly flat set of weights. + // This is also some small guarantee that we actually read something. + // Note: Ideally every value is unique but 25% is about how many an Intel Mac + // returns in a compute stage. + const kMinPercentUniqueWeights = 25; assert( - new Set(weights).size >= ((weights.length * 0.66) | 0), - `stage: ${stage}, expected more unique weights\n${showWeights()}` + new Set(weights).size >= ((weights.length * kMinPercentUniqueWeights * 0.01) | 0), + `stage: ${stage}, expected at least ~${kMinPercentUniqueWeights}% unique weights\n${showWeights()}` ); } @@ -467,7 +392,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({