From c0a85d0e538e04fb56b0f7ca561422119b9f526f Mon Sep 17 00:00:00 2001 From: xbzk Date: Sat, 29 Aug 2026 14:22:09 +0200 Subject: [PATCH] [service, nvhost] added machinery to allow microsleep between nvdec read requests to avoid guest panic in some games (#4316) - [x] I have read and followed the [Contribution Guidelines](https://git.eden-emu.dev/eden-emu/eden/src/branch/master/CONTRIBUTING.md#code-contributions). - [x] I have read and followed the [AI Policy](https://git.eden-emu.dev/eden-emu/eden/src/branch/master/docs/policies/AI.md) - [x] I have read and followed the [Coding Guidelines](https://git.eden-emu.dev/eden-emu/eden/src/branch/master/docs/policies/Coding.md) to the best of my ability. ------------------- This one deserves a long story, but imma try to resume: During investigating Absolum 1.2 black screen of death upon loading intro video, i've discovered guest was aborting for failing to allocate room for the video. By logging everything prior to crash and decoding guest side instructions managed to confirm its media allocator was reading data faster than it was updating free available bucket list. Since the IStorage::Read was happening 247 times before the crash, i've decided to add a very small sleep there, and boom, not only Absolum but some other titles got the same issue fixed. But i was unsatisfied with the sleep and kept tracking guest instructions upstream in order to find a sync point for the read worker and the memory allocation update. But unfortunately the media allocator helpers live in guest, accessing memory directly via MMU, so any sync signaling would need to come from some dynarmic hack. It's been 6 days now, so i've decided to polish the sleep: Moved it upstream to where i could have access for proper predicate, and added machinery to service and nvhost to support that. Now the sleep is restricted only for nvdec istorage reads. Any other reads will flow normally. TL;DR: currently our code is so blazing async that guest is capable to request reads before its very self refresh it have freed room to do so. The sleep accepted as broadly stable was 600 us (MICROseconds), and it affects ONLY nvdec chunk reading. Reports confirm that now videos are smoother now. Code was polished at my knowledge limits. Mostly machinery to track when a request comes from a process with nvdec active, and is istorage read. I can provide more details if it comes to be needed. Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/4316 Reviewed-by: MaranBr Reviewed-by: Samuel --- src/core/core.cpp | 40 ++++++++++++++++--- src/core/core.h | 4 +- .../service/nvdrv/devices/nvhost_nvdec.cpp | 11 ++++- src/core/hle/service/service.cpp | 16 +++++++- src/core/hle/service/service.h | 2 + 5 files changed, 63 insertions(+), 10 deletions(-) diff --git a/src/core/core.cpp b/src/core/core.cpp index afcca1f806..054af8dc4a 100644 --- a/src/core/core.cpp +++ b/src/core/core.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include "game_settings.h" @@ -248,12 +249,30 @@ struct System::Impl { } } - void SetNVDECActive(bool is_nvdec_active) { - nvdec_active = is_nvdec_active; + void NotifyNVDECChannelOpen(u64 process_id) { + std::scoped_lock lock{nvdec_active_mutex}; + ++nvdec_active_channels[process_id]; + } + + void NotifyNVDECChannelClose(u64 process_id) { + std::scoped_lock lock{nvdec_active_mutex}; + const auto it = nvdec_active_channels.find(process_id); + if (it == nvdec_active_channels.end()) { + return; + } + if (--it->second == 0) { + nvdec_active_channels.erase(it); + } } bool GetNVDECActive() { - return nvdec_active; + std::scoped_lock lock{nvdec_active_mutex}; + return !nvdec_active_channels.empty(); + } + + bool IsNVDECActiveForProcess(u64 process_id) { + std::scoped_lock lock{nvdec_active_mutex}; + return nvdec_active_channels.contains(process_id); } void InitializeDebugger(System& system, u16 port) { @@ -505,6 +524,8 @@ struct System::Impl { mutable std::mutex suspend_guard; std::mutex general_channel_mutex; + std::mutex nvdec_active_mutex; + std::unordered_map nvdec_active_channels; std::atomic_bool is_paused{}; std::atomic_bool is_shutting_down{}; std::atomic_bool is_powered_on{}; @@ -512,7 +533,6 @@ struct System::Impl { bool extended_memory_layout : 1 = false; bool exit_locked : 1 = false; bool exit_requested : 1 = false; - bool nvdec_active : 1 = false; void EnsureGeneralChannelInitialized(System& system) { if (!general_channel_event) { @@ -576,14 +596,22 @@ void System::UnstallApplication() { impl->UnstallApplication(); } -void System::SetNVDECActive(bool is_nvdec_active) { - impl->SetNVDECActive(is_nvdec_active); +void System::NotifyNVDECChannelOpen(u64 process_id) { + impl->NotifyNVDECChannelOpen(process_id); +} + +void System::NotifyNVDECChannelClose(u64 process_id) { + impl->NotifyNVDECChannelClose(process_id); } bool System::GetNVDECActive() { return impl->GetNVDECActive(); } +bool System::IsNVDECActiveForProcess(u64 process_id) { + return impl->IsNVDECActiveForProcess(process_id); +} + void System::InitializeDebugger() { impl->InitializeDebugger(*this, Settings::values.gdbstub_port.GetValue()); } diff --git a/src/core/core.h b/src/core/core.h index d88ed64151..e26093f260 100644 --- a/src/core/core.h +++ b/src/core/core.h @@ -191,8 +191,10 @@ public: std::unique_lock StallApplication(); void UnstallApplication(); - void SetNVDECActive(bool is_nvdec_active); + void NotifyNVDECChannelOpen(u64 process_id); + void NotifyNVDECChannelClose(u64 process_id); [[nodiscard]] bool GetNVDECActive(); + [[nodiscard]] bool IsNVDECActiveForProcess(u64 process_id); /** * Initialize the debugger. diff --git a/src/core/hle/service/nvdrv/devices/nvhost_nvdec.cpp b/src/core/hle/service/nvdrv/devices/nvhost_nvdec.cpp index 7ac3dfaa46..f47369adb8 100644 --- a/src/core/hle/service/nvdrv/devices/nvhost_nvdec.cpp +++ b/src/core/hle/service/nvdrv/devices/nvhost_nvdec.cpp @@ -8,6 +8,7 @@ #include "common/assert.h" #include "common/logging.h" #include "core/core.h" +#include "core/hle/kernel/k_process.h" #include "core/hle/service/nvdrv/core/container.h" #include "core/hle/service/nvdrv/devices/ioctl_serialization.h" #include "core/hle/service/nvdrv/devices/nvhost_nvdec.h" @@ -71,17 +72,23 @@ NvResult nvhost_nvdec::Ioctl3(DeviceFD fd, Ioctl command, std::span in void nvhost_nvdec::OnOpen(NvCore::SessionId session_id, DeviceFD fd) { LOG_INFO(Service_NVDRV, "NVDEC video stream started"); - system.SetNVDECActive(true); sessions[fd] = session_id; + if (const auto* session = core.GetSession(session_id); + session != nullptr && session->process != nullptr) { + system.NotifyNVDECChannelOpen(session->process->GetId()); + } host1x.StartDevice(fd, Tegra::Host1x::ChannelType::NvDec, channel_syncpoint); } void nvhost_nvdec::OnClose(DeviceFD fd) { LOG_INFO(Service_NVDRV, "NVDEC video stream ended"); host1x.StopDevice(fd, Tegra::Host1x::ChannelType::NvDec); - system.SetNVDECActive(false); auto it = sessions.find(fd); if (it != sessions.end()) { + if (const auto* session = core.GetSession(it->second); + session != nullptr && session->process != nullptr) { + system.NotifyNVDECChannelClose(session->process->GetId()); + } sessions.erase(it); } } diff --git a/src/core/hle/service/service.cpp b/src/core/hle/service/service.cpp index b4fe2b3ed5..225b92a783 100644 --- a/src/core/hle/service/service.cpp +++ b/src/core/hle/service/service.cpp @@ -4,12 +4,16 @@ // SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later +#include #include +#include +#include #include "common/assert.h" #include "common/logging.h" #include "common/settings.h" #include "core/core.h" #include "core/hle/ipc.h" +#include "core/hle/kernel/k_process.h" #include "core/hle/kernel/kernel.h" #include "core/hle/service/ipc_helpers.h" #include "core/hle/service/service.h" @@ -33,6 +37,7 @@ ServiceFrameworkBase::ServiceFrameworkBase(Core::System& system_, const char* se : SessionRequestHandler(system_.Kernel(), service_name_) , system{system_} , service_name{service_name_} + , is_i_storage{std::string_view{service_name_} == "IStorage"} , handler_invoker{handler_invoker_} , max_sessions{max_sessions_} {} @@ -77,13 +82,22 @@ void ServiceFrameworkBase::ReportUnimplementedFunction(HLERequestContext& ctx, } void ServiceFrameworkBase::InvokeRequest(HLERequestContext& ctx) { - auto it = handlers.find(ctx.GetCommand()); + const auto command = ctx.GetCommand(); + auto it = handlers.find(command); + const bool is_cmd_read = command == 0; FunctionInfoBase const* info = it == handlers.end() ? nullptr : &it->second; if (info == nullptr || info->handler_callback == nullptr) return ReportUnimplementedFunction(ctx, info); LOG_TRACE(Service, "{}", MakeFunctionString(info->name, GetServiceName(), ctx.CommandBuffer())); handler_invoker(this, info->handler_callback, ctx); + + if (is_i_storage && is_cmd_read) { + const auto* const process = ctx.GetThread().GetOwnerProcess(); + if (process != nullptr && system.IsNVDECActiveForProcess(process->GetId())) { + std::this_thread::sleep_for(std::chrono::microseconds{600}); + } + } } void ServiceFrameworkBase::InvokeRequestTipc(HLERequestContext& ctx) { diff --git a/src/core/hle/service/service.h b/src/core/hle/service/service.h index 41429958ad..e52a832a1e 100644 --- a/src/core/hle/service/service.h +++ b/src/core/hle/service/service.h @@ -107,6 +107,8 @@ protected: Core::System& system; /// Identifier string used to connect to the service. const char* service_name; + /// Whether this is the IStorage service. + const bool is_i_storage; /// Function used to safely up-cast pointers to the derived class before invoking a handler. InvokerFn* handler_invoker; /// Maximum number of concurrent sessions that this service can handle.