GPU pipeline¶
A program draws by writing a list of commands into memory and telling the GPU where the list is. The emulator reads that list, collects the settings for each draw, rewrites its Ampere machine code for Vulkan, and draws on the PC's graphics card. For example, a triangle takes a handful of commands: where the shaders are, where the vertices are, the viewport, then "draw".
How does a draw reach Vulkan?¶
sequenceDiagram
participant RAM as Emulated RAM
participant P as Pusher (reads command lists)
participant C as 3D class (drawing registers)
participant E as Engine
participant T as Translator (SASS to SPIR-V)
participant V as Vulkan (PC graphics card)
E->>P: Queue of command lists
P->>RAM: Fetch command words
P->>C: Register writes
C-->>P: Frozen draw settings
P-->>E: Events, in command order
E->>RAM: Read shader header and code
E->>T: NVIDIA shader code
T-->>E: Vulkan shader
E->>RAM: Copy the buffers the draw reads
E->>V: Build pipeline, draw
V-->>E: Pixels
E->>RAM: Write pixels back
Note over E,RAM: The display reads the finished image from emulated RAM
One draw, from the program's command list to pixels back in emulated memory.
The words on this path:
- A pushbuffer is one command list in emulated RAM, and the GPFIFO is the queue of pointers to those lists.
- A method is one command: a write to a GPU engine's register.
- The 3D class is the drawing engine's register set.
- SASS is NVIDIA's shader machine code, and SPIR-V is the shader format Vulkan accepts.
- A Vulkan pipeline bundles the shaders with the settings Vulkan must know before drawing.
What happens at each step?¶
| Step | Code | Output |
|---|---|---|
| Read queue entries and command headers | fifo.rs Pusher::submit |
Register writes, each aimed at one engine |
| Bind engines to slots, run channel commands | fifo.rs channel |
Semaphore events, the GPU's "I got this far" signals |
| Keep the drawing registers, freeze them at each draw | threed.rs ThreeD::write |
Event::Draw(DrawSnapshot) |
| Read the shader header (SPH) and code, translate | engine.rs module, sm86 translate_program |
SPIR-V, cached by a hash of header and code |
| Copy the buffers the draw reads | engine.rs mirror |
PC-side buffers, keyed by where they sit in emulated RAM |
| Run compute launches | engine.rs dispatch |
Emulated RAM pages the program changed |
| Draw and read back | graphics.rs VkState::draw |
RGBA8 pixels |
| Write back and show | engine.rs draw, present |
Emulated RAM, then display::present |
Why does submit wait for every draw?¶
Gpu::submit returns only after every draw has landed in emulated RAM. So any later CPU read sees the result, with no extra waiting. See ADR-0006.
Why freeze the settings at each draw?¶
A draw must see the registers as they were when it was issued. The frozen copy, the snapshot, keeps later commands from changing a draw that is still running.
Does compute work take the same road?¶
Yes, through the compute class 0xC7C0. A QMD, the block in emulated RAM that describes one compute launch, becomes a dispatch snapshot (see command processor). The engine translates the program, runs it over the mapped memory window, writes back changed pages and releases the launch's semaphores.
Verified by: the engine tests in core/src/tests/gpu/engine.rs on Intel and lavapipe (Mesa's CPU-based Vulkan driver), with the Vulkan validation layer reporting no errors. They cover the pushbuffer triangle with a vertex remap and a QMD compute launch. They also cover indexed, instanced, depth-tested, blended and textured draws, render to texture, clears, reports, acquires and both y-flips.
Sources: core/src/gpu/{fifo,threed,engine,graphics}.rs, core/src/tests/gpu/engine.rs.