// The render-feed pacing policy for the audio mirror, factored out of AudioMirror so it can // be unit-tested against synthetic producer cadences (tests/render_pacer_test.cpp) -- the same // "reuse the shipping logic in a headless test" approach as rate_estimator. // // The mirror consumes a ring the injected hook fills (the game's render frames) and re-renders // it to the output device. Producer and consumer run on independent threads/clocks, so the ring // level jitters. The pacing rule: // 1. Build a cushion (prime_frames) before the first write, so brief producer hiccups don't // immediately starve the device. // 2. Each device tick, write whatever is available (a partial fill is fine -- WASAPI keeps // playing the already-buffered audio; we just top it up next tick). // 3. Re-prime (rebuild the cushion) ONLY on a genuine starvation: the device buffer fully // drained AND the ring is empty. Crucially, do NOT re-prime on a mere partial fill. // // Rule 3 is the whole point. The original code re-primed whenever it couldn't completely fill // the free buffer space that tick (`to_write < avail`); that withholds the feed until ~30 ms // has rebuffered, which DRAINS the device and manufactures the very ~30 ms silence gap it meant // to avoid -- turning a one-frame ring dip into a full drop-out. On a jittery game that fired // constantly, producing the choppy / "metallic" mirror audio. coop_audio_validate quantifies it. #pragma once #include #include namespace coop { struct RenderPacer { std::uint32_t prime_frames = 0; // cushion to (re)build before playback resumes bool primed = false; // Decide how many frames to write into the device buffer this tick. // avail = free space in the device buffer (render_frames - padding) // have = frames currently available in the ring // padding = frames still queued in the device buffer (0 = it has drained / under-run) // Returns the frame count to write (0 while still priming or when the ring is empty). std::uint32_t pump(std::uint32_t avail, std::uint32_t have, std::uint32_t padding) { if (!primed && have >= prime_frames) { primed = true; } if (!primed) { return 0; // still building the initial / post-starvation cushion } const std::uint32_t to_write = std::min(avail, have); // Genuine starvation only: the device emptied and the ring has nothing to give. // A partial fill (have < avail) is normal jitter and must NOT trigger a re-prime. if (padding == 0 && have == 0) { primed = false; } return to_write; } void reset() { primed = false; } }; } // namespace coop