in src/webgpu/shader/execution/expression/call/builtin/texture_utils.ts [1642:1875]
function softwareTextureReadMipLevel<T extends Dimensionality>(
call: TextureCall<T>,
softwareTexture: SoftwareTexture,
sampler: GPUSamplerDescriptor | undefined,
mipLevel: number
): PerTexelComponent<number> {
assert(mipLevel % 1 === 0);
const { format } = softwareTexture.texels[0];
const rep = kTexelRepresentationInfo[format];
const { baseMipLevel, baseMipLevelSize, baseArrayLayer, arrayLayerCount } =
getBaseMipLevelInfo(softwareTexture);
const mipLevelSize = virtualMipSize(
softwareTexture.descriptor.dimension || '2d',
baseMipLevelSize,
mipLevel
);
const addressMode: GPUAddressMode[] =
call.builtin === 'textureSampleBaseClampToEdge'
? ['clamp-to-edge', 'clamp-to-edge', 'clamp-to-edge']
: [
sampler?.addressModeU ?? 'clamp-to-edge',
sampler?.addressModeV ?? 'clamp-to-edge',
sampler?.addressModeW ?? 'clamp-to-edge',
];
const isCube = isCubeViewDimension(softwareTexture.viewDescriptor);
const arrayIndexMult = isCube ? 6 : 1;
const numLayers = arrayLayerCount / arrayIndexMult;
assert(numLayers % 1 === 0);
const textureSizeForCube = [mipLevelSize[0], mipLevelSize[1], 6];
const load = (at: number[]) => {
const zFromArrayIndex =
call.arrayIndex !== undefined
? clamp(call.arrayIndex, { min: 0, max: numLayers - 1 }) * arrayIndexMult
: 0;
return softwareTexture.texels[mipLevel + baseMipLevel].color({
x: Math.floor(at[0]),
y: Math.floor(at[1] ?? 0),
z: Math.floor(at[2] ?? 0) + zFromArrayIndex + baseArrayLayer,
sampleIndex: call.sampleIndex,
});
};
switch (call.builtin) {
case 'textureGather':
case 'textureGatherCompare':
case 'textureSample':
case 'textureSampleBias':
case 'textureSampleBaseClampToEdge':
case 'textureSampleCompare':
case 'textureSampleCompareLevel':
case 'textureSampleGrad':
case 'textureSampleLevel': {
let coords = toArray(call.coords!);
if (isCube) {
coords = convertCubeCoordToNormalized3DTextureCoord(coords as vec3);
}
// convert normalized to absolute texel coordinate
// ┌───┬───┬───┬───┐
// │ a │ │ │ │ norm: a = 1/8, b = 7/8
// ├───┼───┼───┼───┤ abs: a = 0, b = 3
// │ │ │ │ │
// ├───┼───┼───┼───┤
// │ │ │ │ │
// ├───┼───┼───┼───┤
// │ │ │ │ b │
// └───┴───┴───┴───┘
let at = coords.map((v, i) => v * (isCube ? textureSizeForCube : mipLevelSize)[i] - 0.5);
// Apply offset in whole texel units
// This means the offset is added at each mip level in texels. There's no
// scaling for each level.
if (call.offset !== undefined) {
at = add(at, toArray(call.offset));
}
const samples: { at: number[]; weight: number }[] = [];
const filter = isBuiltinGather(call.builtin) ? 'linear' : sampler?.minFilter ?? 'nearest';
switch (filter) {
case 'linear': {
// 'p0' is the lower texel for 'at'
const p0 = at.map(v => Math.floor(v));
// 'p1' is the higher texel for 'at'
// If it's cube then don't advance Z.
const p1 = p0.map((v, i) => v + (isCube ? (i === 2 ? 0 : 1) : 1));
// interpolation weights for p0 and p1
const p1W = at.map((v, i) => v - p0[i]);
const p0W = p1W.map(v => 1 - v);
switch (coords.length) {
case 1:
samples.push({ at: p0, weight: p0W[0] });
samples.push({ at: p1, weight: p1W[0] });
break;
case 2: {
// Note: These are ordered to match textureGather
samples.push({ at: [p0[0], p1[1]], weight: p0W[0] * p1W[1] });
samples.push({ at: p1, weight: p1W[0] * p1W[1] });
samples.push({ at: [p1[0], p0[1]], weight: p1W[0] * p0W[1] });
samples.push({ at: p0, weight: p0W[0] * p0W[1] });
break;
}
case 3: {
// cube sampling, here in the software renderer, is the same
// as 2d sampling. We'll sample at most 4 texels. The weights are
// the same as if it was just one plane. If the points fall outside
// the slice they'll be wrapped by wrapFaceCoordToCubeFaceAtEdgeBoundaries
// below.
if (isCube) {
// Note: These are ordered to match textureGather
samples.push({ at: [p0[0], p1[1], p0[2]], weight: p0W[0] * p1W[1] });
samples.push({ at: p1, weight: p1W[0] * p1W[1] });
samples.push({ at: [p1[0], p0[1], p0[2]], weight: p1W[0] * p0W[1] });
samples.push({ at: p0, weight: p0W[0] * p0W[1] });
const ndx = getUnusedCubeCornerSampleIndex(mipLevelSize[0], coords as vec3);
if (ndx >= 0) {
// # Issues with corners of cubemaps
//
// note: I tried multiple things here
//
// 1. distribute 1/3 of the weight of the removed sample to each of the remaining samples
// 2. distribute 1/2 of the weight of the removed sample to the 2 samples that are not the "main" sample.
// 3. normalize the weights of the remaining 3 samples.
//
// none of them matched the M1 in all cases. Checking the dEQP I found this comment
//
// > If any of samples is out of both edges, implementations can do pretty much anything according to spec.
// https://github.com/KhronosGroup/VK-GL-CTS/blob/d2d6aa65607383bb29c8398fe6562c6b08b4de57/framework/common/tcuTexCompareVerifier.cpp#L882
//
// If I understand this correctly it matches the OpenGL ES 3.1 spec it says
// it's implementation defined.
//
// > OpenGL ES 3.1 section 8.12.1 Seamless Cubemap Filtering
// >
// > - If a texture sample location would lie in the texture
// > border in both u and v (in one of the corners of the
// > cube), there is no unique neighboring face from which to
// > extract one texel. The recommended method to generate this
// > texel is to average the values of the three available
// > samples. However, implementations are free to construct
// > this fourth texel in another way, so long as, when the
// > three available samples have the same value, this texel
// > also has that value.
//
// I'm not sure what "average the values of the three available samples"
// means. To me that would be (a+b+c)/3 or in other words, set all the
// weights to 0.33333 but that's not what the M1 is doing.
//
// We could check that, given the 3 texels at the corner, if all 3 texels
// are the same value then the result must be the same value. Otherwise,
// the result must be between the 3 values. For now, the code that
// chooses test coordinates avoids corners. This has the restriction
// that the smallest mip level be at least 4x4 so there are some non
// corners to choose from.
unreachable(
`corners of cubemaps are not testable:\n ${describeTextureCall(call)}`
);
}
} else {
const p = [p0, p1];
const w = [p0W, p1W];
for (let z = 0; z < 2; ++z) {
for (let y = 0; y < 2; ++y) {
for (let x = 0; x < 2; ++x) {
samples.push({
at: [p[x][0], p[y][1], p[z][2]],
weight: w[x][0] * w[y][1] * w[z][2],
});
}
}
}
}
break;
}
}
break;
}
case 'nearest': {
const p = at.map(v => Math.round(quantizeToF32(v)));
samples.push({ at: p, weight: 1 });
break;
}
default:
unreachable();
}
if (isBuiltinGather(call.builtin)) {
const componentNdx = call.component ?? 0;
assert(componentNdx >= 0 && componentNdx < 4);
assert(samples.length === 4);
const component = kRGBAComponents[componentNdx];
const out: PerTexelComponent<number> = {};
samples.forEach((sample, i) => {
const c = isCube
? wrapFaceCoordToCubeFaceAtEdgeBoundaries(mipLevelSize[0], sample.at as vec3)
: applyAddressModesToCoords(addressMode, mipLevelSize, sample.at);
const v = load(c);
const postV = applyCompare(call, sampler, rep.componentOrder, v);
const rgba = convertPerTexelComponentToResultFormat(postV, format);
out[kRGBAComponents[i]] = rgba[component];
});
return out;
}
const out: PerTexelComponent<number> = {};
for (const sample of samples) {
const c = isCube
? wrapFaceCoordToCubeFaceAtEdgeBoundaries(mipLevelSize[0], sample.at as vec3)
: applyAddressModesToCoords(addressMode, mipLevelSize, sample.at);
const v = load(c);
const postV = applyCompare(call, sampler, rep.componentOrder, v);
for (const component of rep.componentOrder) {
out[component] = (out[component] ?? 0) + postV[component]! * sample.weight;
}
}
return convertPerTexelComponentToResultFormat(out, format);
}
case 'textureLoad': {
const out: PerTexelComponent<number> = isOutOfBoundsCall(softwareTexture, call)
? zeroValuePerTexelComponent(rep.componentOrder)
: load(call.coords!);
return convertPerTexelComponentToResultFormat(out, format);
}
default:
unreachable();
}
}