Skip to content

Commit

Permalink
Test texture mix weights separately (#4051)
Browse files Browse the repository at this point in the history
* 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.
  • Loading branch information
greggman authored Nov 20, 2024
1 parent 4f32a04 commit bc88622
Show file tree
Hide file tree
Showing 2 changed files with 150 additions and 87 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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);
});
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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()}`
);
}

Expand Down Expand Up @@ -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({
Expand Down

0 comments on commit bc88622

Please sign in to comment.