// SPDX-FileCopyrightText: 2019-2026 Connor McLaughlin // SPDX-License-Identifier: CC-BY-NC-ND-4.0 #include "video_thread.h" #include "core.h" #include "fullscreenui.h" #include "gpu_backend.h" #include "gpu_hw_texture_cache.h" #include "gpu_types.h" #include "host.h" #include "imgui_overlays.h" #include "performance_counters.h" #include "settings.h" #include "shader_cache_version.h" #include "system.h" #include "system_private.h" #include "video_presenter.h" #include "video_thread_commands.h" #include "video_thread_private.h" #include "util/gpu_device.h" #include "util/imgui_manager.h" #include "util/input_manager.h" #include "util/postprocessing.h" #include "util/state_wrapper.h" #include "util/translation.h" #include "common/align.h" #include "common/error.h" #include "common/log.h" #include "common/threading.h" #include "common/timer.h" #include "IconsEmoji.h" #include "IconsFontAwesome.h" #include "fmt/format.h" #include "imgui.h" #include LOG_CHANNEL(VideoThread); namespace VideoThread { enum : u32 { COMMAND_QUEUE_SIZE = 16 * 1024 * 1024, COMMAND_QUEUE_RESERVED_SPACE = VideoThreadCommand::AlignCommandSize(1), THRESHOLD_TO_WAKE_GPU = 65536, }; static constexpr s32 THREAD_WAKE_COUNT_CPU_THREAD_IS_WAITING = 0x40000000; // CPU thread needs waking static constexpr s32 THREAD_WAKE_COUNT_SLEEPING = -1; // Use a slightly longer spin time on ARM64 due to power management. #ifndef _M_ARM64 static constexpr u32 THREAD_SPIN_TIME_US = 50; #else static constexpr u32 THREAD_SPIN_TIME_US = 200; #endif static bool Reconfigure(std::optional renderer, bool upload_vram, std::optional fullscreen, std::optional start_fullscreen_ui, bool recreate_device, Error* error); // NOTE: Use with care! The handler needs to manually run the destructor. template T* AllocateCommand(u32 size, VideoThreadCommandType type, Args... args); template T* AllocateCommand(VideoThreadCommandType type, Args... args); static u32 GetPendingCommandSize(); static void ResetCommandFIFO(); static bool IsCommandFIFOEmpty(); static void WakeThread(); static void WakeThreadIfSleeping(); static bool SleepThread(bool allow_sleep); static void VideoThreadEntryPoint(); static bool CreateDeviceOnThread(RenderAPI api, bool fullscreen, bool start_fullscreen_ui, bool preserve_imgui_on_failure, Error* error); static void DestroyDeviceOnThread(bool preserve_imgui_state); static void ResizeRenderWindowOnThread(u32 width, u32 height, float scale, float refresh_rate); static void RecreateRenderWindowOnThread(bool fullscreen, bool allow_exclusive_fullscreen); static void RenderWindowResizedOnThread(); static bool CheckExclusiveFullscreenOnThread(); static void ReconfigureOnThread(VideoThreadReconfigureCommand* cmd); static bool CreateGPUBackendOnThread(bool hardware_renderer, bool upload_vram, const GPUSettings* old_settings, GPURenderer* out_renderer, Error* error); static void DestroyGPUBackendOnThread(); static void DestroyGPUPresenterOnThread(); static void SetThreadEnabled(bool enabled); static void UpdateSettingsOnThread(GPUSettings&& new_settings); static void UpdateGameInfoOnThread(VideoThreadUpdateGameInfoCommand* cmd); static void GameInfoChanged(bool serial_changed); static void ClearGameInfoOnThread(); static void UpdateRunIdle(); namespace { struct ALIGN_TO_CACHE_LINE State { // Owned by CPU thread. Timer::Value thread_spin_time = 0; Threading::Thread thread; Common::unique_aligned_ptr command_fifo_data; WindowInfo render_window_info; std::optional requested_renderer; bool requested_fullscreen_ui = false; bool use_thread = false; bool fullscreen_state = false; // Hot variables between both threads. ALIGN_TO_CACHE_LINE std::atomic command_fifo_write_ptr{0}; std::atomic thread_wake_count{0}; // <0 = sleeping, >= 0 = has work Threading::KernelSemaphore thread_wake_semaphore; Threading::KernelSemaphore thread_is_done_semaphore; // Owned by GPU thread. ALIGN_TO_CACHE_LINE Common::unique_aligned_ptr gpu_backend; std::atomic command_fifo_read_ptr{0}; u8 run_idle_reasons = 0; bool run_idle_flag = false; GPUVSyncMode requested_vsync = GPUVSyncMode::Disabled; std::string game_title; std::string game_serial; std::string game_path; GameHash game_hash = 0; }; } // namespace static State s_state; } // namespace VideoThread const Threading::ThreadHandle& VideoThread::GetThreadHandle() { return s_state.thread; } void VideoThread::ResetCommandFIFO() { Assert(!s_state.run_idle_flag && s_state.command_fifo_read_ptr.load(std::memory_order_acquire) == s_state.command_fifo_write_ptr.load(std::memory_order_relaxed)); s_state.command_fifo_write_ptr.store(0, std::memory_order_release); s_state.command_fifo_read_ptr.store(0, std::memory_order_release); } void VideoThread::ProcessStartup() { s_state.thread_spin_time = Timer::ConvertNanosecondsToValue(THREAD_SPIN_TIME_US * 1000.0); s_state.command_fifo_data = Common::make_unique_aligned_for_overwrite(HOST_CACHE_LINE_SIZE, COMMAND_QUEUE_SIZE); s_state.use_thread = g_settings.gpu_use_thread; s_state.run_idle_reasons = static_cast(RunIdleReason::NoGPUBackend); // Thread is always started/persists regardless of whether it is used or not. s_state.thread.Start(&VideoThread::VideoThreadEntryPoint); } void VideoThread::ProcessShutdown() { INFO_LOG("Shutting down video thread..."); SyncThread(false); // Thread must be enabled to shut it down. SetThreadEnabled(true); PushCommandAndWakeThread(AllocateCommand(VideoThreadCommandType::Shutdown, sizeof(VideoThreadCommand))); s_state.thread.Join(); INFO_LOG("Video thread shutdown complete."); } VideoThreadCommand* VideoThread::AllocateCommand(VideoThreadCommandType command, u32 size) { DebugAssert(Host::IsOnCoreThread()); size = VideoThreadCommand::AlignCommandSize(size); DebugAssert(size > 0 && size <= (COMMAND_QUEUE_SIZE - COMMAND_QUEUE_RESERVED_SPACE)); for (;;) { u32 read_ptr = s_state.command_fifo_read_ptr.load(std::memory_order_acquire); u32 write_ptr = s_state.command_fifo_write_ptr.load(std::memory_order_relaxed); if (read_ptr > write_ptr) [[unlikely]] { u32 available_size = read_ptr - write_ptr; while (available_size <= size) { WakeThreadIfSleeping(); read_ptr = s_state.command_fifo_read_ptr.load(std::memory_order_acquire); available_size = (read_ptr > write_ptr) ? (read_ptr - write_ptr) : (COMMAND_QUEUE_SIZE - write_ptr); } } else { const u32 available_size = COMMAND_QUEUE_SIZE - write_ptr; if (size >= available_size) [[unlikely]] { // Can't wrap around until the video thread has at least started processing commands... if (read_ptr == 0) [[unlikely]] { DEV_LOG("Buffer full and unprocessed, spinning"); do { WakeThreadIfSleeping(); read_ptr = s_state.command_fifo_read_ptr.load(std::memory_order_acquire); } while (read_ptr == 0); } // allocate a dummy command to wrap the buffer around VideoThreadCommand* dummy_cmd = reinterpret_cast(&s_state.command_fifo_data[write_ptr]); dummy_cmd->type = VideoThreadCommandType::Wraparound; dummy_cmd->size = available_size; s_state.command_fifo_write_ptr.store(0, std::memory_order_release); continue; } } VideoThreadCommand* cmd = reinterpret_cast(&s_state.command_fifo_data[write_ptr]); cmd->type = command; cmd->size = size; return cmd; } } template T* VideoThread::AllocateCommand(u32 size, VideoThreadCommandType type, Args... args) { VideoThreadCommand* cmd = AllocateCommand(type, size); const u32 alloc_size = cmd->size; new (cmd) T(std::forward(args)...); // constructor may overwrite the fields, need to reset them cmd->type = type; cmd->size = alloc_size; return static_cast(cmd); } template T* VideoThread::AllocateCommand(VideoThreadCommandType type, Args... args) { return AllocateCommand(sizeof(T), type, std::forward(args)...); } u32 VideoThread::GetPendingCommandSize() { const u32 read_ptr = s_state.command_fifo_read_ptr.load(std::memory_order_acquire); const u32 write_ptr = s_state.command_fifo_write_ptr.load(std::memory_order_relaxed); return (write_ptr >= read_ptr) ? (write_ptr - read_ptr) : (COMMAND_QUEUE_SIZE - read_ptr + write_ptr); } bool VideoThread::IsCommandFIFOEmpty() { const u32 read_ptr = s_state.command_fifo_read_ptr.load(std::memory_order_acquire); const u32 write_ptr = s_state.command_fifo_write_ptr.load(std::memory_order_relaxed); return (read_ptr == write_ptr); } void VideoThread::PushCommand(VideoThreadCommand* cmd) { DebugAssert(Host::IsOnCoreThread()); if (!s_state.use_thread) [[unlikely]] { DebugAssert(s_state.gpu_backend); s_state.gpu_backend->HandleCommand(cmd); return; } const u32 new_write_ptr = s_state.command_fifo_write_ptr.load(std::memory_order_relaxed) + cmd->size; DebugAssert(new_write_ptr <= COMMAND_QUEUE_SIZE); s_state.command_fifo_write_ptr.store(new_write_ptr % COMMAND_QUEUE_SIZE, std::memory_order_release); if (GetPendingCommandSize() >= THRESHOLD_TO_WAKE_GPU) // TODO:FIXME: maybe purge this? WakeThread(); } void VideoThread::PushCommandAndWakeThread(VideoThreadCommand* cmd) { DebugAssert(Host::IsOnCoreThread()); if (!s_state.use_thread) [[unlikely]] { DebugAssert(s_state.gpu_backend); s_state.gpu_backend->HandleCommand(cmd); return; } const u32 new_write_ptr = s_state.command_fifo_write_ptr.load(std::memory_order_relaxed) + cmd->size; DebugAssert(new_write_ptr <= COMMAND_QUEUE_SIZE); s_state.command_fifo_write_ptr.store(new_write_ptr % COMMAND_QUEUE_SIZE, std::memory_order_release); WakeThread(); } void VideoThread::PushCommandAndSync(VideoThreadCommand* cmd, bool spin) { DebugAssert(Host::IsOnCoreThread()); if (!s_state.use_thread) [[unlikely]] { DebugAssert(s_state.gpu_backend); s_state.gpu_backend->HandleCommand(cmd); return; } const u32 new_write_ptr = s_state.command_fifo_write_ptr.load(std::memory_order_relaxed) + cmd->size; DebugAssert(new_write_ptr <= COMMAND_QUEUE_SIZE); s_state.command_fifo_write_ptr.store(new_write_ptr % COMMAND_QUEUE_SIZE, std::memory_order_release); WakeThread(); SyncThread(spin); } ALWAYS_INLINE s32 GetThreadWakeCount(s32 state) { return (state & ~VideoThread::THREAD_WAKE_COUNT_CPU_THREAD_IS_WAITING); } void VideoThread::WakeThread() { // If sleeping, state will be <0, otherwise this will increment the pending work count. // We add 2 so that there's a positive work count if we were sleeping, otherwise the thread would go to sleep. if (s_state.thread_wake_count.fetch_add(2, std::memory_order_release) < 0) s_state.thread_wake_semaphore.Post(); } ALWAYS_INLINE_RELEASE void VideoThread::WakeThreadIfSleeping() { if (GetThreadWakeCount(s_state.thread_wake_count.load(std::memory_order_acquire)) < 0) { if (IsCommandFIFOEmpty()) return; WakeThread(); } } void VideoThread::SyncThread(bool spin) { DebugAssert(Host::IsOnCoreThread()); if (!s_state.use_thread) return; if (spin) { // Check if the video thread is done/sleeping. if (GetThreadWakeCount(s_state.thread_wake_count.load(std::memory_order_acquire)) < 0) { if (IsCommandFIFOEmpty()) return; WakeThread(); } const Timer::Value start_time = Timer::GetCurrentValue(); Timer::Value current_time = start_time; do { // Check if the video thread is done/sleeping. if (GetThreadWakeCount(s_state.thread_wake_count.load(std::memory_order_acquire)) < 0) { if (IsCommandFIFOEmpty()) return; WakeThread(); continue; } // Hopefully ought to be enough. MultiPause(); current_time = Timer::GetCurrentValue(); } while ((current_time - start_time) < s_state.thread_spin_time); } // s_thread_wake_count |= THREAD_WAKE_COUNT_CPU_THREAD_IS_WAITING if not zero s32 value; do { // Check if the video thread is done/sleeping. value = s_state.thread_wake_count.load(std::memory_order_acquire); if (GetThreadWakeCount(value) < 0) { if (IsCommandFIFOEmpty()) return; WakeThread(); continue; } } while (!s_state.thread_wake_count.compare_exchange_weak(value, value | THREAD_WAKE_COUNT_CPU_THREAD_IS_WAITING, std::memory_order_acq_rel, std::memory_order_relaxed)); s_state.thread_is_done_semaphore.Wait(); } bool VideoThread::SleepThread(bool allow_sleep) { DebugAssert(!allow_sleep || s_state.thread_wake_count.load(std::memory_order_relaxed) >= 0); for (;;) { // Acknowledge any work that has been queued, but preserve the waiting flag if there is any, since we're not done // yet. s32 old_state, new_state; do { old_state = s_state.thread_wake_count.load(std::memory_order_relaxed); new_state = (GetThreadWakeCount(old_state) > 0) ? (old_state & THREAD_WAKE_COUNT_CPU_THREAD_IS_WAITING) : (allow_sleep ? THREAD_WAKE_COUNT_SLEEPING : 0); } while (!s_state.thread_wake_count.compare_exchange_weak(old_state, new_state, std::memory_order_acq_rel, std::memory_order_relaxed)); // Are we not done yet? if (GetThreadWakeCount(old_state) > 0) return true; // We're done, so wake the CPU thread if it's waiting. if (old_state & THREAD_WAKE_COUNT_CPU_THREAD_IS_WAITING) s_state.thread_is_done_semaphore.Post(); // Sleep until more work is queued. if (allow_sleep) s_state.thread_wake_semaphore.Wait(); else return false; } } void VideoThread::VideoThreadEntryPoint() { Threading::SetNameOfCurrentThread("Video Thread"); // Take a local copy of the FIFO, that way it's not ping-ponging between the threads. u8* const command_fifo_data = s_state.command_fifo_data.get(); for (;;) { u32 write_ptr = s_state.command_fifo_write_ptr.load(std::memory_order_acquire); u32 read_ptr = s_state.command_fifo_read_ptr.load(std::memory_order_relaxed); if (read_ptr == write_ptr) { if (SleepThread(!s_state.run_idle_flag)) { // sleep => wake, need to reload pointers continue; } else { DoRunIdle(); continue; } } write_ptr = (write_ptr < read_ptr) ? COMMAND_QUEUE_SIZE : write_ptr; while (read_ptr < write_ptr) { VideoThreadCommand* cmd = reinterpret_cast(&command_fifo_data[read_ptr]); DebugAssert((read_ptr + cmd->size) <= COMMAND_QUEUE_SIZE); read_ptr += cmd->size; if (cmd->type > VideoThreadCommandType::Shutdown) [[likely]] { DebugAssert(s_state.gpu_backend); s_state.gpu_backend->HandleCommand(cmd); continue; } switch (cmd->type) { case VideoThreadCommandType::Wraparound: { DebugAssert(read_ptr == COMMAND_QUEUE_SIZE); write_ptr = s_state.command_fifo_write_ptr.load(std::memory_order_acquire); read_ptr = 0; // let the CPU thread know as early as possible that we're here s_state.command_fifo_read_ptr.store(read_ptr, std::memory_order_release); } break; case VideoThreadCommandType::AsyncCall: { VideoThreadAsyncCallCommand* acmd = static_cast(cmd); acmd->func(); acmd->~VideoThreadAsyncCallCommand(); } break; case VideoThreadCommandType::AsyncBufferCall: { VideoThreadAsyncBufferCallCommand* acmd = static_cast(cmd); acmd->func(acmd + 1); } break; case VideoThreadCommandType::Reconfigure: { VideoThreadReconfigureCommand* ccmd = static_cast(cmd); ReconfigureOnThread(ccmd); ccmd->~VideoThreadReconfigureCommand(); } break; case VideoThreadCommandType::UpdateSettings: { VideoThreadUpdateSettingsCommand* ccmd = static_cast(cmd); UpdateSettingsOnThread(std::move(ccmd->settings)); ccmd->~VideoThreadUpdateSettingsCommand(); } break; case VideoThreadCommandType::UpdateGameInfo: { VideoThreadUpdateGameInfoCommand* ccmd = static_cast(cmd); UpdateGameInfoOnThread(ccmd); ccmd->~VideoThreadUpdateGameInfoCommand(); } break; case VideoThreadCommandType::Shutdown: { // Should have consumed everything, and be shutdown. DebugAssert(read_ptr == write_ptr); s_state.command_fifo_read_ptr.store(read_ptr, std::memory_order_release); return; } break; DefaultCaseIsUnreachable(); } } // if the command was aligned to the end of the buffer, read_ptr will be COMMAND_QUEUE_SIZE. DebugAssert(read_ptr <= COMMAND_QUEUE_SIZE); s_state.command_fifo_read_ptr.store(read_ptr % COMMAND_QUEUE_SIZE, std::memory_order_release); } } void VideoThread::DoRunIdle() { if (!g_gpu_device->HasMainSwapChain()) [[unlikely]] { // only happens during language switch Timer::NanoSleep(16 * 1000 * 1000); return; } if (!PresentFrameAndRestoreContext()) return; if (g_gpu_device->HasMainSwapChain() && !g_gpu_device->GetMainSwapChain()->IsVSyncModeBlocking()) VideoPresenter::ThrottlePresentation(); } bool VideoThread::Reconfigure(std::optional renderer, bool upload_vram, std::optional fullscreen, std::optional start_fullscreen_ui, bool recreate_device, Error* error) { INFO_LOG("Reconfiguring video thread."); const bool new_requested_fullscreen_ui = start_fullscreen_ui.value_or(s_state.requested_fullscreen_ui); const bool new_fullscreen_state = ((renderer.has_value() || new_requested_fullscreen_ui) && fullscreen.value_or(s_state.fullscreen_state)); GPURenderer created_renderer = GPURenderer::Count; VideoThreadReconfigureCommand::Result result = VideoThreadReconfigureCommand::Result::Failed; VideoThreadReconfigureCommand* cmd = AllocateCommand(VideoThreadCommandType::Reconfigure); cmd->renderer = renderer; cmd->fullscreen = new_fullscreen_state; cmd->start_fullscreen_ui = new_requested_fullscreen_ui; cmd->vsync_mode = System::GetEffectiveVSyncMode(); cmd->present_skip_mode = System::GetEffectivePresentSkipMode(); cmd->force_recreate_device = recreate_device; cmd->upload_vram = upload_vram; cmd->error_ptr = error; cmd->out_result = &result; cmd->out_created_renderer = &created_renderer; cmd->settings = g_settings; if (!s_state.use_thread) [[unlikely]] ReconfigureOnThread(cmd); else PushCommandAndSync(cmd, false); // Update CPU thread state. if (result == VideoThreadReconfigureCommand::Result::FailedWithDeviceLoss) { s_state.requested_renderer.reset(); s_state.requested_fullscreen_ui = false; s_state.fullscreen_state = false; return false; } // But the renderer may not have been successfully switched. Keep our CPU thread state in sync. if (created_renderer == GPURenderer::Count) s_state.requested_renderer.reset(); else s_state.requested_renderer = renderer; s_state.requested_fullscreen_ui = new_requested_fullscreen_ui; s_state.fullscreen_state = new_fullscreen_state; return (result == VideoThreadReconfigureCommand::Result::Success); } bool VideoThread::StartFullscreenUI(bool fullscreen, Error* error) { // Don't need to reconfigure if we already have a system. if (System::IsValid()) { s_state.requested_fullscreen_ui = true; return true; } return Reconfigure(std::nullopt, false, fullscreen, true, false, error); } bool VideoThread::IsFullscreenUIRequested() { return s_state.requested_fullscreen_ui; } void VideoThread::StopFullscreenUI() { // shouldn't be changing this while we have a system if (System::IsValid()) { s_state.requested_fullscreen_ui = false; return; } Reconfigure(std::nullopt, false, std::nullopt, false, false, nullptr); } std::optional VideoThread::GetRequestedRenderer() { return s_state.requested_renderer; } GPUBackend* VideoThread::GetGPUBackend() { DebugAssert(IsOnThread()); return s_state.gpu_backend.get(); } bool VideoThread::CreateGPUBackend(GPURenderer renderer, bool upload_vram, std::optional fullscreen, Error* error) { return Reconfigure(renderer, upload_vram, fullscreen, std::nullopt, false, error); } void VideoThread::DestroyGPUBackend() { Reconfigure(std::nullopt, false, std::nullopt, std::nullopt, false, nullptr); } bool VideoThread::HasGPUBackend() { DebugAssert(IsOnThread()); return (s_state.gpu_backend != nullptr); } bool VideoThread::IsGPUBackendRequested() { return s_state.requested_renderer.has_value(); } bool VideoThread::IsFullscreen() { return s_state.fullscreen_state; } bool VideoThread::CreateDeviceOnThread(RenderAPI api, bool fullscreen, bool start_fullscreen_ui, bool preserve_imgui_on_failure, Error* error) { DebugAssert(!g_gpu_device); INFO_LOG("Trying to create a {} GPU device...", GPUDevice::RenderAPIToString(api)); g_gpu_device = GPUDevice::CreateDeviceForAPI(api); std::optional fullscreen_mode; if (fullscreen && g_gpu_device && g_gpu_device->GetFeatures().exclusive_fullscreen) { fullscreen_mode = GPUDevice::ExclusiveFullscreenMode::Parse(Core::GetTinyStringSettingValue("GPU", "FullscreenMode", "")); } std::optional exclusive_fullscreen_control; if (g_gpu_settings.display_exclusive_fullscreen_control != DisplayExclusiveFullscreenControl::Automatic) { exclusive_fullscreen_control = (g_gpu_settings.display_exclusive_fullscreen_control == DisplayExclusiveFullscreenControl::Allowed); } GPUDevice::CreateFlags create_flags = GPUDevice::CreateFlags::None; if (g_gpu_settings.gpu_prefer_gles_context) create_flags |= GPUDevice::CreateFlags::PreferGLESContext; if (g_gpu_settings.gpu_use_debug_device) create_flags |= GPUDevice::CreateFlags::EnableDebugDevice; if (g_gpu_settings.gpu_use_debug_device && g_gpu_settings.gpu_use_debug_device_gpu_validation) create_flags |= GPUDevice::CreateFlags::EnableGPUValidation; if (g_gpu_settings.gpu_disable_shader_cache) create_flags |= GPUDevice::CreateFlags::DisableShaderCache; if (g_gpu_settings.gpu_disable_dual_source_blend) create_flags |= GPUDevice::CreateFlags::DisableDualSourceBlend; if (g_gpu_settings.gpu_disable_framebuffer_fetch) create_flags |= GPUDevice::CreateFlags::DisableFramebufferFetch; if (g_gpu_settings.gpu_disable_texture_buffers) create_flags |= GPUDevice::CreateFlags::DisableTextureBuffers; if (g_gpu_settings.gpu_disable_texture_copy_to_self) create_flags |= GPUDevice::CreateFlags::DisableTextureCopyToSelf; if (g_gpu_settings.gpu_disable_memory_import) create_flags |= GPUDevice::CreateFlags::DisableMemoryImport; if (g_gpu_settings.gpu_disable_raster_order_views) create_flags |= GPUDevice::CreateFlags::DisableRasterOrderViews; if (g_gpu_settings.gpu_disable_compute_shaders) create_flags |= GPUDevice::CreateFlags::DisableComputeShaders; if (g_gpu_settings.gpu_disable_compressed_textures) create_flags |= GPUDevice::CreateFlags::DisableCompressedTextures; // Only dump shaders on debug builds for Android, users will complain about storage... #if !defined(__ANDROID__) || defined(_DEBUG) const std::string_view shader_dump_directory(EmuFolders::DataRoot); #else const std::string_view shader_dump_directory; #endif Error create_error; std::optional wi; if (!g_gpu_device || !(wi = Host::AcquireRenderWindow(api, fullscreen, fullscreen_mode.has_value(), &create_error)).has_value() || !g_gpu_device->Create(Core::GetStringSettingValue("GPU", "Adapter"), create_flags, shader_dump_directory, EmuFolders::Cache, SHADER_CACHE_VERSION, wi.value(), s_state.requested_vsync, fullscreen_mode.has_value() ? &fullscreen_mode.value() : nullptr, exclusive_fullscreen_control, &create_error)) { ERROR_LOG("Failed to create GPU device: {}", create_error.GetDescription()); if (g_gpu_device) g_gpu_device->Destroy(); g_gpu_device.reset(); if (wi.has_value()) Host::ReleaseRenderWindow(); UpdateRunIdle(); Error::SetStringFmt( error, TRANSLATE_FS("System", "Failed to create render device:\n\n{0}\n\nThis may be due to your GPU not supporting the " "chosen renderer ({1}), or because your graphics drivers need to be updated."), create_error.GetDescription(), GPUDevice::RenderAPIToString(api)); return false; } // might be initialized already if we're recreating the device const bool imgui_was_initialized = ImGuiManager::IsInitialized(); if (!(imgui_was_initialized ? ImGuiManager::CreateGPUResources(&create_error) : ImGuiManager::Initialize(&create_error))) { ERROR_LOG("Failed to initialize ImGuiManager: {}", create_error.GetDescription()); Error::SetStringFmt(error, "Failed to initialize ImGuiManager: {}", create_error.GetDescription()); if (preserve_imgui_on_failure) ImGuiManager::DestroyGPUResources(); else ImGuiManager::Shutdown(); g_gpu_device->Destroy(); g_gpu_device.reset(); if (wi.has_value()) Host::ReleaseRenderWindow(); UpdateRunIdle(); return false; } // still need to update the window size if (imgui_was_initialized && g_gpu_device->HasMainSwapChain()) { const GPUSwapChain* sc = g_gpu_device->GetMainSwapChain(); ImGuiManager::WindowResized(sc->GetWindowInfo().surface_format, static_cast(sc->GetWidth()), static_cast(sc->GetHeight())); } if (start_fullscreen_ui) FullscreenUI::Initialize(); if (const GPUSwapChain* swap_chain = g_gpu_device->GetMainSwapChain()) s_state.render_window_info = swap_chain->GetWindowInfo(); else s_state.render_window_info = WindowInfo(); s_state.render_window_info.surface_width = std::max(s_state.render_window_info.surface_width, 1); s_state.render_window_info.surface_height = std::max(s_state.render_window_info.surface_height, 1); UpdateRunIdle(); // Switch to borderless if exclusive failed. if (fullscreen_mode.has_value() && !CheckExclusiveFullscreenOnThread()) { WARNING_LOG("Failed to get exclusive fullscreen, requesting borderless fullscreen instead."); RecreateRenderWindowOnThread(true, false); } return true; } void VideoThread::DestroyDeviceOnThread(bool preserve_imgui_state) { if (!g_gpu_device) return; // Presenter should be gone by this point Assert(!VideoPresenter::HasDisplayTexture()); if (preserve_imgui_state) { DEV_LOG("Preserving ImGui state on destroy"); ImGuiManager::DestroyGPUResources(); } else { ImGuiManager::Shutdown(); } INFO_LOG("Destroying {} GPU device...", GPUDevice::RenderAPIToString(g_gpu_device->GetRenderAPI())); g_gpu_device->Destroy(); g_gpu_device.reset(); Host::ReleaseRenderWindow(); UpdateRunIdle(); s_state.render_window_info = WindowInfo(); } bool VideoThread::CreateGPUBackendOnThread(bool hardware_renderer, bool upload_vram, const GPUSettings* old_settings, GPURenderer* out_renderer, Error* error) { Error local_error; // Create presenter if we don't already have one. if (!VideoPresenter::IsInitialized()) { if (!VideoPresenter::Initialize(&local_error)) { ERROR_LOG("Failed to create presenter: {}", local_error.GetDescription()); Error::SetStringFmt(error, "Failed to create presenter: {}", local_error.GetDescription()); return false; } } else if (old_settings) { // Settings may have still changed. if (!VideoPresenter::UpdateSettings(*old_settings, &local_error)) { ERROR_LOG("Failed to update presenter settings: {}", local_error.GetDescription()); Error::SetStringFmt(error, "Failed to update presenter settings: {}", local_error.GetDescription()); VideoPresenter::Shutdown(); return false; } } #ifndef __ANDROID__ ImGuiManager::UpdateDebugWindowConfig(); #endif if (hardware_renderer) s_state.gpu_backend = GPUBackend::CreateHardwareBackend(); else s_state.gpu_backend = GPUBackend::CreateSoftwareBackend(); bool okay = s_state.gpu_backend->Initialize(upload_vram, &local_error); if (!okay) { ERROR_LOG("Failed to create {} renderer: {}", hardware_renderer ? "hardware" : "software", local_error.GetDescription()); if (hardware_renderer && !System::IsStartupCancelled()) { Host::AddIconOSDMessage( OSDMessageType::Error, "GPUBackendCreationFailed", ICON_FA_PAINT_ROLLER, fmt::format( "{}\n{}", TRANSLATE_SV("OSDMessage", "Failed to initialize hardware renderer, falling back to software renderer."), local_error.GetDescription())); hardware_renderer = false; s_state.gpu_backend = GPUBackend::CreateSoftwareBackend(); if (!s_state.gpu_backend->Initialize(upload_vram, &local_error)) Panic("Failed to initialize fallback software renderer"); } if (!okay) { if (error) *error = local_error; DestroyGPUBackendOnThread(); return false; } } *out_renderer = hardware_renderer ? Settings::GetRendererForRenderAPI(g_gpu_device->GetRenderAPI()) : GPURenderer::Software; g_gpu_device->SetGPUTimingEnabled(g_gpu_settings.display_show_gpu_usage); s_state.gpu_backend->RestoreDeviceContext(); SetRunIdleReason(RunIdleReason::NoGPUBackend, false); return true; } void VideoThread::ReconfigureOnThread(VideoThreadReconfigureCommand* cmd) { // Store state. s_state.requested_vsync = cmd->vsync_mode; VideoPresenter::SetPresentSkipMode(cmd->present_skip_mode); // Are we shutting down everything? if (!cmd->renderer.has_value() && !cmd->start_fullscreen_ui) { // Serial clear must be after backend destroy, otherwise textures won't dump. DestroyGPUBackendOnThread(); DestroyGPUPresenterOnThread(); DestroyDeviceOnThread(false); ClearGameInfoOnThread(); *cmd->out_result = VideoThreadReconfigureCommand::Result::Success; *cmd->out_created_renderer = GPURenderer::Count; return; } const GPUSettings old_settings = std::move(g_gpu_settings); g_gpu_settings = std::move(cmd->settings); // Readback old VRAM for hardware renderers. const bool had_renderer = static_cast(s_state.gpu_backend); if (had_renderer && cmd->renderer.has_value() && cmd->upload_vram) { GPUBackendReadVRAMCommand read_cmd; read_cmd.type = VideoThreadCommandType::ReadVRAM; read_cmd.size = sizeof(read_cmd); read_cmd.x = 0; read_cmd.y = 0; read_cmd.width = VRAM_WIDTH; read_cmd.height = VRAM_HEIGHT; s_state.gpu_backend->HandleCommand(&read_cmd); } if (s_state.gpu_backend) DestroyGPUBackendOnThread(); // Device recreation? const RenderAPI current_api = g_gpu_device ? g_gpu_device->GetRenderAPI() : RenderAPI::None; const RenderAPI expected_api = (!cmd->force_recreate_device && current_api != RenderAPI::None && (!cmd->renderer.has_value() || cmd->renderer.value() == GPURenderer::Software)) ? current_api : Settings::GetRenderAPIForRenderer(cmd->renderer.value_or(g_gpu_settings.gpu_renderer)); if (cmd->force_recreate_device || !GPUDevice::IsSameRenderAPI(current_api, expected_api)) { Timer timer; DestroyGPUPresenterOnThread(); DestroyDeviceOnThread(true); Error local_error; if (!CreateDeviceOnThread(expected_api, cmd->fullscreen, cmd->start_fullscreen_ui, current_api != RenderAPI::None, &local_error)) { Host::AddIconOSDMessage( OSDMessageType::Error, "DeviceSwitchFailed", ICON_FA_PAINT_ROLLER, fmt::format(TRANSLATE_FS("OSDMessage", "Failed to create {} GPU device, reverting to {}.\n{}"), GPUDevice::RenderAPIToString(expected_api), GPUDevice::RenderAPIToString(current_api), local_error.GetDescription())); if (current_api == RenderAPI::None || !CreateDeviceOnThread(current_api, cmd->fullscreen, cmd->start_fullscreen_ui, false, &local_error)) { if (cmd->error_ptr) *cmd->error_ptr = local_error; *cmd->out_result = VideoThreadReconfigureCommand::Result::FailedWithDeviceLoss; *cmd->out_created_renderer = GPURenderer::Count; return; } } INFO_LOG("GPU device created in {:.2f}ms", timer.GetTimeMilliseconds()); } if (cmd->renderer.has_value()) { Timer timer; // Do we want a renderer? if (CreateGPUBackendOnThread(cmd->renderer.value() != GPURenderer::Software, cmd->upload_vram, &old_settings, cmd->out_created_renderer, cmd->error_ptr)) { *cmd->out_result = VideoThreadReconfigureCommand::Result::Success; INFO_LOG("GPU backend created in {:.2f}ms", timer.GetTimeMilliseconds()); } else { // No renderer created. *cmd->out_result = VideoThreadReconfigureCommand::Result::Failed; *cmd->out_created_renderer = GPURenderer::Count; // If we had a renderer, it means it was a switch, and we need to bail out the thread. if (had_renderer) { VideoThread::ReportFatalErrorAndShutdown("Failed to switch GPU backend."); } else { // No point keeping the presenter around. DestroyGPUPresenterOnThread(); ClearGameInfoOnThread(); // Drop device if we're not running FSUI. if (!cmd->start_fullscreen_ui) DestroyDeviceOnThread(false); } } } else { // Full shutdown case handled above. This is just for running FSUI. DebugAssert(cmd->start_fullscreen_ui); DebugAssert(g_gpu_device); // Don't need to present game frames anymore. DestroyGPUPresenterOnThread(); ClearGameInfoOnThread(); *cmd->out_result = VideoThreadReconfigureCommand::Result::Success; } } void VideoThread::DestroyGPUBackendOnThread() { if (!s_state.gpu_backend) return; VERBOSE_LOG("Shutting down GPU backend..."); SetRunIdleReason(RunIdleReason::NoGPUBackend, true); s_state.gpu_backend.reset(); } void VideoThread::DestroyGPUPresenterOnThread() { if (!VideoPresenter::IsInitialized()) return; VERBOSE_LOG("Shutting down GPU presenter..."); ImGuiManager::DestroyAllDebugWindows(); ImGuiManager::DestroyOverlayTextures(); // Should have no queued frames by this point. Backend can get replaced with null. Assert(!s_state.gpu_backend); Assert(GPUBackend::GetQueuedFrameCount() == 0); // Don't need timing anymore. g_gpu_device->SetGPUTimingEnabled(false); VideoPresenter::Shutdown(); } bool VideoThread::PresentFrameAndRestoreContext() { DebugAssert(IsOnThread()); if (s_state.gpu_backend) s_state.gpu_backend->FlushRender(); if (g_gpu_device->HasMainSwapChain()) { if (!VideoPresenter::PresentFrame(s_state.gpu_backend.get(), 0)) return false; if (s_state.gpu_backend) s_state.gpu_backend->RestoreDeviceContext(); } return true; } void VideoThread::SetThreadEnabled(bool enabled) { if (s_state.use_thread == enabled) return; if (s_state.use_thread) SyncThread(false); // Was anything active? if (!g_gpu_device) { // Thread should be idle. Just reset the FIFO. s_state.use_thread = enabled; ResetCommandFIFO(); return; } const bool fullscreen_state = s_state.fullscreen_state; const bool requested_fullscreen_ui = s_state.requested_fullscreen_ui; const std::optional requested_renderer = s_state.requested_renderer; // Force VRAM download, we're recreating. if (requested_renderer.has_value()) { GPUBackendReadVRAMCommand* cmd = GPUBackend::NewReadVRAMCommand(); cmd->x = 0; cmd->y = 0; cmd->width = VRAM_WIDTH; cmd->height = VRAM_HEIGHT; PushCommand(cmd); } // Shutdown reconfigure. Reconfigure(std::nullopt, false, false, false, false, nullptr); // Thread should be idle at this point. Reset the FIFO. ResetCommandFIFO(); // Update state and reconfigure again. s_state.use_thread = enabled; Error error; if (!Reconfigure(requested_renderer, requested_renderer.has_value(), fullscreen_state, requested_fullscreen_ui, true, &error)) { ERROR_LOG("Reconfigure failed: {}", error.GetDescription()); ReportFatalErrorAndShutdown(fmt::format("Reconfigure failed: {}", error.GetDescription())); } } void VideoThread::UpdateSettingsOnThread(GPUSettings&& new_settings) { VERBOSE_LOG("Updating GPU settings on thread..."); GPUSettings old_settings = std::move(g_gpu_settings); g_gpu_settings = std::move(new_settings); if (g_gpu_device) { if (g_gpu_settings.display_osd_scale != old_settings.display_osd_scale) ImGuiManager::RequestScaleUpdate(); FullscreenUI::CheckForConfigChanges(old_settings); } if (s_state.gpu_backend) { if (g_gpu_settings.display_show_gpu_usage != old_settings.display_show_gpu_usage) g_gpu_device->SetGPUTimingEnabled(g_gpu_settings.display_show_gpu_usage); Error error; if (!VideoPresenter::UpdateSettings(old_settings, &error) || !s_state.gpu_backend->UpdateSettings(old_settings, &error)) [[unlikely]] { ReportFatalErrorAndShutdown(fmt::format("Failed to update settings: {}", error.GetDescription())); return; } if (ImGuiManager::UpdateDebugWindowConfig()) PresentFrameAndRestoreContext(); else s_state.gpu_backend->RestoreDeviceContext(); } } void VideoThread::RunOnThread(AsyncCallType func) { DebugAssert(Host::IsOnCoreThread()); if (!s_state.use_thread) [[unlikely]] { func(); return; } VideoThreadAsyncCallCommand* cmd = AllocateCommand(VideoThreadCommandType::AsyncCall, std::move(func)); PushCommandAndWakeThread(cmd); } void VideoThread::RunOnThreadAndSync(AsyncCallType func) { DebugAssert(Host::IsOnCoreThread()); if (!s_state.use_thread) [[unlikely]] { func(); return; } VideoThreadAsyncCallCommand* cmd = AllocateCommand(VideoThreadCommandType::AsyncCall, std::move(func)); PushCommandAndSync(cmd, false); } std::pair VideoThread::BeginASyncBufferCall(AsyncBufferCallType func, u32 buffer_size) { DebugAssert(Host::IsOnCoreThread()); // this is less than optimal, but it's only used for input osd updates currently, so whatever VideoThreadAsyncBufferCallCommand* const cmd = AllocateCommand( sizeof(VideoThreadAsyncBufferCallCommand) + buffer_size, VideoThreadCommandType::AsyncBufferCall, func); void* const buffer = static_cast(cmd + 1); return std::make_pair(static_cast(cmd), buffer); } void VideoThread::EndASyncBufferCall(VideoThreadCommand* cmd) { DebugAssert(Host::IsOnCoreThread()); if (!s_state.use_thread) [[unlikely]] { VideoThreadAsyncBufferCallCommand* const acmd = static_cast(cmd); acmd->func(static_cast(acmd + 1)); return; } PushCommand(cmd); } void VideoThread::UpdateSettings(bool gpu_settings_changed, bool device_settings_changed, bool thread_changed) { // thread should be a device setting if (thread_changed) { DebugAssert(device_settings_changed); SetThreadEnabled(g_settings.gpu_use_thread); } else if (device_settings_changed) { INFO_LOG("Reconfiguring after device settings changed."); Error error; if (!Reconfigure(s_state.requested_renderer, s_state.requested_renderer.has_value(), std::nullopt, std::nullopt, true, &error)) [[unlikely]] { Host::ReportErrorAsync("Error", fmt::format("Failed to recreate GPU device: {}", error.GetDescription())); } } else if (gpu_settings_changed) { if (s_state.use_thread) [[likely]] { VideoThreadUpdateSettingsCommand* cmd = AllocateCommand(VideoThreadCommandType::UpdateSettings, g_settings); PushCommandAndWakeThread(cmd); } else { UpdateSettingsOnThread(GPUSettings(g_settings)); } } else { #ifndef __ANDROID__ // Not needed on Android, debug windows are not used. RunOnThread([]() { if (s_state.gpu_backend) { if (ImGuiManager::UpdateDebugWindowConfig()) PresentFrameAndRestoreContext(); } }); #endif } } void VideoThread::UpdateGameInfo(const std::string& title, const std::string& serial, const std::string& path, GameHash hash, bool wake_thread /*= true*/) { if (!s_state.use_thread) { const bool serial_changed = (s_state.game_serial != serial); s_state.game_title = title; s_state.game_serial = serial; s_state.game_path = path; s_state.game_hash = hash; GameInfoChanged(serial_changed); return; } VideoThreadUpdateGameInfoCommand* cmd = AllocateCommand( VideoThreadCommandType::UpdateGameInfo, title, serial, path, hash); if (wake_thread) PushCommandAndWakeThread(cmd); else PushCommand(cmd); } void VideoThread::ClearGameInfo() { if (!s_state.use_thread) { ClearGameInfoOnThread(); return; } PushCommandAndWakeThread(AllocateCommand(VideoThreadCommandType::UpdateGameInfo)); } void VideoThread::UpdateGameInfoOnThread(VideoThreadUpdateGameInfoCommand* cmd) { DEV_LOG("Updating game info on GPU thread: {}/{}", cmd->game_serial, cmd->game_title); const bool serial_changed = (s_state.game_serial != cmd->game_serial); s_state.game_title = std::move(cmd->game_title); s_state.game_serial = std::move(cmd->game_serial); s_state.game_path = std::move(cmd->game_path); s_state.game_hash = cmd->game_hash; GameInfoChanged(serial_changed); } void VideoThread::GameInfoChanged(bool serial_changed) { if (!serial_changed) return; if (HasGPUBackend()) GPUTextureCache::GameSerialChanged(); if (SaveStateSelectorUI::IsOpen()) SaveStateSelectorUI::RefreshList(); } void VideoThread::ClearGameInfoOnThread() { DEV_LOG("Clearing game info on GPU thread."); s_state.game_hash = 0; s_state.game_path = {}; s_state.game_serial = {}; s_state.game_title = {}; } void VideoThread::ReportFatalErrorAndShutdown(std::string_view reason) { DebugAssert(IsOnThread()); std::string message = fmt::format("GPU thread shut down with fatal error:\n\n{}", reason); if (!s_state.gpu_backend) { // if we have no backend, it crapped out in fullscreenui alone if (!g_gpu_device) return; Host::RunOnCoreThread([message = std::move(message)]() { Host::OnSystemAbnormalShutdown(message); }); DestroyDeviceOnThread(false); return; } Host::RunOnCoreThread([message = std::move(message)]() { System::AbnormalShutdown(message); }); // replace the renderer with a dummy/null backend, so that all commands get dropped ERROR_LOG("Switching to null renderer: {}", reason); VideoPresenter::ClearDisplayTexture(); s_state.gpu_backend.reset(); s_state.gpu_backend = GPUBackend::CreateNullBackend(); if (!s_state.gpu_backend->Initialize(false, nullptr)) [[unlikely]] Panic("Failed to initialize null GPU backend"); } bool VideoThread::IsOnThread() { return (!s_state.use_thread || s_state.thread.IsCallingThread()); } bool VideoThread::IsUsingThread() { return s_state.use_thread; } void VideoThread::ResizeRenderWindow(s32 width, s32 height, float scale, float refresh_rate) { const u16 clamped_width = static_cast(std::clamp(width, 1, std::numeric_limits::max())); const u16 clamped_height = static_cast(std::clamp(height, 1, std::numeric_limits::max())); const bool size_changed = (s_state.render_window_info.surface_width != clamped_width || s_state.render_window_info.surface_height != clamped_height); const bool refresh_rate_changed = (s_state.render_window_info.surface_refresh_rate != refresh_rate); s_state.render_window_info.surface_width = clamped_width; s_state.render_window_info.surface_height = clamped_height; s_state.render_window_info.surface_scale = scale; s_state.render_window_info.surface_refresh_rate = refresh_rate; RunOnThread([clamped_width, clamped_height, scale, refresh_rate]() { ResizeRenderWindowOnThread(clamped_width, clamped_height, scale, refresh_rate); }); if (System::IsValid()) { if (size_changed) System::RenderWindowResized(); if (refresh_rate_changed) System::UpdateSpeedLimiterState(); } } void VideoThread::ResizeRenderWindowOnThread(u32 width, u32 height, float scale, float refresh_rate) { // We should _not_ be getting this without a device, since we should have shut down. if (!g_gpu_device || !g_gpu_device->HasMainSwapChain()) return; DEV_LOG("Render window resized to {}x{} @ {}x/{}hz", width, height, scale, refresh_rate); Error error; GPUSwapChain* const swap_chain = g_gpu_device->GetMainSwapChain(); if (!swap_chain->ResizeBuffers(width, height, &error)) { // ick, CPU thread read, but this is unlikely to happen in the first place ERROR_LOG("Failed to resize main swap chain: {}", error.GetDescription()); RecreateRenderWindowOnThread(s_state.fullscreen_state, true); return; } swap_chain->SetScale(scale); swap_chain->SetRefreshRate(refresh_rate); RenderWindowResizedOnThread(); } void VideoThread::RecreateRenderWindow() { RunOnThread([fullscreen = s_state.fullscreen_state]() { RecreateRenderWindowOnThread(fullscreen, true); }); } void VideoThread::SetFullscreen(bool fullscreen) { // Technically not safe to read g_gpu_device here on the CPU thread, but we do sync on create/destroy. if (s_state.fullscreen_state == fullscreen || !Host::CanChangeFullscreenMode(fullscreen) || !g_gpu_device) return; s_state.fullscreen_state = fullscreen; RunOnThread([fullscreen]() { RecreateRenderWindowOnThread(fullscreen, true); }); } void VideoThread::SetFullscreenWithCompletionHandler(bool fullscreen, AsyncCallType completion_handler) { if (s_state.fullscreen_state == fullscreen || !Host::CanChangeFullscreenMode(fullscreen) || !g_gpu_device) { if (completion_handler) completion_handler(); return; } s_state.fullscreen_state = fullscreen; RunOnThread([fullscreen, completion_handler = std::move(completion_handler)]() { RecreateRenderWindowOnThread(fullscreen, true); if (completion_handler) completion_handler(); }); } void VideoThread::RecreateRenderWindowOnThread(bool fullscreen, bool allow_exclusive_fullscreen) { // In case we get the event late. if (!g_gpu_device) return; bool exclusive_fullscreen_requested = false; std::optional fullscreen_mode; if (allow_exclusive_fullscreen && fullscreen && g_gpu_device->GetFeatures().exclusive_fullscreen) { fullscreen_mode = GPUDevice::ExclusiveFullscreenMode::Parse(Core::GetTinyStringSettingValue("GPU", "FullscreenMode", "")); exclusive_fullscreen_requested = fullscreen_mode.has_value(); } std::optional exclusive_fullscreen_control; if (g_settings.display_exclusive_fullscreen_control != DisplayExclusiveFullscreenControl::Automatic) { exclusive_fullscreen_control = (g_settings.display_exclusive_fullscreen_control == DisplayExclusiveFullscreenControl::Allowed); } g_gpu_device->DestroyMainSwapChain(); Error error; std::optional wi = Host::AcquireRenderWindow(g_gpu_device->GetRenderAPI(), fullscreen, exclusive_fullscreen_requested, &error); if (!wi.has_value()) { Host::ReportFatalError("Failed to get render window after update", error.GetDescription()); return; } // if surfaceless, just leave it if (!wi->IsSurfaceless()) { if (!g_gpu_device->RecreateMainSwapChain(wi.value(), s_state.requested_vsync, fullscreen_mode.has_value() ? &fullscreen_mode.value() : nullptr, exclusive_fullscreen_control, &error)) { Host::ReportFatalError("Failed to change window after update", error.GetDescription()); return; } } else { WARNING_LOG("Switching to surfaceless rendering"); if (!g_gpu_device->SwitchToSurfacelessRendering(&error)) ERROR_LOG("Failed to switch to surfaceless, rendering commands may fail: {}", error.GetDescription()); } // If exclusive fullscreen failed, switch to borderless fullscreen. if (exclusive_fullscreen_requested && !CheckExclusiveFullscreenOnThread()) { RecreateRenderWindowOnThread(true, false); return; } // Need to notify the core thread of the change, since it won't necessarily get a resize event. Host::RunOnCoreThread([wi = g_gpu_device->HasMainSwapChain() ? g_gpu_device->GetMainSwapChain()->GetWindowInfo() : WindowInfo()]() { s_state.render_window_info = std::move(wi); }); RenderWindowResizedOnThread(); } bool VideoThread::CheckExclusiveFullscreenOnThread() { if (g_gpu_device->HasMainSwapChain() && g_gpu_device->GetMainSwapChain()->IsExclusiveFullscreen()) return true; Host::AddIconOSDMessage( OSDMessageType::Error, "ExclusiveFullscreenFailed", ICON_EMOJI_WARNING, TRANSLATE_STR("OSDMessage", "Failed to switch to exclusive fullscreen, using borderless instead.")); return false; } void VideoThread::RenderWindowResizedOnThread() { // surfaceless is usually temporary, so just ignore it const GPUSwapChain* const swap_chain = g_gpu_device->GetMainSwapChain(); if (!swap_chain) return; // our imgui stuff can't cope with 0x0/hidden windows const float f_width = static_cast(std::max(swap_chain->GetWidth(), 1u)); const float f_height = static_cast(std::max(swap_chain->GetHeight(), 1u)); ImGuiManager::WindowResized(swap_chain->GetFormat(), f_width, f_height); // If we're paused, re-present the current frame at the new window size. if (s_state.gpu_backend && IsSystemPaused()) { // Hackity hack, on some systems, presenting a single frame isn't enough to actually get it // displayed. Two seems to be good enough. Maybe something to do with direct scanout. PresentFrameAndRestoreContext(); PresentFrameAndRestoreContext(); } } const WindowInfo& VideoThread::GetRenderWindowInfo() { return s_state.render_window_info; } void VideoThread::SetVSync(GPUVSyncMode mode, PresentSkipMode present_skip_mode) { RunOnThread([mode, present_skip_mode]() { VideoPresenter::SetPresentSkipMode(present_skip_mode); if (s_state.requested_vsync == mode) return; s_state.requested_vsync = mode; if (!g_gpu_device || !g_gpu_device->HasMainSwapChain()) return; Error error; if (!g_gpu_device->GetMainSwapChain()->SetVSyncMode(s_state.requested_vsync, &error)) ERROR_LOG("Failed to update vsync mode: {}", error.GetDescription()); }); // If we're turning on vsync or turning off present throttle, we want to drain the GPU thread. // Otherwise if it is currently behind, it'll be permanently stuck behind. if (mode != GPUVSyncMode::Disabled) SyncThread(false); } void VideoThread::PresentCurrentFrame() { RunOnThread([]() { if (s_state.run_idle_flag) { // If we're running idle, we're going to re-present anyway. return; } // But we shouldn't be not running idle without a GPU backend. if (s_state.gpu_backend) PresentFrameAndRestoreContext(); }); } bool VideoThread::GetRunIdleReason(RunIdleReason reason) { return (s_state.run_idle_reasons & static_cast(reason)) != 0; } void VideoThread::SetRunIdleReason(RunIdleReason reason, bool enabled) { const u8 bit = static_cast(reason); if (((s_state.run_idle_reasons & bit) != 0) == enabled) return; if (Log::IsLogVisible(Log::Level::Dev, Log::Channel::VideoThread)) { static constexpr const std::array reason_strings = { "NoGPUBackend", "SystemPaused", "OSDMessagesActive", "FullscreenUIActive", "NotificationsActive", "LoadingScreenActive", "AchievementOverlaysActive", }; for (u32 i = 0; i < reason_strings.size(); ++i) { if (static_cast(1u << i) == reason) { DEV_COLOR_LOG(StrongYellow, "Setting run idle reason '{}' to {}", reason_strings[i], enabled); break; } } } s_state.run_idle_reasons = enabled ? (s_state.run_idle_reasons | bit) : (s_state.run_idle_reasons & ~bit); UpdateRunIdle(); } bool VideoThread::IsRunningIdle() { return s_state.run_idle_flag; } bool VideoThread::IsSystemPaused() { return ((s_state.run_idle_reasons & static_cast(RunIdleReason::SystemPaused)) != 0); } void VideoThread::UpdateRunIdle() { DebugAssert(IsOnThread()); // We require either invalid-system or paused for run idle. static constexpr u8 REQUIRE_MASK = static_cast(RunIdleReason::NoGPUBackend) | static_cast(RunIdleReason::SystemPaused) | static_cast(RunIdleReason::LoadingScreenActive); static constexpr u8 ACTIVATE_MASK = static_cast(RunIdleReason::OSDMessagesActive) | static_cast(RunIdleReason::FullscreenUIActive) | static_cast(RunIdleReason::NotificationsActive) | static_cast(RunIdleReason::LoadingScreenActive) | static_cast(RunIdleReason::AchievementOverlaysActive); const bool new_flag = (g_gpu_device && ((s_state.run_idle_reasons & REQUIRE_MASK) != 0) && ((s_state.run_idle_reasons & ACTIVATE_MASK) != 0)); if (s_state.run_idle_flag == new_flag) return; s_state.run_idle_flag = new_flag; if (new_flag) DEV_COLOR_LOG(StrongYellow, "GPU thread now running idle"); else DEV_COLOR_LOG(StrongOrange, "GPU thread now NOT running idle"); Host::OnVideoThreadRunIdleChanged(new_flag); } const std::string& VideoThread::GetGameTitle() { DebugAssert(IsOnThread()); return s_state.game_title; } const std::string& VideoThread::GetGameSerial() { DebugAssert(IsOnThread()); return s_state.game_serial; } const std::string& VideoThread::GetGamePath() { DebugAssert(IsOnThread()); return s_state.game_path; } GameHash VideoThread::GetGameHash() { DebugAssert(IsOnThread()); return s_state.game_hash; }