summaryrefslogtreecommitdiffstats
path: root/docs/03-architecture
diff options
context:
space:
mode:
Diffstat (limited to 'docs/03-architecture')
-rw-r--r--docs/03-architecture/11-physical-backend-guest-wiring.md575
1 files changed, 575 insertions, 0 deletions
diff --git a/docs/03-architecture/11-physical-backend-guest-wiring.md b/docs/03-architecture/11-physical-backend-guest-wiring.md
new file mode 100644
index 0000000..da440eb
--- /dev/null
+++ b/docs/03-architecture/11-physical-backend-guest-wiring.md
@@ -0,0 +1,575 @@
+# Physical Backend Guest Wiring: Driving the Guest-Facing Emulation from the Live GSP-RM Session
+
+> **Status: design for implementation (2026-10-01). No code from this design is
+> in the tree yet.** The physical backend itself is live: the module-owned
+> GSP-RM completes initialization on the real TU104, serves the full
+> static-config reply, and shuts down gracefully. The host resource-manager
+> client ladder has since allocated memory, a virtual-address-space object with
+> a committed page directory, a channel group and a real copy-engine channel,
+> and has executed the **first real work on the physical engine** — a 4 KiB
+> copy in virtual-address mode with the semaphore released and destination
+> equal to source (cycle 25). What this document specifies is the remaining
+> bridge: wiring that live session into the guest-facing mdev/VFIO path so that
+> guest submissions execute on the physical GPU. It extends
+> [`08-physical-gpu-integration.md`](08-physical-gpu-integration.md), which
+> chose the module-owned-GPU mechanism, and
+> [`06-host-module-and-hybrid-model.md`](06-host-module-and-hybrid-model.md),
+> which describes the emulation plane it must not disturb.
+>
+> The design was written against the tree at commit `f8e5d3b`; line numbers are
+> advisory and have moved with later commits — anchor by function name when
+> re-grepping. Behaviour claims below are grounded in live evidence from the
+> 2026-10-01 cycles; anything not yet exercised is labelled "not yet run".
+
+---
+
+## 0. Ground truth: the two planes that exist today
+
+### 0.1 Guest-facing emulation plane (works, green)
+
+* One emulation context per mdev: `nv_vgpu_device.gsp_comm_ctx`
+ (`kernel/include/nvidia-vgpu-vfio.h:115`), created by
+ `gsp_emu_init()` (`kernel/gsp_emulation.c:724`) from
+ `nv_vgpu_device_init_work()` (`kernel/main.c:1553`).
+* The emulation thread (`gsp_emu_thread_fn`, `kernel/gsp_emulation.c:651`)
+ does four things: samples the guest's BAR0 *shadow* falcon registers
+ (`gsp_emu_poll_boot`, `:193`), polls the guest's sysmem msgq
+ (`gsp_rm_emu_poll`, `kernel/gsp_rm_emulation.c:3386`), serves the legacy
+ doorbell/shared-memory RPC class (`gsp_emu_dispatch_rpc`, `:458`), and
+ injects MSI-X through the live eventfd (`gsp_emu_inject_completion_interrupt`,
+ `:626`).
+* The guest's real 570.144 physical-path protocol (msgq over *guest* system
+ memory, vfio_dma_rw transport) is handled entirely in
+ `gsp_rm_emulation.c`: queue-chain parse (`:806`), status-queue link
+ (`:917`), command poll (`:3194`), per-function dispatch (`:3084`),
+ reply construction/transmission (`gsp_rm_send`, `:452`).
+* `GSP_INIT_DONE` is emitted from `gsp_rm_emu_on_booter_done()` (`:3339`)
+ when the *emulated* Booter kick completes (`kernel/gsp_emulation.c:416-431`).
+* Static configuration is a hand-built TU104 model:
+ `gsp_rm_build_static_info()` (`:1025`) with 256 MB FB, fake
+ `TU104_H_INTERNAL_{CLIENT,DEVICE,SUBDEVICE}` handles and hand-tuned engine
+ caps; `gsp_rm_handle_get_static_info()` (`:1170`) sends it.
+* Work is executed in software by the sema service: doorbell entry
+ `gsp_sema_service_doorbell()` (`kernel/gsp_sema_service.c:6369`) called from
+ the VFIO write path (`kernel/main.c:826`), pump
+ `svc_pump_channels()` (`gsp_sema_service.c:5019`), decode
+ `svc_decode_stream()` (`:2802`), release apply `svc_sem_write()` (`:1514`),
+ guest event post `gsp_rm_post_channel_event()` (`gsp_rm_emulation.c:599`).
+
+### 0.2 Physical backend plane (alive, queryable, not wired to the guest)
+
+* Ownership: `gsp_phys_gpu_claim()` (`kernel/gsp_phys_gpu.c:662`) requires
+ both `gsp_phys_gpu=1` (`kernel/main.c:2457`) and `gsp_phys_hazard_ack=1`
+ (`kernel/gsp_phys_gpu.c:670`), locks the whole board + root port into D0
+ (`gsp_phys_power_lock()`, `:523`; runtime-PM ref `:740`), unbinds the
+ distribution driver (`:706`), binds `open-iov-gsp-phys` (`:718`) and verifies
+ config-space liveness (`gsp_phys_board_check()`, `:591`).
+* Boot: claim runs FWSEC (`:792`), staging `gsp_phys_boot_prime()`
+ (`booter:1349`), Booter Load + RISC-V `gsp_phys_boot_run()` (`booter:1524`,
+ pre-boot fn72/73 at `:1559-1568`), then init-done poll
+ `gsp_phys_boot_rmwait()` (`booter:2764`, called `gsp_phys_gpu.c:811`).
+ A full boot has run live multiple times; init-done with `rpc_result=0` and
+ the full 1656-byte static-config reply were observed (cycle 13).
+* Transport: `gsp_phys_queue_tx_submit()` (`booter:2935`) for TX,
+ `gsp_phys_status_queue_*` (`booter:2467/2490/2554`) +
+ `gsp_phys_status_wait()` (`booter:2670`) for RX. All real-GSP traffic is
+ serialized by the single static mutex `gsp_phys_rm_lock`
+ (`booter:3671`), taken by `gsp_phys_boot_rm_unload()` (`:3685`),
+ `gsp_phys_rm_probe()` (`:4090`) and `gsp_phys_rmcall_run()` (`:5117`).
+ `gsp_phys_queue_tx_submit()` and `gsp_phys_status_wait()` themselves take
+ no lock and are not safe to call concurrently.
+* Host RM-client ladder (debugfs only, `booter:5754` write handler):
+ stage 1 root/device/subdevice (`:4575`), stage 2 memory registration
+ (`:4650`), stage 3 TSG + channel variants (`:4761`; variant matrix `:4714`),
+ stage 4 real CE submission (`:4923`), stage 5 adopt static-config handles
+ (`:5068`). Stage 4 now runs end to end: the channel allocates, the page
+ directory is committed, and a 4 KiB virtual-address-mode copy executes on
+ the physical CE with the semaphore observed (`REAL COPY OK`, cycle 25).
+* Release: `gsp_phys_gpu_release()` (`gsp_phys_gpu.c:876`) sends
+ `UNLOADING_GUEST_DRIVER` once (`:909`), quiesces (`:910`), tears down WPR2
+ (`:911`), frees staging (`:914`), and only rebinds the distribution driver
+ if the board answers (`:955`) and WPR2 is gone (`:939`); otherwise the D0
+ locks intentionally stay held (fail-safe).
+
+### 0.3 The gap
+
+The two planes never touch. The guest's static answers, controls and objects
+are emulated; the physical session's real handles, real FB geometry, real
+engine caps and real work capability are only visible to debugfs. The bridge
+must be added at exactly three places (see §2): the reply point
+`gsp_rm_send()`, the inbound dispatch point `gsp_rm_handle_element()`, and the
+lifecycle points in `main.c` / `gsp_phys_gpu.c`.
+
+---
+
+## 1. Target architecture: runtime state machine
+
+### 1.1 Backend session state (module-global; one physical GPU)
+
+```
+ BACKEND_OFF
+ | first mdev create (gsp_phys_gpu=1 + hazard_ack; no other mdev)
+ v
+ BACKEND_CLAIMING power-lock D0 -> unbind dist -> bind phys -> board check
+ | claim ok \ abort signature / lock fail
+ v v
+ BACKEND_BOOTING FWSEC -> prime -> Booter -> RISC-V -> rmwait (init-done)
+ | init-done rpc_result=0 \ init failure / timeout
+ v v
+ BACKEND_READY static-config snapshot cached; RPC/control forwarding legal
+ | last mdev remove | rmmod | board signature
+ v
+ BACKEND_STOPPING stop emulation forwarding; UNLOADING_GUEST_DRIVER;
+ | quiesce; WPR2 teardown; staging free
+ v
+ BACKEND_OFF power unlock; distribution driver rebound and settled
+```
+
+`BACKEND_READY` is the single gate for every live answer. In every other
+state the guest-facing handlers must behave exactly as they do today.
+
+### 1.2 Ownership: who starts/stops what
+
+| Trigger | Action | Where |
+|---|---|---|
+| first `mdev` create | backend refcount++ ; if 0 -> claim + boot asynchronously from `init_work` | `kernel/main.c:535`, `:1553`; new `gsp_phys_backend_start()` |
+| last `mdev` remove | backend refcount-- ; if 0 -> `gsp_phys_gpu_release()` after the emulation thread is stopped | `kernel/main.c:614`, `:1614`; new `gsp_phys_backend_stop()` |
+| `rmmod` with live mdevs | forbidden by policy, but code path must drain: STOPPING first, then stop emu threads, then release | `kernel/main.c:1487` (`nv_vgpu_exit`) |
+| load-time `gsp_phys_gpu=1` | keep today's supervised behavior: claim (and full boot) at module init, owned by the module, released only at `rmmod` (debugfs harness unchanged) | `kernel/main.c:1450` |
+| QEMU close/open only | no backend action; the session persists across VM restarts until mdev remove | `nv_vgpu_vfio_dev_close`, `kernel/main.c:698` |
+| power-loss signature anywhere | STOPPING immediately (no new forwards); release fail-safe keeps D0 locks; no retry in this boot | `gsp_phys_board_check`, `gsp_phys_gpu.c:591` |
+
+The module-owned claim (load-time `gsp_phys_gpu=1`) and the mdev-owned claim
+share the same `phys` singleton. `gsp_phys_gpu_claim()` is idempotent today
+(`gsp_phys_gpu.c:667` returns 0 if already claimed); the refcount/ownership
+flag is new state and decides **who** releases.
+
+### 1.3 Locks and ownership
+
+| Lock | Protects | Taken by | Rule |
+|---|---|---|---|
+| `gsp_phys_session_lock` (new) | backend state, refcount, owner | `backend_start/stop/state`; `nv_vgpu_exit` | outermost; never held while waiting on a guest lock; may be held while calling `rm_lock` |
+| `gsp_phys_rm_lock` (existing, `booter:3671`) | all real-GSP TX/RX: queue cursors, `boot_stage.status_read_ptr`, `tx_seq_num`, probe/ladder/unload | `gsp_phys_boot_rm_unload`, `gsp_phys_rm_probe`, `gsp_phys_rmcall_run`, new `gsp_phys_backend_rpc()` | one real RPC at a time, always; **never** taken while holding `rm->stat_lock` or any guest-memory path |
+| `rm->stat_lock` (`gsp_rm_emulation.c:186`) | guest status-queue writes | `gsp_rm_send` (`:452`) | build the full reply before taking it; release immediately after write; never hold across a backend wait |
+| `vgpu->lock` (`nvidia-vgpu-vfio.h:99`) | per-mdev state transitions | `init_work`/`cleanup_work` | never held across claim/release or RPC waits |
+| `nv_vgpu_driver.lock` | device list | create/remove, sema thread scan | never taken under session/rm locks |
+| `svc.chan_lock` / `svc.vas_lock` | software work pipeline | sema service | unchanged; forwarding reads channel state copied out under these locks |
+
+Lock order: `nv_vgpu_driver.lock` (shortest, leaf) < `gsp_phys_session_lock`
+< `gsp_phys_rm_lock`. The emulation thread must take
+`session_lock -> check state -> drop session_lock -> rm_lock -> re-check a
+generation counter` so a release cannot deadlock against a forward in flight.
+
+### 1.4 Failure paths
+
+* **Guest crash / QEMU killed:** `open_count` drops; the backend stays READY
+ and claimed (device owned, host headless). No device writes. The next
+ `mdev` remove or `rmmod` drives the graceful path. This is the same
+ `--keep` semantics the supervised harness already uses.
+* **Guest RPC while STOPPING/unload already submitted:** every forwarding
+ seam returns "not ready" and the emulation answer is used. This mirrors the
+ defensive re-checks in `gsp_phys_rmcall_run()` (`booter:5119`).
+* **Module unload order (must change):** today `nv_vgpu_exit()` calls
+ `gsp_phys_gpu_release()` at `kernel/main.c:1500` *before*
+ `gsp_sema_service_stop()` (`:1525`) and before mdev unregistration
+ (`:1530`) which is what stops the emulation threads (`:1619`). Once
+ handlers forward to the backend, release must not race live forwards:
+ new order = set STOPPING (session lock) -> `gsp_sema_service_stop()` ->
+ `mdev_unregister_driver()` (drains emulation) -> `gsp_phys_gpu_release()` ->
+ parent/class teardown.
+* **Server wedge (the 0x40 GSP-owned incident):** if `rm_unload` times out,
+ the existing release still quiesces, runs the FWSEC teardown and refuses to
+ rebind with WPR2 up (`gsp_phys_gpu.c:939`). A reboot then needs explicit
+ human approval plus a handoff update.
+* **Static-config served before READY:** bounded wait (recommend 2 s) then
+ pure emulation fallback, with a stat counter; never block the emulation
+ thread longer than the guest's own RPC poll can tolerate.
+
+---
+
+## 2. Seam table
+
+Legend: **L** = live backend, **E** = existing emulation, **H** = hybrid.
+"Change point" is the exact function that must learn the new behavior.
+
+### 2.1 Guest GSP-RM RPC class (msgq function IDs)
+
+| # | Guest message | Today | Target | Change point (file:line) |
+|---|---|---|---|---|
+| 1 | `GSP_SET_SYSTEM_INFO` (72) | consumed, no reply (`gsp_rm_emulation.c:3095`) | **E** - describes the guest client at guest boot; the backend already sent its own fn72 at `booter:1559` | `gsp_rm_handle_element` `gsp_rm_emulation.c:3095` |
+| 2 | `SET_REGISTRY` (73) | consumed, no reply (`:3096`) | **E** - same reason | `:3096` |
+| 3 | `CONTINUATION_RECORD` (71) | SUCCESS reply (`:3105`) | **E** | `:3105` |
+| 4 | `SET_GUEST_SYSTEM_INFO` (1) | echoes guest vgx version (`:982`) | **E** - this is the guest's negotiation, deliberately decoupled from the host session (see §4.3) | `gsp_rm_handle_set_guest_system_info` `:982` |
+| 5 | `GET_GSP_STATIC_INFO` (65) | hard-coded TU104 model (`:1025`), 256 MB FB, fake handles, hand-tuned caps | **H** - overlay live identity/capability fields proven safe; keep model-consistent FB geometry, PDE bases and handles until phases P4/P6 (see §4) | `gsp_rm_build_static_info` `:1025`, `gsp_rm_handle_get_static_info` `:1170` |
+| 6 | `GSP_RM_ALLOC` (103) | echo request + `NV_OK`; channels/events registered locally (`:2322`) | **H** - echo for emulation-only classes; forward selected classes (root/device/subdevice, then channel/memory) once a guest<->server handle map exists; always keep local side effects | `gsp_rm_handle_rm_alloc` `:2322` |
+| 7 | `GSP_RM_CONTROL` (76) | ~25 static per-cmd handlers; unknown -> zeroed success (`:2611`, default `:2807`) | **H** - whitelist of side-effect-free queries goes live with emulation fallback; vGPU-specific controls stay **E** (`COPY_SERVER_RESERVED_PDES` `:2647`, `PROMOTE_FAULT_METHOD_BUFFERS` `:2654`) | `gsp_rm_handle_rm_control` `:2611`; new forward helper before the switch |
+| 8 | `GSP_RM_FREE` (10) | echo + retire channel/events (`:2882`) | **H** - forward when the object was backend-allocated; always retire local bookkeeping | `gsp_rm_handle_free` `:2882` |
+| 9 | `SET_PAGE_DIRECTORY` (54) | recorded for translation + SUCCESS (`:2975`) | **H** - recording stays **E**; forwarding to the real MMU is P6 and gated on guest pages being GPU-DMA-reachable | `gsp_rm_handle_set_page_directory` `:2975` |
+| 10 | `UNSET_PAGE_DIRECTORY` (79) | record remove + SUCCESS (`:3063`) | **H** - same gate as #9 | `:3063` |
+| 11 | `UPDATE_BAR_PDE` (70) | SUCCESS ack (`:3144`) | **E** now; **L** only when BAR1 is backed by real VRAM | `:3144` |
+| 12 | `UNLOADING_GUEST_DRIVER` (47) | SUCCESS ack (`:3155`) | **E** - the guest's own shutdown; the real session's shutdown is host-driven on backend stop (`gsp_phys_gpu.c:909`) | `:3155` |
+| 13 | default/unknown | generic SUCCESS (`:3169`) | **E** (fail-safe: never invent a live forward for an unknown function) | `:3169` |
+
+### 2.2 Outbound events and replies
+
+| # | Record | Today | Target | Change point |
+|---|---|---|---|---|
+| 14 | `GSP_INIT_DONE` (0x1001) | emitted as soon as the emulated Booter finishes (`gsp_rm_emulation.c:3374`) | **H** - emit immediately as today, but live answers gate themselves on READY; alternatively delay emission on a bounded backend wait so the guest's first RM RPC sees READY | `gsp_rm_emu_on_booter_done` `:3339` |
+| 15 | `POST_EVENT` (0x1003) | software pump completion (`gsp_rm_post_channel_event` `:599`) | **E** as the relay mechanism; its *trigger* becomes the observation of a real hardware semaphore (P5) instead of a software op | `gsp_rm_post_channel_event` `:599`, `svc_notify_complete` `gsp_sema_service.c:4151` |
+| 16 | all replies | single writer `gsp_rm_send` (`:452`) | **E** - keep as the only guest-queue writer; live payloads are copied into it | `gsp_rm_send` `:452` |
+
+### 2.3 BAR / register and work-plane classes
+
+| # | Guest-visible thing | Today | Target | Change point |
+|---|---|---|---|---|
+| 17 | GSP falcon boot regs (FWSEC/Booter/RISCV/WPR2), SW interrupt tree, MMU invalidate, L2 flushes | emulated auto-complete (`gsp_emulation.c:193-452`) | **E always** - the two GSP sessions are intentionally independent; guest boot must never depend on the physical session | `gsp_emu_poll_boot` `:193` |
+| 18 | Guest VF doorbell `0x88704` | software pump (`main.c:826` -> `gsp_sema_service_doorbell` `:6369`) | **H** - pump classifies each push: pure state/release stays software; engine work is forwarded (P5) | `svc_pump_channels` `gsp_sema_service.c:5019`, `svc_decode_stream` `:2802` |
+| 19 | BAR0/BAR1/BAR3 shadows, PRAMIN window | host-RAM shadows (`bar_emulation.c:180`, `main.c:1205`) | **E** until real VRAM forwarding (P6) | `nv_vgpu_bar_emul_*`, `nv_vgpu_vfio_dev_mmap` |
+| 20 | legacy doorbell/shared-mem RPC class (`GSP_RPC_*`) | internal emulation client thread (`gsp_emulation.c:458-620`, `:674-694`) | **E / disable when READY** - not part of the guest physical-path protocol; gate it off in backend mode to avoid intercepting stray writes | `gsp_emu_thread_fn` `:674` |
+
+**Highest-leverage seams:** #5/#7 (one reply point, one dispatch point; all
+live answers plug in there) and #6 (whatever alloc/free semantics the guest
+gets determine whether real channels/page tables are possible).
+
+---
+
+## 3. Lifecycle changes in `main.c` / `gsp_emulation.c`
+
+1. **New backend API** (add to `gsp_phys_booter.[ch]`, used by both debugfs
+ and the guest plane; keeps `gsp_phys_rm_lock` private):
+ * `int gsp_phys_backend_state(void)` - OFF/CLAIMING/BOOTING/READY/STOPPING.
+ * `int gsp_phys_backend_rpc(u32 function, const void *req, u32 req_len,
+ void **reply, u32 *reply_len, u32 timeout_ms)` - checks
+ `claimed && rm_init_done && !rm_unload_submitted`, takes
+ `gsp_phys_rm_lock`, calls `gsp_phys_queue_tx_submit()` +
+ `gsp_phys_status_wait()` (both at `booter:2935`/`:2670`), returns a copy
+ of the payload; caller frees. Unmatched records (events, NOCAT) are
+ relayed or summarized like `gsp_phys_status_wait` already does.
+ * `int gsp_phys_backend_static_snapshot(u8 *out, u32 len)` - cached full
+ 1656-byte fn65 payload; filled once at READY by the existing
+ `gsp_phys_rm_probe_cycle()` (`booter:4056`) under the same lock.
+ * `void gsp_phys_backend_begin_stop(void)` / `gsp_phys_backend_end_stop()` -
+ state transitions only (no hardware).
+2. **New session state + refcount** in `gsp_phys_gpu.c` (new mutex, owner
+ flag, state enum). `gsp_phys_gpu_claim()`/`_release()` stay the only
+ hardware entry points; add `gsp_phys_backend_start()/stop()` that call
+ them exactly once.
+3. **`nv_vgpu_mdev_create()` (`main.c:535`)**: after
+ `vfio_register_emulated_iommu_dev()` (`:591`) and before/around
+ `schedule_work(&vgpu->init_work)` (`:598`), call
+ `gsp_phys_backend_start()`. Claiming must happen while no VM holds a live
+ mdev: with `available_instances` computed from per-type max (1) in
+ `nv_vgpu_get_available()` (`main.c:81`) there is at most one mdev of the
+ type in flight, so this holds.
+4. **`nv_vgpu_device_init_work()` (`main.c:1553`)**: keep BAR/IRQ/memory/
+ `gsp_emu_init()` order. The backend boot runs in its own kworker so
+ `mdev` create returns quickly; `init_work` only observes READY when it
+ initializes forwarding tables. Do **not** run the 60 s `rmwait`
+ (`booter:2764`, timeout `GSP_PHYS_RM_INIT_TIMEOUT_MS` 60000 at
+ `booter:446`) inline in `init_work` if it can be avoided: run claim+boot
+ in a dedicated work item so `mdev->remove` is never stuck behind it.
+5. **`nv_vgpu_device_cleanup_work()` (`main.c:1614`)**: unchanged per-mdev,
+ but the last-cleanup path calls `gsp_phys_backend_stop()` **after**
+ `gsp_emu_cleanup()` (`:1619`) so no emulation thread can forward while the
+ backend is being torn down.
+6. **`nv_vgpu_exit()` (`main.c:1487`)**: reorder as in §1.4; the load-time
+ claim path (owner = module) keeps the existing `gsp_phys_gpu_release()`
+ position relative to debugfs fini, but mdev-owned sessions are released in
+ the new order.
+7. **`gsp_emu_init()` (`gsp_emulation.c:724`)**: no structural change; the
+ `gsp_rm_emu` context gains a reference to the module backend (not a new
+ per-mdev session). `gsp_emu_cleanup()` (`:831`) unchanged except it must
+ not be the thing that releases the backend (done by `main.c`, step 5).
+8. **`gsp_rm_emu_on_booter_done()` (`gsp_rm_emulation.c:3339`)**: after
+ linking the queues, optionally wait (bounded) for READY before emitting
+ INIT_DONE, or emit immediately and let each live handler wait. Recommended:
+ emit immediately (the guest's early RPCs are all emulatable), and let
+ `GET_GSP_STATIC_INFO` be the single bounded-wait point.
+9. **`gsp_emu_thread_fn()` (`gsp_emulation.c:651`)**: no new blocking calls in
+ the loop; live forwards happen inside `gsp_rm_handle_element()` on that
+ thread, with the 2 s per-RPC bound. If a longer wait is ever needed, move
+ forwarding to a dedicated backend kthread and keep this loop responsive.
+
+---
+
+## 4. Static-config flow and identity/version negotiation
+
+### 4.1 Current guest model (must stay internally consistent)
+
+`gsp_rm_build_static_info()` (`gsp_rm_emulation.c:1025`) hard-codes:
+`fb_length = 0x10000000` (256 MB, offset 1224), `fbio_mask=0xff`,
+`fbp_mask=0x3f`, `gpuNameString="NVIDIA GeForce RTX 2080 SUPER"` (1260),
+`gpuShortNameString="TU104"` (1324), `bar1PdeBase=bar2PdeBase=0`
+(1536/1544), fake internal handles (1600/1604/1608), engine caps
+`0x60803E02 / 0x00362020 / 0` (1204). The 256 MB figure is coupled to the
+BAR0 shadow's `LOCAL_MEMORY_RANGE` (`bar_emulation.c:280`,
+`0x100ce0=0x18` -> 256 MB) and to the emulated WPR2/FRTS location the guest
+driver checks. Changing it without changing the shadow breaks the guest boot
+(this is why 512 MB was reverted, per the comment at `:1096`).
+
+### 4.2 Live values observed (cycle 13)
+
+```
+static-info internal: hClient=0xc2000006 hDevice=0xabcd0080 hSubdevice=0xabcd2080
+static-info fb: bar1PdeBase=0x1f7650000 bar2PdeBase=0x1f76c2000
+ fb_length=0x200000000 fbio_mask=0xf fbp_mask=0x11
+ busWidth=0 ramType=256 l2CacheSize=15
+static-info gpu: name='NVIDIA GeForce RTX 2080 Super with Max-Q Design'
+ short='TU104-B' vbiosSubVendor=0x4d50 vbiosSubDevice=0x4449
+static-info engineCaps=[0x08083e02,0x00000044,0x00000000]
+```
+
+Two grounded warnings:
+
+* The decoded `busWidth=0`, `ramType=256` are not plausible TU104 values
+ (256-bit, GDDR6). Either the decode offsets for those two fields do not
+ match the real struct, or the emulated values were hand-derived rather than
+ observed. **A/B every field before overlaying it.**
+* Engine caps differ materially (`0x60803E02` vs `0x08083E02`, and the second
+ word). The emulation deliberately sets extra bits (COPY14 etc.) to make
+ `libcuda` accept the engine inventory. Overlaying live caps would change
+ the guest's engine/class topology and can regress the whole CUDA init.
+ Keep emulated caps until the backend can actually service the resulting
+ demands (P4+).
+
+### 4.3 Proposed flow
+
+1. At READY, cache the full fn65 payload (new bytes in the backend session,
+ keyed by the loaded firmware's `phys.fw_crc32`, `gsp_phys_gpu.c:272`), via
+ the existing probe cycle (`booter:4056`) under `gsp_phys_rm_lock`.
+2. `gsp_rm_handle_get_static_info()` (`:1170`) keeps building the emulated
+ 1656-byte model, then applies an allow-listed overlay from the snapshot
+ (start: name/short-name/VBIOS identity/L2 if A/B passes; **not** FB
+ geometry, masks, PDE bases, handles, engine caps).
+3. Any overlay field is independently switchable (`gsp_phys_static_live`
+ module parameter, default off) so a regression is a one-line rollback.
+4. Handles move in P4 and PDE bases/geometry in P6, each behind its own
+ switch and its own A/B.
+5. Version negotiation stays decoupled: the guest sees the emulation's
+ version answer (`SET_GUEST_SYSTEM_INFO`, `gsp_rm_emulation.c:982`); the
+ real server is negotiated by the host as the host client
+ (`gsp_phys_rm_probe_fn1`, `booter:3984`, vgx 0x29/0x0C). The guest must
+ never learn the physical firmware's version; the emulation target remains
+ 570.144 while the host may boot a different, matching TU10x GSP image
+ (`gsp_phys_gpu.c:75`). The wire structures are the same family and the
+ live snapshot is keyed by firmware CRC.
+
+---
+
+## 5. Phased implementation plan (smallest first)
+
+Each phase is independently revertible. "Supervised" means the documented
+harness discipline: `gsp_phys_gpu=1` + `gsp_phys_hazard_ack=1`, human
+go-ahead, recovery plan, pstore, never unattended
+(`docs/03-architecture/08-physical-gpu-integration.md` §7/§8).
+
+### P0 - Backend API skeleton (inert)
+* **Scope:** new state/refcount/lock in `gsp_phys_gpu.c`; locked
+ `backend_rpc()`/`static_snapshot()` in `gsp_phys_booter.c`; no caller
+ changes; no behavior change.
+* **Acceptance:** `make kernel` clean; load/unload with no params; all
+ existing debugfs triggers behave identically; new API logs and refuses when
+ OFF/not-init-done/unloading.
+* **Risk:** none (never sends unless a live init-done session exists).
+* **Rollback:** trivial.
+
+### P1 - Lifecycle wiring (claim on mdev create, release on last remove)
+* **Scope:** `main.c` create/remove/init_work/cleanup_work/exit ordering
+ (§3 items 3-6); no live answers yet.
+* **Acceptance (supervised, harness `phys_claim_cycle.sh` style):** fresh
+ boot -> insmod params -> create mdev -> one real boot log sequence (FWSEC,
+ Booter M0=0, RISC-V active, init-done after sequencer) -> guest boots and
+ the CUDA driver-API smoke test still prints `ALL OK` 64/64 -> stop VM ->
+ remove mdev -> `UNLOADING_GUEST_DRIVER` suspend, scratch14 bits 24-27 DONE,
+ WPR2 down, distribution driver rebound; repeat twice; zero power-domain
+ cycles; no reboot.
+* **Risk:** **highest phase.** Claim is now triggered by a sysfs action, not
+ by module load. The two-param gate and the D0 lock must already be active
+ inside claim (they are, `gsp_phys_gpu.c:670/700`), the claim must refuse a
+ second owner, and the release must never run while a forward is in flight
+ (STOPPING gate). Any power-loss signature -> hold locks, no retry.
+* **Rollback:** `gsp_phys_gpu=0` (no claim) or load-time `gsp_phys_gpu=1`
+ only; `mdev` emits `-EOPNOTSUPP` from `backend_start()`.
+
+### P2 - Static-config identity overlay
+* **Scope:** cache snapshot at READY; overlay allow-list in
+ `gsp_rm_build_static_info()`/`gsp_rm_handle_get_static_info()`; module
+ parameter; stats. Bounded wait (2 s) then emulation fallback.
+* **Acceptance:** guest still `nvidia-smi -L` + smoke `ALL OK`; guest
+ `nvidia-smi -q` name changes to the live `... with Max-Q Design`; dmesg
+ shows which fields came live; backend absent -> byte-identical guest reply.
+* **Risk:** a consumed identity field changes a driver decision (VBIOS
+ validation is the likeliest). Mitigation: field-by-field A/B, default-off,
+ instant fallback; never touch geometry/caps/handles in P2.
+* **Rollback:** parameter off.
+
+### P3 - Read-only control forwarding (whitelist)
+* **Scope:** in `gsp_rm_handle_rm_control()` (`:2611`), before the per-cmd
+ switch: for an allow-list of side-effect-free queries (FB/BUS/KGR/GMMU/
+ CE/display statics), call `backend_rpc()` with the **adopted internal
+ handles** from the snapshot (the guest's emulated handles are not valid on
+ the server yet), copy reply params (payload+24) on `rpc_result==0 &&
+ status==0`, else fall back to the existing handler. Add per-cmd
+ forwarded/fallback counters.
+* **Acceptance:** guest boot and smoke green with forwarding on; dmesg shows
+ N forwarded controls; forced backend timeout -> same behavior as today;
+ repeat calls yield identical params (no server-side state change).
+* **Risk:** a whitelist entry is not actually read-only (display/KGR can
+ allocate on first call). Start with FB/BUS/GMMU static info only, verify
+ against the server's log ring (`gsp-phys-log-dump`), then grow.
+* **Rollback:** parameter/whitelist off.
+
+### P4 - Alloc/free forwarding for the object ladder
+* **Scope:** `gsp_rm_handle_rm_alloc()` (`:2322`) forwards
+ root/device/subdevice (and only those, first) via `backend_rpc(fn103)`;
+ returns the server's chosen handle to the guest; maintains a
+ guest-handle -> server-handle map used by P3 and `gsp_rm_handle_free()`.
+ Any failure falls back to today's echo (never half-forward), and local
+ channel/event registration still runs.
+* **Acceptance:** guest boot + smoke green; dmesg shows adopted handles
+ (`0xc2.../0xabcd0080/0xabcd2080`) matching cycle13; guest FREE returns
+ success; no server-side wedge (no RPC timeout after alloc).
+* **Risk:** the guest's alloc params may not satisfy the server's per-class
+ validation; a bad forward can wedge the RM task (see the `0x40` GSP-owned
+ channel incident). Guard: one class at a time, abort on first failure,
+ `UNLOADING_GUEST_DRIVER` + rmmod remains the only fail-safe; never retry a
+ mutating RPC.
+* **Rollback:** parameter off (guest returns to echo path).
+
+### P5 - Work forwarding + completion relay (interim, host buffers)
+* **Scope:** the debugfs ladder's real-submission half is **done** (cycle 25:
+ channel allocates, page directory commits, `REAL COPY OK` on the physical
+ CE in VA mode). The remaining work: bind each guest CE/GR channel to a real
+ channel; in `svc_pump_channels()` (`gsp_sema_service.c:5019`) classify the
+ decoded push and, for forwarded ops, submit an equivalent real method block
+ built by the existing `gsp_phys_rmcall_build_methods()` (`booter:4879`) +
+ GPFIFO/USERD/doorbell (`:4991-5032`). Observe the real semaphore, then
+ mirror the release into guest memory with `svc_sem_write()`
+ (`gsp_sema_service.c:1514`) and post the guest event with
+ `gsp_rm_post_channel_event()` (`gsp_rm_emulation.c:599`). All hardware
+ operands are module-owned DMA buffers (as in the ladder), so no guest memory
+ is touched by the engine yet.
+* **Acceptance:** ladder: `echo 4 > gsp-phys-rm-call` logs
+ `REAL COPY OK` (`booter:5052`) and src==dst after a hardware copy - met
+ (cycle 25, 4 KiB, semaphore released, dst==src). Guest: a submitted copy
+ completes with the semaphore observed in host memory and the guest tracker
+ advancing; wrong-operand/refused submissions fall back to software and are
+ counted.
+* **Risk:** doorbell to a wedged channel; wrong GPFIFO/USERD (guards:
+ `rmcall_channel_server_mem` refusal `:4944`, work-submit token after
+ `GPFIFO_SCHEDULE` `:4977`, `GP_PUT` bounds `:4995`); hardware writes only
+ host DMA memory, so no cross-tenant exposure yet.
+* **Rollback:** per-channel `forward=0`; software pump is untouched.
+
+### P6 - Guest memory into the real MMU (end state: guest submission on hardware)
+* **Scope:** map guest page-table pages/records so the physical GPU can DMA
+ them; forward `SET_PAGE_DIRECTORY` (`gsp_rm_emulation.c:2975`) to the real
+ server; allocate real memory objects for guest allocations (fn4 with real
+ PFNs); per-guest-channel real channels with client rings (module or guest
+ memory); pump forwards real push streams referencing guest VAs; completion
+ and faults relayed through real MSI-X -> module IRQ -> existing eventfd
+ injection (`gsp_emu_inject_completion_interrupt`, `gsp_emulation.c:626`;
+ the physical driver does not `request_irq()` yet, `gsp_phys_gpu.c:380-461`).
+* **Acceptance:** `vectorAdd` correct results through a forwarded launch;
+ copy with semaphore; second mdev is refused/emulated (single-session
+ isolation until per-mdev clients exist); fault test.
+* **Risk:** wrong page tables -> engine writes to arbitrary host memory;
+ IOMMU/DMA reachability of guest pages is the project's existing
+ known-unknown (`docs/04-implementation/01-known-knowns-and-unknowns.md`
+ §2.2) and is a prerequisite, not solved by this design. Keep
+ `gsp_phys_seq_addr_valid()` (`booter:1800`) and all queue validations.
+* **Rollback:** per-mdev isolation not yet available; disable forwarding and
+ fall back to P5 host-buffer mode.
+
+---
+
+## 6. Top risks and the guards that must be preserved
+
+1. **Host hard-freeze on unbind/rebind.** Unbinding the distribution driver
+ can power-gate the board; re-bind intermittently fails and the host
+ freezes with no crash record. Guards: two-param gate
+ (`gsp_phys_gpu.c:670`), refuse if GPU/root port already
+ runtime-suspended (`:688`), D0 lock on GPU + siblings + root port
+ (`:499-549`), runtime-PM usage ref (`:188-204`), config-space liveness
+ checks after claim and before rebind (`:591`), no retry after a
+ power-loss signature, human approval for any reboot.
+2. **WPR2 held / one-way protected region.** Always run the release sequence
+ in order: `UNLOADING_GUEST_DRIVER` (only if init-done was seen)
+ -> quiesce GSP falcon -> FWSEC teardown -> free staging
+ (`gsp_phys_gpu.c:909-914`); if WPR2 survives, do **not** rebind and keep
+ the locks (`:939`); a FWSEC-only stop is forbidden
+ (`gsp_phys_gpu.c:770`). The debugfs `gsp-phys-unload-rpc` remains
+ write-only and inert outside a live claim.
+3. **RM-task wedge (live 0x40 incident).** Setting the GSP-owned channel
+ `internalFlags` bit (0x40) wedged GSP-RM: the alloc RPC never replied and
+ every later RPC timed out; the fail-safe held the device until reboot.
+ The working tree now bans that bit and refuses the submission when the
+ winning variant used server-allocated memdescs (`booter:281`, `:4714`,
+ `:4942`). Never re-enable it; abort the ladder at the first failure with
+ no object teardown (`:5123` comments); `UNLOADING_GUEST_DRIVER` + rmmod is
+ the only recovery.
+4. **Wrong-page-table / wrong-doorbell writes.** No forwarding seam may
+ submit engine work or program page tables until P6's reachability
+ prerequisite holds. Preserve: queue header validation in
+ `gsp_phys_queue_tx_submit()` (`booter:2983`), free-space check (`:3032`),
+ status-record validation (`booter:2554`), sequencer address validation
+ (`booter:1800`), stage-4 token/bounds guards (`booter:4969-4999`).
+5. **Guest status-queue invariants.** Strict sequence/wrap/checksum and
+ multi-element records; `gsp_rm_send()` (`:452`) is the only writer and
+ must not be called while holding `gsp_phys_rm_lock` or waiting on the
+ backend; the event-burst cap (`:488`) and parse retry backoff (`:3397`)
+ stay.
+6. **Two planes, two BARs.** The guest face must only ever touch
+ `gsp_emu_context.bar0_vaddr` (the vzalloc shadow,
+ `gsp_emulation.c:761-778`); the backend must only ever touch `phys.bar0`
+ (real ioremap, `gsp_phys_gpu.c:420`). Never ioremap a guest GPA (the
+ fixed `gsp_client_init_emulation()` contract).
+7. **Ordering and context rules.** No long scans or backend waits in VFIO
+ callbacks; `vfio_dma_rw` only from kthread context (mm == NULL);
+ never `rmmod` with a live VM; mdev remove before module unload; stop
+ emulation before backend release once P2+ lands (new `nv_vgpu_exit`
+ order).
+8. **Debuggability and evidence.** Keep every real RPC logged at the TX/RX
+ seams (`booter:3050`, `:3127`, status-walk logs), keep the read-only
+ `gsp-phys-state`/`gsp-phys-log-dump` files, and keep the evidence tree
+ (`backups/evidence-*`) current on every live cycle.
+
+---
+
+## Appendix A - line anchor map (function -> file:line)
+
+`kernel/gsp_phys_booter.c` (working-tree revision used for this design):
+`gsp_phys_boot_prime` 1349, `gsp_phys_boot_run` 1524, `gsp_phys_status_queue_init`
+2467, `_link` 2490, `_next` 2554, `gsp_phys_status_wait` 2670,
+`gsp_phys_boot_rmwait` 2764, `gsp_phys_queue_tx_submit` 2935, `gsp_phys_rm_lock`
+3671, `gsp_phys_boot_rm_unload` 3673, `gsp_phys_sci_decode` 3820,
+`gsp_phys_rm_probe_fn65` 3918, `fn1` 3984, `_cycle` 4056, `gsp_phys_rm_probe`
+4086, `gsp_phys_rmcall_rpc` 4329, `_alloc` 4385, `_register_mem` 4454,
+`_control` 4510, `_stage1` 4575, `_stage2` 4650, `_channel_variants` 4714,
+`_stage3` 4761, `_build_methods` 4879, `_stage4` 4923, `_stage5` 5068,
+`_run` 5108, `_fini` 5197, debugfs write handlers 5613/5681/5754,
+`gsp_phys_boot_debugfs_init` 5827, `gsp_phys_boot_quiesce` 5919,
+`gsp_phys_boot_staging_free` 5944.
+
+`kernel/gsp_emulation.c`: thread 651, init 724, cleanup 831, poll_boot 193,
+rm poll call 672, legacy RPC dispatch 458, MSI-X inject 626.
+
+`kernel/gsp_rm_emulation.c`: `gsp_rm_send` 452, parse 806, link 917,
+`set_guest_system_info` 982, `build_static_info` 1025, `get_static_info` 1170,
+`rm_alloc` 2322, `rm_control` 2611 (default 2807), `free` 2882,
+`set_page_directory` 2975, `unset` 3063, `handle_element` 3084,
+`poll_commands` 3194, `on_booter_done` 3339, `poll` 3386.
+
+`kernel/main.c`: `nv_vgpu_mdev_create` 535, `remove` 614, init_work 1553,
+cleanup_work 1614, `nv_vgpu_init` 1388, claim 1450, debugfs init 1461,
+sema start 1468, `nv_vgpu_exit` 1487, release 1500, sema stop 1525,
+mdev unregister 1530, param 2457.
+
+`kernel/gsp_phys_gpu.c`: claim 662, hazard gate 670, suspend refusal 688,
+power lock 523/499, unbind 706, driver register 718, board check 591,
+runtime ref 188, boot 745-848 (FWSEC 792, prime 799, run 805, rmwait 811),
+release 876 (unload 909, quiesce 910, fwsec 911, staging free 914, WPR2
+fail-safe 939, board check 955, rebind 972).
+
+`kernel/gsp_sema_service.c`: doorbell 6369, start 6392, stop 6416, thread
+6232, pump 5019, decode 2802, sem write 1514, notify 4151.