diff --git a/src/webgpu/shader/execution/expression/call/builtin/textureLoad.spec.ts b/src/webgpu/shader/execution/expression/call/builtin/textureLoad.spec.ts
index 08c5217841a..c3b281cc3e4 100644
--- a/src/webgpu/shader/execution/expression/call/builtin/textureLoad.spec.ts
+++ b/src/webgpu/shader/execution/expression/call/builtin/textureLoad.spec.ts
@@ -756,13 +756,14 @@ Parameters:
       .combine('format', kPossibleStorageTextureFormats)
       .beginSubcases()
       .combine('samplePoints', kSamplePointMethods)
+      .combine('baseMipLevel', [0, 1] as const)
       .combine('C', ['i32', 'u32'] as const)
   )
   .beforeAllSubcases(t =>
     t.skipIfLanguageFeatureNotSupported('readonly_and_readwrite_storage_textures')
   )
   .fn(async t => {
-    const { format, stage, samplePoints, C } = t.params;
+    const { format, stage, samplePoints, C, baseMipLevel } = t.params;
 
     t.skipIfTextureFormatNotSupported(format);
     t.skipIfTextureFormatNotUsableAsStorageTexture(format);
@@ -774,12 +775,17 @@ Parameters:
       format,
       size,
       usage: GPUTextureUsage.COPY_DST | GPUTextureUsage.STORAGE_BINDING,
+      mipLevelCount: 3,
+    };
+    const viewDescriptor = {
+      baseMipLevel,
+      mipLevelCount: 1,
     };
     const { texels, texture } = await createTextureWithRandomDataAndGetTexels(t, descriptor);
-
+    const softwareTexture = { texels, descriptor, viewDescriptor };
     const calls: TextureCall<vec2>[] = generateTextureBuiltinInputs2D(50, {
       method: samplePoints,
-      descriptor,
+      softwareTexture,
       hashInputs: [stage, format, samplePoints, C],
     }).map(({ coords }) => {
       return {
@@ -789,7 +795,6 @@ Parameters:
       };
     });
     const textureType = `texture_storage_2d<${format}, read>`;
-    const viewDescriptor = {};
     const sampler = undefined;
     const results = await doTextureCalls(
       t,
@@ -802,7 +807,7 @@ Parameters:
     );
     const res = await checkCallResults(
       t,
-      { texels, descriptor, viewDescriptor },
+      softwareTexture,
       textureType,
       sampler,
       calls,
@@ -837,12 +842,20 @@ Parameters:
       .combine('C', ['i32', 'u32'] as const)
       .combine('A', ['i32', 'u32'] as const)
       .combine('depthOrArrayLayers', [1, 8] as const)
+      .combine('baseMipLevel', [0, 1] as const)
+      .combine('baseArrayLayer', [0, 1] as const)
+      .unless(t => t.depthOrArrayLayers === 1 && t.baseArrayLayer !== 0)
   )
-  .beforeAllSubcases(t =>
-    t.skipIfLanguageFeatureNotSupported('readonly_and_readwrite_storage_textures')
-  )
+  .beforeAllSubcases(t => {
+    t.skipIfLanguageFeatureNotSupported('readonly_and_readwrite_storage_textures');
+    t.skipIf(
+      t.isCompatibility && t.params.baseArrayLayer !== 0,
+      'compatibility mode does not support array layer sub ranges'
+    );
+  })
   .fn(async t => {
-    const { format, stage, samplePoints, C, A, depthOrArrayLayers } = t.params;
+    const { format, stage, samplePoints, C, A, depthOrArrayLayers, baseMipLevel, baseArrayLayer } =
+      t.params;
 
     t.skipIfTextureFormatNotSupported(format);
     t.skipIfTextureFormatNotUsableAsStorageTexture(format);
@@ -856,13 +869,21 @@ Parameters:
       size,
       usage: GPUTextureUsage.COPY_DST | GPUTextureUsage.STORAGE_BINDING,
       ...(t.isCompatibility && { textureBindingViewDimension: '2d-array' }),
+      mipLevelCount: 3,
+    };
+    const viewDescriptor: GPUTextureViewDescriptor = {
+      dimension: '2d-array',
+      baseMipLevel,
+      mipLevelCount: 1,
+      baseArrayLayer,
     };
     const { texels, texture } = await createTextureWithRandomDataAndGetTexels(t, descriptor);
+    const softwareTexture = { texels, descriptor, viewDescriptor };
 
     const calls: TextureCall<vec2>[] = generateTextureBuiltinInputs2D(50, {
       method: samplePoints,
-      descriptor,
-      arrayIndex: { num: texture.depthOrArrayLayers, type: A },
+      softwareTexture,
+      arrayIndex: { num: texture.depthOrArrayLayers - baseArrayLayer, type: A },
       hashInputs: [stage, format, samplePoints, C, A],
     }).map(({ coords, arrayIndex }) => {
       return {
@@ -874,9 +895,6 @@ Parameters:
       };
     });
     const textureType = `texture_storage_2d_array<${format}, read>`;
-    const viewDescriptor: GPUTextureViewDescriptor = {
-      dimension: '2d-array',
-    };
     const sampler = undefined;
     const results = await doTextureCalls(
       t,
diff --git a/src/webgpu/shader/execution/expression/call/builtin/textureSample.spec.ts b/src/webgpu/shader/execution/expression/call/builtin/textureSample.spec.ts
index 46e82fc75d9..b42579af3c3 100644
--- a/src/webgpu/shader/execution/expression/call/builtin/textureSample.spec.ts
+++ b/src/webgpu/shader/execution/expression/call/builtin/textureSample.spec.ts
@@ -154,9 +154,10 @@ Parameters:
       .combine('offset', [false, true] as const)
       .beginSubcases()
       .combine('samplePoints', kSamplePointMethods)
+      .combine('baseMipLevel', [0, 1] as const)
   )
   .fn(async t => {
-    const { format, samplePoints, modeU, modeV, filt: minFilter, offset } = t.params;
+    const { format, samplePoints, modeU, modeV, filt: minFilter, offset, baseMipLevel } = t.params;
     skipIfTextureFormatNotSupportedOrNeedsFilteringAndIsUnfilterable(t, minFilter, format);
 
     // We want at least 4 blocks or something wide enough for 3 mip levels.
@@ -168,7 +169,11 @@ Parameters:
       usage: GPUTextureUsage.COPY_DST | GPUTextureUsage.TEXTURE_BINDING,
       mipLevelCount: 3,
     };
+    const viewDescriptor = {
+      baseMipLevel,
+    };
     const { texels, texture } = await createTextureWithRandomDataAndGetTexels(t, descriptor);
+    const softwareTexture = { texels, descriptor, viewDescriptor };
     const sampler: GPUSamplerDescriptor = {
       addressModeU: kShortAddressModeToAddressMode[modeU],
       addressModeV: kShortAddressModeToAddressMode[modeV],
@@ -180,7 +185,7 @@ Parameters:
     const calls: TextureCall<vec2>[] = generateTextureBuiltinInputs2D(50, {
       sampler,
       method: samplePoints,
-      descriptor,
+      softwareTexture,
       derivatives: true,
       offset,
       hashInputs: [format, samplePoints, modeU, modeV, minFilter, offset],
@@ -193,7 +198,6 @@ Parameters:
         offset,
       };
     });
-    const viewDescriptor = {};
     const textureType = 'texture_2d<f32>';
     const results = await doTextureCalls(
       t,
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 19e8bcbdeef..3243eb9bce1 100644
--- a/src/webgpu/shader/execution/expression/call/builtin/texture_utils.ts
+++ b/src/webgpu/shader/execution/expression/call/builtin/texture_utils.ts
@@ -1115,6 +1115,28 @@ type RandomTextureOptions = {
   generator: PerPixelAtLevel<PerTexelComponent<number>>;
 };
 
+/**
+ * Gets the baseMipLevel, mipLevelCount, size of the baseMipLevel,
+ * baseArrayLayer, and arrayLayerCount
+ * taking into account the texture descriptor and the view descriptor.
+ */
+function getBaseMipLevelInfo(textureInfo: SoftwareTexture) {
+  const baseMipLevel = textureInfo.viewDescriptor.baseMipLevel ?? 0;
+  const mipLevelCount =
+    textureInfo.viewDescriptor.mipLevelCount ??
+    (textureInfo.descriptor.mipLevelCount ?? 1) - baseMipLevel;
+  const baseMipLevelSize = virtualMipSize(
+    textureInfo.descriptor.dimension ?? '2d',
+    textureInfo.descriptor.size,
+    baseMipLevel
+  );
+  const baseArrayLayer = textureInfo.viewDescriptor.baseArrayLayer ?? 0;
+  const arrayLayerCount =
+    textureInfo.viewDescriptor.arrayLayerCount ?? baseMipLevelSize[2] - baseArrayLayer;
+  baseMipLevelSize[2] = arrayLayerCount;
+  return { baseMipLevel, baseMipLevelSize, mipLevelCount, baseArrayLayer, arrayLayerCount };
+}
+
 /**
  * Make a generator for texels for depth comparison tests.
  */
@@ -1492,7 +1514,14 @@ function getUnusedCubeCornerSampleIndex(textureSize: number, coords: vec3) {
 
 const add = (a: number[], b: number[]) => apply(a, b, (x, y) => x + y);
 
-export interface Texture {
+/**
+ * The data needed by the software rendered to simulate a texture.
+ * In particular, it needs texels (the data), it needs a descriptor
+ * for the size, format, and dimension, and it needs a view descriptor
+ * for the viewDimension, baseMipLevel, mipLevelCount, baseArrayLayer,
+ * and arrayLayerCount.
+ */
+export interface SoftwareTexture {
   texels: TexelView[];
   descriptor: GPUTextureDescriptor;
   viewDescriptor: GPUTextureViewDescriptor;
@@ -1612,18 +1641,21 @@ function applyCompare<T extends Dimensionality>(
  */
 function softwareTextureReadMipLevel<T extends Dimensionality>(
   call: TextureCall<T>,
-  texture: Texture,
+  softwareTexture: SoftwareTexture,
   sampler: GPUSamplerDescriptor | undefined,
   mipLevel: number
 ): PerTexelComponent<number> {
   assert(mipLevel % 1 === 0);
-  const { format } = texture.texels[0];
+  const { format } = softwareTexture.texels[0];
   const rep = kTexelRepresentationInfo[format];
-  const textureSize = virtualMipSize(
-    texture.descriptor.dimension || '2d',
-    texture.descriptor.size,
+  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']
@@ -1633,21 +1665,21 @@ function softwareTextureReadMipLevel<T extends Dimensionality>(
           sampler?.addressModeW ?? 'clamp-to-edge',
         ];
 
-  const isCube = isCubeViewDimension(texture.viewDescriptor);
+  const isCube = isCubeViewDimension(softwareTexture.viewDescriptor);
   const arrayIndexMult = isCube ? 6 : 1;
-  const numLayers = textureSize[2] / arrayIndexMult;
+  const numLayers = arrayLayerCount / arrayIndexMult;
   assert(numLayers % 1 === 0);
-  const textureSizeForCube = [textureSize[0], textureSize[1], 6];
+  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 texture.texels[mipLevel].color({
+    return softwareTexture.texels[mipLevel + baseMipLevel].color({
       x: Math.floor(at[0]),
       y: Math.floor(at[1] ?? 0),
-      z: Math.floor(at[2] ?? 0) + zFromArrayIndex,
+      z: Math.floor(at[2] ?? 0) + zFromArrayIndex + baseArrayLayer,
       sampleIndex: call.sampleIndex,
     });
   };
@@ -1678,7 +1710,7 @@ function softwareTextureReadMipLevel<T extends Dimensionality>(
       // ├───┼───┼───┼───┤
       // │   │   │   │ b │
       // └───┴───┴───┴───┘
-      let at = coords.map((v, i) => v * (isCube ? textureSizeForCube : textureSize)[i] - 0.5);
+      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
@@ -1727,7 +1759,7 @@ function softwareTextureReadMipLevel<T extends Dimensionality>(
                 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(textureSize[0], coords as vec3);
+                const ndx = getUnusedCubeCornerSampleIndex(mipLevelSize[0], coords as vec3);
                 if (ndx >= 0) {
                   // # Issues with corners of cubemaps
                   //
@@ -1807,8 +1839,8 @@ function softwareTextureReadMipLevel<T extends Dimensionality>(
         const out: PerTexelComponent<number> = {};
         samples.forEach((sample, i) => {
           const c = isCube
-            ? wrapFaceCoordToCubeFaceAtEdgeBoundaries(textureSize[0], sample.at as vec3)
-            : applyAddressModesToCoords(addressMode, textureSize, sample.at);
+            ? 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);
@@ -1820,8 +1852,8 @@ function softwareTextureReadMipLevel<T extends Dimensionality>(
       const out: PerTexelComponent<number> = {};
       for (const sample of samples) {
         const c = isCube
-          ? wrapFaceCoordToCubeFaceAtEdgeBoundaries(textureSize[0], sample.at as vec3)
-          : applyAddressModesToCoords(addressMode, textureSize, sample.at);
+          ? 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) {
@@ -1832,7 +1864,7 @@ function softwareTextureReadMipLevel<T extends Dimensionality>(
       return convertPerTexelComponentToResultFormat(out, format);
     }
     case 'textureLoad': {
-      const out: PerTexelComponent<number> = isOutOfBoundsCall(texture, call)
+      const out: PerTexelComponent<number> = isOutOfBoundsCall(softwareTexture, call)
         ? zeroValuePerTexelComponent(rep.componentOrder)
         : load(call.coords!);
       return convertPerTexelComponentToResultFormat(out, format);
@@ -1849,25 +1881,25 @@ function softwareTextureReadLevel<T extends Dimensionality>(
   t: GPUTest,
   stage: ShaderStage,
   call: TextureCall<T>,
-  texture: Texture,
+  softwareTexture: SoftwareTexture,
   sampler: GPUSamplerDescriptor | undefined,
   mipLevel: number
 ): PerTexelComponent<number> {
-  const mipLevelCount = texture.texels.length;
-  const maxLevel = mipLevelCount - 1;
-
   if (!sampler) {
-    return softwareTextureReadMipLevel<T>(call, texture, sampler, mipLevel);
+    return softwareTextureReadMipLevel<T>(call, softwareTexture, sampler, mipLevel);
   }
 
+  const { mipLevelCount } = getBaseMipLevelInfo(softwareTexture);
+  const maxLevel = mipLevelCount - 1;
+
   const effectiveMipmapFilter = isBuiltinGather(call.builtin) ? 'nearest' : sampler.mipmapFilter;
   switch (effectiveMipmapFilter) {
     case 'linear': {
       const clampedMipLevel = clamp(mipLevel, { min: 0, max: maxLevel });
-      const baseMipLevel = Math.floor(clampedMipLevel);
+      const rootMipLevel = Math.floor(clampedMipLevel);
       const nextMipLevel = Math.ceil(clampedMipLevel);
-      const t0 = softwareTextureReadMipLevel<T>(call, texture, sampler, baseMipLevel);
-      const t1 = softwareTextureReadMipLevel<T>(call, texture, sampler, nextMipLevel);
+      const t0 = softwareTextureReadMipLevel<T>(call, softwareTexture, sampler, rootMipLevel);
+      const t1 = softwareTextureReadMipLevel<T>(call, softwareTexture, sampler, nextMipLevel);
       const weightType = call.builtin === 'textureSampleLevel' ? 'sampleLevelWeights' : 'identity';
       const mix = getWeightForMipLevel(t, stage, weightType, mipLevelCount, clampedMipLevel);
       assert(mix >= 0 && mix <= 1);
@@ -1884,10 +1916,8 @@ function softwareTextureReadLevel<T extends Dimensionality>(
       return out;
     }
     default: {
-      const baseMipLevel = Math.floor(
-        clamp(mipLevel + 0.5, { min: 0, max: texture.texels.length - 1 })
-      );
-      return softwareTextureReadMipLevel<T>(call, texture, sampler, baseMipLevel);
+      const baseMipLevel = Math.floor(clamp(mipLevel + 0.5, { min: 0, max: maxLevel }));
+      return softwareTextureReadMipLevel<T>(call, softwareTexture, sampler, baseMipLevel);
     }
   }
 }
@@ -1895,9 +1925,9 @@ function softwareTextureReadLevel<T extends Dimensionality>(
 function computeMipLevelFromGradients(
   ddx: readonly number[],
   ddy: readonly number[],
-  size: GPUExtent3D
+  baseMipLevelSize: GPUExtent3D
 ) {
-  const texSize = reifyExtent3D(size);
+  const texSize = reifyExtent3D(baseMipLevelSize);
   const textureSize = [texSize.width, texSize.height, texSize.depthOrArrayLayers];
 
   // Compute the mip level the same way textureSampleGrad does according to the spec.
@@ -1912,7 +1942,7 @@ function computeMipLevelFromGradients(
 
 function computeMipLevelFromGradientsForCall<T extends Dimensionality>(
   call: TextureCall<T>,
-  size: GPUExtent3D
+  baseMipLevelSize: GPUExtent3D
 ) {
   assert(!!call.ddx);
   assert(!!call.ddy);
@@ -1923,7 +1953,7 @@ function computeMipLevelFromGradientsForCall<T extends Dimensionality>(
   const ddx: readonly number[] = typeof call.ddx === 'number' ? [call.ddx] : call.ddx;
   const ddy: readonly number[] = typeof call.ddy === 'number' ? [call.ddy] : call.ddy;
 
-  return computeMipLevelFromGradients(ddx, ddy, size);
+  return computeMipLevelFromGradients(ddx, ddy, baseMipLevelSize);
 }
 
 /**
@@ -1933,18 +1963,25 @@ function softwareTextureReadGrad<T extends Dimensionality>(
   t: GPUTest,
   stage: ShaderStage,
   call: TextureCall<T>,
-  texture: Texture,
+  softwareTexture: SoftwareTexture,
   sampler?: GPUSamplerDescriptor
 ): PerTexelComponent<number> {
   const bias = call.bias === undefined ? 0 : clamp(call.bias, { min: -16.0, max: 15.99 });
   if (call.ddx) {
-    const mipLevel = computeMipLevelFromGradientsForCall(call, texture.descriptor.size);
-    const mipLevelCount = texture.descriptor.mipLevelCount ?? 1;
+    const { mipLevelCount, baseMipLevelSize } = getBaseMipLevelInfo(softwareTexture);
+    const mipLevel = computeMipLevelFromGradientsForCall(call, baseMipLevelSize);
     const clampedMipLevel = clamp(mipLevel + bias, { min: 0, max: mipLevelCount - 1 });
     const weightMipLevel = mapSoftwareMipLevelToGPUMipLevel(t, stage, clampedMipLevel);
-    return softwareTextureReadLevel(t, stage, call, texture, sampler, weightMipLevel);
+    return softwareTextureReadLevel(t, stage, call, softwareTexture, sampler, weightMipLevel);
   } else {
-    return softwareTextureReadLevel(t, stage, call, texture, sampler, (call.mipLevel ?? 0) + bias);
+    return softwareTextureReadLevel(
+      t,
+      stage,
+      call,
+      softwareTexture,
+      sampler,
+      (call.mipLevel ?? 0) + bias
+    );
   }
 }
 
@@ -1987,17 +2024,19 @@ function softwareTextureReadGrad<T extends Dimensionality>(
  * derivativeBase to be 1 otherwise it's 0 which makes the computed mip level
  * be -Infinity which means bias in `textureSampleBias` has no meaning.
  */
-function derivativeBaseForCall<T extends Dimensionality>(texture: Texture, isDDX: boolean) {
-  const texSize = reifyExtent3D(texture.descriptor.size);
-  const textureSize = [texSize.width, texSize.height, texSize.depthOrArrayLayers];
-  if (isCubeViewDimension(texture.viewDescriptor)) {
-    return (isDDX ? [1 / textureSize[0], 0, 1] : [0, 1 / textureSize[1], 1]) as T;
-  } else if (texture.descriptor.dimension === '3d') {
-    return (isDDX ? [1 / textureSize[0], 0, 0] : [0, 1 / textureSize[1], 0]) as T;
-  } else if (texture.descriptor.dimension === '1d') {
-    return [1 / textureSize[0]] as T;
+function derivativeBaseForCall<T extends Dimensionality>(
+  softwareTexture: SoftwareTexture,
+  isDDX: boolean
+) {
+  const { baseMipLevelSize } = getBaseMipLevelInfo(softwareTexture);
+  if (isCubeViewDimension(softwareTexture.viewDescriptor)) {
+    return (isDDX ? [1 / baseMipLevelSize[0], 0, 1] : [0, 1 / baseMipLevelSize[1], 1]) as T;
+  } else if (softwareTexture.descriptor.dimension === '3d') {
+    return (isDDX ? [1 / baseMipLevelSize[0], 0, 0] : [0, 1 / baseMipLevelSize[1], 0]) as T;
+  } else if (softwareTexture.descriptor.dimension === '1d') {
+    return [1 / baseMipLevelSize[0]] as T;
   } else {
-    return (isDDX ? [1 / textureSize[0], 0] : [0, 1 / textureSize[1]]) as T;
+    return (isDDX ? [1 / baseMipLevelSize[0], 0] : [0, 1 / baseMipLevelSize[1]]) as T;
   }
 }
 
@@ -2005,11 +2044,11 @@ function derivativeBaseForCall<T extends Dimensionality>(texture: Texture, isDDX
  * Multiplies derivativeBase by derivativeMult or 1
  */
 function derivativeForCall<T extends Dimensionality>(
-  texture: Texture,
+  softwareTexture: SoftwareTexture,
   call: TextureCall<T>,
   isDDX: boolean
 ) {
-  const dd = derivativeBaseForCall(texture, isDDX);
+  const dd = derivativeBaseForCall(softwareTexture, isDDX);
   return dd.map((v, i) => v * (call.derivativeMult?.[i] ?? 1)) as T;
 }
 
@@ -2017,19 +2056,19 @@ function softwareTextureRead<T extends Dimensionality>(
   t: GPUTest,
   stage: ShaderStage,
   call: TextureCall<T>,
-  texture: Texture,
+  softwareTexture: SoftwareTexture,
   sampler?: GPUSamplerDescriptor
 ): PerTexelComponent<number> {
   // add the implicit derivatives that we use from WGSL in doTextureCalls
   if (builtinNeedsDerivatives(call.builtin) && !call.ddx) {
     const newCall: TextureCall<T> = {
       ...call,
-      ddx: call.ddx ?? derivativeForCall<T>(texture, call, true),
-      ddy: call.ddy ?? derivativeForCall<T>(texture, call, false),
+      ddx: call.ddx ?? derivativeForCall<T>(softwareTexture, call, true),
+      ddy: call.ddy ?? derivativeForCall<T>(softwareTexture, call, false),
     };
     call = newCall;
   }
-  return softwareTextureReadGrad(t, stage, call, texture, sampler);
+  return softwareTextureReadGrad(t, stage, call, softwareTexture, sampler);
 }
 
 export type TextureTestOptions<T extends Dimensionality> = {
@@ -2049,20 +2088,24 @@ export type TextureTestOptions<T extends Dimensionality> = {
  * * level is outside the range [0, textureNumLevels(t))
  * * sample_index is outside the range [0, textureNumSamples(s))
  */
-function isOutOfBoundsCall<T extends Dimensionality>(texture: Texture, call: TextureCall<T>) {
+function isOutOfBoundsCall<T extends Dimensionality>(
+  softwareTexture: SoftwareTexture,
+  call: TextureCall<T>
+) {
   assert(call.coords !== undefined);
 
-  const desc = reifyTextureDescriptor(texture.descriptor);
-  const { coords, mipLevel, arrayIndex, sampleIndex } = call;
+  const desc = reifyTextureDescriptor(softwareTexture.descriptor);
+  const { coords, mipLevel: callMipLevel, arrayIndex, sampleIndex } = call;
+  const { baseMipLevelSize, mipLevelCount, arrayLayerCount } = getBaseMipLevelInfo(softwareTexture);
 
-  if (mipLevel !== undefined && (mipLevel < 0 || mipLevel >= desc.mipLevelCount)) {
+  if (callMipLevel !== undefined && (callMipLevel < 0 || callMipLevel >= mipLevelCount)) {
     return true;
   }
 
   const size = virtualMipSize(
-    texture.descriptor.dimension || '2d',
-    texture.descriptor.size,
-    mipLevel ?? 0
+    softwareTexture.descriptor.dimension || '2d',
+    baseMipLevelSize,
+    callMipLevel ?? 0
   );
 
   for (let i = 0; i < coords.length; ++i) {
@@ -2073,8 +2116,7 @@ function isOutOfBoundsCall<T extends Dimensionality>(texture: Texture, call: Tex
   }
 
   if (arrayIndex !== undefined) {
-    const size = reifyExtent3D(desc.size);
-    if (arrayIndex < 0 || arrayIndex >= size.depthOrArrayLayers) {
+    if (arrayIndex < 0 || arrayIndex >= arrayLayerCount) {
       return true;
     }
   }
@@ -2089,7 +2131,7 @@ function isOutOfBoundsCall<T extends Dimensionality>(texture: Texture, call: Tex
 }
 
 function isValidOutOfBoundsValue(
-  texture: Texture,
+  softwareTexture: SoftwareTexture,
   gotRGBA: PerTexelComponent<number>,
   maxFractionalDiff: number
 ) {
@@ -2099,7 +2141,7 @@ function isValidOutOfBoundsValue(
   // * the value of any texel in the texture
   // * 0,0,0,0 or 0,0,0,1 if not a depth texture
   // * 0 if a depth texture
-  if (texture.descriptor.format.includes('depth')) {
+  if (softwareTexture.descriptor.format.includes('depth')) {
     if (gotRGBA.R === 0) {
       return true;
     }
@@ -2115,14 +2157,14 @@ function isValidOutOfBoundsValue(
   }
 
   // Can be any texel value
-  for (let mipLevel = 0; mipLevel < texture.texels.length; ++mipLevel) {
-    const mipTexels = texture.texels[mipLevel];
+  for (let mipLevel = 0; mipLevel < softwareTexture.texels.length; ++mipLevel) {
+    const mipTexels = softwareTexture.texels[mipLevel];
     const size = virtualMipSize(
-      texture.descriptor.dimension || '2d',
-      texture.descriptor.size,
+      softwareTexture.descriptor.dimension || '2d',
+      softwareTexture.descriptor.size,
       mipLevel
     );
-    const sampleCount = texture.descriptor.sampleCount ?? 1;
+    const sampleCount = softwareTexture.descriptor.sampleCount ?? 1;
     for (let z = 0; z < size[2]; ++z) {
       for (let y = 0; y < size[1]; ++y) {
         for (let x = 0; x < size[0]; ++x) {
@@ -2132,7 +2174,7 @@ function isValidOutOfBoundsValue(
             if (
               texelsApproximatelyEqual(
                 gotRGBA,
-                texture.descriptor.format,
+                softwareTexture.descriptor.format,
                 rgba,
                 mipTexels.format,
                 maxFractionalDiff
@@ -2158,16 +2200,16 @@ function isValidOutOfBoundsValue(
  * * 0 if a depth texture
  */
 function okBecauseOutOfBounds<T extends Dimensionality>(
-  texture: Texture,
+  softwareTexture: SoftwareTexture,
   call: TextureCall<T>,
   gotRGBA: PerTexelComponent<number>,
   maxFractionalDiff: number
 ) {
-  if (!isOutOfBoundsCall(texture, call)) {
+  if (!isOutOfBoundsCall(softwareTexture, call)) {
     return false;
   }
 
-  return isValidOutOfBoundsValue(texture, gotRGBA, maxFractionalDiff);
+  return isValidOutOfBoundsValue(softwareTexture, gotRGBA, maxFractionalDiff);
 }
 
 const kRGBAComponents = [
@@ -2268,7 +2310,7 @@ function getULPFromZeroForComponents(
  */
 export async function checkCallResults<T extends Dimensionality>(
   t: GPUTest,
-  texture: Texture,
+  softwareTexture: SoftwareTexture,
   textureType: string,
   sampler: GPUSamplerDescriptor | undefined,
   calls: TextureCall<T>[],
@@ -2291,19 +2333,19 @@ export async function checkCallResults<T extends Dimensionality>(
   // GPU texture for displaying in diagnostics.
   let gpuTexels: TexelView[] | undefined;
   const errs: string[] = [];
-  const format = texture.texels[0].format;
-  const size = reifyExtent3D(texture.descriptor.size);
+  const format = softwareTexture.texels[0].format;
+  const size = reifyExtent3D(softwareTexture.descriptor.size);
   const maxFractionalDiff =
     sampler?.minFilter === 'linear' ||
     sampler?.magFilter === 'linear' ||
     sampler?.mipmapFilter === 'linear'
-      ? getMaxFractionalDiffForTextureFormat(texture.descriptor.format)
+      ? getMaxFractionalDiffForTextureFormat(softwareTexture.descriptor.format)
       : 0;
 
   for (let callIdx = 0; callIdx < calls.length; callIdx++) {
     const call = calls[callIdx];
     const gotRGBA = results.results[callIdx];
-    const expectRGBA = softwareTextureRead(t, stage, call, texture, sampler);
+    const expectRGBA = softwareTextureRead(t, stage, call, softwareTexture, sampler);
     // Issues with textureSampleBias
     //
     // textureSampleBias tests start to get unexpected results when bias >= ~12
@@ -2405,7 +2447,7 @@ export async function checkCallResults<T extends Dimensionality>(
     if (
       texelsApproximatelyEqual(
         gotRGBA,
-        texture.descriptor.format,
+        softwareTexture.descriptor.format,
         expectRGBA,
         format,
         callSpecificMaxFractionalDiff
@@ -2414,7 +2456,10 @@ export async function checkCallResults<T extends Dimensionality>(
       continue;
     }
 
-    if (!sampler && okBecauseOutOfBounds(texture, call, gotRGBA, callSpecificMaxFractionalDiff)) {
+    if (
+      !sampler &&
+      okBecauseOutOfBounds(softwareTexture, call, gotRGBA, callSpecificMaxFractionalDiff)
+    ) {
       continue;
     }
 
@@ -2453,19 +2498,27 @@ export async function checkCallResults<T extends Dimensionality>(
       rgbaComponentsToCheck.map(component => p[component]!);
 
     if (bad) {
+      const { baseMipLevel, mipLevelCount, baseArrayLayer, arrayLayerCount, baseMipLevelSize } =
+        getBaseMipLevelInfo(softwareTexture);
+      const physicalMipLevelCount = softwareTexture.descriptor.mipLevelCount ?? 1;
+
       const desc = describeTextureCall(call);
       errs.push(`result was not as expected:
-      size: [${size.width}, ${size.height}, ${size.depthOrArrayLayers}]
-  mipCount: ${texture.descriptor.mipLevelCount ?? 1}
-      call: ${desc}  // #${callIdx}`);
-      if (isCubeViewDimension(texture.viewDescriptor)) {
+   physical size: [${size.width}, ${size.height}, ${size.depthOrArrayLayers}]
+    baseMipLevel: ${baseMipLevel}
+   mipLevelCount: ${mipLevelCount}
+  baseArrayLayer: ${baseArrayLayer}
+ arrayLayerCount: ${arrayLayerCount}
+physicalMipCount: ${physicalMipLevelCount}
+            call: ${desc}  // #${callIdx}`);
+      if (isCubeViewDimension(softwareTexture.viewDescriptor)) {
         const coord = convertCubeCoordToNormalized3DTextureCoord(call.coords as vec3);
         const faceNdx = Math.floor(coord[2] * 6);
         errs.push(`          : as 3D texture coord: (${coord[0]}, ${coord[1]}, ${coord[2]})`);
-        for (let mipLevel = 0; mipLevel < (texture.descriptor.mipLevelCount ?? 1); ++mipLevel) {
+        for (let mipLevel = 0; mipLevel < physicalMipLevelCount; ++mipLevel) {
           const mipSize = virtualMipSize(
-            texture.descriptor.dimension ?? '2d',
-            texture.descriptor.size,
+            softwareTexture.descriptor.dimension ?? '2d',
+            softwareTexture.descriptor.size,
             mipLevel
           );
           const t = coord.slice(0, 2).map((v, i) => (v * mipSize[i]).toFixed(3));
@@ -2474,10 +2527,10 @@ export async function checkCallResults<T extends Dimensionality>(
           );
         }
       } else if (call.coordType === 'f') {
-        for (let mipLevel = 0; mipLevel < (texture.descriptor.mipLevelCount ?? 1); ++mipLevel) {
+        for (let mipLevel = 0; mipLevel < physicalMipLevelCount; ++mipLevel) {
           const mipSize = virtualMipSize(
-            texture.descriptor.dimension ?? '2d',
-            texture.descriptor.size,
+            softwareTexture.descriptor.dimension ?? '2d',
+            softwareTexture.descriptor.size,
             mipLevel
           );
           const t = call.coords!.map((v, i) => (v * mipSize[i]).toFixed(3));
@@ -2485,9 +2538,9 @@ export async function checkCallResults<T extends Dimensionality>(
         }
       }
       if (builtinNeedsDerivatives(call.builtin)) {
-        const ddx = derivativeForCall<T>(texture, call, true);
-        const ddy = derivativeForCall<T>(texture, call, false);
-        const mipLevel = computeMipLevelFromGradients(ddx, ddy, size);
+        const ddx = derivativeForCall<T>(softwareTexture, call, true);
+        const ddy = derivativeForCall<T>(softwareTexture, call, false);
+        const mipLevel = computeMipLevelFromGradients(ddx, ddy, baseMipLevelSize);
         const biasStr = call.bias === undefined ? '' : ' (without bias)';
         errs.push(`implicit derivative based mip level: ${fix5(mipLevel)}${biasStr}`);
         if (call.bias) {
@@ -2552,11 +2605,11 @@ export async function checkCallResults<T extends Dimensionality>(
                 t,
                 {
                   format,
-                  dimension: texture.descriptor.dimension ?? '2d',
-                  sampleCount: texture.descriptor.sampleCount ?? 1,
+                  dimension: softwareTexture.descriptor.dimension ?? '2d',
+                  sampleCount: softwareTexture.descriptor.sampleCount ?? 1,
                   depthOrArrayLayers: size.depthOrArrayLayers,
                 },
-                texture.viewDescriptor,
+                softwareTexture.viewDescriptor,
                 textureType,
                 debugSampler,
                 debugCalls,
@@ -2577,7 +2630,7 @@ export async function checkCallResults<T extends Dimensionality>(
             gpuTexels = await readTextureToTexelViews(
               t,
               gpuTexture,
-              texture.descriptor,
+              softwareTexture.descriptor,
               getTexelViewFormatForTextureFormat(gpuTexture.format)
             );
           }
@@ -2588,7 +2641,9 @@ export async function checkCallResults<T extends Dimensionality>(
           // but if it's a compressed texture we use an encodable texture.
           // It's not perfect but we already know it failed. We're just hoping
           // to get sample points.
-          const useTexelFormatForGPUTexture = isCompressedTextureFormat(texture.descriptor.format);
+          const useTexelFormatForGPUTexture = isCompressedTextureFormat(
+            softwareTexture.descriptor.format
+          );
 
           if (useTexelFormatForGPUTexture) {
             errs.push(`
@@ -2602,11 +2657,11 @@ we can not do that easily with compressed textures. ###
           const expectedSamplePoints = [
             'expected:',
             ...(await identifySamplePoints(
-              texture,
+              softwareTexture,
               sampler,
               callForSamplePoints,
               call,
-              texture.texels,
+              softwareTexture.texels,
               (texels: TexelView[]) => {
                 return Promise.resolve(
                   softwareTextureRead(
@@ -2615,8 +2670,8 @@ we can not do that easily with compressed textures. ###
                     callForSamplePoints,
                     {
                       texels,
-                      descriptor: texture.descriptor,
-                      viewDescriptor: texture.viewDescriptor,
+                      descriptor: softwareTexture.descriptor,
+                      viewDescriptor: softwareTexture.viewDescriptor,
                     },
                     checkInfo.sampler
                   )
@@ -2627,13 +2682,13 @@ we can not do that easily with compressed textures. ###
           const gotSamplePoints = [
             'got:',
             ...(await identifySamplePoints(
-              texture,
+              softwareTexture,
               sampler,
               callForSamplePoints,
               call,
               gpuTexels,
               async (texels: TexelView[]) => {
-                const descriptor = { ...texture.descriptor };
+                const descriptor = { ...softwareTexture.descriptor };
                 if (useTexelFormatForGPUTexture) {
                   descriptor.format = texels[0].format;
                 }
@@ -3283,20 +3338,24 @@ const kFaceNames = ['+x', '-x', '+y', '-y', '+z', '-z'] as const;
  * b: at: [7, 2], weights: [R: 0.25000]
  */
 async function identifySamplePoints<T extends Dimensionality>(
-  texture: Texture,
+  softwareTexture: SoftwareTexture,
   sampler: GPUSamplerDescriptor,
   callForSamples: TextureCall<T>,
   originalCall: TextureCall<T>,
   texels: TexelView[] | undefined,
   run: (texels: TexelView[]) => Promise<PerTexelComponent<number>>
 ) {
-  const info = texture.descriptor;
-  const isCube = isCubeViewDimension(texture.viewDescriptor);
-  const mipLevelCount = texture.descriptor.mipLevelCount ?? 1;
-  const mipLevelSize = range(mipLevelCount, mipLevel =>
-    virtualMipSize(texture.descriptor.dimension ?? '2d', texture.descriptor.size, mipLevel)
+  const info = softwareTexture.descriptor;
+  const isCube = isCubeViewDimension(softwareTexture.viewDescriptor);
+  const mipLevelCount = softwareTexture.descriptor.mipLevelCount ?? 1;
+  const mipLevelSizes = range(mipLevelCount, mipLevel =>
+    virtualMipSize(
+      softwareTexture.descriptor.dimension ?? '2d',
+      softwareTexture.descriptor.size,
+      mipLevel
+    )
   );
-  const numTexelsPerLevel = mipLevelSize.map(size => size.reduce((s, v) => s * v));
+  const numTexelsPerLevel = mipLevelSizes.map(size => size.reduce((s, v) => s * v));
   const numTexelsOfPrecedingLevels = (() => {
     let total = 0;
     return numTexelsPerLevel.map(v => {
@@ -3318,7 +3377,7 @@ async function identifySamplePoints<T extends Dimensionality>(
 
   const getTexelCoordFromTexelId = (texelId: number) => {
     const mipLevel = getMipLevelFromTexelId(texelId);
-    const size = mipLevelSize[mipLevel];
+    const size = mipLevelSizes[mipLevel];
     const texelsPerSlice = size[0] * size[1];
     const id = texelId - numTexelsOfPrecedingLevels[mipLevel];
     const layer = Math.floor(id / texelsPerSlice);
@@ -3379,7 +3438,7 @@ async function identifySamplePoints<T extends Dimensionality>(
           TexelView.fromTexelsAsColors(
             format,
             (coords: Required<GPUOrigin3DDict>): Readonly<PerTexelComponent<number>> => {
-              const size = mipLevelSize[mipLevel];
+              const size = mipLevelSizes[mipLevel];
               const texelsPerSlice = size[0] * size[1];
               const texelsPerRow = size[0];
               const texelId =
@@ -3457,7 +3516,9 @@ async function identifySamplePoints<T extends Dimensionality>(
   const letter = (idx: number) => String.fromCodePoint(idx < 30 ? 97 + idx : idx + 9600 - 30); // 97: 'a'
   let idCount = 0;
 
-  const { blockWidth, blockHeight } = getBlockInfoForTextureFormat(texture.descriptor.format);
+  const { blockWidth, blockHeight } = getBlockInfoForTextureFormat(
+    softwareTexture.descriptor.format
+  );
   // range + concatenate results.
   const rangeCat = <T>(num: number, fn: (i: number) => T) => range(num, fn).join('');
   const joinFn = (arr: string[], fn: (i: number) => string) => {
@@ -3499,7 +3560,7 @@ async function identifySamplePoints<T extends Dimensionality>(
       continue;
     }
 
-    const [width, height, depthOrArrayLayers] = mipLevelSize[mipLevel];
+    const [width, height, depthOrArrayLayers] = mipLevelSizes[mipLevel];
     const texelsPerRow = width;
 
     for (let layer = 0; layer < depthOrArrayLayers; ++layer) {
@@ -3587,7 +3648,7 @@ async function identifySamplePoints<T extends Dimensionality>(
           texels &&
           convertToTexelViewFormat(
             texels[mipLevel].color({ x, y, z: layer }),
-            texture.descriptor.format
+            softwareTexture.descriptor.format
           );
 
         const texelStr = formatTexel(texel);
@@ -3687,7 +3748,8 @@ export type CubeSamplePointMethods = (typeof kSamplePointMethods)[number];
 
 type TextureBuiltinInputArgs = {
   textureBuiltin?: TextureBuiltin;
-  descriptor: GPUTextureDescriptor;
+  descriptor?: GPUTextureDescriptor;
+  softwareTexture?: SoftwareTexture;
   sampler?: GPUSamplerDescriptor;
   derivatives?: boolean;
   mipLevel?: RangeDef;
@@ -3729,19 +3791,28 @@ function generateTextureBuiltinInputsImpl<T extends Dimensionality>(
   component?: number;
   depthRef?: number;
 }[] {
-  const { method, descriptor } = args;
-  const dimension = descriptor.dimension ?? '2d';
-  const mipLevelCount = descriptor.mipLevelCount ?? 1;
-  const size = virtualMipSize(dimension, descriptor.size, 0);
+  const { method, descriptor, softwareTexture: info } = args;
+  // MAINTENANCE_TODO: remove descriptor from all builtin tests. use softwareTexture instead
+  assert(!!descriptor !== !!info, 'must pass descriptor or textureInfo');
+  const textureInfo: SoftwareTexture = info ?? {
+    descriptor: descriptor!,
+    texels: [],
+    viewDescriptor: {},
+  };
+
+  const { mipLevelCount, baseMipLevelSize } = getBaseMipLevelInfo(textureInfo);
+  const dimension = textureInfo.descriptor.dimension ?? '2d';
   const coords: T[] = [];
   switch (method) {
     case 'texel-centre': {
       for (let i = 0; i < n; i++) {
         const r = hashU32(i);
-        const x = Math.floor(lerp(0, size[0] - 1, (r & 0xff) / 0xff)) + 0.5;
-        const y = Math.floor(lerp(0, size[1] - 1, ((r >> 8) & 0xff) / 0xff)) + 0.5;
-        const z = Math.floor(lerp(0, size[2] - 1, ((r >> 16) & 0xff) / 0xff)) + 0.5;
-        coords.push(makeValue(x / size[0], y / size[1], z / size[2]));
+        const x = Math.floor(lerp(0, baseMipLevelSize[0] - 1, (r & 0xff) / 0xff)) + 0.5;
+        const y = Math.floor(lerp(0, baseMipLevelSize[1] - 1, ((r >> 8) & 0xff) / 0xff)) + 0.5;
+        const z = Math.floor(lerp(0, baseMipLevelSize[2] - 1, ((r >> 16) & 0xff) / 0xff)) + 0.5;
+        coords.push(
+          makeValue(x / baseMipLevelSize[0], y / baseMipLevelSize[1], z / baseMipLevelSize[2])
+        );
       }
       break;
     }
@@ -3862,13 +3933,13 @@ function generateTextureBuiltinInputsImpl<T extends Dimensionality>(
     return outside + clamp(inside, { min: 1, max: textureDimensionUnits - 1 });
   };
 
-  const numComponents = isDepthOrStencilTextureFormat(descriptor.format) ? 1 : 4;
+  const numComponents = isDepthOrStencilTextureFormat(textureInfo.descriptor.format) ? 1 : 4;
   return coords.map((c, i) => {
     const mipLevel = args.mipLevel
       ? quantizeMipLevel(makeRangeValue(args.mipLevel, i), args.sampler?.mipmapFilter ?? 'nearest')
       : 0;
     const clampedMipLevel = clamp(mipLevel, { min: 0, max: mipLevelCount - 1 });
-    const mipSize = virtualMipSize(dimension, size, clampedMipLevel);
+    const mipSize = virtualMipSize(dimension, baseMipLevelSize, clampedMipLevel);
     const q = mipSize.map(v => v * kSubdivisionsPerTexel);
 
     const coords = c.map((v, i) => {
@@ -4104,19 +4175,19 @@ function convertNormalized3DTexCoordToCubeCoord(uvLayer: vec3) {
  * if the texel was outside of the face, the cube map coord will end up pointing to a different
  * face. We then convert back cube coord -> normalized face coord -> texel based coord
  */
-function wrapFaceCoordToCubeFaceAtEdgeBoundaries(textureSize: number, faceCoord: vec3) {
+function wrapFaceCoordToCubeFaceAtEdgeBoundaries(mipLevelSize: number, faceCoord: vec3) {
   // convert texel based face coord to normalized 2d-array coord
   const nc0: vec3 = [
-    (faceCoord[0] + 0.5) / textureSize,
-    (faceCoord[1] + 0.5) / textureSize,
+    (faceCoord[0] + 0.5) / mipLevelSize,
+    (faceCoord[1] + 0.5) / mipLevelSize,
     (faceCoord[2] + 0.5) / 6,
   ];
   const cc = convertNormalized3DTexCoordToCubeCoord(nc0);
   const nc1 = convertCubeCoordToNormalized3DTextureCoord(cc);
   // convert normalized 2d-array coord back texel based face coord
   const fc = [
-    Math.floor(nc1[0] * textureSize),
-    Math.floor(nc1[1] * textureSize),
+    Math.floor(nc1[0] * mipLevelSize),
+    Math.floor(nc1[1] * mipLevelSize),
     Math.floor(nc1[2] * 6),
   ];
 
@@ -4125,20 +4196,20 @@ function wrapFaceCoordToCubeFaceAtEdgeBoundaries(textureSize: number, faceCoord:
 
 function applyAddressModesToCoords(
   addressMode: GPUAddressMode[],
-  textureSize: number[],
+  mipLevelSize: number[],
   coord: number[]
 ) {
   return coord.map((v, i) => {
     switch (addressMode[i]) {
       case 'clamp-to-edge':
-        return clamp(v, { min: 0, max: textureSize[i] - 1 });
+        return clamp(v, { min: 0, max: mipLevelSize[i] - 1 });
       case 'mirror-repeat': {
-        const n = Math.floor(v / textureSize[i]);
-        v = v - n * textureSize[i];
-        return (n & 1) !== 0 ? textureSize[i] - v - 1 : v;
+        const n = Math.floor(v / mipLevelSize[i]);
+        v = v - n * mipLevelSize[i];
+        return (n & 1) !== 0 ? mipLevelSize[i] - v - 1 : v;
       }
       case 'repeat':
-        return v - Math.floor(v / textureSize[i]) * textureSize[i];
+        return v - Math.floor(v / mipLevelSize[i]) * mipLevelSize[i];
       default:
         unreachable();
     }
@@ -4174,10 +4245,17 @@ export function generateSamplePointsCube(
   component?: number;
   depthRef?: number;
 }[] {
-  const { method, descriptor } = args;
-  const mipLevelCount = descriptor.mipLevelCount ?? 1;
-  const size = virtualMipSize('2d', descriptor.size, 0);
-  const textureWidth = size[0];
+  const { method, descriptor, softwareTexture: info } = args;
+  // MAINTENANCE_TODO: remove descriptor from all builtin tests. use textureInfo.
+  assert(!!descriptor !== !!info, 'must pass descriptor or textureInfo');
+  const textureInfo: SoftwareTexture = info ?? {
+    descriptor: descriptor!,
+    texels: [],
+    viewDescriptor: {},
+  };
+
+  const { mipLevelCount, baseMipLevelSize } = getBaseMipLevelInfo(textureInfo);
+  const textureWidth = baseMipLevelSize[0];
   const coords: vec3[] = [];
   switch (method) {
     case 'texel-centre': {
@@ -4389,7 +4467,7 @@ export function generateSamplePointsCube(
       ? quantizeMipLevel(makeRangeValue(args.mipLevel, i), args.sampler?.mipmapFilter ?? 'nearest')
       : 0;
     const clampedMipLevel = clamp(mipLevel, { min: 0, max: mipLevelCount - 1 });
-    const mipSize = virtualMipSize('2d', size, Math.ceil(clampedMipLevel));
+    const mipSize = virtualMipSize('2d', baseMipLevelSize, Math.ceil(clampedMipLevel));
     const q = [
       mipSize[0] * kSubdivisionsPerTexel,
       mipSize[0] * kSubdivisionsPerTexel,