void VulkanReplay::SavePipelineState()

in renderdoc/driver/vulkan/vk_replay.cpp [1153:2172]


void VulkanReplay::SavePipelineState(uint32_t eventId)
{
  if(!m_VulkanPipelineState)
    return;

  const VulkanRenderState &state = m_pDriver->m_RenderState;
  VulkanCreationInfo &c = m_pDriver->m_CreationInfo;

  VKPipe::State &ret = *m_VulkanPipelineState;

  VulkanResourceManager *rm = m_pDriver->GetResourceManager();

  VkMarkerRegion::Begin(StringFormat::Fmt("FetchShaderFeedback for %u", eventId));

  FetchShaderFeedback(eventId);

  VkMarkerRegion::End();

  {
    // reset the pipeline state, but keep the descriptor set arrays. This prevents needless
    // reallocations, we'll ensure that descriptors are fully overwritten below.
    rdcarray<VKPipe::DescriptorSet> graphicsDescriptors;
    rdcarray<VKPipe::DescriptorSet> computeDescriptors;

    ret.graphics.descriptorSets.swap(graphicsDescriptors);
    ret.compute.descriptorSets.swap(computeDescriptors);

    ret = VKPipe::State();

    ret.graphics.descriptorSets.swap(graphicsDescriptors);
    ret.compute.descriptorSets.swap(computeDescriptors);
  }

  ret.pushconsts.resize(state.pushConstSize);
  memcpy(ret.pushconsts.data(), state.pushconsts, state.pushConstSize);

  // General pipeline properties
  ret.compute.pipelineResourceId = rm->GetUnreplacedOriginalID(state.compute.pipeline);
  ret.graphics.pipelineResourceId = rm->GetUnreplacedOriginalID(state.graphics.pipeline);

  if(state.compute.pipeline != ResourceId())
  {
    const VulkanCreationInfo::Pipeline &p = c.m_Pipeline[state.compute.pipeline];

    ret.compute.pipelineComputeLayoutResourceId = rm->GetOriginalID(p.compLayout);

    ret.compute.flags = p.flags;

    VKPipe::Shader &stage = ret.computeShader;

    int i = 5;    // 5 is the CS idx (VS, TCS, TES, GS, FS, CS)
    {
      stage.resourceId = rm->GetUnreplacedOriginalID(p.shaders[i].module);
      stage.entryPoint = p.shaders[i].entryPoint;

      stage.stage = ShaderStage::Compute;
      if(p.shaders[i].refl)
        stage.reflection = p.shaders[i].refl;

      stage.pushConstantRangeByteOffset = stage.pushConstantRangeByteSize = 0;
      for(const VkPushConstantRange &pr : c.m_PipelineLayout[p.compLayout].pushRanges)
      {
        if(pr.stageFlags & VK_SHADER_STAGE_COMPUTE_BIT)
        {
          stage.pushConstantRangeByteOffset = pr.offset;
          stage.pushConstantRangeByteSize = pr.size;
          break;
        }
      }

      stage.requiredSubgroupSize = p.shaders[i].requiredSubgroupSize;

      stage.specializationData.clear();

      // set up the defaults
      if(p.shaders[i].refl)
      {
        for(size_t cb = 0; cb < p.shaders[i].refl->constantBlocks.size(); cb++)
        {
          if(p.shaders[i].refl->constantBlocks[cb].compileConstants)
          {
            for(const ShaderConstant &sc : p.shaders[i].refl->constantBlocks[cb].variables)
            {
              stage.specializationData.resize_for_index(sc.byteOffset + sizeof(uint64_t));
              memcpy(stage.specializationData.data() + sc.byteOffset, &sc.defaultValue,
                     sizeof(uint64_t));
            }
            break;
          }
        }
      }

      // apply any specializations
      for(const SpecConstant &s : p.shaders[i].specialization)
      {
        int32_t idx = p.shaders[i].patchData->specIDs.indexOf(s.specID);

        if(idx == -1)
        {
          RDCWARN("Couldn't find offset for spec ID %u", s.specID);
          continue;
        }

        size_t offs = idx * sizeof(uint64_t);

        stage.specializationData.resize_for_index(offs + sizeof(uint64_t));
        memcpy(stage.specializationData.data() + offs, &s.value, s.dataSize);
      }
      if(p.shaders[i].patchData)
        stage.specializationIds = p.shaders[i].patchData->specIDs;
    }
  }
  else
  {
    ret.compute.pipelineComputeLayoutResourceId = ResourceId();
    ret.compute.flags = 0;
    ret.computeShader = VKPipe::Shader();
  }

  if(state.graphics.pipeline != ResourceId())
  {
    const VulkanCreationInfo::Pipeline &p = c.m_Pipeline[state.graphics.pipeline];

    ret.graphics.pipelinePreRastLayoutResourceId = rm->GetOriginalID(p.vertLayout);
    ret.graphics.pipelineFragmentLayoutResourceId = rm->GetOriginalID(p.fragLayout);

    ret.graphics.flags = p.flags;

    // Input Assembly
    ret.inputAssembly.indexBuffer.resourceId = rm->GetOriginalID(state.ibuffer.buf);
    ret.inputAssembly.indexBuffer.byteOffset = state.ibuffer.offs;
    ret.inputAssembly.indexBuffer.byteStride = state.ibuffer.bytewidth;
    ret.inputAssembly.primitiveRestartEnable = state.primRestartEnable != VK_FALSE;
    ret.inputAssembly.topology =
        MakePrimitiveTopology(state.primitiveTopology, state.patchControlPoints);

    // Vertex Input
    ret.vertexInput.attributes.resize(state.vertexAttributes.size());
    for(size_t i = 0; i < state.vertexAttributes.size(); i++)
    {
      ret.vertexInput.attributes[i].location = state.vertexAttributes[i].location;
      ret.vertexInput.attributes[i].binding = state.vertexAttributes[i].binding;
      ret.vertexInput.attributes[i].byteOffset = state.vertexAttributes[i].offset;
      ret.vertexInput.attributes[i].format = MakeResourceFormat(state.vertexAttributes[i].format);
    }

    ret.vertexInput.bindings.resize(state.vertexBindings.size());
    for(const VkVertexInputBindingDescription2EXT &b : state.vertexBindings)
    {
      ret.vertexInput.bindings.resize_for_index(b.binding);
      ret.vertexInput.bindings[b.binding].vertexBufferBinding = b.binding;
      ret.vertexInput.bindings[b.binding].perInstance = b.inputRate == VK_VERTEX_INPUT_RATE_INSTANCE;
      ret.vertexInput.bindings[b.binding].instanceDivisor = b.divisor;
    }

    ret.vertexInput.vertexBuffers.resize(state.vbuffers.size());
    for(size_t i = 0; i < state.vbuffers.size(); i++)
    {
      ret.vertexInput.vertexBuffers[i].resourceId = rm->GetOriginalID(state.vbuffers[i].buf);
      ret.vertexInput.vertexBuffers[i].byteOffset = state.vbuffers[i].offs;
      ret.vertexInput.vertexBuffers[i].byteStride = (uint32_t)state.vbuffers[i].stride;
      ret.vertexInput.vertexBuffers[i].byteSize = (uint32_t)state.vbuffers[i].size;
    }

    // Shader Stages
    VKPipe::Shader *stages[] = {
        &ret.vertexShader,
        &ret.tessControlShader,
        &ret.tessEvalShader,
        &ret.geometryShader,
        &ret.fragmentShader,
        // compute
        NULL,
        &ret.taskShader,
        &ret.meshShader,
    };

    for(size_t i = 0; i < ARRAY_COUNT(stages); i++)
    {
      if(stages[i] == NULL)
        continue;

      stages[i]->resourceId = rm->GetUnreplacedOriginalID(p.shaders[i].module);
      stages[i]->entryPoint = p.shaders[i].entryPoint;

      stages[i]->stage = StageFromIndex(i);
      if(p.shaders[i].refl)
        stages[i]->reflection = p.shaders[i].refl;

      stages[i]->pushConstantRangeByteOffset = stages[i]->pushConstantRangeByteSize = 0;
      // don't have to handle separate vert/frag layouts as push constant ranges must be identical
      for(const VkPushConstantRange &pr : c.m_PipelineLayout[p.vertLayout].pushRanges)
      {
        if(pr.stageFlags & ShaderMaskFromIndex(i))
        {
          stages[i]->pushConstantRangeByteOffset = pr.offset;
          stages[i]->pushConstantRangeByteSize = pr.size;
          break;
        }
      }

      stages[i]->specializationData.clear();

      stages[i]->requiredSubgroupSize = p.shaders[i].requiredSubgroupSize;

      // set up the defaults
      if(p.shaders[i].refl)
      {
        for(size_t cb = 0; cb < p.shaders[i].refl->constantBlocks.size(); cb++)
        {
          if(p.shaders[i].refl->constantBlocks[cb].compileConstants)
          {
            for(const ShaderConstant &sc : p.shaders[i].refl->constantBlocks[cb].variables)
            {
              stages[i]->specializationData.resize_for_index(sc.byteOffset + sizeof(uint64_t));
              memcpy(stages[i]->specializationData.data() + sc.byteOffset, &sc.defaultValue,
                     sizeof(uint64_t));
            }
            break;
          }
        }
      }

      // apply any specializations
      for(const SpecConstant &s : p.shaders[i].specialization)
      {
        int32_t idx = p.shaders[i].patchData->specIDs.indexOf(s.specID);

        if(idx == -1)
        {
          RDCWARN("Couldn't find offset for spec ID %u", s.specID);
          continue;
        }

        size_t offs = idx * sizeof(uint64_t);

        stages[i]->specializationData.resize_for_index(offs + sizeof(uint64_t));
        memcpy(stages[i]->specializationData.data() + offs, &s.value, s.dataSize);
      }
      if(p.shaders[i].patchData)
        stages[i]->specializationIds = p.shaders[i].patchData->specIDs;
    }

    // Tessellation
    ret.tessellation.numControlPoints = p.patchControlPoints;

    ret.tessellation.domainOriginUpperLeft =
        state.domainOrigin == VK_TESSELLATION_DOMAIN_ORIGIN_UPPER_LEFT;

    ret.transformFeedback.rasterizedStream = state.rasterStream;

    // Transform feedback
    ret.transformFeedback.buffers.resize(state.xfbbuffers.size());
    for(size_t i = 0; i < state.xfbbuffers.size(); i++)
    {
      ret.transformFeedback.buffers[i].bufferResourceId = rm->GetOriginalID(state.xfbbuffers[i].buf);
      ret.transformFeedback.buffers[i].byteOffset = state.xfbbuffers[i].offs;
      ret.transformFeedback.buffers[i].byteSize = state.xfbbuffers[i].size;

      ret.transformFeedback.buffers[i].active = false;
      ret.transformFeedback.buffers[i].counterBufferResourceId = ResourceId();
      ret.transformFeedback.buffers[i].counterBufferOffset = 0;

      if(i >= state.firstxfbcounter)
      {
        size_t xfb = i - state.firstxfbcounter;
        if(xfb < state.xfbcounters.size())
        {
          ret.transformFeedback.buffers[i].active = true;
          ret.transformFeedback.buffers[i].counterBufferResourceId =
              rm->GetOriginalID(state.xfbcounters[xfb].buf);
          ret.transformFeedback.buffers[i].counterBufferOffset = state.xfbcounters[xfb].offs;
        }
      }
    }

    // Viewport/Scissors
    size_t numViewScissors = state.views.size();
    ret.viewportScissor.viewportScissors.resize(numViewScissors);
    for(size_t i = 0; i < numViewScissors; i++)
    {
      if(i < state.views.size())
      {
        ret.viewportScissor.viewportScissors[i].vp.x = state.views[i].x;
        ret.viewportScissor.viewportScissors[i].vp.y = state.views[i].y;
        ret.viewportScissor.viewportScissors[i].vp.width = state.views[i].width;
        ret.viewportScissor.viewportScissors[i].vp.height = state.views[i].height;
        ret.viewportScissor.viewportScissors[i].vp.minDepth = state.views[i].minDepth;
        ret.viewportScissor.viewportScissors[i].vp.maxDepth = state.views[i].maxDepth;
      }
      else
      {
        RDCEraseEl(ret.viewportScissor.viewportScissors[i].vp);
      }

      if(i < state.scissors.size())
      {
        ret.viewportScissor.viewportScissors[i].scissor.x = state.scissors[i].offset.x;
        ret.viewportScissor.viewportScissors[i].scissor.y = state.scissors[i].offset.y;
        ret.viewportScissor.viewportScissors[i].scissor.width = state.scissors[i].extent.width;
        ret.viewportScissor.viewportScissors[i].scissor.height = state.scissors[i].extent.height;
      }
      else
      {
        RDCEraseEl(ret.viewportScissor.viewportScissors[i].scissor);
      }
    }

    {
      ret.viewportScissor.discardRectangles.resize(p.discardRectangles.size());
      for(size_t i = 0; i < p.discardRectangles.size() && i < state.discardRectangles.size(); i++)
      {
        ret.viewportScissor.discardRectangles[i].x = state.discardRectangles[i].offset.x;
        ret.viewportScissor.discardRectangles[i].y = state.discardRectangles[i].offset.y;
        ret.viewportScissor.discardRectangles[i].width = state.discardRectangles[i].extent.width;
        ret.viewportScissor.discardRectangles[i].height = state.discardRectangles[i].extent.height;
      }

      ret.viewportScissor.discardRectanglesExclusive =
          (p.discardMode == VK_DISCARD_RECTANGLE_MODE_EXCLUSIVE_EXT);
    }

    {
      ret.viewportScissor.depthNegativeOneToOne = state.negativeOneToOne != VK_FALSE;
    }

    // Rasterizer
    ret.rasterizer.depthClampEnable = state.depthClampEnable != VK_FALSE;
    ret.rasterizer.depthClipEnable = state.depthClipEnable != VK_FALSE;
    ret.rasterizer.rasterizerDiscardEnable = state.rastDiscardEnable != VK_FALSE;
    ret.rasterizer.frontCCW = state.frontFace == VK_FRONT_FACE_COUNTER_CLOCKWISE;

    ret.rasterizer.conservativeRasterization = ConservativeRaster::Disabled;
    switch(state.conservativeRastMode)
    {
      case VK_CONSERVATIVE_RASTERIZATION_MODE_UNDERESTIMATE_EXT:
        ret.rasterizer.conservativeRasterization = ConservativeRaster::Underestimate;
        break;
      case VK_CONSERVATIVE_RASTERIZATION_MODE_OVERESTIMATE_EXT:
        ret.rasterizer.conservativeRasterization = ConservativeRaster::Overestimate;
        break;
      default: break;
    }

    ret.rasterizer.pipelineShadingRate = {state.pipelineShadingRate.width,
                                          state.pipelineShadingRate.height};

    ShadingRateCombiner combiners[2] = {};
    for(int i = 0; i < 2; i++)
    {
      switch(state.shadingRateCombiners[i])
      {
        default:
        case VK_FRAGMENT_SHADING_RATE_COMBINER_OP_KEEP_KHR:
          combiners[i] = ShadingRateCombiner::Keep;
          break;
        case VK_FRAGMENT_SHADING_RATE_COMBINER_OP_REPLACE_KHR:
          combiners[i] = ShadingRateCombiner::Replace;
          break;
        case VK_FRAGMENT_SHADING_RATE_COMBINER_OP_MIN_KHR:
          combiners[i] = ShadingRateCombiner::Min;
          break;
        case VK_FRAGMENT_SHADING_RATE_COMBINER_OP_MAX_KHR:
          combiners[i] = ShadingRateCombiner::Max;
          break;
        case VK_FRAGMENT_SHADING_RATE_COMBINER_OP_MUL_KHR:
          combiners[i] = ShadingRateCombiner::Multiply;
          break;
      }
    }
    ret.rasterizer.shadingRateCombiners = {combiners[0], combiners[1]};

    ret.rasterizer.lineRasterMode = LineRaster::Default;

    // "VK_LINE_RASTERIZATION_MODE_DEFAULT_HKR is equivalent to
    // VK_LINE_RASTERIZATION_MODE_RECTANGULAR_KHR if VkPhysicalDeviceLimits::strictLines is VK_TRUE"
    if(m_pDriver->GetDeviceProps().limits.strictLines)
      ret.rasterizer.lineRasterMode = LineRaster::Rectangular;

    switch(state.lineRasterMode)
    {
      case VK_LINE_RASTERIZATION_MODE_RECTANGULAR_KHR:
        ret.rasterizer.lineRasterMode = LineRaster::Rectangular;
        break;
      case VK_LINE_RASTERIZATION_MODE_BRESENHAM_KHR:
        ret.rasterizer.lineRasterMode = LineRaster::Bresenham;
        break;
      case VK_LINE_RASTERIZATION_MODE_RECTANGULAR_SMOOTH_KHR:
        ret.rasterizer.lineRasterMode = LineRaster::RectangularSmooth;
        break;
      default: break;
    }

    ret.rasterizer.lineStippleFactor = 0;    // stippled line disable
    ret.rasterizer.lineStipplePattern = 0;

    if(state.stippledLineEnable)
    {
      ret.rasterizer.lineStippleFactor = state.stippleFactor;
      ret.rasterizer.lineStipplePattern = state.stipplePattern;
    }

    ret.rasterizer.extraPrimitiveOverestimationSize = state.primOverestimationSize;

    switch(state.polygonMode)
    {
      case VK_POLYGON_MODE_POINT: ret.rasterizer.fillMode = FillMode::Point; break;
      case VK_POLYGON_MODE_LINE: ret.rasterizer.fillMode = FillMode::Wireframe; break;
      case VK_POLYGON_MODE_FILL: ret.rasterizer.fillMode = FillMode::Solid; break;
      default:
        ret.rasterizer.fillMode = FillMode::Solid;
        RDCERR("Unexpected value for FillMode %x", state.polygonMode);
        break;
    }

    switch(state.cullMode)
    {
      case VK_CULL_MODE_NONE: ret.rasterizer.cullMode = CullMode::NoCull; break;
      case VK_CULL_MODE_FRONT_BIT: ret.rasterizer.cullMode = CullMode::Front; break;
      case VK_CULL_MODE_BACK_BIT: ret.rasterizer.cullMode = CullMode::Back; break;
      case VK_CULL_MODE_FRONT_AND_BACK: ret.rasterizer.cullMode = CullMode::FrontAndBack; break;
      default:
        ret.rasterizer.cullMode = CullMode::NoCull;
        RDCERR("Unexpected value for CullMode %x", state.cullMode);
        break;
    }

    ret.rasterizer.provokingVertexFirst =
        state.provokingVertexMode == VK_PROVOKING_VERTEX_MODE_FIRST_VERTEX_EXT;

    ret.rasterizer.depthBiasEnable = state.depthBiasEnable != VK_FALSE;
    ret.rasterizer.depthBias = state.bias.depth;
    ret.rasterizer.depthBiasClamp = state.bias.biasclamp;
    ret.rasterizer.slopeScaledDepthBias = state.bias.slope;
    ret.rasterizer.lineWidth = state.lineWidth;

    // MSAA
    ret.multisample.rasterSamples = state.rastSamples;
    ret.multisample.sampleShadingEnable = p.sampleShadingEnable;
    ret.multisample.minSampleShading = p.minSampleShading;
    ret.multisample.sampleMask = state.sampleMask[0];

    ret.multisample.sampleLocations.customLocations.clear();
    if(state.sampleLocEnable)
    {
      ret.multisample.sampleLocations.gridWidth = state.sampleLocations.gridSize.width;
      ret.multisample.sampleLocations.gridHeight = state.sampleLocations.gridSize.height;
      ret.multisample.sampleLocations.customLocations.reserve(state.sampleLocations.locations.size());
      for(const VkSampleLocationEXT &loc : state.sampleLocations.locations)
      {
        ret.multisample.sampleLocations.customLocations.push_back({loc.x, loc.y, 0.0f, 0.0f});
      }
    }

    // Color Blend
    ret.colorBlend.alphaToCoverageEnable = state.alphaToCoverageEnable != VK_FALSE;
    ret.colorBlend.alphaToOneEnable = state.alphaToOneEnable != VK_FALSE;

    // find size if no static pipeline state
    size_t numAttach = RDCMAX(state.colorBlendEnable.size(), state.colorBlendEquation.size());
    numAttach = RDCMAX(numAttach, state.colorWriteEnable.size());
    numAttach = RDCMAX(numAttach, state.colorWriteMask.size());

    ret.colorBlend.blends.resize(numAttach);

    for(size_t i = 0; i < numAttach; i++)
    {
      ret.colorBlend.blends[i].enabled =
          (i < state.colorBlendEnable.size()) ? state.colorBlendEnable[i] != VK_FALSE : VK_FALSE;

      // due to shared structs, this is slightly duplicated - Vulkan doesn't have separate states
      // for logic operations
      ret.colorBlend.blends[i].logicOperationEnabled = state.logicOpEnable != VK_FALSE;
      ret.colorBlend.blends[i].logicOperation = MakeLogicOp(state.logicOp);

      if(ret.colorBlend.blends[i].enabled && i < state.colorBlendEquation.size())
      {
        ret.colorBlend.blends[i].colorBlend.source =
            MakeBlendMultiplier(state.colorBlendEquation[i].srcColorBlendFactor);
        ret.colorBlend.blends[i].colorBlend.destination =
            MakeBlendMultiplier(state.colorBlendEquation[i].dstColorBlendFactor);
        ret.colorBlend.blends[i].colorBlend.operation =
            MakeBlendOp(state.colorBlendEquation[i].colorBlendOp);

        ret.colorBlend.blends[i].alphaBlend.source =
            MakeBlendMultiplier(state.colorBlendEquation[i].srcAlphaBlendFactor);
        ret.colorBlend.blends[i].alphaBlend.destination =
            MakeBlendMultiplier(state.colorBlendEquation[i].dstAlphaBlendFactor);
        ret.colorBlend.blends[i].alphaBlend.operation =
            MakeBlendOp(state.colorBlendEquation[i].alphaBlendOp);
      }
      else
      {
        ret.colorBlend.blends[i].colorBlend.source = MakeBlendMultiplier(VK_BLEND_FACTOR_ZERO);
        ret.colorBlend.blends[i].colorBlend.destination = MakeBlendMultiplier(VK_BLEND_FACTOR_ZERO);
        ret.colorBlend.blends[i].colorBlend.operation = MakeBlendOp(VK_BLEND_OP_ADD);
        ret.colorBlend.blends[i].alphaBlend.source = MakeBlendMultiplier(VK_BLEND_FACTOR_ZERO);
        ret.colorBlend.blends[i].alphaBlend.destination = MakeBlendMultiplier(VK_BLEND_FACTOR_ZERO);
        ret.colorBlend.blends[i].alphaBlend.operation = MakeBlendOp(VK_BLEND_OP_ADD);
      }

      ret.colorBlend.blends[i].writeMask =
          (i < state.colorWriteMask.size()) ? (uint8_t)state.colorWriteMask[i] : 0;

      if(i < state.colorWriteEnable.size() && !state.colorWriteEnable[i])
        ret.colorBlend.blends[i].writeMask = 0;
    }

    ret.colorBlend.blendFactor = state.blendConst;

    // Depth Stencil
    ret.depthStencil.depthTestEnable = state.depthTestEnable != VK_FALSE;
    ret.depthStencil.depthWriteEnable = state.depthWriteEnable != VK_FALSE;
    ret.depthStencil.depthBoundsEnable = state.depthBoundsTestEnable != VK_FALSE;
    ret.depthStencil.depthFunction = MakeCompareFunc(state.depthCompareOp);
    ret.depthStencil.stencilTestEnable = state.stencilTestEnable != VK_FALSE;

    ret.depthStencil.frontFace.passOperation = MakeStencilOp(state.front.passOp);
    ret.depthStencil.frontFace.failOperation = MakeStencilOp(state.front.failOp);
    ret.depthStencil.frontFace.depthFailOperation = MakeStencilOp(state.front.depthFailOp);
    ret.depthStencil.frontFace.function = MakeCompareFunc(state.front.compareOp);

    ret.depthStencil.backFace.passOperation = MakeStencilOp(state.back.passOp);
    ret.depthStencil.backFace.failOperation = MakeStencilOp(state.back.failOp);
    ret.depthStencil.backFace.depthFailOperation = MakeStencilOp(state.back.depthFailOp);
    ret.depthStencil.backFace.function = MakeCompareFunc(state.back.compareOp);

    ret.depthStencil.minDepthBounds = state.mindepth;
    ret.depthStencil.maxDepthBounds = state.maxdepth;

    ret.depthStencil.frontFace.reference = state.front.ref;
    ret.depthStencil.frontFace.compareMask = state.front.compare;
    ret.depthStencil.frontFace.writeMask = state.front.write;

    ret.depthStencil.backFace.reference = state.back.ref;
    ret.depthStencil.backFace.compareMask = state.back.compare;
    ret.depthStencil.backFace.writeMask = state.back.write;
  }
  else
  {
    ret.graphics.pipelinePreRastLayoutResourceId = ResourceId();
    ret.graphics.pipelineFragmentLayoutResourceId = ResourceId();

    ret.graphics.flags = 0;

    ret.vertexInput.attributes.clear();
    ret.vertexInput.bindings.clear();
    ret.vertexInput.vertexBuffers.clear();

    VKPipe::Shader *stages[] = {
        &ret.vertexShader,   &ret.tessControlShader, &ret.tessEvalShader, &ret.geometryShader,
        &ret.fragmentShader, &ret.taskShader,        &ret.meshShader,
    };

    for(size_t i = 0; i < ARRAY_COUNT(stages); i++)
      *stages[i] = VKPipe::Shader();

    ret.viewportScissor.viewportScissors.clear();
    ret.viewportScissor.discardRectangles.clear();
    ret.viewportScissor.discardRectanglesExclusive = true;
    ret.viewportScissor.depthNegativeOneToOne = false;

    ret.colorBlend.blends.clear();
  }

  if(state.dynamicRendering.active)
  {
    VKPipe::RenderPass &rpState = ret.currentPass.renderpass;
    VKPipe::Framebuffer &fbState = ret.currentPass.framebuffer;
    const VulkanRenderState::DynamicRendering &dyn = state.dynamicRendering;

    rpState.dynamic = true;
    rpState.suspended = dyn.suspended;
    rpState.feedbackLoop = false;
    rpState.resourceId = ResourceId();
    rpState.subpass = 0;
    rpState.fragmentDensityOffsets.clear();
    rpState.tileOnlyMSAASampleCount = 0;

    fbState.resourceId = ResourceId();
    // dynamic rendering does not provide a framebuffer dimension, it's implicit from the image
    // views
    fbState.width = 0;
    fbState.height = 0;
    fbState.layers = dyn.layerCount;

    fbState.attachments.clear();
    rpState.inputAttachments.clear();
    rpState.colorAttachments.clear();
    rpState.resolveAttachments.clear();

    size_t attIdx = 0;
    for(size_t i = 0; i < dyn.color.size(); i++)
    {
      fbState.attachments.push_back({});

      ResourceId viewid = GetResID(dyn.color[i].imageView);

      if(viewid != ResourceId())
      {
        fbState.attachments.back().view = rm->GetOriginalID(viewid);
        ret.currentPass.framebuffer.attachments[attIdx].resource =
            rm->GetOriginalID(c.m_ImageView[viewid].image);

        fbState.attachments.back().format = MakeResourceFormat(c.m_ImageView[viewid].format);
        fbState.attachments.back().firstMip = c.m_ImageView[viewid].range.baseMipLevel & 0xff;
        fbState.attachments.back().firstSlice = c.m_ImageView[viewid].range.baseArrayLayer & 0xffff;
        fbState.attachments.back().numMips = c.m_ImageView[viewid].range.levelCount & 0xff;
        fbState.attachments.back().numSlices = c.m_ImageView[viewid].range.layerCount & 0xffff;

        Convert(fbState.attachments.back().swizzle, c.m_ImageView[viewid].componentMapping);
      }
      else
      {
        fbState.attachments.back().view = ResourceId();
        fbState.attachments.back().resource = ResourceId();

        fbState.attachments.back().firstMip = 0;
        fbState.attachments.back().firstSlice = 0;
        fbState.attachments.back().numMips = 1;
        fbState.attachments.back().numSlices = 1;
      }

      rpState.colorAttachments.push_back(uint32_t(attIdx++));

      if(dyn.color[i].resolveMode && dyn.color[i].resolveImageView != VK_NULL_HANDLE)
      {
        fbState.attachments.push_back({});

        viewid = GetResID(dyn.color[i].resolveImageView);

        fbState.attachments.back().view = rm->GetOriginalID(viewid);
        ret.currentPass.framebuffer.attachments[attIdx].resource =
            rm->GetOriginalID(c.m_ImageView[viewid].image);

        fbState.attachments.back().format = MakeResourceFormat(c.m_ImageView[viewid].format);
        fbState.attachments.back().firstMip = c.m_ImageView[viewid].range.baseMipLevel & 0xff;
        fbState.attachments.back().firstSlice = c.m_ImageView[viewid].range.baseArrayLayer & 0xffff;
        fbState.attachments.back().numMips = c.m_ImageView[viewid].range.levelCount & 0xff;
        fbState.attachments.back().numSlices = c.m_ImageView[viewid].range.layerCount & 0xffff;

        Convert(fbState.attachments.back().swizzle, c.m_ImageView[viewid].componentMapping);

        rpState.resolveAttachments.push_back(uint32_t(attIdx++));
      }
    }

    if(dyn.depth.imageView != VK_NULL_HANDLE || dyn.stencil.imageView != VK_NULL_HANDLE)
    {
      fbState.attachments.push_back({});

      ResourceId viewid = GetResID(dyn.depth.imageView);
      if(dyn.depth.imageView == VK_NULL_HANDLE)
        viewid = GetResID(dyn.stencil.imageView);

      fbState.attachments.back().view = rm->GetOriginalID(viewid);
      ret.currentPass.framebuffer.attachments[attIdx].resource =
          rm->GetOriginalID(c.m_ImageView[viewid].image);

      fbState.attachments.back().format = MakeResourceFormat(c.m_ImageView[viewid].format);
      fbState.attachments.back().firstMip = c.m_ImageView[viewid].range.baseMipLevel & 0xff;
      fbState.attachments.back().firstSlice = c.m_ImageView[viewid].range.baseArrayLayer & 0xffff;
      fbState.attachments.back().numMips = c.m_ImageView[viewid].range.levelCount & 0xff;
      fbState.attachments.back().numSlices = c.m_ImageView[viewid].range.layerCount & 0xffff;

      Convert(fbState.attachments.back().swizzle, c.m_ImageView[viewid].componentMapping);

      rpState.depthstencilAttachment = int32_t(attIdx++);
    }
    else
    {
      rpState.depthstencilAttachment = -1;
    }

    if(dyn.fragmentDensityView != VK_NULL_HANDLE)
    {
      fbState.attachments.push_back({});

      ResourceId viewid = GetResID(dyn.fragmentDensityView);

      fbState.attachments.back().view = rm->GetOriginalID(viewid);
      ret.currentPass.framebuffer.attachments[attIdx].resource =
          rm->GetOriginalID(c.m_ImageView[viewid].image);

      fbState.attachments.back().format = MakeResourceFormat(c.m_ImageView[viewid].format);
      fbState.attachments.back().firstMip = c.m_ImageView[viewid].range.baseMipLevel & 0xff;
      fbState.attachments.back().firstSlice = c.m_ImageView[viewid].range.baseArrayLayer & 0xffff;
      fbState.attachments.back().numMips = c.m_ImageView[viewid].range.levelCount & 0xff;
      fbState.attachments.back().numSlices = c.m_ImageView[viewid].range.layerCount & 0xffff;

      Convert(fbState.attachments.back().swizzle, c.m_ImageView[viewid].componentMapping);

      rpState.fragmentDensityAttachment = int32_t(attIdx++);
    }
    else
    {
      rpState.fragmentDensityAttachment = -1;
    }

    if(dyn.shadingRateView != VK_NULL_HANDLE)
    {
      fbState.attachments.push_back({});

      ResourceId viewid = GetResID(dyn.shadingRateView);

      fbState.attachments.back().view = rm->GetOriginalID(viewid);
      ret.currentPass.framebuffer.attachments[attIdx].resource =
          rm->GetOriginalID(c.m_ImageView[viewid].image);

      fbState.attachments.back().format = MakeResourceFormat(c.m_ImageView[viewid].format);
      fbState.attachments.back().firstMip = c.m_ImageView[viewid].range.baseMipLevel & 0xff;
      fbState.attachments.back().firstSlice = c.m_ImageView[viewid].range.baseArrayLayer & 0xffff;
      fbState.attachments.back().numMips = c.m_ImageView[viewid].range.levelCount & 0xff;
      fbState.attachments.back().numSlices = c.m_ImageView[viewid].range.layerCount & 0xffff;

      Convert(fbState.attachments.back().swizzle, c.m_ImageView[viewid].componentMapping);

      rpState.shadingRateAttachment = int32_t(attIdx++);
      rpState.shadingRateTexelSize = {dyn.shadingRateTexelSize.width,
                                      dyn.shadingRateTexelSize.height};
    }
    else
    {
      rpState.shadingRateAttachment = -1;
      rpState.shadingRateTexelSize = {1, 1};
    }

    rpState.multiviews.clear();
    for(uint32_t v = 0; v < 32; v++)
    {
      if(dyn.viewMask & (1 << v))
        rpState.multiviews.push_back(v);
    }
  }
  else if(state.GetRenderPass() != ResourceId())
  {
    // Renderpass
    ret.currentPass.renderpass.dynamic = false;
    ret.currentPass.renderpass.resourceId = rm->GetOriginalID(state.GetRenderPass());
    ret.currentPass.renderpass.subpass = state.subpass;

    ret.currentPass.renderpass.inputAttachments =
        c.m_RenderPass[state.GetRenderPass()].subpasses[state.subpass].inputAttachments;
    ret.currentPass.renderpass.colorAttachments =
        c.m_RenderPass[state.GetRenderPass()].subpasses[state.subpass].colorAttachments;
    ret.currentPass.renderpass.resolveAttachments =
        c.m_RenderPass[state.GetRenderPass()].subpasses[state.subpass].resolveAttachments;
    ret.currentPass.renderpass.depthstencilAttachment =
        c.m_RenderPass[state.GetRenderPass()].subpasses[state.subpass].depthstencilAttachment;
    ret.currentPass.renderpass.depthstencilResolveAttachment =
        c.m_RenderPass[state.GetRenderPass()].subpasses[state.subpass].depthstencilResolveAttachment;
    ret.currentPass.renderpass.fragmentDensityAttachment =
        c.m_RenderPass[state.GetRenderPass()].subpasses[state.subpass].fragmentDensityAttachment;
    ret.currentPass.renderpass.shadingRateAttachment =
        c.m_RenderPass[state.GetRenderPass()].subpasses[state.subpass].shadingRateAttachment;
    VkExtent2D texelSize =
        c.m_RenderPass[state.GetRenderPass()].subpasses[state.subpass].shadingRateTexelSize;
    ret.currentPass.renderpass.shadingRateTexelSize = {texelSize.width, texelSize.height};

    ret.currentPass.renderpass.multiviews =
        c.m_RenderPass[state.GetRenderPass()].subpasses[state.subpass].multiviews;
    ret.currentPass.renderpass.feedbackLoop =
        c.m_RenderPass[state.GetRenderPass()].subpasses[state.subpass].feedbackLoop;
    ret.currentPass.renderpass.tileOnlyMSAASampleCount =
        c.m_RenderPass[state.GetRenderPass()].subpasses[state.subpass].tileOnlyMSAASampleCount;

    ResourceId fb = state.GetFramebuffer();

    ret.currentPass.framebuffer.resourceId = rm->GetOriginalID(fb);

    if(fb != ResourceId())
    {
      ret.currentPass.framebuffer.width = c.m_Framebuffer[fb].width;
      ret.currentPass.framebuffer.height = c.m_Framebuffer[fb].height;
      ret.currentPass.framebuffer.layers = c.m_Framebuffer[fb].layers;

      ret.currentPass.framebuffer.attachments.resize(c.m_Framebuffer[fb].attachments.size());
      for(size_t i = 0; i < c.m_Framebuffer[fb].attachments.size(); i++)
      {
        ResourceId viewid = state.GetFramebufferAttachments()[i];

        if(viewid != ResourceId())
        {
          ret.currentPass.framebuffer.attachments[i].view = rm->GetOriginalID(viewid);
          ret.currentPass.framebuffer.attachments[i].resource =
              rm->GetOriginalID(c.m_ImageView[viewid].image);

          ret.currentPass.framebuffer.attachments[i].format =
              MakeResourceFormat(c.m_ImageView[viewid].format);
          ret.currentPass.framebuffer.attachments[i].firstMip =
              c.m_ImageView[viewid].range.baseMipLevel & 0xff;
          ret.currentPass.framebuffer.attachments[i].firstSlice =
              c.m_ImageView[viewid].range.baseArrayLayer & 0xffff;
          ret.currentPass.framebuffer.attachments[i].numMips =
              c.m_ImageView[viewid].range.levelCount & 0xff;
          ret.currentPass.framebuffer.attachments[i].numSlices =
              c.m_ImageView[viewid].range.layerCount & 0xffff;

          Convert(ret.currentPass.framebuffer.attachments[i].swizzle,
                  c.m_ImageView[viewid].componentMapping);
        }
        else
        {
          ret.currentPass.framebuffer.attachments[i].view = ResourceId();
          ret.currentPass.framebuffer.attachments[i].resource = ResourceId();

          ret.currentPass.framebuffer.attachments[i].firstMip = 0;
          ret.currentPass.framebuffer.attachments[i].firstSlice = 0;
          ret.currentPass.framebuffer.attachments[i].numMips = 1;
          ret.currentPass.framebuffer.attachments[i].numSlices = 1;
        }
      }
    }
    else
    {
      ret.currentPass.framebuffer.width = 0;
      ret.currentPass.framebuffer.height = 0;
      ret.currentPass.framebuffer.layers = 0;
    }

    ret.currentPass.renderpass.fragmentDensityOffsets.resize(state.fragmentDensityMapOffsets.size());
    for(size_t i = 0; i < state.fragmentDensityMapOffsets.size(); i++)
    {
      const VkOffset2D &o = state.fragmentDensityMapOffsets[i];
      ret.currentPass.renderpass.fragmentDensityOffsets[i] = Offset(o.x, o.y);
    }
  }
  else
  {
    ret.currentPass.renderpass.resourceId = ResourceId();
    ret.currentPass.renderpass.subpass = 0;
    ret.currentPass.renderpass.inputAttachments.clear();
    ret.currentPass.renderpass.colorAttachments.clear();
    ret.currentPass.renderpass.resolveAttachments.clear();
    ret.currentPass.renderpass.fragmentDensityOffsets.clear();
    ret.currentPass.renderpass.depthstencilAttachment = -1;
    ret.currentPass.renderpass.depthstencilResolveAttachment = -1;
    ret.currentPass.renderpass.fragmentDensityAttachment = -1;
    ret.currentPass.renderpass.shadingRateAttachment = -1;
    ret.currentPass.renderpass.shadingRateTexelSize = {1, 1};
    ret.currentPass.renderpass.tileOnlyMSAASampleCount = 0;

    ret.currentPass.framebuffer.resourceId = ResourceId();
    ret.currentPass.framebuffer.attachments.clear();
  }

  if(state.GetRenderPass() != ResourceId() || (state.dynamicRendering.active))
  {
    ret.currentPass.renderArea.x = state.renderArea.offset.x;
    ret.currentPass.renderArea.y = state.renderArea.offset.y;
    ret.currentPass.renderArea.width = state.renderArea.extent.width;
    ret.currentPass.renderArea.height = state.renderArea.extent.height;
  }

  ret.currentPass.colorFeedbackAllowed = (state.feedbackAspects & VK_IMAGE_ASPECT_COLOR_BIT) != 0;
  ret.currentPass.depthFeedbackAllowed = (state.feedbackAspects & VK_IMAGE_ASPECT_DEPTH_BIT) != 0;
  ret.currentPass.stencilFeedbackAllowed = (state.feedbackAspects & VK_IMAGE_ASPECT_STENCIL_BIT) != 0;

  // Descriptor sets
  ret.graphics.descriptorSets.resize(state.graphics.descSets.size());
  ret.compute.descriptorSets.resize(state.compute.descSets.size());

  // store dynamic offsets
  {
    rdcarray<VKPipe::DescriptorSet> *dsts[] = {
        &ret.graphics.descriptorSets,
        &ret.compute.descriptorSets,
    };

    const rdcarray<VulkanStatePipeline::DescriptorAndOffsets> *srcs[] = {
        &state.graphics.descSets,
        &state.compute.descSets,
    };

    for(size_t p = 0; p < ARRAY_COUNT(srcs); p++)
    {
      for(size_t i = 0; i < srcs[p]->size(); i++)
      {
        const VulkanStatePipeline::DescriptorAndOffsets &srcData = srcs[p]->at(i);
        ResourceId sourceSet = srcData.descSet;
        const uint32_t *srcOffset = srcData.offsets.begin();
        VKPipe::DescriptorSet &destSet = dsts[p]->at(i);

        destSet.dynamicOffsets.clear();

        if(sourceSet == ResourceId())
          continue;

        destSet.dynamicOffsets.reserve(srcData.offsets.size());

        VKPipe::DynamicOffset dynOffset;

        const WrappedVulkan::DescriptorSetInfo &descSetState =
            m_pDriver->m_DescriptorSetState[sourceSet];
        const DescriptorSetSlot *first =
            descSetState.data.binds.empty() ? NULL : descSetState.data.binds[0];
        for(size_t b = 0; b < descSetState.data.binds.size(); b++)
        {
          const DescSetLayout::Binding &layoutBind =
              c.m_DescSetLayout[descSetState.layout].bindings[b];

          if(layoutBind.layoutDescType != VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC &&
             layoutBind.layoutDescType != VK_DESCRIPTOR_TYPE_STORAGE_BUFFER_DYNAMIC)
            continue;

          uint64_t descriptorByteOffset = descSetState.data.binds[b] - first;

          // inline UBOs aren't dynamic and variable size can't be used with dynamic buffers, so the
          // count is what it is at definition time
          for(uint32_t a = 0; a < layoutBind.descriptorCount; a++)
          {
            dynOffset.descriptorByteOffset = descriptorByteOffset + a;
            dynOffset.dynamicBufferByteOffset = *srcOffset;
            srcOffset++;

            destSet.dynamicOffsets.push_back(dynOffset);
          }
        }
      }
    }
  }

  {
    rdcarray<VKPipe::DescriptorSet> *dsts[] = {
        &ret.graphics.descriptorSets,
        &ret.compute.descriptorSets,
    };

    const rdcarray<VulkanStatePipeline::DescriptorAndOffsets> *srcs[] = {
        &state.graphics.descSets,
        &state.compute.descSets,
    };

    const VKDynamicShaderFeedback &usage = m_BindlessFeedback.Usage[eventId];

    ret.shaderMessages = usage.messages;

    for(size_t p = 0; p < ARRAY_COUNT(srcs); p++)
    {
      for(size_t i = 0; i < srcs[p]->size(); i++)
      {
        ResourceId sourceSet = (*srcs[p])[i].descSet;
        VKPipe::DescriptorSet &destSet = (*dsts[p])[i];

        if(sourceSet == ResourceId())
        {
          destSet.descriptorSetResourceId = ResourceId();
          destSet.pushDescriptor = false;
          destSet.layoutResourceId = ResourceId();
          continue;
        }

        ResourceId layoutId = m_pDriver->m_DescriptorSetState[sourceSet].layout;

        destSet.descriptorSetResourceId = rm->GetOriginalID(sourceSet);
        destSet.pushDescriptor = (c.m_DescSetLayout[layoutId].flags &
                                  VK_DESCRIPTOR_SET_LAYOUT_CREATE_PUSH_DESCRIPTOR_BIT_KHR);

        destSet.layoutResourceId = rm->GetOriginalID(layoutId);
      }
    }
  }

  // image layouts
  {
    size_t i = 0;
    ret.images.resize(m_pDriver->m_ImageStates.size());
    for(auto it = m_pDriver->m_ImageStates.begin(); it != m_pDriver->m_ImageStates.end(); ++it)
    {
      VKPipe::ImageData &img = ret.images[i];

      if(rm->GetOriginalID(it->first) == it->first)
        continue;

      img.resourceId = rm->GetOriginalID(it->first);

      LockedConstImageStateRef imState = it->second.LockRead();
      img.layouts.resize(imState->subresourceStates.size());
      auto subIt = imState->subresourceStates.begin();
      for(size_t l = 0; l < img.layouts.size(); ++l, ++subIt)
      {
        img.layouts[l].name = ToStr(subIt->state().newLayout);
        img.layouts[l].baseMip = subIt->range().baseMipLevel;
        img.layouts[l].numMip = subIt->range().levelCount;
        img.layouts[l].baseLayer = subIt->range().baseArrayLayer;
        img.layouts[l].numLayer = subIt->range().layerCount;
      }

      if(img.layouts.empty())
      {
        img.layouts.push_back(VKPipe::ImageLayout());
        img.layouts[0].name = "Unknown";
      }

      i++;
    }

    ret.images.resize(i);
  }

  if(state.conditionalRendering.buffer != ResourceId())
  {
    ret.conditionalRendering.bufferId = rm->GetOriginalID(state.conditionalRendering.buffer);
    ret.conditionalRendering.byteOffset = state.conditionalRendering.offset;
    ret.conditionalRendering.isInverted =
        state.conditionalRendering.flags == VK_CONDITIONAL_RENDERING_INVERTED_BIT_EXT;

    bytebuf data;
    GetBufferData(state.conditionalRendering.buffer, state.conditionalRendering.offset,
                  sizeof(uint32_t), data);

    uint32_t value;
    memcpy(&value, data.data(), sizeof(uint32_t));

    ret.conditionalRendering.isPassing = value != 0;

    if(ret.conditionalRendering.isInverted)
      ret.conditionalRendering.isPassing = !ret.conditionalRendering.isPassing;
  }
}