// SPDX-License-Identifier: GPL-2.0-or-later #include "VideoCommon/OpenXROpcodeReplay.h" #include #include #include #include #include #include #include "Common/Logging/Log.h" #include "Core/Config/GraphicsSettings.h" #include "VideoCommon/VideoConfig.h" namespace VideoCommon::OpenXROpcodeReplay { namespace { constexpr u64 INVALID_FRAME_INDEX = std::numeric_limits::max(); struct ReplayClearState { bool color_enable = false; bool alpha_enable = false; bool z_enable = false; PixelFormat pixel_format = PixelFormat::RGBA6_Z24; u32 color = 0; u32 clear_z_value = 0; bool valid = false; }; struct ReplayState { std::mutex mutex; // Per-stream mutexes for the capture byte streams: each stream has exactly one producer // (preprocess = CPU thread, main = video thread), so giving each its own lock keeps the // two decoder passes from serializing against each other (or against the scheduling // mutex) during capture frames. Lock order: mutex -> capture_preprocess_mutex -> // capture_main_mutex. std::mutex capture_preprocess_mutex; std::mutex capture_main_mutex; std::vector capture_preprocess; std::vector capture_main; std::vector replay_preprocess; std::vector replay_main; u64 capture_frame_index = INVALID_FRAME_INDEX; u64 scheduled_replay_frame_index = INVALID_FRAME_INDEX; u64 replay_frame_index = INVALID_FRAME_INDEX; u64 last_decided_frame_index = INVALID_FRAME_INDEX; u64 next_frame_index = 0; int scheduled_replay_count = 0; int replay_count = 0; bool capture_enabled = false; bool replaying = false; bool replay_frame_active = false; // Atomic: read per GX command by the opcode decoder (IsReplayLogFrameActive) — taking the // mutex there dominated the video thread. Writers still hold the mutex for the rest of the // state; CaptureCommand re-checks the flag under the mutex, so a stale lock-free read is safe. std::atomic replay_log_frame_active{false}; bool capture_window_armed = false; bool pending_immediate_swap = false; bool pending_immediate_swap_is_frame_boundary = false; u32 pending_xfb_addr = 0; u32 pending_fb_width = 0; u32 pending_fb_stride = 0; u32 pending_fb_height = 0; u32 replay_xfb_count = 0; ReplayClearState clear_states[2]; }; ReplayState s_state; int GetConfiguredInputRefreshRateUnlocked() { switch (g_ActiveConfig.vr_opcode_replay_mode) { case OpenXROpcodeReplayMode::Input25Hz: return 25; case OpenXROpcodeReplayMode::Input30Hz: return 30; case OpenXROpcodeReplayMode::Input50Hz: return 50; case OpenXROpcodeReplayMode::Input60Hz: return 60; case OpenXROpcodeReplayMode::Off: default: return 0; } } int GetConfiguredTargetRefreshRateUnlocked(double display_period_ms) { const int configured_refresh_rate = Config::NormalizeVROpcodeReplayTargetRefreshRate( g_ActiveConfig.vr_opcode_replay_target_refresh_rate); if (configured_refresh_rate == Config::GFX_VR_OPCODE_REPLAY_TARGET_REFRESH_RATE_AUTO) { if (display_period_ms <= 0.0) return 0; const float refresh_rate_hz = static_cast(1000.0 / display_period_ms); return Config::ChooseClosestVRForcedVBIFrequency(refresh_rate_hz); } switch (configured_refresh_rate) { case Config::GFX_VR_OPCODE_REPLAY_TARGET_REFRESH_RATE_72: case Config::GFX_VR_OPCODE_REPLAY_TARGET_REFRESH_RATE_90: case Config::GFX_VR_OPCODE_REPLAY_TARGET_REFRESH_RATE_120: return configured_refresh_rate; default: return 90; } } bool IsConfiguredUnlocked() { return g_ActiveConfig.stereo_mode == StereoMode::OpenXR && GetConfiguredInputRefreshRateUnlocked() > 0; } void ClearUnlocked(ReplayState& state) { // Stop appenders before clearing the capture streams (see CaptureCommand). state.replay_log_frame_active = false; { std::scoped_lock stream_locks(state.capture_preprocess_mutex, state.capture_main_mutex); state.capture_preprocess.clear(); state.capture_main.clear(); } state.replay_preprocess.clear(); state.replay_main.clear(); state.capture_frame_index = INVALID_FRAME_INDEX; state.scheduled_replay_frame_index = INVALID_FRAME_INDEX; state.replay_frame_index = INVALID_FRAME_INDEX; state.last_decided_frame_index = INVALID_FRAME_INDEX; state.next_frame_index = 0; state.scheduled_replay_count = 0; state.replay_count = 0; state.capture_enabled = false; state.replaying = false; state.replay_frame_active = false; state.replay_log_frame_active = false; state.capture_window_armed = false; state.pending_immediate_swap = false; state.pending_immediate_swap_is_frame_boundary = false; state.pending_xfb_addr = 0; state.pending_fb_width = 0; state.pending_fb_stride = 0; state.pending_fb_height = 0; state.replay_xfb_count = 0; state.clear_states[0] = {}; state.clear_states[1] = {}; } int CalculateReplayCountForFrameUnlocked(double display_period_ms, u64 frame_index) { const int input_refresh_rate = GetConfiguredInputRefreshRateUnlocked(); const int target_refresh_rate = GetConfiguredTargetRefreshRateUnlocked(display_period_ms); if (input_refresh_rate <= 0 || target_refresh_rate <= input_refresh_rate) return 0; const double target_period_ms = 1000.0 / static_cast(target_refresh_rate); constexpr double tolerance_ms = 0.8; if (!(display_period_ms > 0.0 && std::abs(display_period_ms - target_period_ms) <= tolerance_ms)) { return 0; } const u64 start_slot = (frame_index * static_cast(target_refresh_rate)) / static_cast(input_refresh_rate); const u64 end_slot = ((frame_index + 1) * static_cast(target_refresh_rate)) / static_cast(input_refresh_rate); if (end_slot <= start_slot) return 0; return std::max(0, static_cast(end_slot - start_slot) - 1); } } // namespace bool IsCaptureEnabled() { std::scoped_lock lock(s_state.mutex); s_state.capture_enabled = IsConfiguredUnlocked(); return s_state.capture_enabled; } bool IsReplaying() { std::scoped_lock lock(s_state.mutex); return s_state.replaying; } bool IsReplayFrameActive() { std::scoped_lock lock(s_state.mutex); return s_state.replay_frame_active; } bool IsReplayLogFrameActive() { // Lock-free: called per GX command from the opcode decoder (see ReplayState). return s_state.replay_log_frame_active.load(std::memory_order_relaxed); } bool IsCaptureArmed() { std::scoped_lock lock(s_state.mutex); return s_state.capture_window_armed; } u64 GetCaptureFrameIndex() { std::scoped_lock lock(s_state.mutex); return s_state.capture_frame_index; } void EnableCaptureForNextFrame(double display_period_ms) { std::scoped_lock lock(s_state.mutex); s_state.capture_enabled = IsConfiguredUnlocked(); const int scheduled_replay_count = s_state.capture_enabled && !s_state.replaying ? CalculateReplayCountForFrameUnlocked(display_period_ms, s_state.next_frame_index) : 0; const bool set_active = scheduled_replay_count > 0; // Stop appenders, then reset the streams under their mutexes before (re-)arming. s_state.replay_log_frame_active = false; { std::scoped_lock stream_locks(s_state.capture_preprocess_mutex, s_state.capture_main_mutex); s_state.capture_preprocess.clear(); s_state.capture_main.clear(); if (set_active) { // One-time growth insurance: keep the append path memcpy-bound from the first // capture frame (capacity persists across clear()). constexpr size_t initial_capacity = 1024 * 1024; if (s_state.capture_preprocess.capacity() < initial_capacity) s_state.capture_preprocess.reserve(initial_capacity); if (s_state.capture_main.capacity() < initial_capacity) s_state.capture_main.reserve(initial_capacity); } } if (set_active) { s_state.capture_frame_index = s_state.next_frame_index; s_state.scheduled_replay_frame_index = s_state.next_frame_index; s_state.scheduled_replay_count = scheduled_replay_count; s_state.replay_log_frame_active = true; s_state.capture_window_armed = true; } else { s_state.capture_window_armed = false; s_state.capture_frame_index = INVALID_FRAME_INDEX; s_state.scheduled_replay_frame_index = INVALID_FRAME_INDEX; s_state.scheduled_replay_count = 0; } } void CaptureCommand(bool is_preprocess, const u8* data, u32 size) { if (data == nullptr || size == 0) return; // Hot path: one call per GX command (from both decoder passes) during capture frames. // Take only this stream's mutex — the other pass and the scheduling mutex are untouched. // Config/replaying state is enforced by the arm/disarm code: replay_log_frame_active is // only true inside a valid capture window, and the frame-boundary code clears it before // taking the stream mutexes, so re-checking it here under the stream mutex is sufficient. auto& stream_mutex = is_preprocess ? s_state.capture_preprocess_mutex : s_state.capture_main_mutex; auto& stream = is_preprocess ? s_state.capture_preprocess : s_state.capture_main; std::scoped_lock lock(stream_mutex); if (!s_state.replay_log_frame_active.load(std::memory_order_acquire)) return; stream.insert(stream.end(), data, data + size); } void NotifyFrameBoundary() { std::scoped_lock lock(s_state.mutex); if (s_state.replaying) { WARN_LOG_FMT(VIDEO, "OpcodeReplay NotifyFrameBoundary: early-out (replaying=true)"); return; } s_state.capture_enabled = IsConfiguredUnlocked(); if (!s_state.capture_enabled) { ClearUnlocked(s_state); return; } if (!s_state.capture_window_armed) { s_state.next_frame_index++; return; } // Close the capture window before touching the streams: appenders re-check the flag // under their stream mutex, so once we hold both stream mutexes no new bytes can land. s_state.replay_log_frame_active = false; std::scoped_lock stream_locks(s_state.capture_preprocess_mutex, s_state.capture_main_mutex); if (s_state.capture_frame_index != s_state.scheduled_replay_frame_index || s_state.capture_frame_index != s_state.next_frame_index) { WARN_LOG_FMT(VIDEO, "OpcodeReplay NotifyFrameBoundary: capture scheduling mismatch capture_idx={} " "scheduled_idx={} expected_idx={}", s_state.capture_frame_index, s_state.scheduled_replay_frame_index, s_state.next_frame_index); s_state.capture_preprocess.clear(); s_state.capture_main.clear(); s_state.replay_preprocess.clear(); s_state.replay_main.clear(); s_state.replay_frame_index = INVALID_FRAME_INDEX; s_state.replay_count = 0; s_state.capture_window_armed = false; s_state.capture_frame_index = INVALID_FRAME_INDEX; s_state.scheduled_replay_frame_index = INVALID_FRAME_INDEX; s_state.scheduled_replay_count = 0; s_state.next_frame_index++; return; } const size_t cap_pre = s_state.capture_preprocess.size(); const size_t cap_main = s_state.capture_main.size(); if (cap_pre == 0 && cap_main == 0) { static int empty_count = 0; if (++empty_count % 30 == 1) { WARN_LOG_FMT(VIDEO, "OpcodeReplay NotifyFrameBoundary: EMPTY capture (count={}) " "replay_log_frame_active={} capture_enabled={} capture_idx={}", empty_count, s_state.replay_log_frame_active.load(std::memory_order_relaxed), s_state.capture_enabled, s_state.capture_frame_index); } s_state.replay_preprocess.clear(); s_state.replay_main.clear(); s_state.replay_frame_index = INVALID_FRAME_INDEX; s_state.replay_count = 0; s_state.replay_log_frame_active = false; s_state.capture_window_armed = false; s_state.capture_frame_index = INVALID_FRAME_INDEX; s_state.scheduled_replay_frame_index = INVALID_FRAME_INDEX; s_state.scheduled_replay_count = 0; s_state.next_frame_index++; return; } s_state.replay_preprocess = std::move(s_state.capture_preprocess); s_state.replay_main = std::move(s_state.capture_main); s_state.capture_preprocess.clear(); s_state.capture_main.clear(); s_state.replay_frame_index = s_state.capture_frame_index; s_state.replay_count = s_state.scheduled_replay_count; s_state.next_frame_index = s_state.replay_frame_index + 1; s_state.replay_log_frame_active = false; s_state.capture_window_armed = false; s_state.capture_frame_index = INVALID_FRAME_INDEX; s_state.scheduled_replay_frame_index = INVALID_FRAME_INDEX; s_state.scheduled_replay_count = 0; static_cast(cap_pre); static_cast(cap_main); } void Clear() { std::scoped_lock lock(s_state.mutex); ClearUnlocked(s_state); } bool HasReplayData() { std::scoped_lock lock(s_state.mutex); return s_state.replay_frame_index != INVALID_FRAME_INDEX && (!s_state.replay_preprocess.empty() || !s_state.replay_main.empty()); } u64 GetReplayFrameIndex() { std::scoped_lock lock(s_state.mutex); return s_state.replay_frame_index; } int GetReplayCount(double display_period_ms) { std::scoped_lock lock(s_state.mutex); static_cast(display_period_ms); s_state.capture_enabled = IsConfiguredUnlocked(); if (!s_state.capture_enabled || s_state.replaying || s_state.replay_frame_index == INVALID_FRAME_INDEX || s_state.replay_count <= 0 || (s_state.replay_preprocess.empty() && s_state.replay_main.empty())) { return 0; } if (s_state.last_decided_frame_index == s_state.replay_frame_index) return 0; s_state.last_decided_frame_index = s_state.replay_frame_index; return s_state.replay_count; } bool BeginReplayIteration() { std::scoped_lock lock(s_state.mutex); if (s_state.replaying || s_state.replay_frame_index == INVALID_FRAME_INDEX || (s_state.replay_preprocess.empty() && s_state.replay_main.empty())) return false; s_state.replaying = true; s_state.capture_enabled = IsConfiguredUnlocked(); s_state.replay_frame_active = true; s_state.replay_log_frame_active = false; s_state.pending_immediate_swap = false; s_state.pending_immediate_swap_is_frame_boundary = false; s_state.pending_xfb_addr = 0; s_state.pending_fb_width = 0; s_state.pending_fb_stride = 0; s_state.pending_fb_height = 0; s_state.replay_xfb_count = 0; return true; } void EndReplayIteration() { std::scoped_lock lock(s_state.mutex); s_state.replaying = false; s_state.replay_frame_active = false; s_state.replay_log_frame_active = false; s_state.pending_immediate_swap = false; s_state.pending_immediate_swap_is_frame_boundary = false; s_state.pending_xfb_addr = 0; s_state.pending_fb_width = 0; s_state.pending_fb_stride = 0; s_state.pending_fb_height = 0; s_state.replay_xfb_count = 0; } void DiscardReplayFrame() { std::scoped_lock lock(s_state.mutex); const u64 discarded_frame_index = s_state.replay_frame_index; s_state.replay_preprocess.clear(); s_state.replay_main.clear(); s_state.replay_frame_index = INVALID_FRAME_INDEX; s_state.replay_count = 0; s_state.pending_immediate_swap = false; s_state.pending_immediate_swap_is_frame_boundary = false; s_state.pending_xfb_addr = 0; s_state.pending_fb_width = 0; s_state.pending_fb_stride = 0; s_state.pending_fb_height = 0; s_state.replay_xfb_count = 0; static_cast(discarded_frame_index); } void ApplyReplayClearState(bool frame_just_rendered, bool* color_enable, bool* alpha_enable, bool* z_enable, PixelFormat* pixel_format, u32* color, u32* clear_z_value) { std::scoped_lock lock(s_state.mutex); s_state.capture_enabled = IsConfiguredUnlocked(); if (!s_state.capture_enabled || color_enable == nullptr || alpha_enable == nullptr || z_enable == nullptr || pixel_format == nullptr || color == nullptr || clear_z_value == nullptr) return; ReplayClearState current = { .color_enable = *color_enable, .alpha_enable = *alpha_enable, .z_enable = *z_enable, .pixel_format = *pixel_format, .color = *color, .clear_z_value = *clear_z_value, .valid = true, }; if (s_state.replay_frame_active && frame_just_rendered) { s_state.clear_states[0] = current; return; } if (!s_state.replay_frame_active && !frame_just_rendered) { s_state.clear_states[1] = current; return; } const ReplayClearState& selected = !s_state.replay_frame_active && frame_just_rendered ? s_state.clear_states[0] : s_state.clear_states[1]; if (!selected.valid) return; *color_enable = selected.color_enable; *alpha_enable = selected.alpha_enable; *z_enable = selected.z_enable; *pixel_format = selected.pixel_format; *color = selected.color; *clear_z_value = selected.clear_z_value; } void RecordReplayImmediateSwap(u32 xfb_addr, u32 fb_width, u32 fb_stride, u32 fb_height, bool frame_just_rendered) { std::scoped_lock lock(s_state.mutex); if (!s_state.replaying) return; s_state.replay_xfb_count++; if (s_state.pending_immediate_swap && (s_state.pending_immediate_swap_is_frame_boundary || !frame_just_rendered)) { return; } s_state.pending_immediate_swap = true; s_state.pending_immediate_swap_is_frame_boundary = frame_just_rendered; s_state.pending_xfb_addr = xfb_addr; s_state.pending_fb_width = fb_width; s_state.pending_fb_stride = fb_stride; s_state.pending_fb_height = fb_height; } bool ConsumeReplayImmediateSwap(u32* xfb_addr, u32* fb_width, u32* fb_stride, u32* fb_height) { std::scoped_lock lock(s_state.mutex); if (!s_state.replaying || !s_state.pending_immediate_swap) return false; if (xfb_addr) *xfb_addr = s_state.pending_xfb_addr; if (fb_width) *fb_width = s_state.pending_fb_width; if (fb_stride) *fb_stride = s_state.pending_fb_stride; if (fb_height) *fb_height = s_state.pending_fb_height; s_state.pending_immediate_swap = false; s_state.pending_immediate_swap_is_frame_boundary = false; s_state.pending_xfb_addr = 0; s_state.pending_fb_width = 0; s_state.pending_fb_stride = 0; s_state.pending_fb_height = 0; return true; } u32 GetReplayXFBCount() { std::scoped_lock lock(s_state.mutex); return s_state.replay_xfb_count; } std::span GetReplayCommands(bool is_preprocess) { std::scoped_lock lock(s_state.mutex); const auto& stream = is_preprocess ? s_state.replay_preprocess : s_state.replay_main; return std::span(stream.data(), stream.size()); } } // namespace VideoCommon::OpenXROpcodeReplay