// Windows present backend: D3D12 + NVAPI DirectMode (spike S2 validated path). // // Encodes the S2 findings (spikes/s2-vulkan-direct/FINDINGS.md): // - Mode selected by VALUE (resolution + refresh + NV_FORMAT 22), never by // index — the DM mode list reorders between runs. // - DSC must be FORCED: v1.1 / 4 slices / outputBPPx16=128 (device JSON // values). Default negotiation genuinely garbles the link. // - DM surfaces open TYPELESS with a VARYING format across runs; RTVs need // an explicit typed desc and the map must fail loudly when unmapped. // - Pacing by the present waitable; NvAPI_Unload skipped at teardown // (driver DLL atexit crash). // // M1 hazard handling (S3 findings): doffed headset -> prox kills panels -> // vsync gone -> present waitable stops blocking and the loop free-runs at // 5000+ fps with ZERO api errors. Detected via frame-interval EMA; while // free-running the loop self-paces with a sleep and flags stats().freeRunning. #pragma once #include "present/present_backend.h" #include #include namespace sauna { struct NvapiPresenterConfig { uint32_t width = 5088, height = 2544; double refreshHz = 75.0; int nvFormat = 22; // X8R8G8B8 — the validated link format bool forceDsc = true; // v1.1 / 4 slices / 8 bpp, from the device JSON bool powerOffOnExit = false; }; class NvapiD3d12Presenter : public PresentBackend { public: explicit NvapiD3d12Presenter(const NvapiPresenterConfig& cfg); ~NvapiD3d12Presenter() override; bool init() override; void run(double seconds, const DrawFn& draw) override; void stop() override { stopRequested_.store(true); } PresentStats stats() const override; // Idle policy (M3 step 4): false = POWER_OFF + the present loop parks // (no render/present; scanout stops and the firmware takes panels, fan, // and LED down with the video signal). true = re-modeset + POWER_ON and // presenting resumes. Thread-safe; the loop acts at frame boundaries. void requestPanelPower(bool on) { panelPowerOn_.store(on); } // Late-latch pacing (M4 step 3): delay each frame's draw until just // before its flip (next vsync minus an adaptive submit margin) so the // pose is sampled as close to photons as the GPU allows (~13 ms newer at // 75 Hz). Margin tracks the measured submit time and degrades to // today's draw-right-after-vsync behavior under GPU contention. Default // ON; thread-safe live toggle ('l' key A/B). void requestLateLatch(bool on) { lateLatch_.store(on); } bool lateLatchEnabled() const { return lateLatch_.load(); } // SteamVR coexistence (M4): false = release the whole DirectMode // acquisition (scanout surfaces destroyed, display released) so another // runtime can take the headset; the loop parks with zero GPU work and // stats().hmdReleased reads true. true = re-acquire, re-modeset, resume // presenting (retries while the other runtime lets go). Thread-safe; // the loop acts at frame boundaries. Distinct from requestPanelPower, // which only stops scanout but KEEPS the acquisition. void requestHmdOwnership(bool own) { hmdOwned_.store(own); } // Doze: warm shallow-sleep (M5, ADR-0005). true = keep the acquisition AND // the panels powered (NO POWER_OFF — the firmware sweeps the panels dark on // its own via the 'H' display-sleep command), but present one black frame // and then idle the present loop to a slow heartbeat instead of presenting // at mode rate. The DirectMode scanout keeps the VXR/link locked off the // last surface, so wake is the firmware 'h' (~95 ms) with no DSC/link // retrain — distinct from both requestPanelPower (POWER_OFF, 3-5 s wake) and // requestHmdOwnership (full release). false = resume presenting at mode rate. // Thread-safe; the loop acts at frame boundaries. The host must keep panel // power ON and ownership held while dozing. void requestDoze(bool on) { dozing_.store(on); } // Mode actually adopted by init() — may differ from the config request // when the headset's firmware rate toggle restricted the offered list // and init() fell back to the same link format at the offered rate. // Valid after a successful init(). The app re-syncs anything it derived // from the requested rate (capture rate cap). double modeHz() const; // Cheap state peeks for UI (tray menu): unlike stats(), these do NOT // reset the per-read watermark window the status line owns. bool panelsOffNow() const; bool hmdReleasedNow() const; bool dozingNow() const; private: struct Impl; // Acquisition halves of init()/the ownership park: acquireDisplay() = // AcquireDirectModeDisplay + scanout surfaces/RTVs + modeset + POWER_ON // + present waitable; releaseDisplay() drains the queue and undoes it // all. The D3D12 device/queue/fence persist across cycles, so app // render resources survive a SteamVR session untouched. bool acquireDisplay(); void releaseDisplay(); // Single-buffer doze (M5, ADR-0005): during doze only buffer 0 is presented // (seed + heartbeat both target it), so the second ~100 MB scanout surface is // dead VRAM. freeScanoutBuffer(1) drops it on doze entry, createScanoutBuffer(1) // recreates it on wake. The rtvHeap + rtv[] descriptor handles persist across // the cycle; only the surface/resource/RTV contents of the slot turn over. // Floor 275 -> ~175 MB. bool createScanoutBuffer(int i); void freeScanoutBuffer(int i); std::unique_ptr impl_; NvapiPresenterConfig cfg_; std::atomic stopRequested_{false}; std::atomic panelPowerOn_{true}; std::atomic lateLatch_{true}; std::atomic hmdOwned_{true}; std::atomic dozing_{false}; }; } // namespace sauna