From 665938d0b157ff1dce056b5c24a57aee9392929b Mon Sep 17 00:00:00 2001 From: snowstormxd <214507868+snowstormxd@users.noreply.github.com> Date: Mon, 2 Nov 2026 18:00:14 +0000 Subject: macro: execute native parameters without a second FIFO copy Use the owned parameter vector for DMA FIFO appends. Add a native JIT regression covering reallocation and repeated invocations against the copied-input path. Assisted by Codex. (cherry picked from commit 5b6803aaab18e332493903ee49cab026c041d5dc) --- src/tests/CMakeLists.txt | 1 + src/tests/video_core/native_macro_fifo.cpp | 74 ++++++++++++++++++++++++++++++ src/video_core/engines/maxwell_3d.cpp | 9 ++-- 3 files changed, 79 insertions(+), 5 deletions(-) create mode 100644 src/tests/video_core/native_macro_fifo.cpp (limited to 'src') diff --git a/src/tests/CMakeLists.txt b/src/tests/CMakeLists.txt index a9be2ec7a8..49fc646dea 100644 --- a/src/tests/CMakeLists.txt +++ b/src/tests/CMakeLists.txt @@ -22,6 +22,7 @@ add_executable(tests video_core/memory_tracker.cpp video_core/vertex_repack.cpp video_core/ampere_returns.cpp + video_core/native_macro_fifo.cpp input_common/calibration_configuration_job.cpp ) diff --git a/src/tests/video_core/native_macro_fifo.cpp b/src/tests/video_core/native_macro_fifo.cpp new file mode 100644 index 0000000000..e19b146192 --- /dev/null +++ b/src/tests/video_core/native_macro_fifo.cpp @@ -0,0 +1,74 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include +#include +#include + +#ifdef ARCHITECTURE_x86_64 +#include "video_core/macro_turing_jit.h" + +namespace { +std::array FifoInstruction(u32 method, bool end) { + std::array bits{}; + const auto field = [&](u32 offset, u32 count, u32 value) { + for (u32 bit = 0; bit < count; ++bit) { + bits[(offset + bit) / 32] |= ((value >> bit) & 1) << ((offset + bit) % 32); + } + }; + field(0, 1, end); + for (u32 lane = 0; lane < 2; ++lane) { + field(15 + lane * 36, 5, 24); + field(20 + lane * 36, 5, 24); + field(25 + lane * 36, 5, 24); + } + field(30, 16, method); + field(82, 3, 5); + field(85, 4, 3); + return {bits[2], bits[1], bits[0]}; +} +} // namespace + +TEST_CASE("Owned native macro FIFOs preserve loads across DMA appends and repeated calls", + "[video_core][native_macro]") { + std::vector code; + for (const auto instruction : {FifoInstruction(0x158, false), FifoInstruction(0x200, true), + FifoInstruction(0x201, false)}) { + code.insert(code.end(), instruction.begin(), instruction.end()); + } + Tegra::TuringMacroJit jit; + jit.Prepare(code, 1); + std::array ram{}; + std::vector parameters; + std::vector reference; + for (u32 invocation = 0; invocation < 1000; ++invocation) { + const u32 count = 2 + invocation % 127; + parameters.push_back(count); + reference.assign(parameters.begin(), parameters.end()); + const auto run = [&](std::vector& input) { + std::vector> writes; + const auto result = jit.Execute( + 0, input, ram, [](u32) { return 0U; }, + [&](u32 method, u32 value) { + writes.emplace_back(method, value); + if (method == 0x158) { + for (u32 word = 0; word < value; ++word) { + input.push_back(invocation * 256 + word); + } + } + return true; + }); + CHECK(result.success); + CHECK(result.consumed == 3); + return writes; + }; + const auto expected = run(reference); + const auto actual = run(parameters); + REQUIRE(actual.size() == 3); + CHECK(actual == expected); + CHECK(parameters == reference); + CHECK(actual[1] == std::pair{0x200, invocation * 256}); + CHECK(actual[2] == std::pair{0x201, invocation * 256 + 1}); + parameters.clear(); + } +} +#endif diff --git a/src/video_core/engines/maxwell_3d.cpp b/src/video_core/engines/maxwell_3d.cpp index 9731f54584..70191ebef0 100644 --- a/src/video_core/engines/maxwell_3d.cpp +++ b/src/video_core/engines/maxwell_3d.cpp @@ -818,7 +818,6 @@ struct Maxwell3D::FrontState { Regs::ShadowRamControl control{}; u32 executing_macro{}; std::vector parameters; - std::vector fifo; u64 calls{}; /// A method the draw manager handles was sent since the back last ran DrawDeferred: only /// then can it have a deferred instanced draw to issue at the end of a macro. @@ -1095,9 +1094,9 @@ void Maxwell3D::FrontRunMacro(Core::System& system, VideoCommon::GpuSplit& split engine.native_jit = std::make_shared(); } engine.native_jit->Prepare(engine.native_code, engine.native_code_generation); - fs.fifo.assign(fs.parameters.begin(), fs.parameters.end()); + const size_t parameter_count = fs.parameters.size(); const auto result = engine.native_jit->Execute( - start, fs.fifo, std::span{native_data_ram}, + start, fs.parameters, std::span{native_data_ram}, [&fs](u32 address) { return address < Regs::NUM_REGS ? fs.regs[address] : 0U; }, [&](u32 address, u32 value) { if (address >= MacroRegistersStart) { @@ -1113,7 +1112,7 @@ void Maxwell3D::FrontRunMacro(Core::System& system, VideoCommon::GpuSplit& split if (!result.success) { engine.native_execution_failed = true; LOG_ERROR(HW_GPU, "Native MME (JIT, split front) failed: start={:#x}, consumed={}/{}, error={}", - start, result.consumed, fs.parameters.size(), result.error); + start, result.consumed, parameter_count, result.error); } } fs.parameters.clear(); @@ -1148,7 +1147,7 @@ void Maxwell3D::FrontDmaRead(VideoCommon::GpuSplit& split, FrontState& fs, u32 m for (u32 i = 0; i < words; ++i) { const u32 word = memory_manager.Read(source + u64{i} * sizeof(u32)); if (method == 0x158) { - fs.fifo.push_back(word); + fs.parameters.push_back(word); } else if (ram + i < native_data_ram.size()) { native_data_ram[ram + i] = word; } -- cgit v1.2.3 From 1a346fb3bb69a5156231392aef9def6ce8d9477c Mon Sep 17 00:00:00 2001 From: snowstormxd <214507868+snowstormxd@users.noreply.github.com> Date: Mon, 2 Nov 2026 18:00:15 +0000 Subject: gpu: make register-write history opt-in in release builds Keep error reporting and debug-build history. EDEN_NX2_3D_HISTORY enables the full ring in release builds without recording every method during normal play. Assisted by Codex. (cherry picked from commit cac6081916e8e5461e2754308cd8986deaec774b) --- src/video_core/engines/maxwell_3d.cpp | 4 ++++ src/video_core/engines/maxwell_3d.h | 10 ++++++++++ 2 files changed, 14 insertions(+) (limited to 'src') diff --git a/src/video_core/engines/maxwell_3d.cpp b/src/video_core/engines/maxwell_3d.cpp index 70191ebef0..e00235d7a9 100644 --- a/src/video_core/engines/maxwell_3d.cpp +++ b/src/video_core/engines/maxwell_3d.cpp @@ -518,6 +518,10 @@ void Maxwell3D::DumpRecentWrites(u32 method) { } LOG_ERROR(HW_GPU, "Write to {:#x} while macro {:#x} collects parameters ({} so far); last writes:", method, executing_macro, macro_params.size()); + if (!record_write_history) { + LOG_ERROR(HW_GPU, "Register write history disabled; EDEN_NX2_3D_HISTORY=1 enables it"); + return; + } for (size_t i = 0; i < recent_writes.size(); ++i) { const auto& w = recent_writes[(recent_write_index + i) % recent_writes.size()]; if (w.amount != 0) { diff --git a/src/video_core/engines/maxwell_3d.h b/src/video_core/engines/maxwell_3d.h index af0e37978e..7b38e176b6 100644 --- a/src/video_core/engines/maxwell_3d.h +++ b/src/video_core/engines/maxwell_3d.h @@ -3349,7 +3349,17 @@ private: }; std::array recent_writes{}; size_t recent_write_index = 0; + const bool record_write_history{ +#ifdef NDEBUG + std::getenv("EDEN_NX2_3D_HISTORY") != nullptr +#else + true +#endif + }; void RecordWrite(u32 method, u32 argument, u32 amount, u32 pending) { + if (!record_write_history) { + return; + } recent_writes[recent_write_index++ % recent_writes.size()] = {method, argument, amount, pending}; } void DumpRecentWrites(u32 method); -- cgit v1.2.3 From 202c6b0356e1fa9366e5b9000df35a63f1eb9dc0 Mon Sep 17 00:00:00 2001 From: gabrielskaminski Date: Sun, 1 Nov 2026 01:49:26 -0300 Subject: video_core/buffer_cache: reuse unchanged small uniform buffers within command buffer (cherry picked from commit 4aa5cd89fd1862751b3297036f2d21a0dc924b23) (cherry picked from commit f1bdd5b13a1bdca81243b4aa1d8529f0485d3671) --- src/video_core/buffer_cache/buffer_cache.h | 20 +++- src/video_core/buffer_cache/uniform_stream_reuse.h | 121 +++++++++++++++++++++ src/video_core/renderer_vulkan/vk_buffer_cache.h | 31 +++++- 3 files changed, 168 insertions(+), 4 deletions(-) create mode 100644 src/video_core/buffer_cache/uniform_stream_reuse.h (limited to 'src') diff --git a/src/video_core/buffer_cache/buffer_cache.h b/src/video_core/buffer_cache/buffer_cache.h index cd176dec27..481b2f0f32 100644 --- a/src/video_core/buffer_cache/buffer_cache.h +++ b/src/video_core/buffer_cache/buffer_cache.h @@ -15,6 +15,7 @@ #include "common/range_sets.inc" #include "video_core/buffer_cache/buffer_cache_base.h" +#include "video_core/buffer_cache/uniform_stream_reuse.h" #include "video_core/guest_memory.h" #include "video_core/host1x/gpu_device_memory_manager.h" #include "video_core/nx2.h" @@ -28,6 +29,9 @@ using Core::DEVICE_PAGESIZE; template BufferCache

