From d43b3d2632b621a0c0d3b5e17f680c4eab0243b1 Mon Sep 17 00:00:00 2001 From: failaip12 <86018517+failaip12@users.noreply.github.com> Date: Tue, 3 Nov 2026 22:34:17 +0100 Subject: [PATCH 01/11] dynarmic: pack both halves in the AVX-512 16-bit vector CLZ vpackusdw on the xmm form only packed the low four lzcnt results, so lanes 4 to 7 of CLZ Vd.8H came out as 0xfff0. Extract the upper half and pack it as the second source. Fixes the A64: CLZ test. Assisted-by: Claude Opus 5.5 (investigation and implementation) Signed-off-by: failaip12 <86018517+failaip12@users.noreply.github.com> --- src/dynarmic/src/dynarmic/backend/x64/emit_x64_vector.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/dynarmic/src/dynarmic/backend/x64/emit_x64_vector.cpp b/src/dynarmic/src/dynarmic/backend/x64/emit_x64_vector.cpp index 1165abb0e4..111effd2bd 100644 --- a/src/dynarmic/src/dynarmic/backend/x64/emit_x64_vector.cpp +++ b/src/dynarmic/src/dynarmic/backend/x64/emit_x64_vector.cpp @@ -899,11 +899,11 @@ void EmitX64::EmitVectorCountLeadingZeros16(EmitContext& ctx, IR::Inst* inst) { auto const tmp0 = ctx.reg_alloc.UseScratchXmm(code, args[0]); auto const tmp1 = ctx.reg_alloc.ScratchXmm(code); // 11-latency: unpack 16-bit(128-bit) XMM into 32-bit(256-bit) YMM vector - // then just lzcnt that, pack it back (unsigned) to 16-bit + // then just lzcnt that, pack both 128-bit halves back (unsigned) to 16-bit // then subtract 16-bit cuz of the zext32 /*4*/ code.vpmovzxwd(tmp0.cvt256(), tmp0); /*4*/ code.vplzcntd(tmp0.cvt256(), tmp0.cvt256()); - /*1*/ code.vpxor(tmp1, tmp1, tmp1); + /*3*/ code.vextracti128(tmp1, tmp0.cvt256(), 1); /*1*/ code.vpackusdw(tmp0, tmp0, tmp1); /*1*/ code.vpaddw(tmp0, tmp0, code.BConst<16>(xword, 65520)); /*4*/ code.vzeroupper(); From 4c0cbe1b0ef1dd6d02b39f61f7a8cbccea7f1ac8 Mon Sep 17 00:00:00 2001 From: failaip12 <86018517+failaip12@users.noreply.github.com> Date: Tue, 3 Nov 2026 22:34:17 +0100 Subject: [PATCH 02/11] audio_core: add renderer systems under the list lock SystemManager::Add appended to systems after releasing mutex1, while the render thread iterates the list under mutex1. Assisted-by: Claude Opus 5.5 (investigation and implementation) Signed-off-by: failaip12 <86018517+failaip12@users.noreply.github.com> --- src/audio_core/renderer/system_manager.cpp | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/audio_core/renderer/system_manager.cpp b/src/audio_core/renderer/system_manager.cpp index d2c924fdf2..9da660d2d3 100644 --- a/src/audio_core/renderer/system_manager.cpp +++ b/src/audio_core/renderer/system_manager.cpp @@ -57,11 +57,9 @@ bool SystemManager::Add(System& system_) { LOG_ERROR(Service_Audio, "Maximum AudioRenderer Systems active, cannot add more!"); return false; } - { - std::scoped_lock l{mutex1}; - if (systems.empty()) - InitializeUnsafe(); - } + std::scoped_lock l{mutex1}; + if (systems.empty()) + InitializeUnsafe(); systems.push_back(&system_); return true; } From ecd4e0f22a81c251f11b54d537c4dfb267f3f583 Mon Sep 17 00:00:00 2001 From: failaip12 <86018517+failaip12@users.noreply.github.com> Date: Tue, 3 Nov 2026 22:34:17 +0100 Subject: [PATCH 03/11] video_core: destroy the renderer before the GPU channels GPU::Impl declares channels after renderer, so the channel states were freed first. The Vulkan rasterizer destructor then finished the scheduler, which wrote invalidation flags into the freed Maxwell3D dirty state of the bound channel. Assisted-by: Claude Opus 5.5 (investigation and implementation) Signed-off-by: failaip12 <86018517+failaip12@users.noreply.github.com> --- src/video_core/gpu.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/video_core/gpu.cpp b/src/video_core/gpu.cpp index 6dbabcb43a..666d5de192 100644 --- a/src/video_core/gpu.cpp +++ b/src/video_core/gpu.cpp @@ -62,7 +62,11 @@ struct GPU::Impl { , is_async{is_async_} {} - ~Impl() = default; + ~Impl() { + // The renderer's teardown still flushes work that touches the bound channel's state, + // and `channels` is declared after `renderer`, so it would be freed first. + renderer.reset(); + } std::shared_ptr CreateChannel(s32 channel_id) { auto channel_state = std::make_shared(channel_id); From d1f6ead1b876d96cbc6db96d26de4d00fe827b7d Mon Sep 17 00:00:00 2001 From: failaip12 <86018517+failaip12@users.noreply.github.com> Date: Tue, 3 Nov 2026 22:34:17 +0100 Subject: [PATCH 04/11] video_core: track exact 64-byte units in the buffer usage tracker The page mask came from size >> 6, so a range under 64 bytes shifted by 64 (undefined; all ones on x86) and a range with a partly used unit at its end left that unit unmarked, letting CanReorderUpload move an upload before a draw that reads it. Build the mask from the first and last touched unit, and stop IsUsed after the first page when the range ends in it, as Track does. Assisted-by: Claude Opus 5.5 (investigation and implementation) Signed-off-by: failaip12 <86018517+failaip12@users.noreply.github.com> --- src/video_core/buffer_cache/usage_tracker.h | 29 ++++++++++++++------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/src/video_core/buffer_cache/usage_tracker.h b/src/video_core/buffer_cache/usage_tracker.h index 3e2da8bc0b..e3a66d7d0c 100644 --- a/src/video_core/buffer_cache/usage_tracker.h +++ b/src/video_core/buffer_cache/usage_tracker.h @@ -6,6 +6,7 @@ #pragma once +#include #include #include #include @@ -67,6 +68,9 @@ class UsageTracker { if (IsPageUsed(page, offset, size)) { return true; } + if (page == page_end) { + return false; + } for (size_t i = page + 1; i < page_end; i++) { if (pages[i] != 0) { return true; @@ -78,21 +82,26 @@ class UsageTracker { } private: - void TrackPage(u64 page, u64 offset, u64 size) noexcept { + // One bit per 64 bytes: set every bit that the bytes [offset, offset + size) touch within + // the page, including partly covered units at both ends. + static u64 PageMask(u64 offset, u64 size) noexcept { const size_t offset_in_page = offset % BUFFER_PAGE_BYTES; + const size_t end_in_page = std::min( + offset_in_page + std::min(size, BUFFER_PAGE_BYTES), BUFFER_PAGE_BYTES); + if (end_in_page <= offset_in_page) { + return 0; + } const size_t first_bit = offset_in_page >> BUFFER_BYTES_PER_BITSHIFT; - const size_t num_bits = std::min(size, BUFFER_PAGE_BYTES) >> BUFFER_BYTES_PER_BITSHIFT; - const size_t mask = ~u64{0} >> (64 - num_bits); - pages[page] |= (~u64{0} & mask) << first_bit; + const size_t last_bit = (end_in_page - 1) >> BUFFER_BYTES_PER_BITSHIFT; + return (~u64{0} >> (63 - (last_bit - first_bit))) << first_bit; + } + + void TrackPage(u64 page, u64 offset, u64 size) noexcept { + pages[page] |= PageMask(offset, size); } bool IsPageUsed(u64 page, u64 offset, u64 size) const noexcept { - const size_t offset_in_page = offset % BUFFER_PAGE_BYTES; - const size_t first_bit = offset_in_page >> BUFFER_BYTES_PER_BITSHIFT; - const size_t num_bits = std::min(size, BUFFER_PAGE_BYTES) >> BUFFER_BYTES_PER_BITSHIFT; - const size_t mask = ~u64{0} >> (64 - num_bits); - const size_t mask2 = (~u64{0} & mask) << first_bit; - return (pages[page] & mask2) != 0; + return (pages[page] & PageMask(offset, size)) != 0; } private: From ea2a03a6714e8067bde2c6785a85c8f0751e88db Mon Sep 17 00:00:00 2001 From: failaip12 <86018517+failaip12@users.noreply.github.com> Date: Tue, 3 Nov 2026 22:34:17 +0100 Subject: [PATCH 05/11] core, common: fix data races in timing, perf stats and file logging pause_end_time is written by the pausing thread and read by the timing thread; make it atomic. Frontend timers poll GetAndResetPerfStats while Load and shutdown emplace and reset perf_stats; guard it with a mutex. FileBackend is written from every logging thread; make its byte count and enabled flag atomic. Assisted-by: Claude Opus 5.5 (investigation and implementation) Signed-off-by: failaip12 <86018517+failaip12@users.noreply.github.com> --- src/common/logging.cpp | 11 +++++++---- src/core/core.cpp | 19 ++++++++++++++----- src/core/core_timing.cpp | 9 +++++---- src/core/core_timing.h | 2 +- 4 files changed, 27 insertions(+), 14 deletions(-) diff --git a/src/common/logging.cpp b/src/common/logging.cpp index ed12d52532..e02ad6a41a 100644 --- a/src/common/logging.cpp +++ b/src/common/logging.cpp @@ -272,7 +272,10 @@ struct FileBackend final : public Backend { boost::replace_all(message, username, "user"); } #endif - bytes_written += file->WriteSpan(std::span{message.begin(), message.end()}); + // Threads log without a shared lock; fwrite locks the stream, these counters are atomic. + const auto written = file->WriteSpan(std::span{message.begin(), message.end()}); + const auto total_written = + bytes_written.fetch_add(written, std::memory_order_relaxed) + written; // Option to log each line rather than 4k buffers if (Settings::values.log_flush_line.GetValue()) @@ -281,7 +284,7 @@ struct FileBackend final : public Backend { using namespace Common::Literals; // Prevent logs from exceeding a set maximum size in the event that log entries are spammed. const auto write_limit = Settings::values.extended_logging.GetValue() ? 1_GiB : 100_MiB; - const bool write_limit_exceeded = bytes_written > write_limit; + const bool write_limit_exceeded = total_written > write_limit; if (entry.log_level >= Level::Error || write_limit_exceeded) { // Stop writing after the write limit is exceeded. // Don't close the file so we can print a stacktrace if necessary @@ -296,8 +299,8 @@ struct FileBackend final : public Backend { } private: std::optional file; - std::size_t bytes_written = 0; - bool enabled = true; + std::atomic bytes_written = 0; + std::atomic_bool enabled = true; }; #endif #ifdef _WIN32 diff --git a/src/core/core.cpp b/src/core/core.cpp index 00ee551ef8..305453c48a 100644 --- a/src/core/core.cpp +++ b/src/core/core.cpp @@ -453,10 +453,14 @@ struct System::Impl { } } - perf_stats.emplace(params.program_id); - // Reset counters and set time origin to current frame - GetAndResetPerfStats(); - perf_stats->BeginSystemFrame(); + { + // Frontend timers poll GetAndResetPerfStats from their own threads. + std::scoped_lock lk{perf_stats_mutex}; + perf_stats.emplace(params.program_id); + // Reset counters and set time origin to current frame + perf_stats->GetAndResetStats(core_timing.GetGlobalTimeUs()); + perf_stats->BeginSystemFrame(); + } const FileSys::PatchManager pm(params.program_id, system.GetFileSystemController(), system.GetContentProvider()); auto const metadata = pm.GetControlMetadata(); @@ -505,7 +509,10 @@ struct System::Impl { audio_core.reset(); gpu_core.reset(); host1x_core.reset(); - perf_stats.reset(); + { + std::scoped_lock lk{perf_stats_mutex}; + perf_stats.reset(); + } cpu_manager.Shutdown(); debugger.reset(); kernel.Shutdown(); @@ -536,6 +543,7 @@ struct System::Impl { } PerfStatsResults GetAndResetPerfStats() { + std::scoped_lock lk{perf_stats_mutex}; if (!perf_stats) { return {}; } @@ -570,6 +578,7 @@ struct System::Impl { std::optional general_channel_context; std::optional general_channel_event; std::optional perf_stats; + std::mutex perf_stats_mutex; std::optional host1x_core; std::optional device_memory; std::optional audio_core; diff --git a/src/core/core_timing.cpp b/src/core/core_timing.cpp index dfc3f6f2a6..491bdffb51 100644 --- a/src/core/core_timing.cpp +++ b/src/core/core_timing.cpp @@ -114,7 +114,7 @@ void CoreTiming::Pause(bool is_paused) { pause_event.Set(); if (!is_paused) { - pause_end_time = GetGlobalTimeNs().count(); + pause_end_time.store(GetGlobalTimeNs().count(), std::memory_order_relaxed); } } @@ -134,7 +134,7 @@ void CoreTiming::SyncPause(bool is_paused) { } if (!is_paused) { - pause_end_time = GetGlobalTimeNs().count(); + pause_end_time.store(GetGlobalTimeNs().count(), std::memory_order_relaxed); } } @@ -283,8 +283,9 @@ std::optional CoreTiming::Advance() { // If this event was scheduled into a pause, its time now is going to be way // behind. Re-set this event to continue from the end of the pause. auto next_time{evt.time + next_schedule_time}; - if (evt.time < pause_end_time) { - next_time = pause_end_time + next_schedule_time; + const s64 last_pause_end = pause_end_time.load(std::memory_order_relaxed); + if (evt.time < last_pause_end) { + next_time = last_pause_end + next_schedule_time; } event_queue.update(evt.handle, Event{next_time, event_fifo_id++, evt.type, diff --git a/src/core/core_timing.h b/src/core/core_timing.h index 298fc9595d..13d9d492d8 100644 --- a/src/core/core_timing.h +++ b/src/core/core_timing.h @@ -149,7 +149,7 @@ class CoreTiming { s64 timer_resolution_ns; #endif u64 event_fifo_id = 0; - s64 pause_end_time{}; + std::atomic pause_end_time{}; /// Cycle timing u64 cpu_ticks{}; s64 downcount{}; From e356a50f94ff35a09c97f838bae9f23bec7b4bb5 Mon Sep 17 00:00:00 2001 From: failaip12 <86018517+failaip12@users.noreply.github.com> Date: Tue, 3 Nov 2026 22:34:17 +0100 Subject: [PATCH 06/11] input_common: take the SDL joystick lock before the joystick map lock SDL runs the event watcher with its joystick lock held, and the watcher takes joystick_map_mutex. InitJoystick and CloseJoysticks took joystick_map_mutex first and then called into SDL, which could deadlock against a joystick event at startup or shutdown. Assisted-by: Claude Opus 5.5 (investigation and implementation) Signed-off-by: failaip12 <86018517+failaip12@users.noreply.github.com> --- src/input_common/drivers/sdl_driver.cpp | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/input_common/drivers/sdl_driver.cpp b/src/input_common/drivers/sdl_driver.cpp index a308554071..a1092e7e38 100644 --- a/src/input_common/drivers/sdl_driver.cpp +++ b/src/input_common/drivers/sdl_driver.cpp @@ -9,6 +9,7 @@ #include "common/logging.h" #include "common/math_util.h" #include "common/param_package.h" +#include "common/scope_exit.h" #include "common/settings.h" #include "common/thread.h" #include "common/vector_math.h" @@ -538,6 +539,12 @@ void SDLDriver::InitJoystick(SDL_JoystickID joystick_id) { } } + // SDL calls its event watcher with the joystick lock held, and the watcher then takes + // joystick_map_mutex. Take SDL's lock first here, since SDLJoystick calls into SDL. + SDL_LockJoysticks(); + SCOPE_EXIT { + SDL_UnlockJoysticks(); + }; std::scoped_lock lock{joystick_map_mutex}; if (joystick_map.find(guid) == joystick_map.end()) { auto joystick = std::make_shared(guid, 0, sdl_joystick, sdl_gamecontroller); @@ -656,6 +663,11 @@ void SDLDriver::HandleGameControllerEvent(const SDL_Event& event) { } void SDLDriver::CloseJoysticks() { + // Same lock order as InitJoystick: clearing the map closes SDL handles. + SDL_LockJoysticks(); + SCOPE_EXIT { + SDL_UnlockJoysticks(); + }; std::scoped_lock lock{joystick_map_mutex}; joystick_map.clear(); } From 14792415c5fb41bbcfe4b160be4f6fb1d47d91b5 Mon Sep 17 00:00:00 2001 From: failaip12 <86018517+failaip12@users.noreply.github.com> Date: Tue, 3 Nov 2026 22:34:17 +0100 Subject: [PATCH 07/11] hid_core: skip unassigned applet slots in the button handler UpdateAllButtonLifo and UpdateCoreBatteryState used the shared memory of every aruid slot, which is null for unassigned ones. Check it like the six-axis handler does. Assisted-by: Claude Opus 5.5 (investigation and implementation) Signed-off-by: failaip12 <86018517+failaip12@users.noreply.github.com> --- .../resources/abstracted_pad/abstract_button_handler.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/hid_core/resources/abstracted_pad/abstract_button_handler.cpp b/src/hid_core/resources/abstracted_pad/abstract_button_handler.cpp index 239fc7656e..2e4eea1659 100644 --- a/src/hid_core/resources/abstracted_pad/abstract_button_handler.cpp +++ b/src/hid_core/resources/abstracted_pad/abstract_button_handler.cpp @@ -79,6 +79,9 @@ void NpadAbstractButtonHandler::UpdateAllButtonLifo() { Core::HID::NpadIdType npad_id = properties_handler->GetNpadId(); for (std::size_t i = 0; i < AruidIndexMax; i++) { auto* data = applet_resource_holder->applet_resource->GetAruidDataByIndex(i); + if (data == nullptr || !data->flag.is_assigned) { + continue; + } auto& npad_entry = data->shared_memory_format->npad.npad_entry[NpadIdTypeToIndex(npad_id)]; UpdateButtonLifo(npad_entry, data->aruid); } @@ -88,6 +91,9 @@ void NpadAbstractButtonHandler::UpdateCoreBatteryState() { Core::HID::NpadIdType npad_id = properties_handler->GetNpadId(); for (std::size_t i = 0; i < AruidIndexMax; i++) { auto* data = applet_resource_holder->applet_resource->GetAruidDataByIndex(i); + if (data == nullptr || !data->flag.is_assigned) { + continue; + } auto& npad_entry = data->shared_memory_format->npad.npad_entry[NpadIdTypeToIndex(npad_id)]; UpdateButtonLifo(npad_entry, data->aruid); } From 6375dc49bf6a453192d5dd2948053557c831cc6f Mon Sep 17 00:00:00 2001 From: failaip12 <86018517+failaip12@users.noreply.github.com> Date: Tue, 3 Nov 2026 22:34:17 +0100 Subject: [PATCH 08/11] audio_core, core, common: fix UB found by UBSan and an out-of-bounds read Read the Opus packet header with memcpy, since guest input has no alignment guarantee. VectorVfsFile::Read underflowed the remaining size for an offset past the end and copied out of bounds; return 0 instead. Skip the ScratchBuffer copy when there is no old buffer. Declare the ASan poisoning functions through the sanitizer header before zbic includes zstd.c, so ASan builds link. Assisted-by: Claude Opus 5.5 (investigation and implementation) Signed-off-by: failaip12 <86018517+failaip12@users.noreply.github.com> --- src/audio_core/opus/decoder.cpp | 14 ++++++++++---- src/common/scratch_buffer.h | 4 +++- src/common/zbic_compression.cpp | 9 +++++++++ src/core/file_sys/vfs/vfs_vector.cpp | 3 +++ 4 files changed, 25 insertions(+), 5 deletions(-) diff --git a/src/audio_core/opus/decoder.cpp b/src/audio_core/opus/decoder.cpp index 1c13cb60e9..d12db9e792 100644 --- a/src/audio_core/opus/decoder.cpp +++ b/src/audio_core/opus/decoder.cpp @@ -4,6 +4,8 @@ // SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later +#include + #include "audio_core/opus/decoder.h" #include "audio_core/opus/hardware_opus.h" #include "audio_core/opus/parameters.h" @@ -117,8 +119,10 @@ Result OpusDecoder::DecodeInterleaved(u32* out_data_size, u64* out_time_taken, R_UNLESS(input_data.size_bytes() > sizeof(OpusPacketHeader), ResultInputDataTooSmall); - auto* header_p{reinterpret_cast(input_data.data())}; - OpusPacketHeader header{ReverseHeader(*header_p)}; + // Guest input has no alignment guarantee. + OpusPacketHeader header_raw; + std::memcpy(&header_raw, input_data.data(), sizeof(header_raw)); + OpusPacketHeader header{ReverseHeader(header_raw)}; R_UNLESS(in_data.size_bytes() >= header.size && header.size + sizeof(OpusPacketHeader) <= input_data.size_bytes(), @@ -160,8 +164,10 @@ Result OpusDecoder::DecodeInterleavedForMultiStream(u32* out_data_size, u64* out R_UNLESS(input_data.size_bytes() > sizeof(OpusPacketHeader), ResultInputDataTooSmall); - auto* header_p{reinterpret_cast(input_data.data())}; - OpusPacketHeader header{ReverseHeader(*header_p)}; + // Guest input has no alignment guarantee. + OpusPacketHeader header_raw; + std::memcpy(&header_raw, input_data.data(), sizeof(header_raw)); + OpusPacketHeader header{ReverseHeader(header_raw)}; LOG_TRACE(Service_Audio, "header size {:#x} input data size {:#x} in_data size {:#x}", header.size, input_data.size_bytes(), in_data.size_bytes()); diff --git a/src/common/scratch_buffer.h b/src/common/scratch_buffer.h index cad1506922..5ef52ea69b 100644 --- a/src/common/scratch_buffer.h +++ b/src/common/scratch_buffer.h @@ -65,7 +65,9 @@ class ScratchBuffer { void resize(size_type size) { if (size > buffer_capacity) { auto new_buffer = std::make_unique_for_overwrite(size); - std::memcpy(new_buffer.get(), buffer.get(), buffer_capacity * sizeof(T)); + if (buffer_capacity > 0) { + std::memcpy(new_buffer.get(), buffer.get(), buffer_capacity * sizeof(T)); + } buffer = std::move(new_buffer); buffer_capacity = size; } diff --git a/src/common/zbic_compression.cpp b/src/common/zbic_compression.cpp index 5e0e4d947b..9653e97e24 100644 --- a/src/common/zbic_compression.cpp +++ b/src/common/zbic_compression.cpp @@ -17,6 +17,15 @@ #endif #include "zstd.h" +// zstd.c declares the ASan poisoning functions itself, which gives them C++ linkage here and +// breaks ASan links. Declaring them first through the sanitizer header keeps C linkage. +#if defined(__SANITIZE_ADDRESS__) +#include +#elif defined(__has_feature) +#if __has_feature(address_sanitizer) +#include +#endif +#endif #define g_ZSTD_threading_useless_symbol g_ZSTD_zbic_threading_useless_symbol #include "zstd.c" #undef g_ZSTD_threading_useless_symbol diff --git a/src/core/file_sys/vfs/vfs_vector.cpp b/src/core/file_sys/vfs/vfs_vector.cpp index 4d4a90f640..dfb35ecc09 100644 --- a/src/core/file_sys/vfs/vfs_vector.cpp +++ b/src/core/file_sys/vfs/vfs_vector.cpp @@ -40,6 +40,9 @@ bool VectorVfsFile::IsReadable() const { } std::size_t VectorVfsFile::Read(u8* data_, std::size_t length, std::size_t offset) const { + if (offset >= data.size()) { + return 0; + } const auto read = (std::min)(length, data.size() - offset); std::memcpy(data_, data.data() + offset, read); return read; From 87548593ac8baa370c1f1a7745b1460d92fee5d0 Mon Sep 17 00:00:00 2001 From: failaip12 <86018517+failaip12@users.noreply.github.com> Date: Tue, 3 Nov 2026 22:34:17 +0100 Subject: [PATCH 09/11] core: make the exit lock, exit request and NVDEC flags atomic They were 1-bit bit-fields in one byte. LockExit on a service thread and SetNVDECActive rewrote the same byte that the VI conductor reads, so a concurrent update could be lost. Assisted-by: Claude Opus 5.5 (investigation and implementation) Signed-off-by: failaip12 <86018517+failaip12@users.noreply.github.com> --- src/core/core.cpp | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/core/core.cpp b/src/core/core.cpp index 305453c48a..410ef8f91d 100644 --- a/src/core/core.cpp +++ b/src/core/core.cpp @@ -611,9 +611,11 @@ struct System::Impl { std::atomic_bool is_powered_on{}; bool is_multicore : 1 = false; bool extended_memory_layout : 1 = false; - bool exit_locked : 1 = false; - bool exit_requested : 1 = false; - bool nvdec_active : 1 = false; + // Separate atomics: these are set from service, NVDEC and frontend threads, and bit-fields + // sharing a byte lost concurrent updates. + std::atomic_bool exit_locked{}; + std::atomic_bool exit_requested{}; + std::atomic_bool nvdec_active{}; void EnsureGeneralChannelInitialized(System& system) { if (!general_channel_event) { From 9932bd647286591a7ebdc3e9e7366a42893493b0 Mon Sep 17 00:00:00 2001 From: failaip12 <86018517+failaip12@users.noreply.github.com> Date: Tue, 3 Nov 2026 22:45:41 +0100 Subject: [PATCH 10/11] tests: cover the buffer usage tracker masks and VectorVfsFile reads Check that the usage tracker marks partly used 64-byte units, does not mark a whole page for small ranges or a range ending on a page boundary, and handles ranges across pages. Check that VectorVfsFile::Read returns 0 for an offset at or past the end. Assisted-by: Claude Opus 5.5 (investigation and implementation) Signed-off-by: failaip12 <86018517+failaip12@users.noreply.github.com> --- src/tests/CMakeLists.txt | 2 ++ src/tests/core/vfs_vector.cpp | 25 +++++++++++++ src/tests/video_core/usage_tracker.cpp | 50 ++++++++++++++++++++++++++ 3 files changed, 77 insertions(+) create mode 100644 src/tests/core/vfs_vector.cpp create mode 100644 src/tests/video_core/usage_tracker.cpp diff --git a/src/tests/CMakeLists.txt b/src/tests/CMakeLists.txt index 6bf14f4cbd..7226224ee6 100644 --- a/src/tests/CMakeLists.txt +++ b/src/tests/CMakeLists.txt @@ -24,6 +24,7 @@ add_executable(tests core/internal_network/socket_proxy.cpp core/nup_writer.cpp core/priority_queue.cpp + core/vfs_vector.cpp core/internal_network/network.cpp video_core/memory_tracker.cpp video_core/vertex_repack.cpp @@ -32,6 +33,7 @@ add_executable(tests video_core/uniform_stream_reuse.cpp video_core/shader_invalidation.cpp video_core/descriptor_table.cpp + video_core/usage_tracker.cpp input_common/calibration_configuration_job.cpp ) diff --git a/src/tests/core/vfs_vector.cpp b/src/tests/core/vfs_vector.cpp new file mode 100644 index 0000000000..c51a0360eb --- /dev/null +++ b/src/tests/core/vfs_vector.cpp @@ -0,0 +1,25 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +#include +#include + +#include + +#include "core/file_sys/vfs/vfs_vector.h" + +namespace { +TEST_CASE("VectorVfsFile reads stop at the end of the data", "[core][vfs]") { + const FileSys::VectorVfsFile file(std::vector{1, 2, 3, 4}); + std::array out{}; + + REQUIRE(file.Read(out.data(), out.size(), 2) == 2); + REQUIRE(out[0] == 3); + REQUIRE(out[1] == 4); + REQUIRE(file.Read(out.data(), out.size(), 4) == 0); + REQUIRE(file.Read(out.data(), out.size(), 100) == 0); + + const FileSys::VectorVfsFile empty; + REQUIRE(empty.Read(out.data(), out.size(), 0) == 0); +} +} // namespace diff --git a/src/tests/video_core/usage_tracker.cpp b/src/tests/video_core/usage_tracker.cpp new file mode 100644 index 0000000000..bff1b57f17 --- /dev/null +++ b/src/tests/video_core/usage_tracker.cpp @@ -0,0 +1,50 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +#include + +#include "video_core/buffer_cache/usage_tracker.h" + +namespace { +using VideoCommon::UsageTracker; + +TEST_CASE("UsageTracker covers partly used 64-byte units", "[video_core][usage_tracker]") { + UsageTracker tracker(1 << 20); + tracker.Track(0, 100); + REQUIRE(tracker.IsUsed(0, 1)); + REQUIRE(tracker.IsUsed(64, 16)); + REQUIRE(tracker.IsUsed(96, 4)); + REQUIRE_FALSE(tracker.IsUsed(128, 64)); + + tracker.Reset(); + tracker.Track(60, 8); + REQUIRE(tracker.IsUsed(0, 4)); + REQUIRE(tracker.IsUsed(64, 4)); + REQUIRE_FALSE(tracker.IsUsed(128, 4)); +} + +TEST_CASE("UsageTracker does not mark a whole page for small ranges", "[video_core][usage_tracker]") { + UsageTracker tracker(1 << 20); + tracker.Track(4096 + 128, 16); + REQUIRE(tracker.IsUsed(4096 + 128, 16)); + REQUIRE_FALSE(tracker.IsUsed(4096 + 256, 64)); + REQUIRE_FALSE(tracker.IsUsed(4096, 64)); + + // A range ending on a page boundary marks nothing in the next page. + tracker.Reset(); + tracker.Track(4096 - 64, 64); + REQUIRE(tracker.IsUsed(4096 - 64, 64)); + REQUIRE_FALSE(tracker.IsUsed(4096, 64)); + REQUIRE_FALSE(tracker.IsUsed(4096 + 2048, 64)); +} + +TEST_CASE("UsageTracker spans pages", "[video_core][usage_tracker]") { + UsageTracker tracker(1 << 20); + tracker.Track(4096 - 32, 4096 + 64); + REQUIRE(tracker.IsUsed(4096 - 32, 1)); + REQUIRE(tracker.IsUsed(4096 + 1024, 1)); + REQUIRE(tracker.IsUsed(2 * 4096, 16)); + REQUIRE_FALSE(tracker.IsUsed(2 * 4096 + 64, 64)); + REQUIRE_FALSE(tracker.IsUsed(0, 64)); +} +} // namespace From 131a34258e4f167679178dccfa6e0fc7c3896f83 Mon Sep 17 00:00:00 2001 From: failaip12 <86018517+failaip12@users.noreply.github.com> Date: Tue, 3 Nov 2026 23:31:23 +0100 Subject: [PATCH 11/11] core: publish the pause end time before resuming the timer Pause(false) cleared paused and woke the timer thread before storing pause_end_time, so Advance could reschedule overdue looping events from the previous boundary. Store it first; the timer's load of paused orders it. Drop the second store in SyncPause. Reported by CodeRabbit. Assisted-by: Claude Opus 5.5 (investigation and implementation) Signed-off-by: failaip12 <86018517+failaip12@users.noreply.github.com> --- src/core/core_timing.cpp | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/core/core_timing.cpp b/src/core/core_timing.cpp index 491bdffb51..1f677c15c7 100644 --- a/src/core/core_timing.cpp +++ b/src/core/core_timing.cpp @@ -110,12 +110,13 @@ void CoreTiming::ClearPendingEvents() { } void CoreTiming::Pause(bool is_paused) { - paused = is_paused; - pause_event.Set(); - if (!is_paused) { + // Store the boundary before clearing paused: the timer thread runs Advance as soon as + // it reads paused == false, and that (sequentially consistent) load makes it visible. pause_end_time.store(GetGlobalTimeNs().count(), std::memory_order_relaxed); } + paused = is_paused; + pause_event.Set(); } void CoreTiming::SyncPause(bool is_paused) { @@ -132,10 +133,6 @@ void CoreTiming::SyncPause(bool is_paused) { while (paused_set != is_paused) ; } - - if (!is_paused) { - pause_end_time.store(GetGlobalTimeNs().count(), std::memory_order_relaxed); - } } bool CoreTiming::IsRunning() const {