// Present backend seam (PLAN.md locked decision 2): per-platform backends // behind one interface. v1 Windows = D3D12 + NVAPI DirectMode; Linux rung // (Vulkan + DRM lease) later. M1 shape: backend owns acquire/modeset/pacing, // app draws each frame through a callback. #pragma once #include #include namespace sauna { struct FrameContext { void* cmdList; // backend-native command list (D3D12: ID3D12GraphicsCommandList*) void* rtv; // backend-native render target view (D3D12: D3D12_CPU_DESCRIPTOR_HANDLE*) uint32_t width, height; // full stereo surface, left eye = left half uint32_t rtvFormat; // backend-native format of rtv (D3D12: DXGI_FORMAT) double timeSec; // seconds since present loop start uint64_t frameIndex; // Predicted photon time for THIS frame (steady_clock epoch, ns; 0 = // unknown — no vsync reference yet, free-running, or no waitable). M4 // step 3: last observed vsync + one period (the flip this frame catches) // + half a period (mid-scanout). Pose prediction spans to this instant // instead of a fixed guess. int64_t photonTimeNs; }; struct PresentStats { uint64_t frames = 0; uint64_t presentErrors = 0; uint64_t waitTimeouts = 0; double avgFps = 0.0; bool freeRunning = false; // vsync lost (doffed headset) — throttled uint64_t freeRunEvents = 0; // distinct free-run episodes bool displayLost = false; // device removed / waitable dead — loop exited bool panelsOff = false; // idle-policy power-down active (M3 step 4) bool hmdReleased = false; // DirectMode acquisition released (SteamVR) bool dozing = false; // warm shallow-sleep present-idle (M5/ADR-0005): // acquisition + scanout held, present idled to a // heartbeat, firmware drives the panels dark // Max frame-to-frame interval since the last stats() read (ms, reset on // read). Rubber-band diagnosis: a present/GPU stall (driver hitch, GPU // P-state ramp, missed vsyncs) spikes here; a clean 75 Hz run sits ~13. double maxFrameIntervalMs = 0.0; // Split of the above (same window/reset): time blocked on the render // fence (GPU still executing our work — GPU contention/overload) vs // time blocked on the present waitable (display link stall — vsync // missing: DSC retrain, firmware dim cycle, panel power transition). double maxFenceWaitMs = 0.0; double maxVsyncWaitMs = 0.0; // Finer split of the remaining loop time (same window/reset). When // frame= spikes while gpu= and vsw= stay small the stall is CPU-side — // these name it: latch-timer oversleep past its target (OS timer // coalescing / power-save scheduling), draw-callback CPU (command // recording + capture consumer), and the DirectMode present call // itself (driver CPU block). All three small while frame= still // spikes => the loop thread was preempted between sections (process // starvation / power throttling). double maxLatchOverMs = 0.0; double maxDrawCpuMs = 0.0; double maxPresentCpuMs = 0.0; // Current GPU graphics-domain clock (MHz, 0 = unavailable), sampled at // the stats() read. P-state triage companion to the gpu= split: gpu= // ballooning at FULL clock = real contention (another app's GPU load); // at idle clock = the P-state downclock is back despite the DRS pin. int gpuCoreClockMHz = 0; }; using DrawFn = std::function; class PresentBackend { public: virtual ~PresentBackend() = default; // Acquire display, modeset, power on. False = no display / acquire failed. virtual bool init() = 0; // Run the paced present loop until stop() or seconds elapse (0 = forever). // draw is called once per frame on the calling thread. virtual void run(double seconds, const DrawFn& draw) = 0; virtual void stop() = 0; virtual PresentStats stats() const = 0; }; } // namespace sauna