::BufferCache(Tegra::MaxwellDeviceMemoryManager& device_memory_, Runtime& runtime_) : runtime{runtime_}, device_memory{device_memory_}, memory_tracker{device_memory} { + // A new runtime has a new scheduler (its ticks restart) and a new stream buffer: whatever the + // stream uniform reuse remembers refers to the old ones. Before the early return below. + VideoCommon::ResetUniformStreamReuse(); // Ensure the first slot is used for the null buffer void(slot_buffers.insert(runtime, NullBufferParams{})); gpu_modified_ranges.Clear(); @@ -1078,8 +1082,20 @@ void BufferCache

::BindHostGraphicsUniformBuffer(size_t stage, u32 index, u32 channel_state->fast_bound_uniform_buffers[stage] |= 1u << binding_index; channel_state->uniform_buffer_binding_sizes[stage][binding_index] = size; // Stream buffer path to avoid stalling on non-Nvidia drivers or Vulkan - const std::span span = runtime.BindMappedUniformBuffer(stage, binding_index, size); - device_memory.ReadBlockUnsafe(device_addr, span.data(), size); + bool streamed = false; + if constexpr (!IS_OPENGL && VideoCommon::StreamUniformReusable) { + // The same bytes as this slot's previous bind, in the same command buffer: bind that + // stream allocation again instead of allocating and copying (uniform_stream_reuse.h). + // EDEN_NX2_UBOREUSE=0 turns it off. + if (VideoCommon::UniformStreamReuseEnabled()) { + streamed = VideoCommon::BindSmallUniformReusing( + runtime, device_memory, stage, index, binding_index, device_addr, size); + } + } + if (!streamed) { + const std::span span = runtime.BindMappedUniformBuffer(stage, binding_index, size); + device_memory.ReadBlockUnsafe(device_addr, span.data(), size); + } return; } // Classic cached path diff --git a/src/video_core/buffer_cache/uniform_stream_reuse.h b/src/video_core/buffer_cache/uniform_stream_reuse.h new file mode 100644 index 0000000000..7ba31c4717 --- /dev/null +++ b/src/video_core/buffer_cache/uniform_stream_reuse.h @@ -0,0 +1,121 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +#include "common/common_types.h" + +namespace VideoCommon { + +/// What a buffer cache runtime offers for stream uniform reuse. +/// - StreamUniformRef: a small trivially copyable description of one stream allocation (the +/// host buffer, its offset and whatever else the runtime needs to add it to a descriptor +/// payload again, e.g. the device address of the descriptor buffer path). +/// - LastStreamUniformRef(): the allocation made by the latest BindMappedUniformBuffer call. +/// - RebindStreamUniform(ref, size): adds `ref` to the descriptor payload exactly as +/// BindMappedUniformBuffer added the allocation the first time. +template +concept StreamUniformReusable = requires(R& runtime, u32 size) { + typename R::StreamUniformRef; + requires std::is_trivially_copyable_v; + { runtime.LastStreamUniformRef() } -> std::same_as; + runtime.RebindStreamUniform(runtime.LastStreamUniformRef(), size); + { runtime.CurrentTick() } -> std::convertible_to; +}; + +/// EDEN_NX2_UBOREUSE=0 turns the reuse off (every small uniform buffer is copied, as before). +[[nodiscard]] inline bool UniformStreamReuseEnabled() noexcept { + static const bool enabled = [] { + const char* const value = std::getenv("EDEN_NX2_UBOREUSE"); + return value == nullptr || value[0] != '0'; + }(); + return enabled; +} + +template +class UniformStreamReuse { +public: + static constexpr u32 MaxBytes = 4096; // BufferCache's DEFAULT_SKIP_CACHE_SIZE + static constexpr size_t NumStages = 5; + static constexpr size_t NumSlots = 18; // Maxwell constant buffers per stage + + struct Slot { + Ref ref{}; + u64 tick{}; + u32 size{}; // 0: nothing streamed yet + std::array bytes{}; + }; + + void Reset() noexcept { + for (auto& stage : slots) { + for (Slot& slot : stage) { + slot.size = 0; + slot.tick = 0; + } + } + } + + std::array, NumStages> slots{}; + std::array scratch{}; +}; + +template +[[nodiscard]] inline UniformStreamReuse& UniformStreamReuseState() noexcept { + static UniformStreamReuse state; + return state; +} + +/// Forgets every slot. Call it when a buffer cache (and so its runtime, scheduler and stream +/// buffer) is created: ticks restart with a new scheduler and the old references are dead. +template +inline void ResetUniformStreamReuse() noexcept { + if constexpr (StreamUniformReusable) { + UniformStreamReuseState().Reset(); + } +} + +/// Binds the small uniform buffer at `device_addr` for (stage, cbuf `index`) through the stream +/// buffer, reusing the slot's previous allocation when the bytes and the command buffer are the +/// same. Returns false (nothing bound) when the buffer does not qualify, so the caller binds it +/// the classic way. +template +[[nodiscard]] bool BindSmallUniformReusing(Runtime& runtime, DeviceMemory& device_memory, + size_t stage, u32 index, u32 binding_index, + u64 device_addr, u32 size) { + using Reuse = UniformStreamReuse; + if (size == 0 || size > Reuse::MaxBytes || stage >= Reuse::NumStages || + index >= Reuse::NumSlots) { + return false; + } + Reuse& reuse = UniformStreamReuseState(); + typename Reuse::Slot& slot = reuse.slots[stage][index]; + u8* const scratch = reuse.scratch.data(); + device_memory.ReadBlockUnsafe(device_addr, scratch, size); + + const bool same_bytes = + slot.size == size && std::memcmp(slot.bytes.data(), scratch, size) == 0; + if (same_bytes && slot.tick == runtime.CurrentTick()) { + runtime.RebindStreamUniform(slot.ref, size); + return true; + } + const std::span span = runtime.BindMappedUniformBuffer(stage, binding_index, size); + std::memcpy(span.data(), scratch, size); + slot.ref = runtime.LastStreamUniformRef(); + // After the allocation: the tick the stream region is tagged with is the one current now. + slot.tick = runtime.CurrentTick(); + if (!same_bytes) { + slot.size = size; + std::memcpy(slot.bytes.data(), scratch, size); + } + return true; +} + +} // namespace VideoCommon diff --git a/src/video_core/renderer_vulkan/vk_buffer_cache.h b/src/video_core/renderer_vulkan/vk_buffer_cache.h index d3295ec6ed..0587f28c64 100644 --- a/src/video_core/renderer_vulkan/vk_buffer_cache.h +++ b/src/video_core/renderer_vulkan/vk_buffer_cache.h @@ -168,15 +168,40 @@ public: void BindTransformFeedbackBuffers(VideoCommon::HostBindings& bindings); + /// This and the two members below are the hooks of VideoCommon::StreamUniformReusable + /// (buffer_cache/uniform_stream_reuse.h). + struct StreamUniformRef { + VkBuffer buffer; + VkDeviceAddress device_address; // base of `buffer`, 0 without buffer device address + u32 offset; + }; + std::span BindMappedUniformBuffer([[maybe_unused]] size_t stage, [[maybe_unused]] u32 binding_index, u32 size) { const StagingBufferRef ref = staging_pool.Request(size, MemoryUsage::Upload); - guest_descriptor_queue.AddBuffer(ref.buffer, ref.device_address, - static_cast(ref.offset), size); + last_stream_uniform = StreamUniformRef{ + .buffer = ref.buffer, + .device_address = ref.device_address, + .offset = static_cast(ref.offset), + }; + RebindStreamUniform(last_stream_uniform, size); return ref.mapped_span; } + /// The allocation of the latest BindMappedUniformBuffer call. + [[nodiscard]] StreamUniformRef LastStreamUniformRef() const noexcept { + return last_stream_uniform; + } + + /// Adds `ref` to the descriptor payload exactly as BindMappedUniformBuffer did when it made + /// the allocation (it is the very same call). The caller guarantees the allocation is still + /// live: made on the scheduler tick that is current now, so the staging pool has not recycled + /// it and the GPU has not even started on it. + void RebindStreamUniform(const StreamUniformRef& ref, u32 size) { + guest_descriptor_queue.AddBuffer(ref.buffer, ref.device_address, ref.offset, size); + } + [[nodiscard]] VkDeviceSize SparseAlignmentFor(bool sparse_compatible) const noexcept { if (!sparse_compatible || !multi_range_buffers.use_sparse) { return 0; @@ -276,6 +301,8 @@ private: boost::container::small_vector multi_range_sources; VkDeviceSize multi_range_total{}; + StreamUniformRef last_stream_uniform{}; + bool limit_dynamic_storage_buffers = false; u32 max_dynamic_storage_buffers = (std::numeric_limits::max)(); }; -- cgit v1.2.3 From b2430b4d90016eb6a221224687459c1876bc5552 Mon Sep 17 00:00:00 2001 From: snowstormxd <214507868+snowstormxd@users.noreply.github.com> Date: Sun, 1 Nov 2026 21:32:04 +0000 Subject: shader_cache: batch memory-write invalidations without skipping ranges Adapt the batching from ymatrix1/Eglantine PR #237, retaining all CPU invalidations. Acquire the lock once and remove pending shaders once per write batch. Compare real cache contents with sequential invalidation across randomized overlapping writes and pending removals. Assisted-by: Codex (implementation, review, commit text and automated validation) (cherry picked from commit b65f818d7789ffadfb58fe4543b18060fd88ce53) --- src/tests/CMakeLists.txt | 1 + src/tests/video_core/shader_invalidation.cpp | 120 +++++++++++++++++++++++ src/video_core/renderer_vulkan/vk_rasterizer.cpp | 8 +- src/video_core/shader_cache.cpp | 11 +++ src/video_core/shader_cache.h | 4 + 5 files changed, 139 insertions(+), 5 deletions(-) create mode 100644 src/tests/video_core/shader_invalidation.cpp (limited to 'src') diff --git a/src/tests/CMakeLists.txt b/src/tests/CMakeLists.txt index 49fc646dea..be5528ef46 100644 --- a/src/tests/CMakeLists.txt +++ b/src/tests/CMakeLists.txt @@ -23,6 +23,7 @@ add_executable(tests video_core/vertex_repack.cpp video_core/ampere_returns.cpp video_core/native_macro_fifo.cpp + video_core/shader_invalidation.cpp input_common/calibration_configuration_job.cpp ) diff --git a/src/tests/video_core/shader_invalidation.cpp b/src/tests/video_core/shader_invalidation.cpp new file mode 100644 index 0000000000..806fb5c58d --- /dev/null +++ b/src/tests/video_core/shader_invalidation.cpp @@ -0,0 +1,120 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include +#include +#include +#include +#include +#include +#include +#include "core/device_memory.h" +#include "video_core/shader_cache.h" + +namespace VideoCommon { +struct ShaderCacheTestAccess { + static std::unique_ptr Create(Tegra::MaxwellDeviceMemoryManager& memory) { + return std::unique_ptr{new ShaderCache{memory}}; + } + static void Register(ShaderCache& cache, VAddr address, size_t size, u64 hash) { + auto data = std::make_unique(); + data->unique_hash = hash; + cache.Register(std::move(data), address, size); + } + static const ShaderInfo* Find(const ShaderCache& cache, VAddr address) { + return cache.TryGet(address); + } +}; +} // namespace VideoCommon + +using VideoCommon::ShaderCacheTestAccess; + +TEST_CASE("Shader batch invalidation matches sequential cache contents", "[shader_invalidation]") { + Core::DeviceMemory memory; + Tegra::MaxwellDeviceMemoryManager device_memory{memory}; + auto batched = ShaderCacheTestAccess::Create(device_memory); + auto sequential = ShaderCacheTestAccess::Create(device_memory); + std::mt19937 random{0x16'67}; + constexpr VAddr base = 0x100000; + for (unsigned iteration = 0; iteration < 600; ++iteration) { + for (unsigned slot = 0; slot < 128; ++slot) { + const auto address = base + slot * 4096; + if (!ShaderCacheTestAccess::Find(*batched, address)) { + const auto size = 1 + random() % 65536; + ShaderCacheTestAccess::Register(*batched, address, size, iteration + slot); + ShaderCacheTestAccess::Register(*sequential, address, size, iteration + slot); + } + } + std::vector> writes; + for (unsigned i = 0, count = random() % 24; i < count; ++i) { + writes.emplace_back(base + random() % (128 * 4096), random() % 32768); + if (i % 4 == 0) { + writes.push_back(writes.back()); + } + } + for (const auto& [address, size] : writes) { + sequential->InvalidateRegion(address, size); + } + batched->InvalidateRegions(writes); + for (unsigned slot = 0; slot < 128; ++slot) { + const auto address = base + slot * 4096; + const auto actual = ShaderCacheTestAccess::Find(*batched, address); + const auto expected = ShaderCacheTestAccess::Find(*sequential, address); + REQUIRE(bool(actual) == bool(expected)); + if (actual) { + REQUIRE(actual->unique_hash == expected->unique_hash); + } + } + } +} + +TEST_CASE("Shader empty batches preserve pending invalidations", "[shader_invalidation]") { + Core::DeviceMemory memory; + Tegra::MaxwellDeviceMemoryManager device_memory{memory}; + auto cache = ShaderCacheTestAccess::Create(device_memory); + constexpr VAddr address = 0x100000; + ShaderCacheTestAccess::Register(*cache, address, 32768, 123); + cache->OnCacheInvalidation(address, 1); + cache->InvalidateRegions({}); + REQUIRE(ShaderCacheTestAccess::Find(*cache, address)); + const std::array unrelated{std::pair{address + 0x100000, 4096}}; + cache->InvalidateRegions(unrelated); + REQUIRE_FALSE(ShaderCacheTestAccess::Find(*cache, address)); +} + +TEST_CASE("Shader invalidation CPU benchmark", "[.][cache_bench]") { + Core::DeviceMemory memory; + Tegra::MaxwellDeviceMemoryManager device_memory{memory}; + auto batched = ShaderCacheTestAccess::Create(device_memory); + auto sequential = ShaderCacheTestAccess::Create(device_memory); + std::vector> writes; + constexpr VAddr base = 0x100000; + for (unsigned slot = 0; slot < 768; ++slot) { + writes.emplace_back(base + slot * 65536, 1); + } + double sequential_ms{}, batched_ms{}; + for (unsigned repeat = 0; repeat < 20; ++repeat) { + for (unsigned slot = 0; slot < 1024; ++slot) { + const auto address = base + slot * 65536; + if (!ShaderCacheTestAccess::Find(*batched, address)) { + ShaderCacheTestAccess::Register(*batched, address, 64, slot); + ShaderCacheTestAccess::Register(*sequential, address, 64, slot); + } + } + auto start = std::chrono::steady_clock::now(); + for (const auto& [address, size] : writes) { + sequential->InvalidateRegion(address, size); + } + sequential_ms += + std::chrono::duration(std::chrono::steady_clock::now() - start) + .count(); + start = std::chrono::steady_clock::now(); + batched->InvalidateRegions(writes); + batched_ms += + std::chrono::duration(std::chrono::steady_clock::now() - start) + .count(); + REQUIRE_FALSE(ShaderCacheTestAccess::Find(*batched, base)); + REQUIRE(ShaderCacheTestAccess::Find(*batched, base + 1023 * 65536)); + } + std::cout << "Shader invalidation: sequential=" << sequential_ms << " ms batched=" << batched_ms + << " ms (CPU only, not gameplay FPS)\n"; +} diff --git a/src/video_core/renderer_vulkan/vk_rasterizer.cpp b/src/video_core/renderer_vulkan/vk_rasterizer.cpp index 978f8be547..dc34260de5 100644 --- a/src/video_core/renderer_vulkan/vk_rasterizer.cpp +++ b/src/video_core/renderer_vulkan/vk_rasterizer.cpp @@ -1127,12 +1127,10 @@ void RasterizerVulkan::InnerInvalidation(std::span> sequences) { + if (sequences.empty()) { + return; + } + std::scoped_lock lock{invalidation_mutex}; + for (const auto& [addr, size] : sequences) { + InvalidatePagesInRegion(addr, size); + } + RemovePendingShaders(); +} + void ShaderCache::OnCacheInvalidation(VAddr addr, size_t size) { std::scoped_lock lock{invalidation_mutex}; InvalidatePagesInRegion(addr, size); diff --git a/src/video_core/shader_cache.h b/src/video_core/shader_cache.h index 281a90cb6f..7c370e217c 100644 --- a/src/video_core/shader_cache.h +++ b/src/video_core/shader_cache.h @@ -67,6 +67,8 @@ public: /// @param size Number of bytes of the invalidation void InvalidateRegion(VAddr addr, size_t size); + void InvalidateRegions(std::span> sequences); + /// @brief Unmarks a memory region as cached and marks it for removal /// @param addr Start address of the CPU write operation /// @param size Number of bytes of the CPU write operation @@ -104,6 +106,8 @@ protected: bool last_shaders_valid = false; private: + friend struct ShaderCacheTestAccess; + /// @brief Tries to obtain a cached shader starting in a given address /// @note Doesn't check for ranges, the given address has to be the start of the shader /// @param addr Start address of the shader, this doesn't cache for region -- cgit v1.2.3 From ddbd076cfe952170b5ad331fcdbff3afdb38453f Mon Sep 17 00:00:00 2001 From: snowstormxd <214507868+snowstormxd@users.noreply.github.com> Date: Sun, 1 Nov 2026 22:04:02 +0000 Subject: shader_cache: use sorted membership for large removal batches Keep the linear path for sixteen or fewer removals. For larger batches, sort the auxiliary pointer list and binary-search it while preserving storage order and the same invalidation decisions. Existing real-cache differential tests cover both paths. Assisted-by: Codex (implementation, review, commit text and automated validation) (cherry picked from commit 7e2c8a535230d9810db07706d79130ce0f1b88f1) --- src/video_core/shader_cache.cpp | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) (limited to 'src') diff --git a/src/video_core/shader_cache.cpp b/src/video_core/shader_cache.cpp index cafebaa56f..5d41dc9697 100644 --- a/src/video_core/shader_cache.cpp +++ b/src/video_core/shader_cache.cpp @@ -6,6 +6,7 @@ #include #include +#include #include #include #include @@ -315,10 +316,17 @@ void ShaderCache::RemovePendingShaders() { marked_for_removal.clear(); if (!removed_shaders.empty()) { - // Remove the given shaders from the cache - std::erase_if(storage, [&removed_shaders](const std::unique_ptr& shader) { - return std::ranges::find(removed_shaders, shader.get()) != removed_shaders.end(); - }); + if (removed_shaders.size() > 16) { + std::ranges::sort(removed_shaders, std::less{}); + std::erase_if(storage, [&removed_shaders](const std::unique_ptr& shader) { + return std::binary_search(removed_shaders.begin(), removed_shaders.end(), + shader.get(), std::less{}); + }); + } else { + std::erase_if(storage, [&removed_shaders](const std::unique_ptr& shader) { + return std::ranges::find(removed_shaders, shader.get()) != removed_shaders.end(); + }); + } } } -- cgit v1.2.3 From 1e37847a805e87a0cac9e78755fe103571329eaa Mon Sep 17 00:00:00 2001 From: snowstormxd <214507868+snowstormxd@users.noreply.github.com> Date: Sun, 1 Nov 2026 21:32:04 +0000 Subject: tests: verify deferred descriptors against eager invalidation Exercise 40,000 mutations and table switches plus repeated deletion across five channels. Make the existing descriptor read work with a memory reader fixture and add an explicit CPU benchmark with opaque invalidation calls. Assisted-by: Codex (implementation, review, commit text and automated validation) (cherry picked from commit 3b62a551da8ec0b4edf19a1c4e14fa2519dd20e2) --- src/tests/CMakeLists.txt | 1 + src/tests/video_core/descriptor_table.cpp | 134 ++++++++++++++++++++++++ src/video_core/texture_cache/descriptor_table.h | 5 +- 3 files changed, 138 insertions(+), 2 deletions(-) create mode 100644 src/tests/video_core/descriptor_table.cpp (limited to 'src') diff --git a/src/tests/CMakeLists.txt b/src/tests/CMakeLists.txt index be5528ef46..e064792e19 100644 --- a/src/tests/CMakeLists.txt +++ b/src/tests/CMakeLists.txt @@ -24,6 +24,7 @@ add_executable(tests video_core/ampere_returns.cpp video_core/native_macro_fifo.cpp video_core/shader_invalidation.cpp + video_core/descriptor_table.cpp input_common/calibration_configuration_job.cpp ) diff --git a/src/tests/video_core/descriptor_table.cpp b/src/tests/video_core/descriptor_table.cpp new file mode 100644 index 0000000000..b4c226241d --- /dev/null +++ b/src/tests/video_core/descriptor_table.cpp @@ -0,0 +1,134 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include +#include +#include +#include +#include +#include +#include "video_core/texture_cache/descriptor_table.h" + +namespace { +struct DescriptorMemory { + std::array, 2> banks{}; + + void ReadBlockUnsafe(GPUVAddr address, void* output, size_t size) const { + const auto bank = (address - 0x1000) / 0x1000; + const auto offset = (address - 0x1000) % 0x1000; + std::memcpy(output, reinterpret_cast(banks[bank].data()) + offset, size); + } +}; +} // namespace + +TEST_CASE("Descriptor invalidation matches eager refresh across mutations", "[descriptor_table]") { + DescriptorMemory memory; + VideoCommon::DescriptorTable table; + REQUIRE(table.Synchronize(0x1000, 127)); + std::array seen{}; + std::array previous{}; + std::mt19937 random{0x5080}; + unsigned bank{}; + u32 limit = 127; + for (unsigned i = 0; i < 40000; ++i) { + switch (random() % 5) { + case 0: + table.Invalidate(); + seen.fill(false); + break; + case 1: + memory.banks[random() % 2][random() % 128] = random(); + break; + case 2: { + const auto next_bank = random() % 2; + const u32 next_limit = random() % 2 ? 63 : 127; + const bool changed = next_bank != bank || next_limit != limit; + REQUIRE(table.Synchronize(0x1000 + next_bank * 0x1000, next_limit) == changed); + if (changed) { + seen.fill(false); + } + bank = next_bank; + limit = next_limit; + break; + } + default: { + const u32 index = random() % (limit + 1); + const u64 expected = memory.banks[bank][index]; + const bool changed = !seen[index] || previous[index] != expected; + const auto actual = table.Read(memory, index); + REQUIRE(actual.first == expected); + REQUIRE(actual.second == changed); + seen[index] = true; + previous[index] = expected; + } + } + } +} + +TEST_CASE("Repeated image deletion cannot reuse stale descriptor view IDs", "[descriptor_table]") { + DescriptorMemory memory; + std::array, 5> channels; + std::array, 5> view_ids{}; + unsigned live_generation = 1; + const auto visit = [&](size_t channel, u32 index) { + if (channels[channel].Read(memory, index).second) { + view_ids[channel][index] = live_generation; + } + return view_ids[channel][index]; + }; + for (size_t channel = 0; channel < channels.size(); ++channel) { + REQUIRE(channels[channel].Synchronize(0x1000, 127)); + for (u32 index = 0; index < 128; ++index) { + REQUIRE(visit(channel, index) == live_generation); + } + } + for (unsigned generation = 0; generation < 32; ++generation) { + ++live_generation; + for (unsigned deletion = 0; deletion < 50; ++deletion) { + for (auto& table : channels) { + table.Invalidate(); + } + } + for (size_t channel = 0; channel < channels.size(); ++channel) { + for (u32 index = 0; index < 128; ++index) { + REQUIRE(visit(channel, index) == live_generation); + REQUIRE_FALSE(channels[channel].Read(memory, index).second); + } + } + } +} + +TEST_CASE("Descriptor invalidation CPU benchmark", "[.][cache_bench]") { + DescriptorMemory memory; + const auto measure = [&](bool deferred) { + std::array, 5> tables; + for (auto& table : tables) { + REQUIRE(table.Synchronize(0x1000, 127)); + } + void (*volatile invalidate)(VideoCommon::DescriptorTable&) = + deferred ? +[](VideoCommon::DescriptorTable& table) { table.Invalidate(); } + : +[](VideoCommon::DescriptorTable& table) { + std::ranges::fill(table.read_descriptors, 0); + }; + size_t changed{}; + const auto start = std::chrono::steady_clock::now(); + for (unsigned batch = 0; batch < 2000; ++batch) { + for (unsigned deletion = 0; deletion < 50; ++deletion) { + for (auto& table : tables) { + invalidate(table); + } + } + for (auto& table : tables) { + changed += table.Read(memory, 0).second; + } + } + const auto ms = + std::chrono::duration(std::chrono::steady_clock::now() - start) + .count(); + REQUIRE(changed == 10000); + return ms; + }; + const double eager = measure(false); + const double deferred = measure(true); + std::cout << "Descriptor bitmap invalidation: eager=" << eager << " ms deferred=" << deferred + << " ms (CPU only, not gameplay FPS)\n"; +} diff --git a/src/video_core/texture_cache/descriptor_table.h b/src/video_core/texture_cache/descriptor_table.h index 75383809c5..32ce01eeed 100644 --- a/src/video_core/texture_cache/descriptor_table.h +++ b/src/video_core/texture_cache/descriptor_table.h @@ -35,9 +35,10 @@ public: invalidated = true; } - [[nodiscard]] std::pair Read(Tegra::MemoryManager const& gpu_memory, u32 index) noexcept { + template + [[nodiscard]] std::pair Read(const MemoryManager& gpu_memory, u32 index) noexcept { DEBUG_ASSERT(index <= current_limit); - if (invalidated) [[unlikely]] { + if (invalidated) { std::ranges::fill(read_descriptors, 0); invalidated = false; } -- cgit v1.2.3 From 24da16703dd2a9f883d80f3c5c0280d12767b5ea Mon Sep 17 00:00:00 2001 From: snowstormxd <214507868+snowstormxd@users.noreply.github.com> Date: Sun, 1 Nov 2026 22:04:01 +0000 Subject: texture_cache: clear only exposed descriptor bitmap words Avoid zeroing unused preallocation during invalidation and pool refresh. Clear newly exposed words whenever limits grow, bound clearing to allocated storage, and test word/allocation boundaries, shrink/regrowth and malformed large limits. Assisted-by: Codex (implementation, review, commit text and automated validation) (cherry picked from commit 4bc1ae01f79cef43be016504c850ca8a6998457a) --- src/tests/video_core/descriptor_table.cpp | 36 +++++++++++++++++++++++++ src/video_core/texture_cache/descriptor_table.h | 13 ++++++--- 2 files changed, 45 insertions(+), 4 deletions(-) (limited to 'src') diff --git a/src/tests/video_core/descriptor_table.cpp b/src/tests/video_core/descriptor_table.cpp index b4c226241d..8b20564949 100644 --- a/src/tests/video_core/descriptor_table.cpp +++ b/src/tests/video_core/descriptor_table.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include #include "video_core/texture_cache/descriptor_table.h" @@ -97,6 +98,41 @@ TEST_CASE("Repeated image deletion cannot reuse stale descriptor view IDs", "[de } } +TEST_CASE("Descriptor limits clear exposed slots after shrink and growth", "[descriptor_table]") { + struct AddressMemory { + void ReadBlockUnsafe(GPUVAddr address, void* output, size_t size) const { + std::memcpy(output, &address, size); + } + } memory; + constexpr std::array limits{0, 63, 64, 127, 128, 0x7ffff, 0x80000, 0x80001, 0x100000}; + for (const u32 limit : limits) { + VideoCommon::DescriptorTable table; + REQUIRE(table.Synchronize(0x1000, limit)); + CHECK(table.Read(memory, limit).second); + CHECK_FALSE(table.Read(memory, limit).second); + table.Refresh(0x1000, 0); + table.Invalidate(); + CHECK(table.Read(memory, 0).second); + CHECK_FALSE(table.Read(memory, 0).second); + table.Refresh(0x1000, limit); + CHECK(table.Read(memory, limit).second); + CHECK_FALSE(table.Read(memory, limit).second); + table.Invalidate(); + table.Invalidate(); + CHECK(table.Read(memory, limit).second); + CHECK_FALSE(table.Read(memory, limit).second); + } + for (const u32 limit : + {std::numeric_limits::max(), std::numeric_limits::max() - 0x1000}) { + VideoCommon::DescriptorTable table; + table.Refresh(0x1000, limit); + CHECK(table.Read(memory, 0).second); + table.Invalidate(); + CHECK(table.Read(memory, 0).second); + CHECK_FALSE(table.Read(memory, 0).second); + } +} + TEST_CASE("Descriptor invalidation CPU benchmark", "[.][cache_bench]") { DescriptorMemory memory; const auto measure = [&](bool deferred) { diff --git a/src/video_core/texture_cache/descriptor_table.h b/src/video_core/texture_cache/descriptor_table.h index 32ce01eeed..a441e572de 100644 --- a/src/video_core/texture_cache/descriptor_table.h +++ b/src/video_core/texture_cache/descriptor_table.h @@ -39,7 +39,7 @@ public: [[nodiscard]] std::pair Read(const MemoryManager& gpu_memory, u32 index) noexcept { DEBUG_ASSERT(index <= current_limit); if (invalidated) { - std::ranges::fill(read_descriptors, 0); + ClearReadDescriptors(); invalidated = false; } const GPUVAddr gpu_addr = current_gpu_addr + index * sizeof(T); @@ -63,10 +63,8 @@ public: // Mario Brothership reallocates a lot of times, so use aggressive pre-alloc sizes // std::vector by default uses quadratic growth, but that isn't even enough to satisfy brothership const size_t num_descriptors = ((limit + 0x80000) & (~0x7ffff)) + 1; - size_t old_size = read_descriptors.size(); read_descriptors.resize(Common::DivCeil(num_descriptors, 64U)); - old_size = (std::min)(old_size, read_descriptors.size()); - std::fill(read_descriptors.begin(), read_descriptors.begin() + old_size, 0); + ClearReadDescriptors(); invalidated = false; // descriptors.resize(num_descriptors); @@ -77,6 +75,13 @@ public: GPUVAddr current_gpu_addr{}; u32 current_limit{}; bool invalidated{}; ///< read_descriptors is to be cleared before the next read + +private: + void ClearReadDescriptors() noexcept { + // Reserved capacity is not addressable. A later limit increase clears its new range. + const size_t words = (std::min)(read_descriptors.size(), size_t{current_limit} / 64 + 1); + std::fill_n(read_descriptors.begin(), words, u64{0}); + } }; } // namespace VideoCommon -- cgit v1.2.3 From 226162edf525f13bb0c1fa5c4788e510c474989d Mon Sep 17 00:00:00 2001 From: snowstormxd <214507868+snowstormxd@users.noreply.github.com> Date: Mon, 2 Nov 2026 19:07:53 +0000 Subject: vulkan: submit native work sooner and use the active draw count Adapted from sylvieistaken/selevet-eglantine performance builds, including 8b076b167ffc5ca1ba8ffedcd00e255accbe018c and d8c95fef. Preserve upstream command-list coherency and submission ordering. Assisted by Codex. --- src/video_core/engines/draw_manager.cpp | 3 +++ src/video_core/engines/maxwell_3d.h | 1 + .../renderer_vulkan/vk_pipeline_cache.cpp | 2 +- src/video_core/renderer_vulkan/vk_rasterizer.cpp | 21 +++++++++++++++++++-- 4 files changed, 24 insertions(+), 3 deletions(-) (limited to 'src') diff --git a/src/video_core/engines/draw_manager.cpp b/src/video_core/engines/draw_manager.cpp index 038e5a2e69..9419dd5758 100644 --- a/src/video_core/engines/draw_manager.cpp +++ b/src/video_core/engines/draw_manager.cpp @@ -301,6 +301,8 @@ void Maxwell3D::DrawManager::UpdateTopology(Maxwell3D& maxwell3d) { } void Maxwell3D::DrawManager::ProcessDraw(Maxwell3D& maxwell3d, bool draw_indexed, u32 instance_count) { + draw_state.draw_element_count = + draw_indexed ? draw_state.index_buffer.count : draw_state.vertex_buffer.count; LOG_TRACE(HW_GPU, "called, topology={}, count={}", draw_state.topology, draw_indexed ? draw_state.index_buffer.count : draw_state.vertex_buffer.count); UpdateTopology(maxwell3d); if (maxwell3d.ShouldExecute()) { @@ -309,6 +311,7 @@ void Maxwell3D::DrawManager::ProcessDraw(Maxwell3D& maxwell3d, bool draw_indexed } void Maxwell3D::DrawManager::ProcessDrawIndirect(Maxwell3D& maxwell3d) { + draw_state.draw_element_count = 0; LOG_TRACE(HW_GPU, "called, topology={}, is_indexed={}, includes_count={}, buffer_size={}, max_draw_count={}", draw_state.topology, indirect_state.is_indexed, indirect_state.include_count, indirect_state.buffer_size, indirect_state.max_draw_counts); UpdateTopology(maxwell3d); if (maxwell3d.ShouldExecute()) { diff --git a/src/video_core/engines/maxwell_3d.h b/src/video_core/engines/maxwell_3d.h index 7b38e176b6..553760e843 100644 --- a/src/video_core/engines/maxwell_3d.h +++ b/src/video_core/engines/maxwell_3d.h @@ -3064,6 +3064,7 @@ public: Maxwell3D::Regs::PrimitiveTopology topology{}; DrawMode draw_mode{}; bool draw_indexed{}; + u32 draw_element_count{}; // Zero for indirect draws whose count is not known yet. u32 base_index{}; Maxwell3D::Regs::VertexBuffer vertex_buffer; Maxwell3D::Regs::IndexBuffer index_buffer; diff --git a/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp b/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp index ff8e84b019..22e54ad75c 100644 --- a/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp +++ b/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp @@ -1056,7 +1056,7 @@ GraphicsPipeline* PipelineCache::BuiltPipeline(GraphicsPipeline* pipeline) const // Usually these shaders are only used once for building textures so we can assume they // can't be built async const auto& draw_state = maxwell3d->draw_manager.draw_state; - if (draw_state.index_buffer.count <= 6 || draw_state.vertex_buffer.count <= 6) { + if (draw_state.draw_element_count <= 6) { return pipeline; } return nullptr; diff --git a/src/video_core/renderer_vulkan/vk_rasterizer.cpp b/src/video_core/renderer_vulkan/vk_rasterizer.cpp index dc34260de5..3898c571f5 100644 --- a/src/video_core/renderer_vulkan/vk_rasterizer.cpp +++ b/src/video_core/renderer_vulkan/vk_rasterizer.cpp @@ -1422,10 +1422,27 @@ void RasterizerVulkan::FlushWork() { #endif // __ANDROID__ static_assert(DRAWS_TO_DISPATCH % (CHECK_MASK + 1) == 0); - if ((++draw_counter & CHECK_MASK) != CHECK_MASK) { + if ((++draw_counter & CHECK_MASK) != 0) { return; } - if (draw_counter < DRAWS_TO_DISPATCH) { + // Keep the host GPU working while native commands are still being recorded. + static const u32 native_flush_draws = [] { + constexpr u32 fallback = 128; + const char* value = std::getenv("EDEN_NX2_FLUSH_DRAWS"); + if (value == nullptr || value[0] < '0' || value[0] > '9') { + return fallback; + } + char* end = nullptr; + const unsigned long parsed = std::strtoul(value, &end, 10); + if (*end != '\0' || parsed == 0) { + return fallback; + } + return static_cast(std::clamp(parsed, 32UL, 1'048'576UL)); + }(); + const u32 draws_to_dispatch = Tegra::g_nx2_ampere.load(std::memory_order_relaxed) + ? native_flush_draws + : DRAWS_TO_DISPATCH; + if (draw_counter < draws_to_dispatch) { scheduler.DispatchWork(); return; } -- cgit v1.2.3 From 00285925faebcd9fce6f12b4ebfaccdab491203c Mon Sep 17 00:00:00 2001 From: snowstormxd <214507868+snowstormxd@users.noreply.github.com> Date: Mon, 2 Nov 2026 19:07:54 +0000 Subject: core: skip empty bitmap ranges during cache and scheduler traversal Adapted bitmap scans from sylvieistaken/selevet-eglantine performance build 4. Keep the wide-priority fallback and fix the first live slot after empty bitmap words. Include randomized lifecycle and ordering checks. Assisted by Codex. --- src/common/slot_vector.h | 16 ++-- src/core/hle/kernel/k_priority_queue.h | 20 +++-- src/tests/CMakeLists.txt | 3 + src/tests/common/slot_vector.cpp | 54 ++++++++++++ src/tests/core/priority_queue.cpp | 111 ++++++++++++++++++++++++ src/tests/video_core/uniform_stream_reuse.cpp | 116 ++++++++++++++++++++++++++ 6 files changed, 307 insertions(+), 13 deletions(-) create mode 100644 src/tests/common/slot_vector.cpp create mode 100644 src/tests/core/priority_queue.cpp create mode 100644 src/tests/video_core/uniform_stream_reuse.cpp (limited to 'src') diff --git a/src/common/slot_vector.h b/src/common/slot_vector.h index f6da7a59d1..8e1c06e35a 100644 --- a/src/common/slot_vector.h +++ b/src/common/slot_vector.h @@ -52,12 +52,16 @@ public: const u64* const bitset = slot_vector->stored_bitset.data(); const u32 size = static_cast(slot_vector->stored_bitset.size()) * 64; if (id.Value() < size) { - do { - ++id.index; - } while (id.Value() < size && !IsValid(bitset)); - if (id.Value() == size) { - id = SlotId{}; + u32 index = id.Value() + 1; + while (index < size) { + const u64 word = bitset[index / 64] >> (index % 64); + if (word != 0) { + index += static_cast(std::countr_zero(word)); + break; + } + index = (index / 64 + 1) * 64; } + id = index < size ? SlotId{index} : SlotId{}; } return *this; } @@ -139,7 +143,7 @@ public: if (it == stored_bitset.end()) { return end(); } - const u32 word_index = static_cast(std::distance(it, stored_bitset.begin())); + const u32 word_index = static_cast(std::distance(stored_bitset.begin(), it)); const SlotId first_id{word_index * 64 + static_cast(std::countr_zero(*it))}; return Iterator(this, first_id); } diff --git a/src/core/hle/kernel/k_priority_queue.h b/src/core/hle/kernel/k_priority_queue.h index 89a0c5bbea..5d93455d33 100644 --- a/src/core/hle/kernel/k_priority_queue.h +++ b/src/core/hle/kernel/k_priority_queue.h @@ -11,6 +11,7 @@ #include #include #include +#include #include "common/alignment.h" #include "common/assert.h" @@ -196,12 +197,7 @@ public: constexpr Member* GetFront(s32 core) const { ASSERT(IsValidCore(core)); - const s32 priority = s32([](auto const& e) { - for (size_t i = 0; i < e.size(); ++i) - if (e[i]) - return i; - return e.size(); - }(m_available_priorities[core])); + const s32 priority = s32(GetNextSet(m_available_priorities[core], size_t(-1))); if (priority <= LowestPriority) { return m_queues[priority].GetFront(core); } else { @@ -222,7 +218,17 @@ public: template constexpr size_t GetNextSet(std::bitset const& bit, size_t n) const { - for (size_t i = n + 1; i < bit.size(); i++) + const size_t first = n + 1; + if (first >= bit.size()) { + return bit.size(); + } + if constexpr (N <= 64) { + if (!std::is_constant_evaluated()) { + const u64 remaining = bit.to_ullong() >> first; + return remaining != 0 ? first + size_t(std::countr_zero(remaining)) : bit.size(); + } + } + for (size_t i = first; i < bit.size(); i++) if (bit[i]) return i; return bit.size(); diff --git a/src/tests/CMakeLists.txt b/src/tests/CMakeLists.txt index e064792e19..461098954c 100644 --- a/src/tests/CMakeLists.txt +++ b/src/tests/CMakeLists.txt @@ -14,15 +14,18 @@ add_executable(tests common/range_map.cpp common/ring_buffer.cpp common/scratch_buffer.cpp + common/slot_vector.cpp common/undefined_fix.cpp common/unique_function.cpp core/core_timing.cpp core/nup_writer.cpp + core/priority_queue.cpp core/internal_network/network.cpp video_core/memory_tracker.cpp video_core/vertex_repack.cpp video_core/ampere_returns.cpp video_core/native_macro_fifo.cpp + video_core/uniform_stream_reuse.cpp video_core/shader_invalidation.cpp video_core/descriptor_table.cpp input_common/calibration_configuration_job.cpp diff --git a/src/tests/common/slot_vector.cpp b/src/tests/common/slot_vector.cpp new file mode 100644 index 0000000000..d8f242085c --- /dev/null +++ b/src/tests/common/slot_vector.cpp @@ -0,0 +1,54 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +#include +#include +#include +#include +#include "common/slot_vector.h" + +TEST_CASE("SlotVector visits live entries across empty bitmap words", "[common]") { + Common::SlotVector slots; + std::vector ids; + for (u32 i = 0; i < 513; ++i) { + ids.push_back(slots.insert(i)); + } + for (u32 i = 0; i < 512; ++i) { + slots.erase(ids[i]); + } + auto it = slots.begin(); + REQUIRE(it != slots.end()); + REQUIRE((*it).first == ids[512]); + REQUIRE(*(*it).second == 512); + REQUIRE(++it == slots.end()); + slots.erase(ids[512]); + REQUIRE(slots.begin() == slots.end()); +} + +TEST_CASE("SlotVector sparse iteration matches a live-entry model", "[common]") { + Common::SlotVector slots; + std::map live; + std::mt19937 random{9813}; + for (u32 batch = 0; batch < 200; ++batch) { + for (u32 i = 0; i < 100; ++i) { + if (!live.empty() && (random() % 3) == 0) { + auto selected = live.begin(); + std::advance(selected, random() % live.size()); + slots.erase(Common::SlotId{selected->first}); + live.erase(selected); + } else { + const u32 value = random(); + const auto id = slots.insert(value); + live.emplace(id.Value(), value); + } + } + auto expected = live.begin(); + for (const auto [id, value] : slots) { + REQUIRE(expected != live.end()); + REQUIRE(id.Value() == expected->first); + REQUIRE(*value == expected->second); + ++expected; + } + REQUIRE(expected == live.end()); + } +} diff --git a/src/tests/core/priority_queue.cpp b/src/tests/core/priority_queue.cpp new file mode 100644 index 0000000000..0d7174f793 --- /dev/null +++ b/src/tests/core/priority_queue.cpp @@ -0,0 +1,111 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +#include +#include +#include +#include +#include +#include "core/hle/kernel/k_priority_queue.h" + +namespace { +struct Affinity { + u64 GetAffinityMask() const { + return 3; + } + void SetAffinityMask(u64) {} + bool GetAffinity(s32) const { + return true; + } + void SetAffinity(s32, bool) {} + void SetAll() {} +}; + +struct Member { + struct QueueEntry { + Member* prev{}; + Member* next{}; + void Initialize() { + prev = next = nullptr; + } + void SetPrev(Member* value) { + prev = value; + } + void SetNext(Member* value) { + next = value; + } + Member* GetNext() const { + return next; + } + Member* GetPrev() const { + return prev; + } + }; + std::array entries{}; + s32 priority{}; + QueueEntry& GetPriorityQueueEntry(s32 core) { + return entries[core]; + } + const QueueEntry& GetPriorityQueueEntry(s32 core) const { + return entries[core]; + } + Affinity GetAffinityMask() const { + return {}; + } + s32 GetActiveCore() const { + return 0; + } + s32 GetPriority() const { + return priority; + } + bool IsDummyThread() const { + return false; + } +}; + +template +void CheckQueue() { + using Queue = Kernel::KPriorityQueue; + typename Queue::KPriorityQueueImpl queue; + std::array members{}; + std::array, 160> present{}; + std::array, Priorities>, 2> model{}; + std::mt19937 random{6152}; + for (size_t i = 0; i < members.size(); ++i) { + members[i].priority = static_cast(i % Priorities); + } + REQUIRE(queue.GetFront(0) == nullptr); + for (u32 operation = 0; operation < 3000; ++operation) { + const size_t index = random() % members.size(); + const s32 core = random() % 2; + auto& member = members[index]; + auto& expected = model[core][member.priority]; + if (present[index][core]) { + queue.Remove(member.priority, core, &member); + std::erase(expected, &member); + } else if ((random() & 1) != 0) { + queue.PushFront(member.priority, core, &member); + expected.insert(expected.begin(), &member); + } else { + queue.PushBack(member.priority, core, &member); + expected.push_back(&member); + } + present[index][core] = !present[index][core]; + for (s32 checked_core = 0; checked_core < 2; ++checked_core) { + Member* actual = queue.GetFront(checked_core); + for (const auto& priority : model[checked_core]) { + for (Member* wanted : priority) { + REQUIRE(actual == wanted); + actual = queue.GetNext(checked_core, actual); + } + } + REQUIRE(actual == nullptr); + } + } +} +} // namespace + +TEST_CASE("Priority queue bitmap traversal preserves thread order", "[core]") { + CheckQueue<64>(); + CheckQueue<128>(); +} diff --git a/src/tests/video_core/uniform_stream_reuse.cpp b/src/tests/video_core/uniform_stream_reuse.cpp new file mode 100644 index 0000000000..6059b70eeb --- /dev/null +++ b/src/tests/video_core/uniform_stream_reuse.cpp @@ -0,0 +1,116 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "video_core/buffer_cache/uniform_stream_reuse.h" + +namespace { +void Check(bool condition, const char* message) { + INFO(message); + REQUIRE(condition); +} + +struct Memory { + std::array bytes{}; + void ReadBlockUnsafe(u64 address, void* destination, u32 size) { + Check(address + size <= bytes.size(), "read outside guest memory"); + std::memcpy(destination, bytes.data() + address, size); + } +}; + +struct Runtime { + struct StreamUniformRef { + size_t allocation; + }; + std::vector> allocations; + StreamUniformRef last{}, bound{}; + u64 tick{1}; + u64 CurrentTick() const { + return tick; + } + StreamUniformRef LastStreamUniformRef() const { + return last; + } + std::span BindMappedUniformBuffer(size_t, u32, u32 size) { + allocations.emplace_back(size); + last = bound = StreamUniformRef{allocations.size() - 1}; + return allocations.back(); + } + void RebindStreamUniform(StreamUniformRef ref, u32 size) { + Check(ref.allocation < allocations.size(), "reused a recycled allocation"); + Check(allocations[ref.allocation].size() == size, "reused wrong allocation size"); + bound = ref; + } + void NextTick() { + ++tick; + allocations.clear(); + } +}; + +void CheckBound(const Runtime& runtime, const Memory& memory, u64 address, u32 size) { + const auto& bytes = runtime.allocations.at(runtime.bound.allocation); + Check(bytes.size() == size && + std::memcmp(bytes.data(), memory.bytes.data() + address, size) == 0, + "bound uniform data differs from guest memory"); +} +} // namespace + +TEST_CASE("Uniform stream reuse preserves data through mutation and session resets", + "[video_core]") { + using VideoCommon::BindSmallUniformReusing; + Memory memory; + Runtime runtime; + VideoCommon::ResetUniformStreamReuse(); + auto bind = [&](size_t stage, u32 slot, u64 address, u32 size) { + Check(BindSmallUniformReusing(runtime, memory, stage, slot, slot, address, size), + "valid small uniform rejected"); + CheckBound(runtime, memory, address, size); + }; + bind(0, 0, 0, 64); + bind(0, 0, 0, 64); + Check(runtime.allocations.size() == 1, "identical bytes were copied twice"); + memory.bytes[7] = 23; + bind(0, 0, 0, 64); + Check(runtime.allocations.size() == 2, "changed bytes reused stale data"); + bind(0, 1, 0, 64); + bind(1, 0, 0, 64); + Check(runtime.allocations.size() == 4, "stage or slot shared the wrong allocation"); + runtime.NextTick(); + bind(0, 0, 0, 64); + Check(runtime.allocations.size() == 1, "a new command buffer reused an old allocation"); + VideoCommon::ResetUniformStreamReuse(); + bind(0, 0, 0, 64); + Check(runtime.allocations.size() == 2, "session reset retained an old allocation"); + bind(4, 17, 0, 4096); + const auto before = runtime.allocations.size(); + for (auto [stage, slot, size] : + {std::array{5, 0, 64}, {0, 18, 64}, {0, 0, 0}, {0, 0, 4097}}) { + Check(!BindSmallUniformReusing(runtime, memory, stage, slot, slot, 0, size), + "invalid uniform range did not use the fallback"); + } + Check(runtime.allocations.size() == before, "fallback allocated stream memory"); + + u32 random = 17; + for (u32 index = 0; index < 10000; ++index) { + random = random * 1664525U + 1013904223U; + if (index % 37 == 0) + runtime.NextTick(); + if (index % 503 == 0) + VideoCommon::ResetUniformStreamReuse(); + const u32 size = 1 + random % 4096; + const u64 address = (random >> 12) % (memory.bytes.size() - 4096); + if (index % 3 == 0) + memory.bytes[address + random % size] ^= u8{0x5a}; + const size_t stage = (random >> 20) % 5; + const u32 slot = (random >> 24) % 18; + bind(stage, slot, address, size); + bind(stage, slot, address, size); + } +} -- cgit v1.2.3 From 9e5f9bbdb9fe6a717f7c26ffbd13e213a74435de Mon Sep 17 00:00:00 2001 From: snowstormxd <214507868+snowstormxd@users.noreply.github.com> Date: Mon, 2 Nov 2026 19:07:54 +0000 Subject: settings: default to handheld mode on all platforms Assisted by Codex. --- src/common/settings.h | 4 ---- 1 file changed, 4 deletions(-) (limited to 'src') diff --git a/src/common/settings.h b/src/common/settings.h index 74e2006d15..f253deb64b 100644 --- a/src/common/settings.h +++ b/src/common/settings.h @@ -770,11 +770,7 @@ struct Values { Setting current_user{linkage, 0, "current_user", Category::System}; SwitchableSetting use_docked_mode{linkage, -#ifdef __ANDROID__ ConsoleMode::Handheld, -#else - ConsoleMode::Docked, -#endif "use_docked_mode", Category::System, Specialization::Radio, -- cgit v1.2.3 From 43490c0417f6ef07a5999c2f8cf1c18e4e4a8520 Mon Sep 17 00:00:00 2001 From: snowstormxd <214507868+snowstormxd@users.noreply.github.com> Date: Mon, 2 Nov 2026 19:31:11 +0000 Subject: kernel: define thread-local state out of line for LTO Give the implementation-local TLS object one definition so ThinLTO retains its dynamic initializer. The state and its lifetime are unchanged. Assisted by Codex. --- src/core/hle/kernel/kernel.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/src/core/hle/kernel/kernel.cpp b/src/core/hle/kernel/kernel.cpp index f006d129a7..9e6ccc2d89 100644 --- a/src/core/hle/kernel/kernel.cpp +++ b/src/core/hle/kernel/kernel.cpp @@ -79,7 +79,7 @@ struct KernelCore::Impl { // And we have the guarantee that the data won't move out of the way so we can safely // take a reference to it. This isn't always universally true but this is "global" data // so it will be statically given a TLS slot anyways. - static inline thread_local ThreadLocalData tls_data = {}; + static thread_local ThreadLocalData tls_data; explicit Impl(Core::System& system_, KernelCore& kernel_) : system{system_} { tls_data.lock = true; @@ -861,6 +861,8 @@ struct KernelCore::Impl { Core::System& system; }; +thread_local ThreadLocalData KernelCore::Impl::tls_data{}; + KernelCore::KernelCore(Core::System& system) : impl{std::make_unique(system, *this)} {} KernelCore::~KernelCore() = default; -- cgit v1.2.3 From 6a0d50bcca77987d27857b029f5e3f90dd3d141c Mon Sep 17 00:00:00 2001 From: snowstormxd <214507868+snowstormxd@users.noreply.github.com> Date: Sun, 1 Nov 2026 19:02:26 +0000 Subject: tests: restore batched page accounting and isolate timer fixtures Apply batch updates in the rasterizer mock, check cached writes do not untrack twice, and reset callback state between timer cases. Assisted-by: Codex (implementation, review and automated validation) (cherry picked from commit 225ad4e5d1da4ec9883b17aea7273b6c8f76fe17) --- src/tests/core/core_timing.cpp | 3 +++ src/tests/video_core/memory_tracker.cpp | 38 ++++++++++++++++++++++++++------- 2 files changed, 33 insertions(+), 8 deletions(-) (limited to 'src') diff --git a/src/tests/core/core_timing.cpp b/src/tests/core/core_timing.cpp index 81898a1d38..6b424cc52c 100644 --- a/src/tests/core/core_timing.cpp +++ b/src/tests/core/core_timing.cpp @@ -33,6 +33,9 @@ std::optional HostCallbackTemplate(s64 time, struct ScopeInit final { ScopeInit() { + callbacks_ran_flags.reset(); + delays.fill(0); + expected_callback = 0; core_timing.SetMulticore(true); core_timing.Initialize([]() {}); } diff --git a/src/tests/video_core/memory_tracker.cpp b/src/tests/video_core/memory_tracker.cpp index c2236848b5..3ac4cee840 100644 --- a/src/tests/video_core/memory_tracker.cpp +++ b/src/tests/video_core/memory_tracker.cpp @@ -30,6 +30,10 @@ class RasterizerInterface { public: void UpdatePagesCachedCount(DAddr addr, size_t size, s32 delta) { ++update_calls; + ApplyPagesCachedCount(addr, size, delta); + } + + void ApplyPagesCachedCount(DAddr addr, size_t size, s32 delta) { calls.emplace_back(addr, size, delta); const u64 page_start{addr >> Core::DEVICE_PAGEBITS}; const u64 page_end{(addr + size + Core::DEVICE_PAGESIZE - 1) >> Core::DEVICE_PAGEBITS}; @@ -45,7 +49,13 @@ public: } void UpdatePagesCachedBatch(std::span> ranges, s32 delta) { - // TODO: for now assume fine? + if (ranges.empty()) { + return; + } + ++update_calls; + for (const auto& [addr, size] : ranges) { + ApplyPagesCachedCount(addr, size, delta); + } } [[nodiscard]] size_t UpdateCalls() const noexcept { return update_calls; } @@ -557,20 +567,32 @@ TEST_CASE("MemoryTracker: Cached write downloads") { REQUIRE(rasterizer.Count() == 0); } -TEST_CASE("MemoryTracker: FlushCachedWrites batching") { +TEST_CASE("MemoryTracker: cached writes preserve tracking counts") { RasterizerInterface rasterizer; std::optional memory_track(rasterizer); memory_track->UnmarkRegionAsCpuModified(c, WORD * 2); + REQUIRE(rasterizer.UpdateCalls() == 1); + REQUIRE(rasterizer.UpdateCallsList().size() == 1); + REQUIRE(rasterizer.UpdateCallsList().front() == std::tuple{c, WORD * 2, 1}); + memory_track->CachedCpuWrite(c + PAGE, PAGE); memory_track->CachedCpuWrite(c + PAGE * 2, PAGE); memory_track->CachedCpuWrite(c + PAGE * 4, PAGE); - REQUIRE(rasterizer.UpdateCalls() == 0); + REQUIRE(rasterizer.Count() == 125); + REQUIRE(rasterizer.Count(c + PAGE) == 0); + REQUIRE(rasterizer.Count(c + PAGE * 2) == 0); + REQUIRE(rasterizer.Count(c + PAGE * 3) == 1); + REQUIRE(rasterizer.Count(c + PAGE * 4) == 0); + const auto update_calls = rasterizer.UpdateCalls(); memory_track->FlushCachedWrites(); - // Now we expect a single batch call (coalesced ranges) to the device memory manager - REQUIRE(rasterizer.UpdateCalls() == 1); - const auto& calls = rasterizer.UpdateCallsList(); - REQUIRE(std::get<0>(calls[0]) == c + PAGE); - REQUIRE(std::get<1>(calls[0]) == PAGE * 3); + REQUIRE(rasterizer.UpdateCalls() == update_calls); + REQUIRE(rasterizer.Count() == 125); + REQUIRE(memory_track->IsRegionCpuModified(c + PAGE, PAGE)); + REQUIRE(memory_track->IsRegionCpuModified(c + PAGE * 2, PAGE)); + REQUIRE_FALSE(memory_track->IsRegionCpuModified(c + PAGE * 3, PAGE)); + REQUIRE(memory_track->IsRegionCpuModified(c + PAGE * 4, PAGE)); + memory_track->MarkRegionAsCpuModified(c, WORD * 2); + REQUIRE(rasterizer.Count() == 0); } TEST_CASE("DeviceMemoryManager: UpdatePagesCachedBatch basic") { -- cgit v1.2.3 From 5e7a14c057578d1c8262bec6f0e252801b071e87 Mon Sep 17 00:00:00 2001 From: snowstormxd <214507868+snowstormxd@users.noreply.github.com> Date: Sun, 1 Nov 2026 19:02:26 +0000 Subject: input: join calibration workers before destroying their owner The detached worker could use complete_event after its owner was destroyed. Retain and join it after signaling stop. Bind the test server to loopback on Windows and verify capture release across 32 cancellations. Assisted-by: Codex (implementation, review and automated validation) (cherry picked from commit 0eb7b4855f499838d056a0d5d678109318517e9d) --- src/input_common/drivers/udp_client.cpp | 4 ++-- src/input_common/drivers/udp_client.h | 1 + .../input_common/calibration_configuration_job.cpp | 20 +++++++++++++++++++- 3 files changed, 22 insertions(+), 3 deletions(-) (limited to 'src') diff --git a/src/input_common/drivers/udp_client.cpp b/src/input_common/drivers/udp_client.cpp index 620e0cf630..a7840db32f 100644 --- a/src/input_common/drivers/udp_client.cpp +++ b/src/input_common/drivers/udp_client.cpp @@ -603,7 +603,7 @@ CalibrationConfigurationJob::CalibrationConfigurationJob( const std::string& host, u16 port, std::function status_callback, std::function data_callback) { - std::thread([=, this] { + worker = std::jthread([=, this] { u16 min_x{UINT16_MAX}; u16 min_y{UINT16_MAX}; u16 max_x{}; @@ -647,7 +647,7 @@ CalibrationConfigurationJob::CalibrationConfigurationJob( complete_event.Wait(); socket.Stop(); worker_thread.join(); - }).detach(); + }); } CalibrationConfigurationJob::~CalibrationConfigurationJob() { diff --git a/src/input_common/drivers/udp_client.h b/src/input_common/drivers/udp_client.h index cea9f579ac..eac3df07aa 100644 --- a/src/input_common/drivers/udp_client.h +++ b/src/input_common/drivers/udp_client.h @@ -183,6 +183,7 @@ public: private: Common::Event complete_event; + std::jthread worker; }; void TestCommunication(const std::string& host, u16 port, diff --git a/src/tests/input_common/calibration_configuration_job.cpp b/src/tests/input_common/calibration_configuration_job.cpp index 6ded0dfc86..c45ef157a5 100644 --- a/src/tests/input_common/calibration_configuration_job.cpp +++ b/src/tests/input_common/calibration_configuration_job.cpp @@ -5,6 +5,7 @@ // SPDX-License-Identifier: GPL-2.0-or-later #include +#include #include #include #include @@ -19,7 +20,7 @@ class FakeCemuhookServer { public: FakeCemuhookServer() - : socket(io_context, boost::asio::ip::udp::endpoint(boost::asio::ip::udp::v4(), 0)) {} + : socket(io_context, boost::asio::ip::udp::endpoint(boost::asio::ip::address_v4::loopback(), 0)) {} ~FakeCemuhookServer() { boost::system::error_code error_code; @@ -132,3 +133,20 @@ TEST_CASE("CalibrationConfigurationJob completed", "[input_common]") { REQUIRE(max_x == 200); REQUIRE(max_y == 200); } + +TEST_CASE("CalibrationConfigurationJob cancellation joins its worker", "[input_common]") { + FakeCemuhookServer server; + for (unsigned i = 0; i < 32; ++i) { + auto lifetime = std::make_shared(0); + std::weak_ptr observer = lifetime; + { + InputCommon::CemuhookUDP::CalibrationConfigurationJob job( + server.GetHost(), server.GetPort(), + [lifetime](InputCommon::CemuhookUDP::CalibrationConfigurationJob::Status) {}, + [](u16, u16, u16, u16) {}); + lifetime.reset(); + job.Stop(); + } + REQUIRE(observer.expired()); + } +} -- cgit v1.2.3 From 6cdf4c28a77af1f3a4431c129bed11895630d83f Mon Sep 17 00:00:00 2001 From: snowstormxd <214507868+snowstormxd@users.noreply.github.com> Date: Mon, 2 Nov 2026 19:42:01 +0000 Subject: loader: find mixed-case loose RomFS directories consistently Linux missed base version metadata for Base/RomFS while Windows included it. Use a case-insensitive fallback for the loose directory and check the version and resulting archive checksum. Assisted by Codex. --- src/core/loader/deconstructed_rom_directory.cpp | 22 ++++++++++++++++++++-- src/tests/core/nup_writer.cpp | 5 ++++- 2 files changed, 24 insertions(+), 3 deletions(-) (limited to 'src') diff --git a/src/core/loader/deconstructed_rom_directory.cpp b/src/core/loader/deconstructed_rom_directory.cpp index 4b1168a59b..49352a5b49 100644 --- a/src/core/loader/deconstructed_rom_directory.cpp +++ b/src/core/loader/deconstructed_rom_directory.cpp @@ -81,6 +81,24 @@ struct PatchCollection { namespace { +FileSys::VirtualDir FindLooseRomFSDirectory(const FileSys::VirtualDir& parent) { + if (parent == nullptr) { + return nullptr; + } + if (const auto dir = parent->GetSubdirectory("romfs")) { + return dir; + } + for (const auto& dir : parent->GetSubdirectories()) { + const auto name = dir->GetName(); + if (name.size() == 5 && std::equal(name.begin(), name.end(), "romfs", [](char a, char b) { + return std::tolower(static_cast(a)) == b; + })) { + return dir; + } + } + return nullptr; +} + // RomFS for a loose ExeFS: the first .romfs file next to main, else a romfs/ directory next to // the exefs/ directory (the usual layout of an extracted title). FileSys::VirtualFile FindLooseRomFS(const FileSys::VirtualDir& exefs_dir) { @@ -93,7 +111,7 @@ FileSys::VirtualFile FindLooseRomFS(const FileSys::VirtualDir& exefs_dir) { } } if (const auto parent = exefs_dir->GetParentDirectory()) { - if (const auto romfs_dir = parent->GetSubdirectory("romfs")) { + if (const auto romfs_dir = FindLooseRomFSDirectory(parent)) { // Building it walks every file and keeps an object for each (title 1: 206k files, ~150 // MiB), so loaders of the same directory share one build while any of them holds it. static std::mutex cache_mutex; @@ -268,7 +286,7 @@ AppLoader_DeconstructedRomDirectory::AppLoader_DeconstructedRomDirectory(FileSys romfs_exefs_dir = file_dir; if (const auto title_dir = file_dir->GetParentDirectory()) { - romfs_version = RomFSProductVersion(title_dir->GetSubdirectory("romfs")); + romfs_version = RomFSProductVersion(FindLooseRomFSDirectory(title_dir)); } } diff --git a/src/tests/core/nup_writer.cpp b/src/tests/core/nup_writer.cpp index c0230d8694..79a4015e2c 100644 --- a/src/tests/core/nup_writer.cpp +++ b/src/tests/core/nup_writer.cpp @@ -197,9 +197,11 @@ TEST_CASE("NUP packs base, updates and add-ons and reads back through NupContain REQUIRE(packages[0].kind == NupContainer::PackageKind::Base); REQUIRE(packages[0].title_id == TitleId); REQUIRE(packages[0].version == 0); + REQUIRE(packages[0].display_version == "1.0.0"); REQUIRE(packages[0].npdm.size() == 0x100); REQUIRE(packages[1].kind == NupContainer::PackageKind::Update); REQUIRE(packages[1].version == 65536); + REQUIRE(packages[1].display_version == "1.1.0"); REQUIRE(packages[1].title_id == TitleId); REQUIRE_FALSE(packages[1].standalone_update); REQUIRE(packages[2].kind == NupContainer::PackageKind::Update); @@ -388,7 +390,8 @@ TEST_CASE("NUP output is reproducible", "[nup]") { for (const u8 b : Sha256(ReadAll(archive))) { hex += fmt::format("{:02x}", b); } - REQUIRE(hex == "df8446903ab2c39a51af0304935eb4aa5d8ead105440976ceae7ab97133f6143"); + // The mixed-case Base/RomFS directory supplies base version 1.0.0 on every platform. + REQUIRE(hex == "718231e0974ca04c7be7bd6ed4334015b777c6e5f1ed51de23f899f81ee004ab"); } TEST_CASE("NUP writes metadata overrides to the base and the newest update", "[nup]") { -- cgit v1.2.3 From b75ea7bcba60b70349274747cca04549908797bd Mon Sep 17 00:00:00 2001 From: snowstormxd <214507868+snowstormxd@users.noreply.github.com> Date: Mon, 2 Nov 2026 20:51:10 +0000 Subject: settings: restore the recommended MKW settings prompt Apply accepted settings visibly and save them, then reapply after per-game configuration. Include handheld mode. Assisted by Codex. --- src/common/game_presets.h | 35 ++++++++++++++ src/tests/CMakeLists.txt | 1 + src/tests/common/game_presets.cpp | 99 +++++++++++++++++++++++++++++++++++++++ src/yuzu/main_window.cpp | 30 ++++++++++++ src/yuzu/main_window.h | 1 + 5 files changed, 166 insertions(+) create mode 100644 src/common/game_presets.h create mode 100644 src/tests/common/game_presets.cpp (limited to 'src') diff --git a/src/common/game_presets.h b/src/common/game_presets.h new file mode 100644 index 0000000000..f8299d948f --- /dev/null +++ b/src/common/game_presets.h @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include "common/settings.h" + +namespace Settings { + +inline constexpr u64 MarioKartWorldProgramId = 0x0400C3F00006E000ULL; + +inline void SetMarioKartWorldRecommendedSettings(Values& settings, bool global) { + const auto session = [global](auto& setting, auto value) { + setting.SetGlobal(global); + setting.SetValue(value); + }; + session(settings.renderer_backend, RendererBackend::Vulkan); + session(settings.resolution_setup, ResolutionSetup::Res1X); + session(settings.gpu_accuracy, GpuAccuracy::Low); + session(settings.use_asynchronous_shaders, true); + session(settings.astc_recompression, AstcRecompression::Bc3); + session(settings.vram_usage_mode, VramUsageMode::Conservative); + session(settings.use_reactive_flushing, false); + session(settings.vsync_mode, VSyncMode::FifoRelaxed); + session(settings.use_docked_mode, ConsoleMode::Handheld); +} + +inline bool ApplyMarioKartWorldPreset(Values& settings, u64 program_id, bool accepted) { + if (!accepted || program_id != MarioKartWorldProgramId) { + return false; + } + SetMarioKartWorldRecommendedSettings(settings, false); + return true; +} + +} // namespace Settings diff --git a/src/tests/CMakeLists.txt b/src/tests/CMakeLists.txt index 461098954c..1eb1040e34 100644 --- a/src/tests/CMakeLists.txt +++ b/src/tests/CMakeLists.txt @@ -9,6 +9,7 @@ add_executable(tests common/cityhash.cpp common/container_hash.cpp common/fibers.cpp + common/game_presets.cpp common/host_memory.cpp common/param_package.cpp common/range_map.cpp diff --git a/src/tests/common/game_presets.cpp b/src/tests/common/game_presets.cpp new file mode 100644 index 0000000000..c036895ab8 --- /dev/null +++ b/src/tests/common/game_presets.cpp @@ -0,0 +1,99 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include +#include +#include "common/game_presets.h" + +namespace { +void SetConflictingPreferences(Settings::Values& settings, bool per_game) { + const auto set = [per_game](auto& setting, auto value) { + setting.SetGlobal(!per_game); + setting.SetValue(value); + }; + set(settings.renderer_backend, Settings::RendererBackend::Null); + set(settings.resolution_setup, Settings::ResolutionSetup::Res2X); + set(settings.gpu_accuracy, Settings::GpuAccuracy::High); + set(settings.use_asynchronous_shaders, false); + set(settings.astc_recompression, Settings::AstcRecompression::Uncompressed); + set(settings.vram_usage_mode, Settings::VramUsageMode::Aggressive); + set(settings.use_reactive_flushing, true); + set(settings.vsync_mode, Settings::VSyncMode::Immediate); +} + +void CheckPreset(const Settings::Values& settings) { + CHECK(settings.renderer_backend.GetValue() == Settings::RendererBackend::Vulkan); + CHECK(settings.resolution_setup.GetValue() == Settings::ResolutionSetup::Res1X); + CHECK(settings.gpu_accuracy.GetValue() == Settings::GpuAccuracy::Low); + CHECK(settings.use_asynchronous_shaders.GetValue()); + CHECK(settings.astc_recompression.GetValue() == Settings::AstcRecompression::Bc3); + CHECK(settings.vram_usage_mode.GetValue() == Settings::VramUsageMode::Conservative); + CHECK_FALSE(settings.use_reactive_flushing.GetValue()); + CHECK(settings.vsync_mode.GetValue() == Settings::VSyncMode::FifoRelaxed); + CHECK(settings.use_docked_mode.GetValue() == Settings::ConsoleMode::Handheld); +} + +void CheckPreferences(const Settings::Values& settings, bool global = false) { + CHECK(settings.renderer_backend.GetValue(global) == Settings::RendererBackend::Null); + CHECK(settings.resolution_setup.GetValue(global) == Settings::ResolutionSetup::Res2X); + CHECK(settings.gpu_accuracy.GetValue(global) == Settings::GpuAccuracy::High); + CHECK_FALSE(settings.use_asynchronous_shaders.GetValue(global)); + CHECK(settings.astc_recompression.GetValue(global) == + Settings::AstcRecompression::Uncompressed); + CHECK(settings.vram_usage_mode.GetValue(global) == Settings::VramUsageMode::Aggressive); + CHECK(settings.use_reactive_flushing.GetValue(global)); + CHECK(settings.vsync_mode.GetValue(global) == Settings::VSyncMode::Immediate); +} +} // namespace + +TEST_CASE("Accepted MKW settings replace visible global preferences", "[game_presets]") { + auto settings = std::make_unique(); + SetConflictingPreferences(*settings, false); + Settings::SetMarioKartWorldRecommendedSettings(*settings, true); + CheckPreset(*settings); + CHECK(settings->renderer_backend.UsingGlobal()); + CHECK(settings->gpu_accuracy.UsingGlobal()); + CHECK(settings->renderer_backend.GetValue(true) == Settings::RendererBackend::Vulkan); + CHECK(settings->gpu_accuracy.GetValue(true) == Settings::GpuAccuracy::Low); +} + +TEST_CASE("MKW preset overrides saved settings for the session", "[game_presets]") { + auto settings = std::make_unique(); + SetConflictingPreferences(*settings, false); + SetConflictingPreferences(*settings, true); + REQUIRE( + Settings::ApplyMarioKartWorldPreset(*settings, Settings::MarioKartWorldProgramId, true)); + CheckPreset(*settings); + CheckPreferences(*settings, true); + for (const auto& restore : settings->linkage.restore_functions) { + restore(); + } + CheckPreferences(*settings); + CHECK(settings->renderer_backend.UsingGlobal()); + CHECK(settings->resolution_setup.UsingGlobal()); + REQUIRE( + Settings::ApplyMarioKartWorldPreset(*settings, Settings::MarioKartWorldProgramId, true)); + CheckPreset(*settings); +} + +TEST_CASE("MKW preset does not change unrelated games", "[game_presets]") { + auto settings = std::make_unique(); + SetConflictingPreferences(*settings, false); + CHECK_FALSE(Settings::ApplyMarioKartWorldPreset(*settings, 0, true)); + CHECK_FALSE(Settings::ApplyMarioKartWorldPreset(*settings, + Settings::MarioKartWorldProgramId + 1, true)); + CheckPreferences(*settings); + CHECK(settings->renderer_backend.UsingGlobal()); + CHECK(settings->resolution_setup.UsingGlobal()); +} + +TEST_CASE("Declining the MKW preset preserves global and per-game preferences", "[game_presets]") { + auto settings = std::make_unique(); + for (bool per_game : {false, true}) { + SetConflictingPreferences(*settings, per_game); + CHECK_FALSE(Settings::ApplyMarioKartWorldPreset(*settings, + Settings::MarioKartWorldProgramId, false)); + CheckPreferences(*settings); + CHECK(settings->renderer_backend.UsingGlobal() == !per_game); + CHECK(settings->resolution_setup.UsingGlobal() == !per_game); + } +} diff --git a/src/yuzu/main_window.cpp b/src/yuzu/main_window.cpp index e5a8f81424..d0cd130f6f 100644 --- a/src/yuzu/main_window.cpp +++ b/src/yuzu/main_window.cpp @@ -10,6 +10,7 @@ #include #include "common/fs/path_util.h" +#include "common/game_presets.h" #include "common/settings.h" #include "common/settings_enums.h" #include "frontend_common/settings_generator.h" @@ -457,6 +458,29 @@ MainWindow::MainWindow(bool has_broken_vulkan) // Gen keys if necessary OnCheckFirmwareDecryption(false); + auto* recommendations = new QMessageBox( + QMessageBox::Question, tr("Recommended Mario Kart World settings"), + tr("Use the recommended settings for Mario Kart World?\n\n" + "Vulkan, 1x resolution, GPU Fast, asynchronous shaders, BC3, conservative VRAM, " + "reactive flushing off, FIFO Relaxed VSync, and handheld mode.\n\n" + "Yes replaces your saved settings. No or closing this window keeps your settings."), + QMessageBox::Yes | QMessageBox::No, this); + recommendations->setAttribute(Qt::WA_DeleteOnClose); + recommendations->setDefaultButton(QMessageBox::No); + recommendations->setEscapeButton(QMessageBox::No); + recommendations->setWindowModality(Qt::NonModal); + connect(recommendations, &QMessageBox::finished, this, [this](int result) { + use_recommended_mkw_settings = result == QMessageBox::Yes; + if (use_recommended_mkw_settings && !emulation_running && !boot_in_progress) { + Settings::SetMarioKartWorldRecommendedSettings(Settings::values, true); + config->SaveAllValues(); + QtCommon::system->ApplySettings(); + UpdateStatusButtons(); + } + LOG_INFO(Config, "MKW recommended settings accepted: {}", use_recommended_mkw_settings); + }); + recommendations->show(); + #ifdef __unix__ OnCheckGraphicsBackend(); #endif @@ -1964,6 +1988,12 @@ void MainWindow::BootGame(const QString& filename, Service::AM::FrontendAppletPa QtCommon::system->ApplySettings(); } + if (Settings::ApplyMarioKartWorldPreset(Settings::values, title_id, + use_recommended_mkw_settings)) { + QtCommon::system->ApplySettings(); + Settings::UpdateGPUAccuracy(); + LOG_INFO(Config, "Applied recommended MKW settings after per-game configuration"); + } Settings::LogSettings(); if (UISettings::values.select_user_on_boot && !user_flag_cmd_line) { diff --git a/src/yuzu/main_window.h b/src/yuzu/main_window.h index e2c5a0f6db..6fe967267a 100644 --- a/src/yuzu/main_window.h +++ b/src/yuzu/main_window.h @@ -501,6 +501,7 @@ private: #endif MultiplayerState* multiplayer_state = nullptr; + bool use_recommended_mkw_settings = false; GRenderWindow* render_window = nullptr; GameList* game_list = nullptr; -- cgit v1.2.3 From 06e211e0d0a1634d16f1d74d75682092fc92a5de Mon Sep 17 00:00:00 2001 From: snowstormxd <214507868+snowstormxd@users.noreply.github.com> Date: Sun, 1 Nov 2026 14:21:03 +0000 Subject: nx2: rearm clean read areas when the GPU writes them again Port the coherency changes from alarok/Eglantine 77fc365f2 without its diagnostic framework or shutdown changes. Only GPU-dirty CPU reads become permanently preflushable, clean areas can be trapped on the next write, non-core readers avoid indexing core arrays, and collected CPU writes precede readback. Assisted-by: Codex (source review, selective integration and verification) (cherry picked from commit b64e211957180aa218ec6d9a3bcb020332279c3f) --- src/core/memory.cpp | 15 +++++++++++++++ src/video_core/buffer_cache/buffer_cache.h | 13 +++++++++++-- src/video_core/gpu.cpp | 4 +++- 3 files changed, 29 insertions(+), 3 deletions(-) (limited to 'src') diff --git a/src/core/memory.cpp b/src/core/memory.cpp index cfc39916db..1f7315f3c1 100644 --- a/src/core/memory.cpp +++ b/src/core/memory.cpp @@ -549,6 +549,8 @@ struct Memory::Impl { } void RasterizerArmReadTraps(u64 vaddr, u64 size) { + // Clean cached read areas also need invalidation when reactive flushing is enabled. + read_trap_generation.fetch_add(1, std::memory_order_release); if (vaddr == 0 || size == 0 || !AddressSpaceContains(*current_page_table, vaddr, size) || !ReadTrapsEnabled()) { return; @@ -781,6 +783,19 @@ struct Memory::Impl { gpu_device_memory = &system.Host1x().MemoryManager(); } const size_t core = system.GetCurrentHostThreadID(); + if (core >= Core::Hardware::NUM_CPU_CORES) { + // A service thread has no slot in the arrays below, and it does not use a slot of a + // core. It keeps no area, so it asks on each read. + if (static std::atomic_flag logged; !logged.test_and_set(std::memory_order_relaxed)) { + LOG_INFO(HW_Memory, "Read of cached guest memory from a thread that is not a core"); + } + Common::ScratchBuffer scratch; + gpu_device_memory->ApplyOpOnPointer(p, scratch, [&](DAddr address) { + DisarmReadTraps(v_address, size); + (void)system.GPU().OnCPURead(address, size); + }); + return; + } auto& current_area = rasterizer_read_areas[core]; if (const u64 generation = read_trap_generation.load(std::memory_order_acquire); read_area_generations[core] != generation) { diff --git a/src/video_core/buffer_cache/buffer_cache.h b/src/video_core/buffer_cache/buffer_cache.h index 481b2f0f32..f6d394ce81 100644 --- a/src/video_core/buffer_cache/buffer_cache.h +++ b/src/video_core/buffer_cache/buffer_cache.h @@ -234,8 +234,17 @@ std::optional BufferCache

::GetFlushArea(DA }; area->preemtive = !IsRegionGpuModified(device_addr_start_aligned, device_addr_end_aligned - device_addr_start_aligned); - memory_tracker.MarkRegionAsPreflushable(device_addr_start_aligned, - device_addr_end_aligned - device_addr_start_aligned); + // Mark only data that the GPU wrote. A mark stays for the session and sends later GPU writes + // in the range to a download at each fence. + if (!area->preemtive) { + memory_tracker.MarkRegionAsPreflushable(device_addr_start_aligned, + device_addr_end_aligned - device_addr_start_aligned); + } else { + // The core keeps this area and has no mark. The next GPU write here must arm the range + // again, which ends the area and sets the trap. + read_trapped_ranges.Subtract(device_addr_start_aligned, + device_addr_end_aligned - device_addr_start_aligned); + } return area; } diff --git a/src/video_core/gpu.cpp b/src/video_core/gpu.cpp index 53f8f063f7..681a4a232d 100644 --- a/src/video_core/gpu.cpp +++ b/src/video_core/gpu.cpp @@ -228,7 +228,9 @@ struct GPU::Impl { raster_area.start_address, raster_area.end_address, reads.load(std::memory_order_relaxed)); } - const u64 fence = RequestSyncOperation([this, &raster_area]() { + const u64 fence = RequestSyncOperation([this, raster_area]() { + // Apply collected guest writes before downloading GPU data over guest memory. + InvalidateGPUCache(); renderer->ReadRasterizer()->FlushRegion(raster_area.start_address, raster_area.end_address - raster_area.start_address); }); gpu_thread.TickGPU(is_async); -- cgit v1.2.3 From 85436a5acd36108f8163efbb638945feba567ab4 Mon Sep 17 00:00:00 2001 From: snowstormxd <214507868+snowstormxd@users.noreply.github.com> Date: Mon, 2 Nov 2026 21:14:39 +0000 Subject: kernel: keep migrating fiber TLS lookups outside link-time optimization The game aborted during early thread synchronization with cross-module kernel TLS lookups. Compiling the kernel TLS implementation without LTO restores successful game startup while the other targets retain their build options. Assisted by Codex. --- src/core/CMakeLists.txt | 5 +++++ 1 file changed, 5 insertions(+) (limited to 'src') diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index eeb3202d12..1ff9f271d2 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -1290,4 +1290,9 @@ else() target_sources(core PRIVATE hle/service/ssl/ssl_backend_none.cpp) endif() +if (ENABLE_LTO AND CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang" AND NOT MSVC) + # Fibers can migrate between host threads; keep kernel TLS lookups outside LTO callers. + set_source_files_properties(hle/kernel/kernel.cpp PROPERTIES COMPILE_OPTIONS -fno-lto) +endif() + create_target_directory_groups(core) -- cgit v1.2.3 From 405adb698e5ed76b1252fec41d3cb8780d1e6a49 Mon Sep 17 00:00:00 2001 From: snowstormxd <214507868+snowstormxd@users.noreply.github.com> Date: Mon, 2 Nov 2026 21:14:39 +0000 Subject: settings: default to ScaleForce and FXAA Include both filters in the accepted MKW preset and verify defaults and saved-preference overrides. Assisted by Codex. --- src/common/game_presets.h | 2 ++ src/common/settings.h | 4 ++-- src/tests/common/game_presets.cpp | 12 ++++++++++++ src/yuzu/main_window.cpp | 2 +- 4 files changed, 17 insertions(+), 3 deletions(-) (limited to 'src') diff --git a/src/common/game_presets.h b/src/common/game_presets.h index f8299d948f..83dd4d3c3f 100644 --- a/src/common/game_presets.h +++ b/src/common/game_presets.h @@ -22,6 +22,8 @@ inline void SetMarioKartWorldRecommendedSettings(Values& settings, bool global) session(settings.use_reactive_flushing, false); session(settings.vsync_mode, VSyncMode::FifoRelaxed); session(settings.use_docked_mode, ConsoleMode::Handheld); + session(settings.scaling_filter, ScalingFilter::ScaleForce); + session(settings.anti_aliasing, AntiAliasing::Fxaa); } inline bool ApplyMarioKartWorldPreset(Values& settings, u64 program_id, bool accepted) { diff --git a/src/common/settings.h b/src/common/settings.h index f253deb64b..6669f98f4c 100644 --- a/src/common/settings.h +++ b/src/common/settings.h @@ -358,7 +358,7 @@ struct Values { true}; SwitchableSetting scaling_filter{linkage, - ScalingFilter::NearestNeighbor, + ScalingFilter::ScaleForce, "scaling_filter", Category::Renderer, Specialization::Default, @@ -387,7 +387,7 @@ struct Values { true}; SwitchableSetting anti_aliasing{linkage, - AntiAliasing::None, + AntiAliasing::Fxaa, "anti_aliasing", Category::Renderer, Specialization::Default, diff --git a/src/tests/common/game_presets.cpp b/src/tests/common/game_presets.cpp index c036895ab8..481f5c5e1d 100644 --- a/src/tests/common/game_presets.cpp +++ b/src/tests/common/game_presets.cpp @@ -18,6 +18,8 @@ void SetConflictingPreferences(Settings::Values& settings, bool per_game) { set(settings.vram_usage_mode, Settings::VramUsageMode::Aggressive); set(settings.use_reactive_flushing, true); set(settings.vsync_mode, Settings::VSyncMode::Immediate); + set(settings.scaling_filter, Settings::ScalingFilter::NearestNeighbor); + set(settings.anti_aliasing, Settings::AntiAliasing::Smaa); } void CheckPreset(const Settings::Values& settings) { @@ -30,6 +32,8 @@ void CheckPreset(const Settings::Values& settings) { CHECK_FALSE(settings.use_reactive_flushing.GetValue()); CHECK(settings.vsync_mode.GetValue() == Settings::VSyncMode::FifoRelaxed); CHECK(settings.use_docked_mode.GetValue() == Settings::ConsoleMode::Handheld); + CHECK(settings.scaling_filter.GetValue() == Settings::ScalingFilter::ScaleForce); + CHECK(settings.anti_aliasing.GetValue() == Settings::AntiAliasing::Fxaa); } void CheckPreferences(const Settings::Values& settings, bool global = false) { @@ -42,9 +46,17 @@ void CheckPreferences(const Settings::Values& settings, bool global = false) { CHECK(settings.vram_usage_mode.GetValue(global) == Settings::VramUsageMode::Aggressive); CHECK(settings.use_reactive_flushing.GetValue(global)); CHECK(settings.vsync_mode.GetValue(global) == Settings::VSyncMode::Immediate); + CHECK(settings.scaling_filter.GetValue(global) == Settings::ScalingFilter::NearestNeighbor); + CHECK(settings.anti_aliasing.GetValue(global) == Settings::AntiAliasing::Smaa); } } // namespace +TEST_CASE("Display filter defaults use ScaleForce and FXAA", "[game_presets]") { + auto settings = std::make_unique(); + CHECK(settings->scaling_filter.GetDefault() == Settings::ScalingFilter::ScaleForce); + CHECK(settings->anti_aliasing.GetDefault() == Settings::AntiAliasing::Fxaa); +} + TEST_CASE("Accepted MKW settings replace visible global preferences", "[game_presets]") { auto settings = std::make_unique(); SetConflictingPreferences(*settings, false); diff --git a/src/yuzu/main_window.cpp b/src/yuzu/main_window.cpp index d0cd130f6f..91afcd56ae 100644 --- a/src/yuzu/main_window.cpp +++ b/src/yuzu/main_window.cpp @@ -462,7 +462,7 @@ MainWindow::MainWindow(bool has_broken_vulkan) QMessageBox::Question, tr("Recommended Mario Kart World settings"), tr("Use the recommended settings for Mario Kart World?\n\n" "Vulkan, 1x resolution, GPU Fast, asynchronous shaders, BC3, conservative VRAM, " - "reactive flushing off, FIFO Relaxed VSync, and handheld mode.\n\n" + "reactive flushing off, FIFO Relaxed VSync, handheld mode, ScaleForce, and FXAA.\n\n" "Yes replaces your saved settings. No or closing this window keeps your settings."), QMessageBox::Yes | QMessageBox::No, this); recommendations->setAttribute(Qt::WA_DeleteOnClose); -- cgit v1.2.3