Synchronization 2 and frame pacing in this engine

Vulkan Synchronization 2 makes barriers and submissions easier to read. This sample uses it to keep uploads and rendering in step without stalls.

The goal here isn’t “maximum cleverness.” It’s predictable ordering:

  • the transfer queue moves data onto the GPU

  • the graphics queue draws using whatever is ready

  • the CPU only mutates per-frame resources when it knows the GPU is done with them

The moving parts

  • Timeline semaphore on the transfer queue — batches of texture uploads signal increasing values.

  • Graphics submit waits on the latest uploads value — by the time we draw, textures are ready to sample.

  • Frame fences — each frame‑in‑flight has a fence we wait on at the start of the next frame’s CPU work.

Barriers we rely on

Uploads path:

  • UNDEFINED → TRANSFER_DST_OPTIMAL (dstStage: TRANSFER, dstAccess: TRANSFER_WRITE)

  • After copy: TRANSFER_DST_OPTIMAL → SHADER_READ_ONLY_OPTIMAL (srcStage: TRANSFER, dstStage: FRAGMENT_SHADER)

Render path:

  • Attachment images transition outside active dynamic rendering blocks using vkCmdPipelineBarrier2.

  • Swapchain transitions: to COLOR_ATTACHMENT_OPTIMAL before composite/transparent, to PRESENT_SRC_KHR after ending the last rendering pass.

Descriptor updates at the safe point

At the start of a frame, after waiting on the frame fence, we refresh only this frame’s descriptor sets. That avoids “update‑after‑bind” pitfalls and frame‑to‑frame flicker during streaming.

Takeaways

  • Keep transitions outside active beginRendering/endRendering scopes.

  • Use clear stage/access pairs; prefer Synchronization 2 for readability.

  • Pair timeline semaphores with fences: timelines coordinate queues; fences bound the CPU turn.

Where to look in the code

  • Upload submission and timeline semaphore signaling:

    • renderer_resources.cpp

    • renderer_utils.cpp

  • Graphics submit waits (including “latest upload value”):

    • renderer_rendering.cpp

  • Image barriers for the render path (attachments + swapchain):

    • renderer_rendering.cpp

  • Swapchain and present integration:

    • swap_chain.h

    • renderer_rendering.cpp

Future work ideas

If you want to experiment with pacing and latency:

  • Add a UI toggle for the frames-in-flight count and measure input latency vs throughput.

  • Add a “fixed camera path” mode (development-only) to produce repeatable GPU timing comparisons.

  • Add GPU timestamp queries around the big passes to visualize where time goes.

  • Add async compute experiments (if your device supports it) for things like Forward+ light list building.