Clear Out

glClear

Once more jumping around, here’s a brief look at an interesting issue I came across while implementing ARB_clear_texture.

When I started looking at the existing clear code in zink, I discovered that the only existing codepath for clears required being inside a renderpass using vkCmdClearAttachments. This meant that if a renderpass wasn’t active, we started one, which was suboptimal for performance, as unnecessarily starting a renderpass means we may end up having to end the renderpass just as quickly in order to emit some non-renderpass commands.

I decided to do something about this along the way. Vulkan has specific commands for clearing color and depth outside of renderpasses, and the usage is very straightforward. The key is detecting whether they’re capable of being used.

Detection

There’s a number of conditions for using these specialized Vulkan clear functions:

  • cannot be in a renderpass
  • must be intending to clear the entire surface (i.e., no scissor or full-surface scissor)
  • no 3D surfaces unless clearing all layers of the surface

The last is a product of a spec issue which allows drivers to interpret a surface’s layer count as possessing a single “3D” image layer instead of the actual count of layers inside that layer. As a result, any time a 3D surface is cleared with a non-zero layer specified, it can’t be cleared except with vkCmdClearAttachments.

The second criterion less tricky, requiring only that the specified scissor region of the clear covers the entire surface being cleared. That can be accomplished like so with some mesa helper APIs:

static bool
clear_needs_rp(unsigned width, unsigned height, struct u_rect *region)
{
   struct u_rect intersect = {0, width, 0, height};

   if (!u_rect_test_intersection(region, &intersect))
      return true;

    u_rect_find_intersection(region, &intersect);
    if (intersect.x0 != 0 || intersect.y0 != 0 ||
        intersect.x1 != width || intersect.y1 != height)
       return true;

   return false;
}

If the intersection of the surface and the scissor region isn’t the entire size of the surface, false is returned and the renderpass codepath is used.

With this check done, and having verified previously that no renderpass is active on the current batch’s command buffer, some helper functions to perform the individual clear operations can be added:

static void
clear_color_no_rp(struct zink_batch *batch, struct zink_resource *res, const union pipe_color_union *pcolor, unsigned level, unsigned layer, unsigned layerCount)
{
   VkImageSubresourceRange range = {};
   range.baseMipLevel = level;
   range.levelCount = 1;
   range.baseArrayLayer = layer;
   range.layerCount = layerCount;
   range.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;

   VkClearColorValue color;
   color.float32[0] = pcolor->f[0];
   color.float32[1] = pcolor->f[1];
   color.float32[2] = pcolor->f[2];
   color.float32[3] = pcolor->f[3];

   if (res->layout != VK_IMAGE_LAYOUT_GENERAL && res->layout != VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL)
      zink_resource_barrier(batch, res, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 0);
   vkCmdClearColorImage(batch->cmdbuf, res->image, res->layout, &color, 1, &range);
}

static void
clear_zs_no_rp(struct zink_batch *batch, struct zink_resource *res, VkImageAspectFlags aspects, double depth, unsigned stencil, unsigned level, unsigned layer, unsigned layerCount)
{
   VkImageSubresourceRange range = {};
   range.baseMipLevel = level;
   range.levelCount = 1;
   range.baseArrayLayer = layer;
   range.layerCount = layerCount;
   range.aspectMask = aspects;

   VkClearDepthStencilValue zs_value = {depth, stencil};

   if (res->layout != VK_IMAGE_LAYOUT_GENERAL && res->layout != VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL)
      zink_resource_barrier(batch, res, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 0);
   vkCmdClearDepthStencilImage(batch->cmdbuf, res->image, res->layout, &zs_value, 1, &range);
}

This is as simple as populating the required structs, adding in a pipeline barrier if necessary to set the image layout, and then emitting the commands.

Gotcha

As per usual, there’s a gotcha here. Clearing color surfaces in this way doesn’t take into account SRGBness as the renderpass codepath does, which means it’ll actually clear using the wrong colors if the surface is a RGBA view of a SRGB image. This means that the color value has to be converted prior to calling the clear_color_no_rp() helper:

if (psurf->format != res->base.format &&
    !util_format_is_srgb(psurf->format) && util_format_is_srgb(res->base.format)) {
   /* if SRGB mode is disabled for the fb with a backing srgb image then we have to
    * convert this to srgb color
    */
   color.f[0] = util_format_srgb_to_linear_float(pcolor->f[0]);
   color.f[1] = util_format_srgb_to_linear_float(pcolor->f[1]);
   color.f[2] = util_format_srgb_to_linear_float(pcolor->f[2]);
}

Here, psurf is the struct pipe_surface given by the gallium struct pipe_context::clear hook, and res is the struct zink_resource (aka subclassed struct pipe_resource) image that the surface is a view of. The util_format API takes care of checking srgb status, and then it also can be used to handle the conversion back to the expected format.

Now the color can be safely passed along to achieve the expected results, and out-of-renderpass clearing can safely be used for all cases where the original conditions are met.

Written on August 26, 2020