Render Graph TextureHandles - wrong number of mip levels?

(First time asking a question here so I do apologise if I’ve somehow submitted it wrong/etc)

I’m trying to get to grips with ScriptableRenderFeatures (6000.0.30f1) and I’m struggling with handling textures in my custom passes.

I’m finding the TextureHandle / RTHandle / RenderTextureDescriptor / TextureDesc APIs pretty confusing, but I’ve successfully got my created texture both into the next render pass and out as a global texture for use in other shaders. But I’m still having difficulty with the mip levels.

I’ve been referring to the documentation here:
Unity - Manual: Example of a complete Scriptable Renderer Feature in URP
Unity - Manual: Transfer a texture between render passes in URP

My use case is I’m trying to implement a Hi-Z map for later use in occlusion culling of vegetation. I’ve got two custom ScriptableRenderPass-es : one raster pass to render the scene depth of the occluding geometry, followed by a compute shader pass to fill the depth map’s mips with the downsampled values.

(My depth map is a single channel “color” texture because the depth-only format doesn’t support mip levels.)

The code works and I can view the generated mipmaps by accessing the global texture. But it generates a pile of errors on some (I think not all?) frames:

Attempting to bind MIP 4 of Texture ID 1007 as an UAV, but the texture only has 4 MIP levels!

+etc for MIPs 5 through 7. The textures are generated with mipLevels = 8 (and indeed I can view all 8) so I don’t understand why it only has 4 at the point of binding.

(My best guesses are A) something something asynchronous? or B) something like mip streaming?)

What’s going on here? How do I force the compute pass to successfully access all 8 mip levels? Why does it work even though it throws these errors?

My render passes:

class TerrainDepthPass : ScriptableRenderPass
{
    private Material material;
    private RenderTextureDescriptor hiZRenderTextureDescriptor;
    private TextureDesc hiZTextureDesc;
    private const string k_HiZTextureName = "_HiZTexture";
    private int hiZTextureID = Shader.PropertyToID(k_HiZTextureName);
    private int2 resolution;

    private class PassData
    {
        public RendererListHandle objectsToDraw;
    }

    public TerrainDepthPass(Material material, int mipLevels)
    {
        this.material = material;

        resolution = new int2(Screen.width, Screen.height);
        resolution /= 4;

        hiZRenderTextureDescriptor = new RenderTextureDescriptor(
            resolution.x, resolution.y,
            RenderTextureFormat.RFloat, 0,
            mipLevels, RenderTextureReadWrite.Linear);

        hiZTextureDesc = new TextureDesc(hiZRenderTextureDescriptor);
        hiZTextureDesc.useMipMap = true;
        hiZTextureDesc.autoGenerateMips = false;
        hiZTextureDesc.enableRandomWrite = true;
    }

    public override void RecordRenderGraph(RenderGraph renderGraph, ContextContainer frameData)
    {
        UniversalCameraData cameraData = frameData.Get<UniversalCameraData>();

        resolution = new int2(cameraData.cameraTargetDescriptor.width,
                            cameraData.cameraTargetDescriptor.height);
        resolution /= 4;

        hiZTextureDesc.width = resolution.x;
        hiZTextureDesc.height = resolution.y;

        TextureHandle dst = renderGraph.CreateTexture(hiZTextureDesc);

        if (!dst.IsValid())
            return;

        using (var builder = renderGraph.AddRasterRenderPass<PassData>(
            "HiZTerrainDepth", out var passData))
        {
            UniversalRenderingData renderingData = frameData.Get<UniversalRenderingData>();
            UniversalLightData lightData = frameData.Get<UniversalLightData>();

            SortingCriteria sortFlags = cameraData.defaultOpaqueSortFlags;
            RenderQueueRange renderQueueRange = RenderQueueRange.opaque;
            int layerMask = ~0;
            //int layerMask = (1 << 8);
            FilteringSettings filterSettings = new FilteringSettings(renderQueueRange, layerMask: layerMask);

            ShaderTagId shadersToOverride = new ShaderTagId("UniversalForward");

            DrawingSettings drawSettings = RenderingUtils.CreateDrawingSettings(
                shadersToOverride, renderingData, cameraData, lightData, sortFlags);

            drawSettings.overrideMaterial = material;

            var rendererListParams = new RendererListParams(
                renderingData.cullResults, drawSettings, filterSettings);

            passData.objectsToDraw = renderGraph.CreateRendererList(rendererListParams);

            builder.UseRendererList((passData.objectsToDraw));
            builder.SetRenderAttachment(dst, 0, AccessFlags.Write);

            builder.SetRenderFunc((PassData data, RasterGraphContext context) =>
                ExecutePass(data, context));

            builder.SetGlobalTextureAfterPass(dst, hiZTextureID);

            DepthContext depthContext = frameData.Create<DepthContext>();
            depthContext.hiZDepthTexture = dst;
        }
        

    }

