function softwareTextureReadMipLevel()

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();
  }
}