/** ****************************************************************************** * Xenia : Xbox 360 Emulator Research Project * ****************************************************************************** * Copyright 2026 Ben Vanik. All rights reserved. * * Released under the BSD license - see LICENSE in the root for more details. * ****************************************************************************** */ #ifndef XENIA_APU_AUDIO_WATCHDOG_H_ #define XENIA_APU_AUDIO_WATCHDOG_H_ #include #include // Read-only diagnostics for "audio stops mid-mission and never recovers". // // The audio pipeline has four stages, each of which can die independently: // // guest XAudio callback -> SubmitFrame -> ring buffer -> ALSA write // (guest_pumps) (frames_submitted) (alsa_writes) // XMA decoding feeds the guest (xma_works) // // Each stage bumps a counter here. The watchdog thread in audio_system.cc // samples them once a second and, when audio dies, reports which stage stopped // advancing -- turning "no sound" into a named culprit. // // Note the APU worker runs the guest's audio callback IN-LINE // (AudioSystem::WorkerThreadMain -> processor_->Execute). If that guest call // ever blocks, pumping stops forever and audio can never recover, so we also // track how long we have been inside guest code. // // Everything here is counters only: cvar-gated (--audio_watchdog), no // behaviour change. namespace xe { namespace apu { namespace watchdog { // Pumps of the guest XAudio callback (AudioSystem worker). inline std::atomic guest_pumps{0}; // steady_clock microseconds at which the worker entered guest code; 0 = not // currently inside the guest callback. inline std::atomic in_guest_callback_since_us{0}; // Frames the guest handed to the driver (AudioSystem::SubmitFrame). inline std::atomic frames_submitted{0}; // XMA decoder Work() calls that actually decoded something. inline std::atomic xma_works{0}; // ALSA: periods of real guest audio written, silence keepalive periods // written, and underruns recovered. inline std::atomic alsa_writes{0}; inline std::atomic alsa_silence{0}; inline std::atomic alsa_xruns{0}; // Last observed snd_pcm_state_t (-1 = unknown / driver not ALSA). inline std::atomic alsa_state{-1}; // Why is the APU worker not pumping? Three very different causes look the same // from outside the process, so distinguish them here: // worker_loops == 0 && clients_in_use == 0 -> no client (guest unregistered) // worker_loops == 0 && clients_in_use > 0 -> parked despite a client (pacing) // worker_loops > 0 && pump_skips > 0 -> looping, but no free output slot inline std::atomic worker_loops{0}; inline std::atomic pump_skips{0}; inline std::atomic clients_in_use{-1}; inline std::atomic register_calls{0}; inline std::atomic unregister_calls{0}; // Exactly where the APU worker thread is sitting. A parked worker and a worker // blocked on the kernel global lock are indistinguishable from outside the // process (both are futex waits with zero context switches), so record it. enum class WorkerPhase : int { kStart = 0, kAcquiringGlobalLock, // blocked here => a kernel lock is held elsewhere kParkedNoClient, // blocked here => guest has no audio client kPacingSleep, // normal: waiting for the next 5.33ms deadline kInGuestCallback, // normal: running the game's mixer kSubmitting, }; inline std::atomic worker_phase{WorkerPhase::kStart}; inline const char* WorkerPhaseName(WorkerPhase p) { switch (p) { case WorkerPhase::kStart: return "start"; case WorkerPhase::kAcquiringGlobalLock: return "ACQUIRING-GLOBAL-LOCK"; case WorkerPhase::kParkedNoClient: return "PARKED-NO-CLIENT"; case WorkerPhase::kPacingSleep: return "pacing"; case WorkerPhase::kInGuestCallback: return "in-guest-callback"; case WorkerPhase::kSubmitting: return "submitting"; default: return "?"; } } } // namespace watchdog } // namespace apu } // namespace xe #endif // XENIA_APU_AUDIO_WATCHDOG_H_