    static void ExecutePass(PassData passData, RasterGraphContext context)
    {
        // Clear the render target to black
        context.cmd.ClearRenderTarget(true, true, Color.black);

        // Draw the objects in the list
        context.cmd.DrawRendererList(passData.objectsToDraw);
    }
}
public class DownsamplePass : ScriptableRenderPass
{
    private ComputeShader shader;
    private int mipLevels;

    static int idSizePrev = Shader.PropertyToID("_SizePrev");
    static int idSizeNew = Shader.PropertyToID("_SizeNew");
    static int idDepthMapPrev = Shader.PropertyToID("_DepthMapPrev");
    static int idDepthMapNew = Shader.PropertyToID("_DepthMapNew");

    public DownsamplePass(ComputeShader shader, int mipLevels)
    {
        //Debug.Log("DownsamplePass ctor");
        this.shader = shader;
        this.mipLevels = mipLevels;
    }

    private class PassData
    {
        public ComputeShader computeShader;
        public int mipLevels;
        public TextureHandle depthTexture;
        public int[] origSize;
    }

    public override void RecordRenderGraph(RenderGraph renderGraph, ContextContainer frameData)
    {
        using (var builder = renderGraph.AddComputePass("HiZDownsamplePass", out PassData passData))
        {
            builder.AllowPassCulling(false);

            passData.computeShader = shader;
            passData.mipLevels = mipLevels;

            DepthContext depthContext = frameData.Get<DepthContext>();
            passData.depthTexture = depthContext.hiZDepthTexture;

            builder.UseTexture(passData.depthTexture);

            passData.origSize = new int[] { depthContext.hiZDepthTexture.GetDescriptor(renderGraph).width,
                                            depthContext.hiZDepthTexture.GetDescriptor(renderGraph).height };

            builder.SetRenderFunc((PassData passData, ComputeGraphContext context) =>
                ExecutePass(passData, context));
        }
    }

    static void ExecutePass(PassData passData, ComputeGraphContext context)
    {
        TextureHandle depthTexture = passData.depthTexture;

        ComputeShader shader = passData.computeShader;
        int[] sizePrev = passData.origSize;
        int[] sizeNew;
        for (int newMip = 1; newMip < passData.mipLevels; newMip++)
        {
            sizeNew = new int[] { Mathf.CeilToInt((float)sizePrev[0] / 2f),
                                    Mathf.CeilToInt((float)sizePrev[1] / 2f)};
            sizeNew[0] = math.max(sizeNew[0], 1);
            sizeNew[1] = math.max(sizeNew[1], 1);

            context.cmd.SetComputeIntParams(shader, idSizePrev, sizePrev);
            context.cmd.SetComputeIntParams(shader, idSizeNew, sizeNew);

            context.cmd.SetComputeTextureParam(shader, 0, idDepthMapPrev, depthTexture, mipLevel: newMip - 1);
            context.cmd.SetComputeTextureParam(shader, 0, idDepthMapNew, depthTexture, mipLevel: newMip);

            int groupSize_X = Mathf.CeilToInt((float)sizeNew[0] / 8f);
            int groupSize_Y = Mathf.CeilToInt((float)sizeNew[1] / 8f);
            context.cmd.DispatchCompute(shader, 0, groupSize_X, groupSize_Y, 1);

            sizePrev = sizeNew;
        }

    }
}

I could share the shaders too, but they seem to be doing their job so I don’t think the problem is with the shaders…

Here’s a confusing thing, or at least it’s confusing to me:

After I take a capture with RenderDoc, the errors go away until I next enter/leave play mode, and/or change the project code. (At which point they come back.)

Update: I believe I’ve found the problem. The error is generated when the texture is too small for the specified number of mips. What was really throwing me off is that it wasn’t being generated from the main/scene camera, but from I believe the material previews in the Inspector.