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({