summaryrefslogtreecommitdiffstats
path: root/docs/03-architecture
diff options
context:
space:
mode:
Diffstat (limited to 'docs/03-architecture')
-rw-r--r--docs/03-architecture/01-system-overview.md183
-rw-r--r--docs/03-architecture/02-guest-stack-and-cudasw.md139
-rw-r--r--docs/03-architecture/03-gsp-and-gsp-rm.md204
-rw-r--r--docs/03-architecture/04-uvm.md172
-rw-r--r--docs/03-architecture/05-gmmu-amap-hal-gr.md198
-rw-r--r--docs/03-architecture/06-host-module-and-hybrid-model.md227
-rw-r--r--docs/03-architecture/07-driver-lifecycle-field-guide.md188
7 files changed, 1311 insertions, 0 deletions
diff --git a/docs/03-architecture/01-system-overview.md b/docs/03-architecture/01-system-overview.md
new file mode 100644
index 0000000..ed9b24b
--- /dev/null
+++ b/docs/03-architecture/01-system-overview.md
@@ -0,0 +1,183 @@
+# System Overview
+
+> The one document to read if you want to understand *how Open-IOV works* before
+> diving into any subsystem.
+
+## 1. The core architectural principle
+
+> **Emulate the interface. Pass through the work.**
+
+A virtual GPU is a *mediated passthrough* device. The guest must believe it
+owns a real NVIDIA GPU with a real GPU System Processor (GSP) running NVIDIA's
+firmware. The host must arrange for the guest's work to execute on real
+hardware, because emulating compute in software is 100–1000× too slow to be
+useful.
+
+Everything therefore falls into two buckets:
+
+```
+┌─────────────────────────────────────────────────────────────────────────┐
+│ EMULATE (software; guest expects virtual hardware) │
+│ • PCI configuration space, capability structures, power management │
+│ • BAR0 register interface (shadow, not the physical GPU's BAR0) │
+│ • BAR1 VRAM aperture (guest-visible video memory) │
+│ • Instance/PRAMIN window │
+│ • GSP firmware boot handshake (FWSEC → booter → RISCV → boot complete)│
+│ • GSP-RM message queues, RPC transport, resource-manager controls │
+│ • Event/notification channels │
+│ • MSI-X injection (via eventfd, from the hypervisor's perspective) │
+└─────────────────────────────────────────────────────────────────────────┘
+
+┌─────────────────────────────────────────────────────────────────────────┐
+│ PASS THROUGH (real hardware; for correctness and performance) │
+│ • GPU memory: guest VRAM objects backed by real VRAM (goal) │
+│ • Page tables / GMMU state used by the GPU (goal) │
+│ • Channel execution: the guest's push buffers run on real channels │
+│ • Compute (CUDA) and, later, 3D/video work │
+│ • DMA, completion and fault interrupts relayed back to the guest │
+└─────────────────────────────────────────────────────────────────────────┘
+```
+
+The line between the two is the **interface boundary**, and it moves as the
+project matures: early work emulates more (everything the guest can observe),
+later work forwards more (everything that must be fast).
+
+## 2. Layer map
+
+```
+GUEST
+ application ── CUDA runtime (libcudart) ─┐
+ application ── CUDA driver (libcuda) ─┤ userspace: CUDASW stack
+ OpenGL/Vulkan ── libGLX/libvulkan ──────┘
+ │ ioctls / mmaps
+ nvidia.ko (unmodified) ── RM core, resource server, UVM, modeset/drm
+ │ MMIO + DMA + MSI-X (a virtual PCIe device)
+════════╪════════════════════════════════════════════════════════════════
+HYPERVISOR (KVM/Xen/...) VFIO mdev transport + IOMMU isolation
+ │
+HOST MODULE open-iov-vfio.ko
+ ├─ virtual device: config space, BAR0/BAR1/BAR3 windows, MSI-X
+ ├─ emulation core: GSP boot + GSP-RM (RPC msgq, controls, objects, events)
+ ├─ memory model: guest RAM access, VA↔PA maps, VRAM backing
+ └─ work path: channel/push forwarding to the physical GPU [in progress]
+ │
+HOST DRIVER nvidia.ko (unmodified) and/or direct GPU interfaces
+ │
+PHYSICAL GPU
+```
+
+## 3. Host module architecture
+
+`open-iov-vfio.ko` is a **VFIO mediated-device (mdev) driver**. It registers a
+parent on the physical GPU and exposes virtual GPU types; each `mdev` instance
+is one virtual GPU for one VM.
+
+| Subsystem | Files (illustrative) | Responsibility |
+|---|---|---|
+| mdev/VFIO registration | `kernel/main.c` | parent/type registration, VFIO device, region and IRQ setup |
+| Config space | `kernel/main.c`, `kernel/bar_init.c` | identify/command/status, BARs, PCIe caps, MSI-X caps, VF windows |
+| BAR0 shadow | `kernel/bar_emulation.c` | 16 MB emulated register space; register reads/writes the guest driver depends on |
+| VRAM aperture (BAR1) | `kernel/main.c`, `memory.c` | 256 MB guest-visible video memory (host RAM today; real VRAM later) |
+| Instance memory (BAR3) | `kernel/bar_emulation.c` | instance/PRAMIN window behaviour |
+| GSP emulation thread | `kernel/gsp_emulation.c` | boot stage completion, doorbell polling, MSI-X injection |
+| GSP-RM emulation | `kernel/gsp_rm_emulation.c` | message queues, RPC dispatch, resource-manager controls, objects |
+| RPC handlers (firmware-derived) | `kernel/gsp_rpc_client.c` | behaviours decompiled from the GSP firmware |
+| Firmware analysis (runtime) | `kernel/firmware_analyzer.c` | CRC/SHA signatures, pattern matching, version identification |
+| Version dispatch | `kernel/dynamic_rpc_dispatcher.c`, `kernel/version/` | version-aware handler selection |
+| Hardware abstraction | `kernel/architecture/` | per-architecture constants and behaviours |
+| Userspace library | `userspace/lib/libvgpu.c` | management API (`vgpu_*`), diagnostics |
+
+**Data flow for an MMIO access from the guest:**
+
+```
+guest nvidia.ko writes BAR0 offset X
+ → KVM exit → QEMU → VFIO region write into the mdev
+ → nv_vgpu_vfio_dev_write() (or directly into the BAR0 shadow if mmap'd)
+ → BAR0 shadow state machine (doorbells, VF registers, notify)
+ → may wake the GSP emulation thread
+```
+
+**Data flow for an RPC from the guest to "firmware":**
+
+```
+guest writes a command-queue element into a guest-memory msgq and rings the
+GSP doorbell (BAR0)
+ → emulation thread reads the queue through the hypervisor's guest-RAM
+ accessor (vfio_dma_rw), parses the element, dispatches the RPC
+ → handler produces a reply element in the status queue
+ → emulation injects an MSI-X interrupt to the guest
+```
+
+## 4. Guest stack summary
+
+* `nvidia.ko` is the **unmodified** NVIDIA kernel driver. It contains the
+ resource manager (RM) core, the resource server (`resserv`), the memory
+ manager, the FIFO/runlist code, the GPU System Processor client path, and
+ UVM (the unified memory/virtual-memory module, `nvidia_uvm.ko`).
+* User space consists of the CUDA driver (`libcuda.so`), the CUDA runtime
+ (`libcudart.so`), plus graphics/video libraries. The family of NVIDIA
+ *software* components that mediate between applications and the driver is
+ referred to in this project as **CUDASW**.
+* The guest expects to program a real GPU: it writes registers, allocates
+ objects, creates channels, submits push buffers and waits on semaphores —
+ exactly as on bare metal.
+
+Detailed subsystem documents:
+
+* [`02-guest-stack-and-cudasw.md`](02-guest-stack-and-cudasw.md)
+* [`03-gsp-and-gsp-rm.md`](03-gsp-and-gsp-rm.md)
+* [`04-uvm.md`](04-uvm.md)
+* [`05-gmmu-amap-hal-gr.md`](05-gmmu-amap-hal-gr.md)
+* [`06-host-module-and-hybrid-model.md`](06-host-module-and-hybrid-model.md)
+* [`07-driver-lifecycle-field-guide.md`](07-driver-lifecycle-field-guide.md)
+
+## 5. Why a GSP emulation is possible at all
+
+The guest driver never talks to GPU silicon directly for management: since the
+Turing generation, NVIDIA GPUs boot a **RISC-V firmware (the GSP)** that hosts
+the Resource Manager. The driver's normal path is to talk to that firmware over
+shared memory queues using a small RPC protocol.
+
+That protocol is:
+
+* **small** (a few dozen message types at the levels we need),
+* **stable** (message layouts are versioned and compiled into the driver), and
+* **observable** (everything arrives in guest memory we can read).
+
+So instead of emulating a GPU, we emulate **the firmware's interface**. The
+guest driver is satisfied because the protocol is satisfied; the guest's
+*work* is then given to real hardware.
+
+The firmware itself we study directly: it is a 28 MB RV64IMAC ELF that we have
+decompiled and mapped (see `wiki/GSP-570.144W-Firmware-Analysis.md` and
+`wiki/Address-Reference.md`).
+
+## 6. What runs today
+
+1. Guest boots, enumerates the virtual PCIe device, binds `nvidia.ko`.
+2. The driver performs the GSP boot handshake with our emulation and receives
+ plausible firmware identity/configuration.
+3. The driver sets up the RPC message queues (guest sysmem), negotiates
+ versions, and performs the resource-manager initialization exchanges.
+4. Object creation (clients, devices, subdevices), capability queries,
+ vaspaces, channels, CE caps, GMMU static info — every control the driver
+ probes — is answered with the correct shape and values.
+5. `nvidia-smi` works; CUDA user-space starts; UVM begins channel-manager and
+ page-tree initialization.
+
+## 7. What is next (and why)
+
+CUDA kernel execution requires the guest's *work* to run on the physical GPU.
+That means the host module must:
+
+1. know the memory the guest's GPU work refers to (the VA↔PA model),
+2. reproduce or forward the channel/page-table state that the guest expects the
+ GSP to own, and
+3. submit the guest's compute work to real engine(s) and relay completions,
+ faults and semaphores back.
+
+The design for this, including what is known and what remains to be learned, is
+in:
+[`../04-implementation/01-known-knowns-and-unknowns.md`](../04-implementation/01-known-knowns-and-unknowns.md)
+and
+[`../04-implementation/02-practical-challenges-and-adversarial-interop.md`](../04-implementation/02-practical-challenges-and-adversarial-interop.md).
diff --git a/docs/03-architecture/02-guest-stack-and-cudasw.md b/docs/03-architecture/02-guest-stack-and-cudasw.md
new file mode 100644
index 0000000..4ec614b
--- /dev/null
+++ b/docs/03-architecture/02-guest-stack-and-cudasw.md
@@ -0,0 +1,139 @@
+# The Guest Stack and CUDASW
+
+> What runs inside the guest, how it is layered, and what it expects from the
+> device. This is the "what must keep working, unmodified" document.
+
+## 1. Vocabulary: what "CUDASW" means here
+
+**CUDASW** is the family of NVIDIA *software* components that sit between an
+application and the GPU driver. In this project the term is used deliberately
+broadly, because our compatibility target is the whole stack, not one library:
+
+```
+application
+ │ CUDA API (runtime)
+libcudart.so ──────────────────────────────┐ CUDA runtime: device/stream/
+ │ CUDA driver API │ event management, kernel launch
+libcuda.so ────────────────────────────────┤ CUDA driver: contexts, modules,
+ │ ioctls │ memory, channel submission
+/dev/nvidia* ──────────────────────────────┘
+nvidia.ko (RM + UVM + modeset)
+```
+
+Adjacent components in the same family:
+
+* **libnvidia-ptxjitcompiler** — JIT compilation of PTX to SASS. (No GPU
+ interaction beyond driver interfaces; usually "just works" once the driver
+ initializes.)
+* **libnvidia-fatbinaryloader / nvrtc** — fatbinary loading.
+* **UVM** (`nvidia_uvm.ko`) — unified memory and GPU page-fault-driven memory
+ management; see [`04-uvm.md`](04-uvm.md).
+* **Graphics stack** — `libGLX_nvidia`, `libnvidia-glcore`, `libvulkan` ICD,
+ plus the kernel-side `nvidia_modeset`/`nvidia_drm` (roadmap).
+* **Video stack** — NVENC/NVDEC userspace + kernel interfaces (roadmap).
+
+## 2. Layering rules the emulation must respect
+
+1. **The kernel driver is the contract.** Everything above it (libcuda,
+ libcudart, libnvidia-*) talks to `nvidia.ko` through stable ioctls, memory
+ mappings and `/dev/nvidia*` nodes. If the kernel driver initializes and
+ behaves, user space follows.
+2. **User space is opaque to us.** We never intercept user-space APIs. All our
+ hooks are in the device model (host side) or in the *hardware interface* the
+ kernel driver sees (guest side).
+3. **Version skew is normal.** The driver version, the CUDA version and the
+ firmware version are three independent axes. Our compatibility target is
+ "the driver family that matches the emulated firmware profile", with
+ configuration-driven adaptation for others.
+
+## 3. Kernel driver anatomy (what the guest brings)
+
+`nvidia.ko` is a large, self-contained stack. The parts that matter to
+virtualization:
+
+| Component | Role | Why it matters to us |
+|---|---|---|
+| **RM core** | object model, handles, locks, device lifecycle | drives the GSP boot and the entire RPC/control surface |
+| **Resource server (`resserv`)** | allocates and tracks resources (objects, memory, channels) | the client-side half of the client/server RM split (see the GSP-RM doc) |
+| **Memory manager (`memmgr`)** | VRAM/sysmem objects, mappings, page tables | the guest expects the GSP to own page tables for its VA spaces |
+| **FIFO/runlist** | channels, TSGs, GPFIFO, USERD, doorbells | where the guest's work is submitted |
+| **GSP client** | message queues + RPC transport + event handling | the interface we emulate |
+| **UVM** | unified memory, page-fault handling, channel manager | starts early in CUDA initialization; extremely stateful |
+| **`nvidia_modeset` / `nvidia_drm`** | display/DRM | needed later for graphics |
+| **CE / GR HALs** | engine programming | needed to *execute* work (forwarded) |
+
+## 4. Driver initialization vs. runtime
+
+Understanding the split between these two phases is the key to debugging:
+
+**Initialization (boot/probe)** — deterministic, mostly one-shot:
+
+```
+PCI probe → identify GPU from config space and BAR0 registers
+ → boot the GSP (our emulated handshake)
+ → read static configuration (device info tables, FB/BUS info, VBIOS, clocks)
+ → create root client/device/subdevice objects
+ → initialize RM subsystems (interrupts, FIFO, memory, GMMU, PMU)
+ → expose /dev/nvidia* and register with the kernel
+```
+
+**Runtime (per-process, per-context)** — driven by user-space workloads:
+
+```
+open /dev/nvidia* → cuInit → UVM registration (per process)
+ → cuCtxCreate → object/allocations → memory maps
+ → channel creation + push buffers + semaphores
+ → kernel launches (GR) / copies (CE) / memops
+ → faults, completions, events
+```
+
+The initialization path is where compatibility is won or lost; the runtime
+path is where *execution* is won or lost. Open-IOV's current focus is the
+runtime path (forwarding), having already reached the end of initialization.
+
+## 5. What the guest expects from the device, per phase
+
+| Phase | Guest action | Device/firmware expectation |
+|---|---|---|
+| Probe | config space reads, BAR sizing, MSI-X capability | a plausible TU104-family PCIe GPU |
+| Boot | BAR0 register writes, WPR setup, firmware boot sequence | GSP boots, booter completes, "GSP init done" |
+| Config | RPC `GET_GSP_STATIC_INFO`, device tables, VBIOS reads | consistent, self-describing tables |
+| RM init | `GSP_RM_CONTROL` battery (FB/BUS/CE/INTR/GMMU/...) | correct shapes and plausible values |
+| Objects | `GSP_RM_ALLOC` (root, device, subdevice, vaspace, channel, CE objects) | success + correct handles/params |
+| Runtime | `GSP_RM_ALLOC`/`GSP_RM_CONTROL` for maps, channels; pushes | executed work, semaphores, faults |
+
+The field guide [`07-driver-lifecycle-field-guide.md`](07-driver-lifecycle-field-guide.md)
+lists the actual message/control IDs observed at each step.
+
+## 6. Why user-space-only interception does not work
+
+Two hard-won facts from this project:
+
+1. **`libcuda` issues raw `syscall` instructions**, so an `LD_PRELOAD` shim
+ around `ioctl()` sees nothing. If you need to observe CUDA's kernel calls,
+ use a `ptrace`-based tracer (PTRACE_SYSCALL + PTRACE_GET_SYSCALL_INFO), as
+ our tools do.
+2. **The CUDA driver talks to the *kernel driver*, not to the device.** All
+ device interaction happens in `nvidia.ko`. This is good news: it means the
+ emulation boundary is a small number of kernel-side interfaces (MMIO, DMA,
+ interrupts, and the GSP protocol) rather than a large user-space API
+ surface.
+
+## 7. Windows guests
+
+Windows brings its own `nvlddmkm.sys` driver, which uses the same underlying
+device/firmware model (GSP) but different host interfaces (WDDM, MSI-X
+semantics, ROM/VBIOS quirks). The emulation target is the same: satisfy the
+firmware interface, forward the work. Windows support is a roadmap item (see
+the portability matrix), and the design goal is that no Windows-specific code
+lives in the guest.
+
+## 8. Compatibility notes for versions
+
+* The guest's driver and the emulated firmware profile must agree on the RPC
+ message versions. The guest negotiates these at start-up
+ (`SET_GUEST_SYSTEM_INFO` carries the VGX/RPC version); our emulation answers
+ with a profile that matches the driver family in use.
+* CUDA versions ride on driver versions; the test matrix (see
+ [`../05-operations/01-performance-correctness-security.md`](../05-operations/01-performance-correctness-security.md))
+ tracks (driver, CUDA, firmware) triples.
diff --git a/docs/03-architecture/03-gsp-and-gsp-rm.md b/docs/03-architecture/03-gsp-and-gsp-rm.md
new file mode 100644
index 0000000..20d335b
--- /dev/null
+++ b/docs/03-architecture/03-gsp-and-gsp-rm.md
@@ -0,0 +1,204 @@
+# GSP and GSP-RM
+
+> The GPU System Processor hosts NVIDIA's Resource Manager in firmware. Since
+> Turing, the guest driver's management path is *talking to that firmware*, not
+> to silicon. This document explains that architecture and the interface we
+> emulate.
+
+## 1. What the GSP is
+
+The **GSP** is a small embedded RISC-V processor (RV64IMAC) inside NVIDIA GPUs.
+It runs a firmware image (for the 570.144 generation, a ~28 MB ELF) that
+contains, among other things, a full port of NVIDIA's **Resource Manager (RM)**
+— the component that owns GPU initialization, memory, engines, interrupts and
+scheduling.
+
+Why this matters: a GPU driver as complex as NVIDIA's is not written to
+program silicon directly. It is written to talk to an **RM**, and on modern
+GPUs a *copy* of the RM runs on the GSP. The driver is then an **RM client**:
+it issues requests, receives callbacks, and observes state through a
+well-defined protocol.
+
+```
+ ┌──────────────────────────── GUEST ────────────────────────────┐
+ │ nvidia.ko │
+ │ RM client path ── RPC ──┐ │
+ │ GSP boot code │ │
+ └────────────────────────────┼──────────────────────────────────┘
+ │ shared memory queues + doorbells
+ ┌────────────────────────────┼──────────────────────────────────┐
+ │ GSP (firmware, RISC-V) │ │
+ │ RM core (RMCore) ◄─────┘ │
+ │ RM API (object model, clients, handles) │
+ │ Resource server (resserv): allocation & lifetime │
+ │ Memory manager, GMMU, FIFO/runlist, interrupts, PMU │
+ └───────────────────────────────────────────────────────────────┘
+```
+
+## 2. GSP boot: the handshake we emulate
+
+The guest driver boots the GSP exactly as it would on bare metal:
+
+1. **WPR (Write-Protected Region) setup** — carve a protected region of VRAM
+ for firmware, write descriptors into BAR0 registers.
+2. **FWSEC** — the driver verifies firmware signatures (our emulation answers
+ the microcode/falcon handshake plausibly).
+3. **Booter** — the boot ROM/booter is released and consumes the firmware
+ image (we emulate completion).
+4. **RISCV start** — the RISC-V core starts executing the firmware; on real
+ hardware the firmware then initializes.
+5. **GSP init done** — the firmware signals readiness through a status mailbox;
+ the driver proceeds to initialization RPCs.
+
+Our emulation implements stage completion rather than the stages themselves —
+which is legitimate because the *only observable* to the driver is the state
+machine it polls.
+
+## 3. The transport: message queues and RPC
+
+GSP-RM's transport is a shared-memory message queue pair:
+
+```
+ guest system memory (allocated by the guest driver, registered with us)
+ ┌───────────────────────────────┐
+ │ command queue (guest→"GSP") │ 512 KB element ring, 4 KB elements
+ ├───────────────────────────────┤
+ │ status queue ("GSP"→guest) │ 512 KB element ring
+ └───────────────────────────────┘
+ + a page table describing the queue pages (the buffer is
+ NV_MEMORY_NONCONTIGUOUS: each 4 KB page is an independent allocation)
+```
+
+Each element begins with a transport header (`authTag`, `aad`, checksum,
+sequence, element count) followed by an **RPC message**:
+
+```
+struct rpc_message_header_v {
+ u32 header_version; u32 signature; u32 length; u32 function;
+ u32 rpc_result; u32 rpc_result_private; u32 sequence;
+ union { u32 spare; u32 cpuRmGfid; };
+}; // 32 bytes; payload follows in a union of message structs
+```
+
+Observations worth knowing (all verified on live hardware):
+
+* `length` is `sizeof(header) + sizeof(fixed message struct)` — for messages
+ with variable payloads (e.g. `GSP_RM_ALLOC` with parameters) it does **not**
+ include the parameters. Do not use it to bound a payload.
+* Function numbers are a flat enum shared by several protocols
+ (GSP-client RPCs, vGPU plugin RPCs). Examples observed from a stock 570.144
+ guest: `SET_GUEST_SYSTEM_INFO` (1), `FREE` (10), `UNLOADING_GUEST_DRIVER`
+ (47), `GET_GSP_STATIC_INFO` (65), `UPDATE_BAR_PDE` (70),
+ `CONTINUATION_RECORD` (71), `GSP_SET_SYSTEM_INFO` (72), `SET_REGISTRY` (73),
+ `GSP_RM_CONTROL` (76), `GSP_RM_ALLOC` (103).
+* Timing matters: a reply must be matched to the request sequence number;
+ the driver runs a reply-matching state machine, and a slow reply is
+ indistinguishable from a hang.
+
+## 4. RMCore, RM API and the resource server
+
+Three concepts from the RM show up constantly in traces and in the guest's
+driver. Understanding them makes every log readable.
+
+### 4.1 RMCore
+
+RMCore is the object system. Everything in the RM is an **object** with:
+
+* a **class** (a 32-bit class ID, e.g. `0x2080` = `NV20_SUBDEVICE_0`,
+ `0x906f`-family = channels, `0xc46f` = `TURING_CHANNEL_GPFIFO_A`),
+* a **handle** allocated in a per-client handle space (e.g. `0xc1d00002`),
+* a **parent** object (forming a tree rooted at a client).
+
+Traces of `GSP_RM_ALLOC` are therefore traces of object creation, and traces
+of `GSP_RM_CONTROL` are method calls on those objects.
+
+### 4.2 RM API
+
+The **RM API** is how in-kernel components (and user space via ioctls) allocate
+objects and call controls. Two important variants exist in the client:
+
+* `Alloc` — allocates an object with an implicit handle,
+* `AllocWithHandle` — the caller chooses the handle.
+
+For a GSP client, the *kernel-internal* API table is overridden so that some
+calls become RPCs while others are handled locally. Knowing which is which is
+the difference between "we see it in the trace" and "we must derive it another
+way". Notably, memory objects are allocated **locally by the client RM** (the
+pages are guest RAM), while *server-owned* ranges (see §5) are managed by the
+GSP.
+
+### 4.3 Resource server (`resserv`)
+
+The resource server is the allocation authority: it owns the handle spaces,
+enforces privileges, and tracks object lifetimes. Its directives include flags
+that matter to us, for example `RMAPI_ALLOC_FLAGS_SERIALIZED` (the parameters
+in an RPC are a serialized form that must be deserialized by the receiver) and
+`RMAPI_ALLOC_FLAGS_SKIP_RPC` (the allocation is handled locally and never
+reaches the server).
+
+## 5. The client/server split (and why page tables are ours)
+
+Modern RM has a **client/server** architecture:
+
+* The **client** (guest `nvidia.ko`) owns per-process state and submits work.
+* The **server** (GSP-RM on the GPU, under our emulation) owns GPU-global
+ state: routing, page tables, scheduling domains, engine configuration.
+
+For virtualized/partitioned setups, the client tells the server which VA
+ranges it wants the *server* to own, and the server manages the deeper page
+table levels for those ranges. We receive this contract explicitly in the
+`NV90F1_CTRL_CMD_VASPACE_COPY_SERVER_RESERVED_PDES` control, which carries the
+**physical addresses of the client's page-table levels** plus the VA range and
+page shifts they cover:
+
+```
+levels[0] phys=0x1297ac000 pageShift=47 (root page directory)
+levels[1] phys=0x1297ad000 pageShift=38
+levels[2] phys=0x1297ae000 pageShift=29
+```
+
+In a real system, the server then *fills in* the lower levels for
+server-owned ranges; the client never writes them. This is a central fact for
+the forwarding work: **when the guest's UVM asks the "GPU" to map memory, the
+mapping is expected to materialize in page tables that only we can write.**
+
+## 6. Event channels and notifications
+
+Beyond request/reply, the guest needs asynchronous events:
+
+* **Event objects** (`NV01_EVENT_*` classes) are created by the client and
+ registered with a *notification* configuration: MSI-X vector, or a memory
+ "semaphore" the GPU can write.
+* **Completion events** are delivered when objects change state (channels
+ finish work, errors occur).
+* `cl2080_notification.h` documents the notification layout; the emulation
+ must deliver exactly the bit/handle/state triple the guest expects, on the
+ exact vector.
+
+Our MSI-X path is: emulation thread → `eventfd` → KVM → guest interrupt
+vector. The PBA (pending bit array) is emulated so a masked vector's interrupt
+is not lost.
+
+## 7. What we emulate today, precisely
+
+| Layer | Emulated | Notes |
+|---|---|---|
+| GSP boot state machine | ✅ | stages complete; the driver's poll loop is satisfied |
+| Message queue transport | ✅ | including the non-contiguous page table for the queue buffer |
+| RPC request/reply | ✅ | all functions the driver uses during init are answered |
+| Static info tables | ✅ | device/FB/BUS/INTR/GMMU tables with plausible TU104 values |
+| Controls during init | ✅ | ~160 distinct control IDs observed per CUDA run; the ones needed to proceed carry real values |
+| Object creation | ✅ | correct handle/parameter shapes |
+| Page tables for server-owned ranges | 🟡 | contract received and recorded; leaf population is the forwarding work |
+| Event/notification delivery | 🟡 | MSI-X works; richer event semantics as forwarding lands |
+| Firmware *behaviour* (routing, scheduling, real execution) | 🔴 | the forwarding work; see the implementation docs |
+
+## 8. Where the firmware analysis lives
+
+* `wiki/GSP-570.144W-Firmware-Analysis.md` — how the 28.5 MB image was
+ decompiled, RPC dispatchers located, jump tables mapped.
+* `wiki/Address-Reference.md` — firmware addresses for structures, functions
+ and tables.
+* `wiki/RPC-Message-Format.md`, `wiki/GSP-RPC-Emulation.md` — protocol details.
+* `wiki/WPR-Management.md` — write-protected region semantics during boot.
+* `kernel/gsp_rpc_client.c` — the firmware-derived handlers expressed as C.
diff --git a/docs/03-architecture/04-uvm.md b/docs/03-architecture/04-uvm.md
new file mode 100644
index 0000000..299f46b
--- /dev/null
+++ b/docs/03-architecture/04-uvm.md
@@ -0,0 +1,172 @@
+# UVM — Unified Memory in the Guest
+
+> UVM (`nvidia_uvm.ko`) is the most demanding consumer of the virtual GPU's
+> semantics, and today the best measurement of how faithful our emulation is.
+> This document explains what UVM does, what it needs, and what we learned
+> from watching it run against our device.
+
+## 1. What UVM is
+
+UVM provides unified memory and GPU-driven page-fault handling. In practice it
+is a second GPU driver, layered on the RM:
+
+* **VA spaces** (`uvm_va_space`) — per-process GPU address spaces.
+* **Channel managers** — pools of GPU channels (CE, SEC2, GR-as-needed) that
+ UVM itself uses to execute its own memory-management work.
+* **Push buffers / GPFIFO** — UVM builds its own pushes and submits them.
+* **Semaphores and trackers** — UVM tracks GPU progress by waiting on
+ semaphores that the GPU writes when work completes.
+* **Page trees** — UVM manages GPU page tables for its VA spaces.
+* **Fault handling** — GPU page faults are delivered to UVM (via a fault
+ buffer and a notification channel), and UVM resolves them by populating page
+ tables and replaying/cancelling faults.
+
+For CUDA, UVM is entered early: `cuInit` → driver init → `UVM_REGISTER_GPU`.
+If UVM cannot complete GPU registration, CUDA does not start.
+
+## 2. The registration path (traced end to end)
+
+```
+libcuda: UVM_REGISTER_GPU ioctl (0x25)
+ uvm_api_register_gpu
+ uvm_va_space_register_gpu
+ uvm_gpu_retain_by_uuid
+ nvUvmInterfaceRegisterGpu / GetGpuInfo
+ alloc_parent_gpu / add_gpu
+ nvUvmInterfaceDeviceCreate (RM object creation)
+ uvm_hal_init_gpu / get_gpu_caps (CE caps! see below)
+ uvm_ats_add_gpu / uvm_gpu_init_isr (fault/intr plumbing)
+ init_channel_manager
+ create semaphore pool (sysmem)
+ create channels per CE (each channel: GPFIFO + pushbuffer + semaphores)
+ channel_init() → first push → semaphore release → wait
+ uvm_page_tree_init (page tables for the VA space)
+```
+
+Each stage has a distinct failure signature; our debugging docs map them.
+
+## 3. What UVM requires from the "GPU"
+
+| Requirement | Detail | Our status |
+|---|---|---|
+| GPU identity | must match the uuid from RM | ✅ |
+| CE capabilities | per-engine support bits; UVM *requires* `supported && !grce && sysmem` for CPU_TO_GPU/GPU_TO_CPU channels | ✅ (we answer CE_GET_PHYSICAL_CAPS with the correct bits) |
+| Channel creation | RM channel objects with GPFIFO/userd/instance parameters | ✅ objects created |
+| Pushes executed | UVM's first action on a new channel is an *init push* followed by a semaphore-tracked wait | 🟡 we can complete them (see below) |
+| Page trees | UVM initializes page trees with its own pushes; the *server-owned* ranges must be populated by us | 🟡 |
+| Fault delivery | fault buffer + notification; replay/cancel semantics | 🔴 |
+| Semantics of completions | tracker values must advance exactly | 🟡 |
+
+## 4. The most important discovery: how UVM waits
+
+UVM's tracker (`uvm_tracker.c`) is the single most visible symptom of an
+unfaithful GPU. It spins, and after 30 seconds it dumps:
+
+```
+uvm_tracker.c:285 Tracker entry for value 1 (sema VA 0x121000000)
+ channel ID 1:3 (0x1:0x3) CE 0
+uvm_channel.c:4184 pending push '' started at uvm_channel.c:2535 in channel_init()
+ releasing value 1
+```
+
+What this means, mechanically:
+
+1. UVM built a push in a push buffer and wrote a **semaphore release** into it:
+ a method sequence (CE channel class, e.g. `TURING_DMA_COPY_A`) that sets an
+ address and a payload and then launches a DMA operation that writes the
+ payload to memory.
+2. UVM submitted the push (GPFIFO entry + GPPUT/doorbell) and now waits for the
+ semaphore payload to reach `value`.
+3. On real hardware, the copy engine writes the payload into the semaphore's
+ memory. On our device, *nobody does* unless we do it.
+
+The push we found in guest RAM (found by scanning for its distinctive byte
+sequence) decodes as:
+
+```
+20010000 0000c5b5 set-object (TURING_DMA_COPY_A)
+2001a000 0000c076 set-object (GP100_UVM_SW)
+20038090 00000001 incrementing-method header: count 3, addr 0x240
+ + data: upper 32 bits of the semaphore VA
+21000000 00000001 lower 32 bits of the VA; payload (1)
+200180c0 0000000c LAUNCH_DMA with SEMAPHORE_TYPE=RELEASE_ONE_WORD
+```
+
+So: **the emulation's job for this push is to write `payload` to the memory at
+the semaphore's GPU VA.** Which requires a GPU-VA → guest-PA translation.
+
+## 5. The memory model UVM assumes (and what we learned)
+
+* UVM's semaphore pool is **system memory** (`pool->aperture = UVM_APERTURE_SYS`),
+ one 4 KiB page per channel manager, addressed as 4-byte slots
+ (`UVM_SEMAPHORE_SIZE = 4`, 1024 slots per page).
+* Each semaphore page has a *GPU VA* (chosen by the client's RM) and a *CPU VA*
+ (a kernel mapping). UVM polls the CPU VA; the GPU writes the GPU VA.
+* **Both addresses are recorded in UVM's structures in guest memory.** This is
+ the key that makes translation possible without any extra help from the
+ guest (see the host-module doc for the exact structures and offsets).
+* The CPU VA is a **direct-map** address, so
+ `PA = cpu_va - page_offset_base`. Verified live: writing the payload to that
+ physical address completes the push and UVM advances.
+* Values are *per channel* and *monotonic*: a channel's first push releases 1,
+ the next 2, and so on. UVM has its own sanity check
+ (`UVM_GPU_SEMAPHORE_MAX_JUMP`) — writing a wrong value is detected and
+ reported, which is useful for validation.
+* The client's **GPU page tables for server-owned ranges are empty chains**:
+ the client hands us the *physical addresses of its page-table levels* in
+ `NV90F1_CTRL_CMD_VASPACE_COPY_SERVER_RESERVED_PDES` and expects the server to
+ own the lower levels. (See the GSP-RM doc, §5.)
+
+## 6. Page trees and faults
+
+* `uvm_page_tree_init` initializes the page tables for a VA space using pushes
+ on UVM's own channels. These pushes are memops / page-table writes; on real
+ hardware they are executed by the copy engine.
+* Fault handling is event-driven: UVM programs a **fault buffer** (a
+ replayable and a non-replayable buffer, whose sizes come from
+ `INTERNAL_GMMU_GET_STATIC_INFO` — answered by our emulation) and receives
+ notifications when the GPU takes a fault. UVM then:
+ 1. reads the fault entry,
+ 2. populates the page table (possibly migrating memory),
+ 3. **replays or cancels the fault** with a `MEM_OP` push.
+* For our forwarding design, faults are where "the guest's memory model" and
+ "the real GPU's memory model" must be reconciled. This is the deepest part
+ of the remaining work, and is tracked as a known-unknown in
+ [`../04-implementation/01-known-knowns-and-unknowns.md`](../04-implementation/01-known-knowns-and-unknowns.md).
+
+## 7. How to observe UVM
+
+In the guest:
+
+```bash
+# UVM prints nothing by default. Turn it on:
+echo 1 | sudo tee /sys/module/nvidia_uvm/parameters/uvm_debug_prints
+# Messages then appear with the "nvidia-uvm:" prefix.
+# Other knobs: uvm_release_asserts, uvm_release_asserts_dump_stack,
+# uvm_enable_debug_procfs, uvm_debug_enable_push_desc.
+```
+
+Known-good markers, in order:
+
+```
+uvm_gpu_retain_by_uuid → ... → channel manager created
+→ uvm_page_tree_init pushes
+→ (on success) UVM_REGISTER_GPU returns and CUDA proceeds
+```
+
+Failure signatures we have characterized:
+
+| Message | Meaning |
+|---|---|
+| `Failed to initialize the channel manager: NV_ERR_NOT_SUPPORTED` | CE caps were wrong/missing (we fixed this: see the field guide) |
+| `stuck waiting` + `Tracker entry ... channel_init()` | the push's semaphore was never written (the emulation must write it) |
+| `Assert failed ... unexpected semaphore ... jump from 0x0 to 0x1010101` | we wrote a wrong payload (and UVM caught us — useful!) |
+
+## 8. Why UVM is the perfect correctness oracle
+
+UVM exercises in minutes what a driver test suite would test in hours:
+memory allocation, page tables, channel setup, push generation, semaphore
+semantics, fault handling, error recovery — all with continuous self-checks and
+loud diagnostics. When UVM registers cleanly and CUDA workloads pass, the
+device is behaving like a real GPU. That is why "UVM advances" is the project's
+primary correctness metric on the way to CUDA.
diff --git a/docs/03-architecture/05-gmmu-amap-hal-gr.md b/docs/03-architecture/05-gmmu-amap-hal-gr.md
new file mode 100644
index 0000000..6ff9257
--- /dev/null
+++ b/docs/03-architecture/05-gmmu-amap-hal-gr.md
@@ -0,0 +1,198 @@
+# GMMU, AMAPlib, HALs, Runlists, and GR
+
+> The engine side: how addresses translate, how work is scheduled, and what
+> "GR" actually is. Read this when you need to know what the guest's work
+> *means*.
+
+## 1. GMMU — the GPU memory management unit
+
+The GMMU translates the GPU virtual addresses (VAs) used by engines into
+physical addresses (VRAM offsets or system-memory addresses).
+
+### 1.1 Page-table hierarchy
+
+NVIDIA's GMMU uses a multi-level page table whose shape depends on the AS
+generation. Turing (the reference) uses a four-level hierarchy with entry
+coverage:
+
+| Level | Entries | VA bits consumed | Coverage per entry |
+|---|---|---|---|
+| Root page directory (PDB) | 4 | top bits | large |
+| PDE level 1 | 512 | 9 bits | e.g. 2^38 |
+| PDE level 2 | 512 | 9 bits | e.g. 2^29 |
+| PTE level | 512+ | 9+ bits | 4 KiB / 64 KiB / 2 MiB pages |
+
+Verified properties (from the live guest):
+
+* **PTE encoding: `PA[47:12] | flags`**, where `flags = 0xe` for a valid
+ system-memory leaf entry in the format the client produces.
+* Page sizes in use: 4 KiB (system memory objects) and larger (big pages) for
+ large allocations.
+* **Apertures**: during enumeration the guest uses `ADDR_SYSMEM = 1` and
+ `ADDR_FBMEM = 2` to describe memory descriptors (e.g. channel instance
+ memory in VRAM at offset `0xffed000`, method buffers in system memory at a
+ physical address). The client hands the **physical** address in these
+ descriptors — a fact we rely on for the memory model.
+* The page-table geometry arrives over
+ `NV90F1_CTRL_CMD_VASPACE_COPY_SERVER_RESERVED_PDES` (level physical
+ addresses, page shifts, VA range) — see
+ [`03-gsp-and-gsp-rm.md`](03-gsp-and-gsp-rm.md) §5.
+
+### 1.2 What "aperture" means for us
+
+Two address spaces matter:
+
+* **System memory (sysmem)**: guest RAM. For a virtual device, the "GPU's view"
+ of guest memory is established by the host (in real vGPU, by the vGPU manager
+ via the platform IOMMU; here, by our module).
+* **Video memory (vidmem / FB)**: the GPU's local memory. In the current
+ emulation this is a host RAM buffer exposed as the BAR1 aperture; the
+ forwarding phase replaces it with real GPU memory.
+
+## 2. AMAPlib and the HALs
+
+NVIDIA's driver abstracts hardware in layers:
+
+```
+driver logic (memmgr, FIFO, engines)
+ │
+ ├─ HAL layer: per-generation C functions that program registers and formats
+ │ e.g. gmmu HAL (page-table formats, invalidations),
+ │ engine HALs (CE/GR methods), host HAL (doorbells/PUT)
+ │
+ └─ AMAPlib: the "address mapping" library shared by RM and simulations
+ (DRAM swizzling, bank/channel mapping, physical-to-aperture rules)
+```
+
+Developers study two component families to understand these layers:
+
+* **AMAPlib** — the shared "address mapping" component used by the RM and by
+ hardware simulations: DRAM swizzling, bank/channel mapping, aperture rules.
+* **The HALs** — per-generation C code that programs registers and formats
+ page-table entries and engine commands.
+
+Our knowledge of them comes from reverse engineering and decompilation of
+driver and firmware behaviour, plus measurement of live devices. What is
+published here is our observations, numeric IDs and layouts — which is what
+another engineer needs.
+
+Why we care:
+
+* When we **populate page tables ourselves** (server-owned ranges), we must
+ produce entries the hardware and the guest both accept — AMAPlib/HAL code is
+ the specification.
+* When we **forward work**, page tables we write must match what real engines
+ expect, including big-page encodings and invalidation semantics.
+
+## 3. Channels, GPFIFO, USERD, doorbells
+
+A **channel** is a hardware execution context. Its components:
+
+| Piece | Where | Purpose |
+|---|---|---|
+| **GPFIFO** | GPU memory (sysmem or vidmem) | ring of entries; each points to a push buffer |
+| **Push buffer** | GPU memory | the actual method stream the engine executes |
+| **USERD** | vidmem (per channel) | channel control block: PUT/GET pointers, doorbell-related state |
+| **Instance block** | vidmem or sysmem | the hardware context (engine state, page directory base, etc.) |
+| **Runlist / TSG** | scheduled by the host | the list of channels currently eligible to run |
+
+The submission flow (what a driver does, and what the guest therefore does):
+
+```
+write methods into a push buffer (CPU, via a mapping)
+write a GPFIFO entry pointing at the push (CPU)
+update the channel's PUT pointer (CPU → USERD or doorbell register)
+ ... the scheduler eventually runs the channel; the engine fetches the push
+ ... on completion the engine writes the GET pointer and/or a semaphore
+```
+
+Two details that repeatedly matter in practice:
+
+* **The push buffer format** is a sequence of method/data pairs preceded by
+ *incrementing-method headers* that encode the method offset (`address >> 2`),
+ a subchannel and a count. A decoder for this format is the first tool you
+ need to read a trace (our `scan_seq` module parameter found UVM's pushes by
+ searching for the decoded data pattern).
+* **Engine classes** are the vocabulary of pushes: `TURING_DMA_COPY_A`
+ (0xc5b5) for copy engines, `TURING_CHANNEL_GPFIFO_A` (0xc46f) for channel
+ objects, `GP100_UVM_SW` (0xc076) for UVM's software object, etc.
+
+## 4. Runlist and scheduling
+
+The GPU schedules channels through a **runlist**: a list of channel
+descriptors the scheduler walks, giving each channel a time slice. A **TSG**
+(Time-Slice Group) bundles channels that share a scheduling slot — this is the
+mechanism for fair sharing and for interleaving contexts.
+
+For virtualization this is a focal point:
+
+* In real vGPU, the host vGPU manager owns the runlist and interleaves
+ guest channels with host channels.
+* In our target architecture, *we* own the schedule: guest channels must be
+ mapped onto real GPU scheduling primitives with correct isolation (a guest
+ must never observe or influence another guest's channels).
+* The guest driver observes scheduling only indirectly: PUT/GET progress,
+ semaphores, and fault timing. Faithful *semantics* (not exact timing) is the
+ requirement.
+
+## 5. GR — the graphics/compute engine
+
+**GR** is the engine that executes shaders: both graphics pipelines and CUDA
+compute kernels. It is the reason a GPU is a GPU.
+
+What GR needs to run work:
+
+* **A context** (the "golden context" + per-context state): register state,
+ memory layout, page directory, shader code.
+* **A channel** with a push buffer that issues *method* calls to GR:
+ loading shader code, binding surfaces, launching grids (`QMD`s in modern
+ architectures), and signaling completion.
+* **Memory** containing the shader binary (SASS), kernel parameters,
+ descriptors and data.
+* **Interrupts/semaphores** to signal completion and errors.
+
+For CUDA specifically:
+
+```
+cuLaunchKernel
+ → CUDA driver builds a GR push (grid config, QMD, parameters)
+ → submits on the process's GR channel
+ → GR executes the shader on real hardware
+ → completion semaphore / event; UVM handles memory effects
+```
+
+**Consequence for Open-IOV:** CUDA kernel execution is *real hardware
+execution*. No emulation of GR can be correct and fast enough; the only viable
+path is forwarding the guest's GR work to real hardware (see
+[`06-host-module-and-hybrid-model.md`](06-host-module-and-hybrid-model.md) and
+the challenges document). This is also why "CUDA works" is a much stronger
+statement than "`nvidia-smi` works".
+
+## 6. Copy engines (CE) and other engines
+
+* **CE (copy engines)** — DMA between memory regions; also used for UVM's
+ internal work (page tables, memsets, semaphores). UVM cannot even initialize
+ without working CE channels, which is why our CE capability reporting was
+ the first big unblock (`CE_GET_PHYSICAL_CAPS`, `CE_GET_CE_PCE_MASK`).
+* **SEC2** — secure copy engine used by UVM for protected operations.
+* **NVENC/NVDEC/NVJPG/OFA** — video and image engines; roadmap for graphics
+ and video phases.
+
+Engine types are enumerated by the client during init (`FIFO_GET_DEVICE_INFO_TABLE`)
+and reported in engine/capability queries. A guest's channel is bound to one
+engine type (e.g. `COPY0 = 0x9`, `GR = 0x1`).
+
+## 7. Practical reading list inside this repository
+
+| Topic | Where |
+|---|---|
+| Page-table formats and HAL concepts | this tree's architecture notes plus the decompiled analysis notes |
+| Class/method definitions (classes, methods, notifications) | our decompilation notes and reformatted tables in `wiki/` |
+| Channel allocation parameter structures | reformatted in the field guide and in the reference wiki pages |
+| GMMU controls and static info | `wiki/Address-Reference.md`, `wiki/RPC-Message-Format.md` |
+| Notification formats | the reference wiki pages |
+| Firmware structure and dispatchers | `wiki/GSP-570.144W-Firmware-Analysis.md` |
+
+**Rule:** tracked documentation describes components and behaviour, with our
+own observations, IDs and layouts. Exact analysis sources live in the
+maintainers' study notes.
diff --git a/docs/03-architecture/06-host-module-and-hybrid-model.md b/docs/03-architecture/06-host-module-and-hybrid-model.md
new file mode 100644
index 0000000..fc4bb23
--- /dev/null
+++ b/docs/03-architecture/06-host-module-and-hybrid-model.md
@@ -0,0 +1,227 @@
+# The Host Module and the Hybrid Model
+
+> `open-iov-vfio.ko` in detail: the virtual device, the emulation engine, the
+> memory model, and how work will be forwarded. This is the "where the code
+> lives" document.
+
+## 1. Role and position
+
+```
+ guest VM
+ │ PCIe MMIO / DMA / MSI-X
+ ┌────┴──────────────────────────────────────────────┐
+ │ hypervisor (KVM/QEMU or Xen) │
+ │ VFIO mdev transport, IOMMU (per-tenant memory) │
+ └────┬──────────────────────────────────────────────┘
+ │
+ ┌────┴───────────────────────────────────────────────────────────────┐
+ │ open-iov-vfio.ko │
+ │ virtual device ── emulation core ── memory model ── work path │
+ └────┬─────────────────────────────────────┬─────────────────────────┘
+ │ host driver interfaces │ (future) vendor-neutral
+ ┌────┴────────────────────┐ │
+ │ unmodified host nvidia.ko│ direct GPU interfaces / VF backends
+ └──────────────────────────┘
+```
+
+The module registers itself as an **mdev parent** on the physical GPU and
+exposes one device type per supported profile. Each `mdev` instance is an
+independent virtual GPU with its own state, its own BAR windows, its own
+interrupts and its own emulation context.
+
+## 2. Virtual device
+
+### 2.1 PCI configuration space
+
+The guest sees a plausible Turing-class GPU: vendor/device IDs, class code,
+BARs, capability list (PCIe, MSI-X, power management), and the subsystem
+identifiers the driver keys on. Several fields exist only because the driver
+reads them for validation:
+
+* **PCIe link capabilities** — the guest's driver derives bus width/speed and
+ availability. The emulation must report them; the *hypervisor* can
+ accidentally rewrite them (see the debugging doc's root-port note).
+* **MSI-X table/PBA** — emulated, with per-vector masking and pending bits.
+* **VF (virtual function) windows** — registers the SR-IOV-aware driver paths
+ read during initialization; the emulation fills them with values consistent
+ with a non-SR-IOV or lightly-SR-IOV configuration as appropriate.
+
+### 2.2 BAR layout
+
+| BAR | Size | Backing | Notes |
+|---|---|---|---|
+| BAR0 | 16 MB | host `vzalloc` shadow | register interface: boot, doorbells, VF registers, PRAMIN window, VBIOS |
+| BAR1 | 256 MB | host `vzalloc` vidmem buffer | guest-visible VRAM aperture; replaced by real VRAM backing in the forwarding phase |
+| BAR3 | — | instance window | instance/PRAMIN behaviour |
+
+MMIO reads and writes are served either by the module's VFIO region callbacks
+or directly from the shadow when the hypervisor has mapped it into the guest.
+
+### 2.3 VRAM backing
+
+Today BAR1 is host RAM. This is deliberate: correctness first, performance and
+real VRAM semantics later. Consequences and their mitigations:
+
+* Guest reads/writes of "VRAM" are cheap and coherent. ✅
+* The real GPU cannot read it. The forwarding phase must either migrate these
+ objects to real VRAM or expose a DMA-mapped system-memory region to the GPU
+ (see §5).
+
+## 3. Emulation core
+
+### 3.1 GSP emulation thread
+
+A kernel thread drives the device's "firmware" behaviour:
+
+1. Watches BAR0 doorbells (and VF registers) for guest actions.
+2. Reads message-queue elements from guest memory.
+3. Dispatches RPCs to handlers.
+4. Writes replies into the status queue.
+5. Injects MSI-X interrupts to the guest.
+
+Guest memory is accessed through the hypervisor's DMA interface
+(`vfio_dma_rw()`), which requires calls to originate from a kernel thread
+context — a fact the debug tooling codifies.
+
+### 3.2 RPC handling
+
+Handlers are organized by function and, inside controls, by control ID. The
+implementation philosophy:
+
+* **Correct shape first.** A reply must have the exact structure the driver
+ expects, or the driver walks off into uninitialized memory.
+* **Plausible values second.** Values must be self-consistent (e.g. FB size,
+ BAR sizes, engine counts, page sizes) or later code paths compute nonsense.
+* **Never echo garbage.** Queries that pass uninitialized input must receive
+ zeroed/valid output; echoing the request's stack garbage has caused real
+ failures.
+
+The handlers currently cover: the boot/static-info exchanges, the device/FB/
+BUS/INTR control tables, CE capability and PCE-mask queries, GMMU static info,
+vaspace reserved-PDE contracts, channel/CE object allocation, and the general
+"unknown control returns success with zeroed output" fallback.
+
+### 3.3 Event channels and interrupts
+
+Guest-visible interrupts (MSI-X) are delivered via `eventfd` to the hypervisor.
+The emulation tracks vector masking and pending state so that an interrupt
+raised while masked is delivered when unmasked, not lost.
+
+## 4. Memory model (the heart of the project)
+
+The guest's GPU work refers to *GPU virtual addresses*. To act on that work the
+host module must translate them to memory it can touch.
+
+### 4.1 What the client gives us
+
+* **Channel memory descriptors** (`NV_MEMORY_DESC_PARAMS`) with *physical*
+ addresses: instance memory (VRAM offset), method buffer, userd, ramfc,
+ error notifiers. These are precise and trustworthy.
+* **The page-table contract** for server-owned VA ranges
+ (`COPY_SERVER_RESERVED_PDES`): page-table level physical addresses, page
+ shifts, VA range — but with *empty* leaf levels, because the server owns the
+ leaves.
+* **No mapping RPCs for the client's own system-memory objects.** The client
+ allocates sysmem locally and never tells the server its physical pages.
+
+### 4.2 What we derive instead
+
+Because the client does not hand us its sysmem mappings, the module derives
+them from the guest's own in-memory bookkeeping:
+
+1. **VA↔PA pairs live in guest memory.** UVM and the RM keep structures that
+ record, for every mapped object, both the GPU VA and the kernel CPU VA.
+ Example (verified): `uvm_rm_mem_t.vas[CPU]` at +0x30 and `vas[GPU]` at
+ +0x38; channel tracking semaphores at `uvm_channel_t+0x80/0x88`.
+2. **CPU VAs are direct-map addresses**, so
+ `PA = cpu_va - page_offset_base`. The direct-map base can be recovered from
+ guest memory itself (structures that store their own address), from
+ `/proc/kallsyms` during development, or from a debug poke session.
+3. Therefore a translation table can be built **without any cooperation from
+ the guest driver** — a crucial property for adversarial interoperability,
+ and a general technique applicable to future versions.
+
+### 4.3 Acting on translations
+
+The module can read and write guest physical memory (via the hypervisor's DMA
+interface). That is enough to:
+
+* complete semaphore releases (write the payload to the mapped slot),
+* inspect push buffers and page tables,
+* and later, create page tables for server-owned ranges.
+
+## 5. The forwarding path (design)
+
+```
+guest push submitted
+ → module detects submission (doorbell/GPPUT or push-buffer scan)
+ → decode the push (method stream)
+ → for each command decide:
+ (a) EMULATE: pure state/notification (semaphore release, membar, fence)
+ (b) FORWARD: real engine work (GR grid launch, CE copy, memop)
+ → for (b): submit equivalent work on a real channel bound to the physical GPU
+ → relay completions (semaphore/fault/interrupt) back into guest memory
+```
+
+Three forwarding backends are candidates, in increasing order of capability:
+
+1. **Host RM/GSP proxy** — use the unmodified host driver's RM to allocate real
+ memory and create real channels, then replay guest work through them.
+2. **Direct engine submission** — build our own channel/push streams to real
+ engines using the GMMU/HAL definitions, bypassing the host driver.
+3. **Hybrid** — proxy for management (memory, channels) and direct submission
+ for hot-path work (grid launches, copies).
+
+The decision between them is driven by the practical challenges document; the
+short version is that (1) is safest for correctness, (2) is best for latency,
+and both must be isolated to be acceptable.
+
+## 6. Debug tooling in the module
+
+The module exposes debug parameters (writes to
+`/sys/module/open_iov_vfio/parameters/*`) that make the guest's memory and the
+emulated device inspectable without rebuilding:
+
+| Parameter | Purpose |
+|---|---|
+| `scan_guest_ram=<v>` | find a 64-bit value in guest RAM (0–8 GB), report GPAs |
+| `scan_vidmem=<v>` | same for the emulated VRAM shadow |
+| `scan_seq=<v1,v2,...>` | find a 32-bit *sequence* (e.g. a method stream) |
+| `scan_pt=<pa>` | find page-table entries pointing at a physical address |
+| `scan_masked=<v>,<mask>` | find `(x & mask) == v` (e.g. a page-base VA) |
+| `dump_vidmem=<off>,<len>` / `summary` | dump/scan the VRAM shadow |
+| `poke_guest=<gpa>,<v>` | write a 32-bit value to guest physical memory |
+| `poke_fill=<gpa>,<v>,<n>` | fill a range |
+| `fill_vidmem=<v>` | fill the VRAM shadow |
+
+These are development-grade but production-useful: they are how the memory
+model was reverse-engineered, and they remain the fastest way to diagnose a
+mismatch. Their workflows are documented in
+[`../05-operations/02-debugging-and-maintenance-workflows.md`](../05-operations/02-debugging-and-maintenance-workflows.md).
+
+## 7. Build and symbol contracts
+
+* `open-iov-vfio.ko` exports the `nvidia_vgpu_vfio_*` interface
+ (`nvidia_vgpu_vfio_probe/remove`, ops get/set, mdev parent registration) for
+ hosts that want to pair our device with the NVIDIA OpenRM host modules.
+* When NVIDIA's OpenRM modules are built from source, they must be built with
+ `KBUILD_EXTRA_SYMBOLS=$(pwd)/kernel/Module.symvers`, and **after** the
+ Open-IOV module, or symbols will not resolve.
+* On typical single-GPU developer systems the distribution's NVIDIA driver
+ manages the physical GPU and the Open-IOV module is used standalone; the
+ OpenRM submodule path is only needed for the native-vGPU-management
+ workflows.
+
+## 8. What is implemented vs. designed
+
+| Area | Status |
+|---|---|
+| mdev/VFIO device, config space, BARs, MSI-X | implemented |
+| BAR0 register state machine, boot handshake | implemented |
+| Message queues + RPC + all init controls observed from stock 570.144 | implemented |
+| Guest sysmem access + VA↔PA discovery | implemented (debug-grade, to be productized) |
+| Push decoding + semaphore completion | demonstrated live; being productized |
+| Page-table population for server-owned ranges | designed, not implemented |
+| GPU work forwarding (GR/CE execution) | designed, not implemented |
+| Real VRAM backing | designed, not implemented |
+| Windows guest, Xen transport, aarch64 hosts | designed, not implemented |
diff --git a/docs/03-architecture/07-driver-lifecycle-field-guide.md b/docs/03-architecture/07-driver-lifecycle-field-guide.md
new file mode 100644
index 0000000..8d80694
--- /dev/null
+++ b/docs/03-architecture/07-driver-lifecycle-field-guide.md
@@ -0,0 +1,188 @@
+# Driver Lifecycle Field Guide
+
+> A walking tour of what actually happens between "guest boots" and "CUDA
+> initializes", with the real message and control IDs we observe from an
+> unmodified 570.144 guest. Use this to interpret logs and to place new
+> observations.
+
+## 0. How to use this document
+
+Each stage lists: what the guest does, what the device/firmware must answer,
+what our emulation does today, and the log signature to look for
+(`dmesg | grep gsp-resman-vfio` on the host; `dmesg` in the guest).
+
+The IDs are for the 570.144 generation on the Turing (TU104) profile. Other
+generations will have additions and version differences; the *stages* are
+stable.
+
+## 1. PCI probe and config space
+
+**Guest:** reads vendor/device/class IDs, walks capabilities, sizes BARs,
+enables Memory Space and Bus Master, programs MSI-X.
+
+**Device must:** report a coherent TU104-family GPU with 16 MB BAR0, 256 MB
+BAR1, an MSI-X capability, and a link capability the driver accepts.
+
+**Emulation:** config space is synthesized; BAR sizes are enforced; the
+*hypervisor* is the usual source of surprises (a device attached to the root
+bus can have its PCIe capability rewritten to "root complex integrated
+endpoint", which removes link information the driver wants — attach behind a
+root port).
+
+**Log signature:** none from us; guest-side `lspci -vvv` is the reference.
+
+## 2. GSP boot handshake
+
+**Guest:** writes WPR descriptors, releases FWSEC, then the booter, then the
+RISC-V core; polls a status mailbox for "GSP init done".
+
+**Device must:** complete every stage in the order the driver polls, and leave
+the mailbox in the state the driver expects.
+
+**Emulation:** `kernel/gsp_emulation.c` models the stage state machine and
+auto-clears self-clearing triggers (e.g. MMU invalidate registers) so polls
+terminate.
+
+**Log signature:**
+```
+gsp-resman-vfio: GSP-BOOT: ... trigger auto-cleared ...
+```
+
+## 3. Static configuration
+
+**Guest:** `GET_GSP_STATIC_INFO` (65) → receives a large `GspStaticConfigInfo`
+structure; reads the VBIOS image from the BAR0 window; queries device/FB/BUS
+info.
+
+**Device must:** provide self-consistent sizes, engine inventories, memory
+geometry, interrupt topology, and a VBIOS blob the driver can parse.
+
+**Emulation:** static structures are synthesized for TU104 (FB size, BAR
+layout, engine list, vGPU-friendly VF values).
+
+**Log signature:**
+```
+GSPRM: GET_GSP_STATIC_INFO → ...
+```
+
+## 4. Message queues
+
+**Guest:** sends `SET_GUEST_SYSTEM_INFO` (1) with libos regions, a queue
+buffer description, and a page table (the queue buffer is
+`NV_MEMORY_NONCONTIGUOUS`). It also sends `GSP_SET_SYSTEM_INFO` (72),
+`SET_REGISTRY` (73) and `CONTINUATION_RECORD` (71) as init traffic.
+
+**Device must:** consume them silently (async) and start consuming the command
+queue exactly in the order the guest writes elements.
+
+**Emulation:** parses the queue chain, builds the queue geometry, links the
+status queue, and consumes elements one by one. **Element-based, not
+byte-stream-based**: each message is one element and may span pages.
+
+**Log signature:**
+```
+gsp-resman-vfio: libos region[i] id8=... pa=... size=...
+gsp-resman-vfio: queue args sharedPA=... ptes=... cmdOff=... statOff=...
+```
+
+## 5. Resource-manager initialization (the control battery)
+
+**Guest:** sends `GSP_RM_CONTROL` (76) for every subsystem it initializes.
+Representative controls (all observed, all answered with correct shapes):
+
+| Control | Meaning | What matters |
+|---|---|---|
+| `0x20800a40`-family | internal device info table | per-index values, entry count |
+| `0x20801112` | FIFO device info table | engine inventory; count must match engine queries |
+| `0x20800a5c` | interrupt kernel table | vector topology the driver maps |
+| `0x20800a59` | GMMU static info | **fault buffer sizes** (replayable/legacy) — a wrong value breaks UVM early |
+| `0x20800a01` | display static info | display count (headless values are fine) |
+| `0x20801823/-a/b` | BUS/FB info V2 | sysmem connection type and PCIe link caps feed bandwidth calculations |
+| `0x20802a07` | CE physical caps | per-engine support bits; UVM refuses to make channels without `sysmem` support |
+| `0x20802a0a/0b` | all-CE caps | caps table for all engines |
+| `0x20802a02` | CE PCE mask | one bit per engine |
+| `0x20802a08` | CE fault method buffer size | method buffer sizing |
+| `0x90f10106` | vaspace reserved-PDE contract | **carries page-table physical addresses** (see GSP-RM doc) |
+| `0x20803039` | NVLink masks/VBIOS info | unpopulated but must be shaped |
+| `0x20800a70` | BUS flush with sysmembar | UVM issues these frequently; success suffices |
+
+**Log signature:**
+```
+GSPRM: GSP_RM_CONTROL cmd=0x... hClient=0x... hObject=0x... paramsSize=...
+```
+
+## 6. Object creation
+
+**Guest:** `GSP_RM_ALLOC` (103) creates the object tree. Typical order for a
+CUDA process:
+
+```
+0x0 NV01_ROOT (client root, 120-byte params)
+0x80 NV01_DEVICE_0 (56 bytes)
+0x2080 NV20_SUBDEVICE_0 (4 bytes)
+0x90f1 FERMI_VASPACE_A (48 bytes) ← the GPU address space
+0xa06c KEPLER_CHANNEL_GROUP_A (20 bytes) ← CE channel group
+0xc46f TURING_CHANNEL_GPFIFO_A(368 bytes) ← the channel (GPFIFO VA, instance/userd descriptors)
+0xc076 GP100_UVM_SW (0 bytes)
+0xc5b5 TURING_DMA_COPY_A (8 bytes) ← CE object
+```
+
+Channel parameters decode into: `gpFifoOffset` (a GPU VA),
+`gpFifoEntries`, `engineType` (e.g. COPY0 = 0x9), and
+`NV_MEMORY_DESC_PARAMS` for instance/userd/ramfc/method buffers with
+**physical** addresses and apertures (`ADDR_SYSMEM = 1`, `ADDR_FBMEM = 2`).
+
+**Emulation:** handle management and parameter shapes are validated against
+the reference structures; values are logged in a form that makes the object
+tree reconstructible from `dmesg`.
+
+**Log signature:**
+```
+GSPRM: GSP_RM_ALLOC class=0x... paramsSize=... hClient=... hParent=... hObject=...
+```
+
+## 7. UVM registration (the wall we climbed)
+
+**Guest:** `cuInit` → `UVM_REGISTER_GPU` → channel manager creation →
+page-tree init.
+
+**Device must:** let UVM's channels initialize (each channel does a first push
+and waits on a semaphore), and let page-tree initialization proceed.
+
+**Emulation (today):** we can decode the pushes UVM generates and complete
+their semaphore releases by translating the semaphore VA to a guest physical
+address (see the UVM and host-module docs). This is the live development front.
+
+**Log signatures (guest side):**
+```
+nvidia-uvm: ... Tracker entry for value N (sema VA 0x...) channel ID ... CE ...
+nvidia-uvm: ... pending push '' started at ... in channel_init() releasing value N
+```
+
+## 8. What comes after registration (roadmap)
+
+```
+UVM registers → cuInit returns → context creation → memory allocation
+→ channel creation for the process → kernel launch → (real execution)
+```
+
+Each step will need the same treatment: satisfy the interface, forward the
+work, relay the completions. The known-unknowns document lists what we expect
+to be hard.
+
+## 9. Quick reference: message/control IDs seen from a stock 570.144 guest
+
+| Function | Name | Frequency per CUDA run (approx.) |
+|---|---|---|
+| 1 | SET_GUEST_SYSTEM_INFO | once |
+| 10 | FREE | ~14 |
+| 47 | UNLOADING_GUEST_DRIVER | on unload |
+| 65 | GET_GSP_STATIC_INFO | once |
+| 70 | UPDATE_BAR_PDE | a few |
+| 71/72/73 | CONTINUATION_RECORD / GSP_SET_SYSTEM_INFO / SET_REGISTRY | once each |
+| 76 | GSP_RM_CONTROL | ~160 |
+| 103 | GSP_RM_ALLOC | ~60 |
+
+If a new driver version produces **new function numbers**, do not guess: add a
+handler that logs the raw payload, then decode it against the decompiled
+study notes for that driver version. \ No newline at end of file