Skip to content

vulkan: Common dynamic state tracking and pipeline libraries helpers

Faith Ekstrand requested to merge gfxstrand/mesa:vulkan/dynamic-state into main

Last week I started looking at graphics pipelines, dynamic state, etc. for NVK and remembered just how much typing that requires. I didn't want to re-type all the same nonsense I've already typed for ANV and others have typed for RADV, panvk, etc... So let's try and make it common code and "solve" pipeline libraries while we're at it.

This took most of the week just to type and I've not tested it yet beyond compiling but I'm going to throw it out here now to get some early feedback. Next week, I'll attempt to convert a driver to see how it goes. Here's what this MR gets us:

  1. A vk_graphics_pipeline_state struct encapsulating all of the state in a graphics pipeline, including extensions
  2. In theory, it should all handle pipeline libraries
  3. A common vk_dynamic_graphics_state struct for dynamic state as well as helpers to populate it from a vk_graphics_pipeline_state
  4. Common implementations of all of the relevant vkCmdSet* helpers

Pipeline construction:

Building a pipeline without libraries looks something like this:

struct vk_graphics_pipeline_state state = { };
struct vk_graphics_pipeline_all_state all;
vk_graphics_pipeline_state_fill(&device->vk, &state, pCreateInfo,
                                &all, NULL, 0, NULL);

/* Emit stuff using the state in `state` */

The all gives guaranteed stack space for everything so there's no dynamic memory allocation involved.

Building a pipeline library might look like this:

/* Assuming we have a vk_graphics_pipeline_state in pipeline */
memset(&pipeline->state, 0, sizeof(pipeline->state));

for (uint32_t i = 0; i < lib_info->libraryCount; i++) {
   VK_FROM_HANDLE(drv_graphics_pipeline_library, lib, lib_info->pLibraries[i]);
   vk_graphics_pipeline_state_merge(&pipeline->state, &lib->state);
}

/* This assumes you have a void **state_mem in pipeline */
result = vk_graphics_pipeline_state_fill(&device->vk, &pipeline->state,
                                         pCreateInfo, NULL, pAllocator,
                                         VK_SYSTEM_ALLOCATION_SCOPE_OBJECT,
                                         &pipeline->state_mem);
if (result != VK_SUCCESS)
   return result;

This form will allocate heap memory for any state we need to populate that isn't already in state at the time vk_graphics_pipeline_state_fill() is called. The last argument is an out argument for the pointer. It's you're responsibility to free it when the pipeline is freed.

Finally, if you want to special-case things a bit for building a pipeline with libraries but where you don't want to do any allocations, you can combine the two a bit:

struct vk_graphics_pipeline_state state = { };

for (uint32_t i = 0; i < lib_info->libraryCount; i++) {
   VK_FROM_HANDLE(drv_graphics_pipeline_library, lib, lib_info->pLibraries[i]);
   vk_graphics_pipeline_state_merge(&state, &lib->state);
}

struct vk_graphics_pipeline_all_state all;
vk_graphics_pipeline_state_fill(&device->vk, &state, pCreateInfo,
                                &all, NULL, 0, NULL);

I suppose you can also stash a vk_graphics_pipeline_state_all in your pipeline if you really don't want two allocations per pipeline but that seems a bit pointless.

Dynamic state handling:

In your pipeline, you'll need a vk_dynamic_graphics_state struct and you'll have to initialize it as part of pipeline creation. You may also need to set up a couple extra pointers because I allow you to get a smaller version if you don't implement dynamic sample locations or vertex input state:

struct drv_graphics_pipeline {
   ....
   struct vk_vertex_input_state vi_state;
   struct vk_sample_locations_state sl_state;
   struct vk_dynamic_graphics_state dynamic;
   ...
};

/* In your pipeline create function */
memset(&pipeline->dynamic, 0, sizeof(pipeline->dynamic));
pipeline->dynamic->vi = &pipeline->vi_state;
pipeline->dynamic->ms.sample_locations = &pipeline->sl_state;
vk_dynamic_graphics_state_init(&pipeline->dynamic, &state);

where state is the same vk_graphics_pipeline_state struct we used above. You can either do this once as part of the pipeline link at the end or you can do it per-library and use vk_dynamic_graphics_state_merge() to merge them together. Doing it for just the final linked pipeline is probably best.

In your implementation of vkCmdBindPipelines(), you'll need to set the dynamic state:

vk_cmd_set_dynamic_state(&cmd->vk, &pipeline->dynamic_state);

From there, all the vkCmdSet*() helpers are handled for you. In your draw function when you're handling state dirty bits, you'll have code like this:

static void
emit_dynamic_state(struct drv_cmd_buffer *cmd)
{
   struct vk_dynamic_graphics_state *dyn = &cmd->vk.dynamic_graphics_state;

   if (!BITSET_TEST_RANGE(dyn->dirt, 0, MESA_VK_DYNAMIC_GRAPHICS_STATE_ENUM_MAX))
      return;

   if (BITSET_TEST(dyn->dirty, MESA_VK_DYNAMIC_VP_VIEWPORTS) |
       BITSET_TEST(dyn->dirty, MESA_VK_DYNAMIC_VP_VIEWPORT_COUNT)) {
      /* Re-emit viewports */
   }

   if (BITSET_TEST(dyn->dirty, MESA_VK_DYNAMIC_VP_SCISSORS) |
       BITSET_TEST(dyn->dirty, MESA_VK_DYNAMIC_VP_SCISSOR_COUNT)) {
      /* Re-emit scissors */
   }

   /* etc... */

   BITSET_CLEAR_RANGE(dyn->dirt, 0, MESA_VK_DYNAMIC_GRAPHICS_STATE_ENUM_MAX);
}

Conclusion

I think that about covers it. Much thanks to the RADV folks for the idea for the vk_foo_state structs. The ones here aren't quite identical to RADV. I've tweaked them a bit but they're pretty similar. One thing they don't have is any of the pre-compilation work that RADV does for vertex inputs, for instance.

The one bit I don't quite have sorted yet is render passes. Likely, we'll need a flag for whether or not you use your own VkRenderPass or if you want everything converted to dynamic rendering. That should't be hard to wire up either way. I just need to figure out how I want to plumb it.

cc @bnieuwenhuizen @llandwerlin @cmarcelo @bbrezillon @airlied @zmike

Edited by Faith Ekstrand

Merge request reports