Step 32 · Graphics and performance · D3
One draw: PM4 to GPU completion
Trace a selected non-indexed draw through state, shader programs, resource bindings, Vulkan recording and completion.
Before this page: PM4 & the command processor · Shaders: RDNA 2 → SPIR-V · The program cache · Page faults & CPU/GPU coherency · Concurrency & the memory model. View the learning route.
Use the packet, register, shader, Vulkan and coherency explanations to follow one request across the graphics subsystem. This source trace at d2413fc2 follows an ordinary, non-indexed triangle-list draw with a vertex shader, an active pixel shader and a color target. It is not a standalone three-word program or a captured frame.
Accepting a guest submission, recording a Vulkan draw, completing that submission on the GPU, and displaying an image are different events. A “DrawComplete” CPU log records progress through the host draw function; it is not a GPU fence and does not prove that any pixels were displayed.
1. Identify the packet and the state it consumes
At CpOpDrawIndexAuto in src/graphics/guest_gpu/command_processor/pm4Handlers.cpp, the current parser accepts header 0xC0012D00, consumes two payload dwords and rejects flag bits outside 0x22. The first payload dword becomes the vertex count, despite the parser's local variable name index_count.
Illustrative packet, not a complete render workload: header count flags 0xC0012D00 0x00000003 0x00000002 Header = 1 dword; payload = 2 dwords; total = 3 dwords = 12 bytes. Count = 3 vertices; flag 0x02 is inside the accepted mask 0x22.
At this handler, buffer points to the payload, not the header. Its return value 2 is the number of payload dwords consumed. Confusing these coordinate systems shifts every subsequent packet.
The packet does not contain a full draw state. Earlier commands establish shader addresses, user-data registers/descriptors, targets, topology, offsets and instance count. Locate those writes in the same submission and preceding preserved state. Three vertices with no usable shader or targets need not reach a Vulkan draw.
2. Follow the guest-to-renderer handoffs
Guest-side command generation and state writes -> AgcDriverSubmitCommandBuffer (graphics queue) -> submit_dcb -> GuestGpu::Submit -> queued Submission -> GuestGpu::ThreadRun -> GuestGpu::Process -> CommandProcessor::Process -> CpOpDrawIndexAuto -> CommandProcessor::DrawIndexAuto -> RenderExecutor::DrawAuto -> PrepareDrawRenderState -> ExecutePreparedDraw -> recorded Vulkan draw -> BufferFlush / CommandScheduler::Submit -> timeline signal observed -> eligible deferred work -> presentation is a separate downstream path
The AGC submission route above is one route, not the only entry point. AgcDriverSubmitCommandBuffer chooses graphics versus compute based on its queue context; this trace assumes graphics. submit_dcb passes a span of guest commands. GuestGpu::Submit queues that span; a span is a borrowed view, not an owning copy. Follow the guest's submission/lifetime protocol before assuming it may overwrite the command storage.
The GPU worker processes submissions, retaining suspended execution state when a command stream blocks. GuestGpu::Process associates the command processor with a guest submit ID, initializes its buffer, drives constant/draw streams and flushes after progress. A guest submit ID is a debug/guest-processing identity; it is not the Vulkan scheduler's completion tick.
3. Prepare the draw before committing commands
| Source boundary | What becomes concrete | What to check |
|---|---|---|
CommandProcessor::DrawIndexAuto | Uses current instance state when the incoming count is unspecified, then forwards to DrawAuto. | Vertex count, final instance count, submit ID and current register state. |
RenderExecutor::DrawAuto | Drains eligible pending operations, locks render context, checks counts and special cases. | Zero counts, metadata/color/depth operations, invalid shaders and unsupported topology can return before ordinary rendering. Record the branch, not just the function entry. |
PrepareDrawRenderState / RefreshShaders | Determines pixel-shader activity, gets programs, resolves written color attachments and depth target. | Shader output mask intersected with enabled color outputs; actual attachment identities and subresources, not just addresses. |
PipelineCache::GetGraphicsPrograms | Chooses stage programs and specialization according to current resources/state. | Shader identity, logical/native stage, resource snapshot and selected permutation. See C9. |
PrepareBindings / PrepareGraphicsBindings | Discovers buffers, prepares BDA when needed, rebinds images/targets, then buffers. | Descriptor bytes, ownership, offsets/ranges, aliasing, final image identities and required synchronization. |
AcquireVertexBuffers / GetGraphicsPipeline | Prepared vertex bindings and a compatible host pipeline. | Guest descriptor bounds survive cache allocation merging. Pipeline key must include the relevant static state and shader identities. |
AcquireRenderTargets | Rendering attachments, transitions and feedback aspects. | Correct mip/layer, formats, layouts, extents and attachment interactions. |
These are not independent immutable snapshots. RenderContext owns the caches, and resource preparation can read guest memory, read back metadata, submit work, wait and restart the scheduler. PrepareGraphicsBindings deliberately finalizes image identities before reserving draw buffers.
ExecutePreparedDraw fetches the current buffer.Handle() after resource preparation. From that point onward, the commit path must not introduce guest-memory accesses that can finish/restart the scheduler. Holding an earlier Vulkan handle across preparation can target an obsolete command buffer even when the C++ wrapper reference remains valid.
Trace depth-copy validation at the point the draw consumes state: DecodeRenderControl() only records the bits, while ResolveRenderDepthTarget() rejects unsupported use after checking attachment state. See register decoding versus draw validation.
4. Distinguish three different cache questions
- Program/resource analysis: the program cache retains analysis and resource-plan data, plus mutable scratch/snapshot storage borrowed by runtime stage data. Do not assume borrowed pointers own independent immutable results.
- Shader permutation: a new permutation rebuilds temporary translated IR before compilation; a matching compiled permutation can be reused. “Same shader code” does not necessarily mean the same specialization.
- Host graphics pipeline: the key combines stage identities with packed static state. At this snapshot
PipelineStaticParametersis 116 bytes, excludes dynamic depth bounds and stencil state, and stores canonical effective blend factors. Logical-alpha remapping also affects the pixel-shader identity. A driver disk-cache hit does not by itself bypass the emulator's RDNA translation.
Record which layer hit or missed. Do not use a generic “cache hit” log as proof that resources were refreshed correctly or that the pipeline matches today's draw.
5. Commit bindings and record the draw
On the selected ordinary path, ExecutePreparedDraw commits vertex bindings and stage descriptors, applies dynamic graphics state, starts rendering, binds the graphics pipeline and calls EmitDrawPrimitives. For a non-indexed triangle list, that function records vk_buffer.draw(count, instance_count, first_vertex, first_instance). A local variable named index_count in the common draw description still supplies the vertex count here.
If shaders write buffers, the function ends rendering and records a shader-write barrier. Reaching its final log means these CPU-side recording steps ran; it does not mean the GPU has executed them. Indexed, mesh, tessellation, indirect and rectangle conversion paths add different preparation or emission steps. They are outside this selected trace.
6. Cross the submission and completion boundary
CommandScheduler::Submit ends the current command buffer. Under the graphics queue mutex, it obtains the next timeline tick, adds the master semaphore signal and submits the buffer and semaphore values to the Vulkan queue. It then invalidates the recorded Vulkan buffer handle. Flush submits and begins the next buffer; it is not a wait. FlushAndWait also waits for the returned tick before beginning the next buffer.
MasterSemaphore::Refresh obtains observed GPU progress; IsFree(tick) means KnownGpuTick() >= tick. PopPendingOperations checks completion and priority-operation ordering before running eligible deferred callbacks. Keeping a resource alive until the CPU call returns is insufficient if a submitted command still references it.
Illustrative timeline (not measured frame data) CPU records draw using resource A for tick 6 CPU submits tick 6 observed GPU tick remains 4 CPU moves to later work A must remain alive GPU completion reaches tick 6 A's tick-6 use is complete Eligible deferred callback runs retirement still follows owner/dependency rules
Completion of this submission alone does not prove presentation, guest CPU visibility of every GPU write, or absence of future uses of A. A readback also needs the correct copy, visibility and CPU-side completion path. Oversized downloads in this snapshot retain temporary backing until deferred completion; the ordinary ring's lifetime rules are not permission to free that backing early.
Work through Lab B to reproduce a wrong-retirement-tick bug without a GPU. It tests caller bookkeeping, not Vulkan synchronization itself.
7. Make a trace ledger for one real draw
For optional live work, use a known scene from your own lawful content, a matching symbol build and bounded diagnostics. Existing command dumps, shader logs and a graphics capture can help; preserve exact source/configuration and compare the same draw. Do not add broad logging to a timing-sensitive path or disable validation to hide a failure.
Source / binary / scene / diagnostic settings: Guest queue / submit ID / DCB address / packet dword offset: Header / payload / effective instance count: Topology / shader addresses and identities: User data -> descriptor -> guest address/range: Program permutation hit/miss / pipeline hit/miss: Target format / mip / layer / layout: Host bindings and command buffer AFTER preparation: Recorded command / early-return branch, if any: Host submission tick / observed completed tick: Deferred operation / last-use ownership: Image result / presentation evidence (separate):
If the image is wrong, find the first boundary where observed state differs from the contract. Correct packet parsing does not prove a correct descriptor; correct bindings do not prove correct shader semantics; completed GPU work can still render the wrong image.
Pass the draw gate
Checkpoint
Self-check: DrawComplete was logged, but completed tick is 4 and the draw used tick 6. May A be destroyed?
No. The log is CPU-side recording evidence. Tick 6 has not been observed complete. Even after it completes, check later uses and the owner's deferred-release rules; the log alone never grants permission.
Source anchors
src/libs/agc.cpp:AgcDriverSubmitCommandBuffer,submit_dcb.src/graphics/guest_gpu/graphicsRun.cpp:GuestGpu::Submit,ThreadRun,Process,CommandProcessor::DrawIndexAuto,BufferFlush.src/graphics/guest_gpu/command_processor/pm4Handlers.cpp:CpOpDrawIndexAuto.src/graphics/host_gpu/renderer/renderDraw.cpp:DrawAuto,PrepareDrawRenderState,ExecutePreparedDraw,EmitDrawPrimitives.src/graphics/host_gpu/renderer/pipeline/pipelineCache.cppandsrc/graphics/host_gpu/renderer/pipeline/descriptors.cpp: programs, pipelines and bindings.src/graphics/host_gpu/renderer/commandScheduler.cpp,src/graphics/host_gpu/renderer/masterSemaphore.handsrc/graphics/host_gpu/renderer/masterSemaphore.cpp: ticks, waiting and deferred work.
Next: connect this draw path to performance, then review the contribution gates and learn diagnostics before the debugging labs. The coverage map lists the branches this lesson does not teach in depth. For a missing-feature implementation plan, read C11: ray tracing; it covers the implemented software intersection path and optional host-query research.