From d50ba85bf1cf90cb21d33dffd441562bcf6c50b8 Mon Sep 17 00:00:00 2001 From: Alleexxi <66967403+Alleexxi@users.noreply.github.com> Date: Fri, 6 Nov 2026 16:22:25 +0100 Subject: [PATCH 1/3] fix Wonder NS2E 30 FPS lock --- src/core/hle/service/nx2_gsv.h | 102 +++++++++++++++++++++++++++++---- 1 file changed, 92 insertions(+), 10 deletions(-) diff --git a/src/core/hle/service/nx2_gsv.h b/src/core/hle/service/nx2_gsv.h index e2060c90a7..5402369e7b 100644 --- a/src/core/hle/service/nx2_gsv.h +++ b/src/core/hle/service/nx2_gsv.h @@ -1698,6 +1698,14 @@ class INx2Vi2Session final : public ServiceFramework { } ~INx2Vi2Session() override { + if (m_release_timer) { + system.CoreTiming().UnscheduleEvent(m_release_timer); + auto& syncpoints = system.Host1x().GetSyncpointManager(); + while (syncpoints.GetGuestSyncpointValue(m_release_syncpoint) != m_release_value) { + syncpoints.IncrementGuest(m_release_syncpoint); + } + NvdrvSyncpoints().FreeSyncpoint(m_release_syncpoint); + } // The vsync callback may be running on the HostTiming thread: empty its event list under // its lock first (it holds the state, not this session), then close the events. { @@ -1775,7 +1783,10 @@ class INx2Vi2Session final : public ServiceFramework { const auto waited = std::chrono::steady_clock::now(); Composite(w[5], ~w[22], w[24], w[7]); const auto composited = std::chrono::steady_clock::now(); - PaceToVblank(w[12]); + const auto release = PresentQueue() ? QueueRelease(w[12]) : std::array{}; + if (!PresentQueue()) { + PaceToVblank(w[12]); + } const auto paced = std::chrono::steady_clock::now(); // Where each frame's time goes on the present thread, averaged per 600 presents: the // game is blocked here while Eden's GPU thread catches up (fence) and composites. @@ -1814,9 +1825,70 @@ class INx2Vi2Session final : public ServiceFramework { } constexpr u64 AvailableSlots = 0x7fff; TipcReplyWords(ctx, {static_cast(AvailableSlots), - static_cast(AvailableSlots >> 32), 0, 0, 0, 0}); + static_cast(AvailableSlots >> 32), release[0], release[1], + release[2], release[3]}); + } + + /// Returns presentation fences instead of blocking the present thread until the next flip. + /// This keeps the display wait out of Wonder's CPU frame measurement. + /// EDEN_NX2_PRESENT_QUEUE=0 restores blocking presentation pacing. + static bool PresentQueue() { + static const bool enabled = [] { + const char* value = std::getenv("EDEN_NX2_PRESENT_QUEUE"); + return value == nullptr || value[0] != '0'; + }(); + return enabled; + } + + /// Returns the allocator for display release syncpoints. + Nvidia::NvCore::SyncpointManager& NvdrvSyncpoints() { + return system.ServiceManager() + .GetService("nvdrv", true) + ->GetModule() + ->GetContainer() + .GetSyncpointManager(); } + /// Returns the NativeSyncHandle {~syncpoint, 0, value, 0} of a fence signalled when this frame's + /// swap interval has passed after the previous one (at once when the game is slower). + std::array QueueRelease(u32 timing) { + if (m_release_syncpoint == ~0U) { + m_release_syncpoint = NvdrvSyncpoints().AllocateSyncpoint(true); + m_release_value = system.Host1x().GetSyncpointManager().GetGuestSyncpointValue( + m_release_syncpoint); + m_release_timer = Core::Timing::CreateEvent( + "nx2:vi2 buffer release", + [&syncpoints = system.Host1x().GetSyncpointManager(), id = m_release_syncpoint]( + s64, std::chrono::nanoseconds) -> std::optional { + syncpoints.IncrementGuest(id); + return std::nullopt; + }); + LOG_INFO(Service, "nx2:vi2 display release syncpoint {}", m_release_syncpoint); + } + const u32 swap_interval = SwapInterval(timing); + const u32 hz = timing & 0xffff; + const u32 refresh_rate = hz >= 30 && hz <= 240 ? hz : 60; + if (timing != std::exchange(m_swap_word, timing)) { + LOG_INFO(Service, "nx2:vi2 present #{} queued swap interval {} at {} Hz", + m_presents, swap_interval, refresh_rate); + } + const auto now = system.CoreTiming().GetGlobalTimeNs(); + std::chrono::nanoseconds interval{}; + if (Settings::values.use_speed_limit.GetValue() && Settings::SpeedLimit() != 0) { + interval = std::chrono::nanoseconds{ + 100'000'000'000LL * swap_interval / refresh_rate / Settings::SpeedLimit()}; + } + m_release_time = interval.count() == 0 ? now : (std::max)(now, m_release_time); + system.CoreTiming().ScheduleEvent(m_release_time, m_release_timer, true); + m_release_time += interval; + return {~m_release_syncpoint, 0, ++m_release_value, 0}; + } + + u32 m_release_syncpoint{~0U}; + u32 m_release_value{}; + std::chrono::nanoseconds m_release_time{}; + std::shared_ptr m_release_timer; + // The console's compositor frees a buffer only once its frame's GPU fence has passed, which // is what keeps a game from running ahead of the GPU. This reply always reports every buffer // free, so pace the game here instead: wait (bounded, in case the GPU waits on the CPU) until @@ -1838,13 +1910,8 @@ class INx2Vi2Session final : public ServiceFramework { std::chrono::steady_clock::time_point m_next_vblank{}; u32 m_swap_word{}; - // timing: present word 12, (swap interval << 16) | refresh rate. Fire Emblem: Fortune's Weave - // sends 0x1003c during its boot logos and 0x2003c (two vblanks per frame, 30 fps) after; paced - // at every vblank it ran at double speed. EDEN_NX2_SWAP_INTERVAL= forces the interval. - void PaceToVblank(u32 timing) { - if (!Settings::values.use_speed_limit.GetValue() || Settings::SpeedLimit() == 0) { - return; - } + /// Resolves the requested interval and existing title/debug overrides for both pacing paths. + u32 SwapInterval(u32 timing) { static const u32 forced_interval = [] { const char* value = std::getenv("EDEN_NX2_SWAP_INTERVAL"); return value ? static_cast(std::strtoul(value, nullptr, 10)) : 0U; @@ -1860,6 +1927,17 @@ class INx2Vi2Session final : public ServiceFramework { if (forced_interval != 0) { swap_interval = forced_interval; } + return swap_interval; + } + + // timing: present word 12, (swap interval << 16) | refresh rate. Fire Emblem: Fortune's Weave + // sends 0x1003c during its boot logos and 0x2003c (two vblanks per frame, 30 fps) after; paced + // at every vblank it ran at double speed. EDEN_NX2_SWAP_INTERVAL= forces the interval. + void PaceToVblank(u32 timing) { + if (!Settings::values.use_speed_limit.GetValue() || Settings::SpeedLimit() == 0) { + return; + } + const u32 swap_interval = SwapInterval(timing); const u32 refresh_rate = (timing & 0xffff) >= 30 && (timing & 0xffff) <= 240 ? timing & 0xffff : 60; if (timing != std::exchange(m_swap_word, timing)) { LOG_INFO(Service, "nx2:vi2 present #{} swap interval {} at {} Hz (word 12 = {:#x})", @@ -2040,7 +2118,11 @@ class INx2Vi2Session final : public ServiceFramework { system.GPU().RequestComposite(std::move(layers), std::move(fences)); CaptureFrame(); DumpRanges(); - system.SpeedLimiter().DoSpeedLimiting(system.CoreTiming().GetGlobalTimeUs()); + // With the release fence pacing the game, limiting here would hold the present thread + // again (the wait the games' frame timers must not see). + if (!PresentQueue()) { + system.SpeedLimiter().DoSpeedLimiting(system.CoreTiming().GetGlobalTimeUs()); + } system.GetPerfStats().EndSystemFrame(); system.GetPerfStats().BeginSystemFrame(); } From 243484a34df4ce20f64149c6a14355ac555620b6 Mon Sep 17 00:00:00 2001 From: Alleexxi <66967403+Alleexxi@users.noreply.github.com> Date: Fri, 6 Nov 2026 18:08:51 +0100 Subject: [PATCH 2/3] fix all the rabbit complains --- src/core/CMakeLists.txt | 1 + .../service/nvdrv/core/syncpoint_manager.cpp | 16 +- .../service/nvdrv/core/syncpoint_manager.h | 4 + src/core/hle/service/nx2_gsv.h | 65 ++++---- src/core/hle/service/nx2_present_queue.h | 110 ++++++++++++++ src/tests/CMakeLists.txt | 1 + src/tests/core/nx2_present_queue.cpp | 139 ++++++++++++++++++ 7 files changed, 303 insertions(+), 33 deletions(-) create mode 100644 src/core/hle/service/nx2_present_queue.h create mode 100644 src/tests/core/nx2_present_queue.cpp diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index 1ff9f271d2..a6fc970094 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -1044,6 +1044,7 @@ add_library(core STATIC hle/service/nx2_streamplay.h hle/service/nx2_stub_interface.h hle/service/nx2_gsv.h + hle/service/nx2_present_queue.h hle/service/services.cpp hle/service/services.h hle/service/set/factory_settings_server.cpp diff --git a/src/core/hle/service/nvdrv/core/syncpoint_manager.cpp b/src/core/hle/service/nvdrv/core/syncpoint_manager.cpp index c4c4c25932..6b2fd8fa5c 100644 --- a/src/core/hle/service/nvdrv/core/syncpoint_manager.cpp +++ b/src/core/hle/service/nvdrv/core/syncpoint_manager.cpp @@ -41,19 +41,31 @@ u32 SyncpointManager::ReserveSyncpoint(u32 id, bool client_managed) { return id; } +/// Finds an unreserved nonzero ID, returning zero if the pool is exhausted. u32 SyncpointManager::FindFreeSyncpoint() { for (u32 i{1}; i < syncpoints.size(); i++) { if (!syncpoints[i].reserved) { return i; } } - ASSERT_MSG(false, "Failed to find a free syncpoint!"); return 0; } +/// Allocates a syncpoint, retaining the assertion and zero result for callers requiring one. u32 SyncpointManager::AllocateSyncpoint(bool client_managed) { + const auto id = TryAllocateSyncpoint(client_managed); + ASSERT_MSG(id.has_value(), "Failed to find a free syncpoint!"); + return id.value_or(0); +} + +/// Reserves a nonzero ID atomically, or reports exhaustion without changing reservations. +std::optional SyncpointManager::TryAllocateSyncpoint(bool client_managed) { std::lock_guard lock(reservation_lock); - return ReserveSyncpoint(FindFreeSyncpoint(), client_managed); + const u32 id = FindFreeSyncpoint(); + if (id == 0) { + return std::nullopt; + } + return ReserveSyncpoint(id, client_managed); } void SyncpointManager::FreeSyncpoint(u32 id) { diff --git a/src/core/hle/service/nvdrv/core/syncpoint_manager.h b/src/core/hle/service/nvdrv/core/syncpoint_manager.h index 7728ff5967..da4243f351 100644 --- a/src/core/hle/service/nvdrv/core/syncpoint_manager.h +++ b/src/core/hle/service/nvdrv/core/syncpoint_manager.h @@ -7,6 +7,7 @@ #include #include #include +#include #include "common/common_types.h" #include "core/hle/service/nvdrv/nvdata.h" @@ -52,6 +53,9 @@ class SyncpointManager final { */ u32 AllocateSyncpoint(bool client_managed); + /// Reserves a free syncpoint, or returns no value on exhaustion without reserving ID 0. + std::optional TryAllocateSyncpoint(bool client_managed); + /** * @url * https://github.com/Jetson-TX1-AndroidTV/android_kernel_jetson_tx1_hdmi_primary/blob/8f74a72394efb871cb3f886a3de2998cd7ff2990/drivers/gpu/host1x/syncpt.c#L259 diff --git a/src/core/hle/service/nx2_gsv.h b/src/core/hle/service/nx2_gsv.h index 5402369e7b..0ecb71f34a 100644 --- a/src/core/hle/service/nx2_gsv.h +++ b/src/core/hle/service/nx2_gsv.h @@ -67,6 +67,7 @@ #include "core/hle/service/nvdrv/devices/nx2_stub_device.h" #include "core/hle/service/nvdrv/nvdrv.h" #include "core/hle/service/nvdrv/nvdrv_interface.h" +#include "core/hle/service/nx2_present_queue.h" #include "core/hle/service/nx2_stub_interface.h" #include "core/hle/service/sm/sm.h" #include "core/hle/service/server_manager.h" @@ -1697,13 +1698,10 @@ class INx2Vi2Session final : public ServiceFramework { }); } + /// Cancels display callbacks before releasing their syncpoints and kernel events. ~INx2Vi2Session() override { - if (m_release_timer) { - system.CoreTiming().UnscheduleEvent(m_release_timer); - auto& syncpoints = system.Host1x().GetSyncpointManager(); - while (syncpoints.GetGuestSyncpointValue(m_release_syncpoint) != m_release_value) { - syncpoints.IncrementGuest(m_release_syncpoint); - } + if (m_release_queue) { + m_release_queue.reset(); NvdrvSyncpoints().FreeSyncpoint(m_release_syncpoint); } // The vsync callback may be running on the HostTiming thread: empty its event list under @@ -1725,9 +1723,8 @@ class INx2Vi2Session final : public ServiceFramework { TipcHandshake(ctx, "nx2:vi2"); } - // 229(u64 client token, u32 slot, u32 image, ...fences): result, u64 mask of buffer slots the - // client may acquire next, four zero words. Nothing is shown yet; every slot is handed back - // at once (changes.patch's default mask is 0x7fff). + /// Presents a native buffer and returns available slots plus its release fence. + /// The queued path keeps display pacing outside the guest's CPU frame measurement. void PresentClientBuffer(HLERequestContext& ctx) { const auto* w = ctx.CommandBuffer(); ++m_presents; @@ -1781,9 +1778,15 @@ class INx2Vi2Session final : public ServiceFramework { m_previous_fence_id = ~w[22]; m_previous_fence_value = w[24]; const auto waited = std::chrono::steady_clock::now(); + const auto release = PresentQueue() ? QueueRelease(w[12], ~w[22], w[24]) + : std::optional{std::array{}}; + if (!release) { + IPC::ResponseBuilder rb{ctx, 2}; + rb.PushRaw(Kernel::ResultOutOfResource.raw); + return; + } Composite(w[5], ~w[22], w[24], w[7]); const auto composited = std::chrono::steady_clock::now(); - const auto release = PresentQueue() ? QueueRelease(w[12]) : std::array{}; if (!PresentQueue()) { PaceToVblank(w[12]); } @@ -1825,8 +1828,8 @@ class INx2Vi2Session final : public ServiceFramework { } constexpr u64 AvailableSlots = 0x7fff; TipcReplyWords(ctx, {static_cast(AvailableSlots), - static_cast(AvailableSlots >> 32), release[0], release[1], - release[2], release[3]}); + static_cast(AvailableSlots >> 32), (*release)[0], (*release)[1], + (*release)[2], (*release)[3]}); } /// Returns presentation fences instead of blocking the present thread until the next flip. @@ -1849,20 +1852,16 @@ class INx2Vi2Session final : public ServiceFramework { .GetSyncpointManager(); } - /// Returns the NativeSyncHandle {~syncpoint, 0, value, 0} of a fence signalled when this frame's - /// swap interval has passed after the previous one (at once when the game is slower). - std::array QueueRelease(u32 timing) { - if (m_release_syncpoint == ~0U) { - m_release_syncpoint = NvdrvSyncpoints().AllocateSyncpoint(true); - m_release_value = system.Host1x().GetSyncpointManager().GetGuestSyncpointValue( - m_release_syncpoint); - m_release_timer = Core::Timing::CreateEvent( - "nx2:vi2 buffer release", - [&syncpoints = system.Host1x().GetSyncpointManager(), id = m_release_syncpoint]( - s64, std::chrono::nanoseconds) -> std::optional { - syncpoints.IncrementGuest(id); - return std::nullopt; - }); + /// Returns {~syncpoint, 0, value, 0}, released after the swap deadline and frame's GPU fence. + /// Returns no fence if the syncpoint pool or bounded release queue is exhausted. + std::optional> QueueRelease(u32 timing, u32 fence_id, u32 fence_value) { + if (!m_release_queue) { + const auto id = NvdrvSyncpoints().TryAllocateSyncpoint(true); + if (!id) { + return std::nullopt; + } + m_release_syncpoint = *id; + m_release_queue.emplace(system.CoreTiming(), system.Host1x().GetSyncpointManager(), *id); LOG_INFO(Service, "nx2:vi2 display release syncpoint {}", m_release_syncpoint); } const u32 swap_interval = SwapInterval(timing); @@ -1879,15 +1878,17 @@ class INx2Vi2Session final : public ServiceFramework { 100'000'000'000LL * swap_interval / refresh_rate / Settings::SpeedLimit()}; } m_release_time = interval.count() == 0 ? now : (std::max)(now, m_release_time); - system.CoreTiming().ScheduleEvent(m_release_time, m_release_timer, true); + const auto value = m_release_queue->Queue(m_release_time, fence_id, fence_value); + if (!value) { + return std::nullopt; + } m_release_time += interval; - return {~m_release_syncpoint, 0, ++m_release_value, 0}; + return std::array{~m_release_syncpoint, 0, *value, 0}; } u32 m_release_syncpoint{~0U}; - u32 m_release_value{}; std::chrono::nanoseconds m_release_time{}; - std::shared_ptr m_release_timer; + std::optional m_release_queue; // The console's compositor frees a buffer only once its frame's GPU fence has passed, which // is what keeps a game from running ahead of the GPU. This reply always reports every buffer @@ -1930,6 +1931,7 @@ class INx2Vi2Session final : public ServiceFramework { return swap_interval; } + /// Blocks until the next requested swap interval when queued presentation is disabled. // timing: present word 12, (swap interval << 16) | refresh rate. Fire Emblem: Fortune's Weave // sends 0x1003c during its boot logos and 0x2003c (two vblanks per frame, 30 fps) after; paced // at every vblank it ran at double speed. EDEN_NX2_SWAP_INTERVAL= forces the interval. @@ -2022,7 +2024,8 @@ class INx2Vi2Session final : public ServiceFramework { return syncpoints.IsReadyGuest(fence_id, fence_value); } - // Shows a presented native image: word 5 is the image id, ~word 22 / word 24 the GPU fence + /// Shows a native image, keeping CPU speed limiting disabled when release fences pace it. + // Word 5 is the image id, ~word 22 / word 24 the GPU fence // (syncpoint, value) to wait for (layout from changes.patch). The image is a block-linear // RGBA8 surface in the console's native GOB layout at GPU address == CPU address. // shown_size: the present's word 7, ((height - 1) << 16) | (width - 1) of the area the game drew diff --git a/src/core/hle/service/nx2_present_queue.h b/src/core/hle/service/nx2_present_queue.h new file mode 100644 index 0000000000..c196eadb66 --- /dev/null +++ b/src/core/hle/service/nx2_present_queue.h @@ -0,0 +1,110 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include +#include +#include +#include +#include + +#include "common/common_types.h" +#include "core/core_timing.h" +#include "video_core/host1x/syncpoint_manager.h" + +namespace Service { + +/// Releases native display buffers in submission order after their deadline and GPU fence pass. +class Nx2PresentReleaseQueue { +public: + // The service advertises fifteen buffer slots. Clients must wait for releases before reuse. + static constexpr size_t Capacity = 15; + + /// Starts an empty queue using an already allocated, session-owned release syncpoint. + Nx2PresentReleaseQueue(Core::Timing::CoreTiming& timing_, + Tegra::Host1x::SyncpointManager& syncpoints_, u32 release_id_) + : timing{timing_}, syncpoints{syncpoints_}, release_id{release_id_}, + next_value{syncpoints.GetGuestSyncpointValue(release_id)} { + timer = Core::Timing::CreateEvent( + "nx2:vi2 buffer release", + [this](s64, std::chrono::nanoseconds) -> std::optional { + ReleaseReady(); + return std::nullopt; + }); + } + + /// Stops rescheduling before waiting for the callback, then wakes outstanding release waits. + /// At most Capacity increments are needed, even if another producer advanced the syncpoint. + ~Nx2PresentReleaseQueue() { + { + std::scoped_lock lock{mutex}; + stopped = true; + } + timing.UnscheduleEvent(timer); + for (const auto& entry : pending) { + Signal(entry.value); + } + } + + /// Enqueues without blocking the present thread; returns no value when all slots are pending. + std::optional Queue(std::chrono::nanoseconds deadline, u32 fence_id, u32 fence_value) { + std::scoped_lock lock{mutex}; + if (stopped || pending.size() == Capacity) { + return std::nullopt; + } + pending.push_back({deadline, fence_id, fence_value, ++next_value}); + if (pending.size() == 1) { + timing.ScheduleEvent(deadline, timer, true); + } + return next_value; + } + +private: + struct PendingRelease { + std::chrono::nanoseconds deadline; + u32 fence_id; + u32 fence_value; + u32 value; + }; + + /// Advances once per queued release, without chasing a counter that has already passed it. + void Signal(u32 value) { + if (static_cast(value - syncpoints.GetGuestSyncpointValue(release_id)) > 0) { + syncpoints.IncrementGuest(release_id); + } + } + + /// Polls only the oldest release; GPU waits never block the timing or presentation thread. + void ReleaseReady() { + std::scoped_lock lock{mutex}; + if (stopped) { + return; + } + while (!pending.empty()) { + const auto& entry = pending.front(); + const auto now = timing.GetGlobalTimeNs(); + if (now < entry.deadline) { + timing.ScheduleEvent(entry.deadline, timer, true); + return; + } + if (entry.fence_id < Tegra::Host1x::SyncpointManager::NUM_MAX_SYNCPOINTS && + !syncpoints.IsReadyGuest(entry.fence_id, entry.fence_value)) { + timing.ScheduleEvent(std::chrono::microseconds{100}, timer); + return; + } + Signal(entry.value); + pending.pop_front(); + } + } + + Core::Timing::CoreTiming& timing; + Tegra::Host1x::SyncpointManager& syncpoints; + const u32 release_id; + u32 next_value; + std::mutex mutex; + bool stopped{}; + std::deque pending; + std::shared_ptr timer; +}; + +} // namespace Service diff --git a/src/tests/CMakeLists.txt b/src/tests/CMakeLists.txt index 998a62821a..c0fd7b5f6e 100644 --- a/src/tests/CMakeLists.txt +++ b/src/tests/CMakeLists.txt @@ -24,6 +24,7 @@ add_executable(tests core/ldn.cpp core/internal_network/socket_proxy.cpp core/nup_writer.cpp + core/nx2_present_queue.cpp core/priority_queue.cpp core/internal_network/network.cpp video_core/memory_tracker.cpp diff --git a/src/tests/core/nx2_present_queue.cpp b/src/tests/core/nx2_present_queue.cpp new file mode 100644 index 0000000000..9b15a9433c --- /dev/null +++ b/src/tests/core/nx2_present_queue.cpp @@ -0,0 +1,139 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include +#include +#include +#include + +#include "core/core.h" +#include "core/core_timing.h" +#include "core/hle/service/nvdrv/core/syncpoint_manager.h" +#include "core/hle/service/nx2_present_queue.h" +#include "video_core/host1x/host1x.h" + +namespace { +using namespace std::chrono_literals; +constexpr u32 ReleaseId = 1; +constexpr u32 FrameId = 2; + +struct Fixture { + Fixture() { + timing.SetMulticore(false); + timing.Initialize([] {}); + } + + void Advance() { + timing.AddTicks(1'000'000'000); + timing.Advance(); + } + + Core::Timing::CoreTiming timing; + Tegra::Host1x::SyncpointManager syncpoints; +}; +} // namespace + +TEST_CASE("Native present release needs both the deadline and GPU fence", "[nx2_present]") { + Fixture f; + Service::Nx2PresentReleaseQueue queue{f.timing, f.syncpoints, ReleaseId}; + REQUIRE(queue.Queue(10ms, FrameId, 1) == 1U); + + SECTION("GPU finishes first") { + f.syncpoints.IncrementGuest(FrameId); + f.timing.Advance(); + REQUIRE(f.syncpoints.GetGuestSyncpointValue(ReleaseId) == 0); + f.Advance(); + } + SECTION("Presentation deadline passes first") { + f.Advance(); + REQUIRE(f.syncpoints.GetGuestSyncpointValue(ReleaseId) == 0); + f.Advance(); + REQUIRE(f.syncpoints.GetGuestSyncpointValue(ReleaseId) == 0); + f.syncpoints.IncrementGuest(FrameId); + f.Advance(); + } + REQUIRE(f.syncpoints.GetGuestSyncpointValue(ReleaseId) == 1); + REQUIRE_FALSE(f.timing.Advance().has_value()); +} + +TEST_CASE("Native present releases remain ordered when a later frame is ready", "[nx2_present]") { + Fixture f; + Service::Nx2PresentReleaseQueue queue{f.timing, f.syncpoints, ReleaseId}; + REQUIRE(queue.Queue(0ns, FrameId, 1) == 1U); + REQUIRE(queue.Queue(0ns, ~0U, 0) == 2U); + f.timing.Advance(); + REQUIRE(f.syncpoints.GetGuestSyncpointValue(ReleaseId) == 0); + f.syncpoints.IncrementGuest(FrameId); + f.Advance(); + REQUIRE(f.syncpoints.GetGuestSyncpointValue(ReleaseId) == 2); + + // An empty queue must schedule its timer again on the next submission. + REQUIRE(queue.Queue(0ns, ~0U, 0) == 3U); + f.timing.Advance(); + REQUIRE(f.syncpoints.GetGuestSyncpointValue(ReleaseId) == 3); + REQUIRE_FALSE(f.timing.Advance().has_value()); +} + +TEST_CASE("Native present queue bounds pending releases and recovers capacity", "[nx2_present]") { + Fixture f; + Service::Nx2PresentReleaseQueue queue{f.timing, f.syncpoints, ReleaseId}; + for (u32 value = 1; value <= Service::Nx2PresentReleaseQueue::Capacity; ++value) { + REQUIRE(queue.Queue(0ns, FrameId, 1) == value); + } + REQUIRE_FALSE(queue.Queue(0ns, FrameId, 1).has_value()); + f.timing.Advance(); + REQUIRE(f.syncpoints.GetGuestSyncpointValue(ReleaseId) == 0); + f.syncpoints.IncrementGuest(FrameId); + f.Advance(); + REQUIRE(f.syncpoints.GetGuestSyncpointValue(ReleaseId) == 15); + REQUIRE(queue.Queue(0ns, ~0U, 0) == 16U); + f.timing.Advance(); + REQUIRE(f.syncpoints.GetGuestSyncpointValue(ReleaseId) == 16); +} + +TEST_CASE("Native present shutdown cancels retries and resolves only pending releases", + "[nx2_present]") { + Fixture f; + u32 expected = 2; + { + Service::Nx2PresentReleaseQueue queue{f.timing, f.syncpoints, ReleaseId}; + REQUIRE(queue.Queue(0ns, FrameId, 1) == 1U); + REQUIRE(queue.Queue(0ns, FrameId, 2) == 2U); + f.timing.Advance(); + SECTION("Outstanding fences are signalled") {} + SECTION("Counter already reached the target") { + f.syncpoints.IncrementGuest(ReleaseId); + f.syncpoints.IncrementGuest(ReleaseId); + } + SECTION("Counter already passed the target") { + expected = 3; + for (int i = 0; i < 3; ++i) { + f.syncpoints.IncrementGuest(ReleaseId); + } + } + } + const auto value = f.syncpoints.GetGuestSyncpointValue(ReleaseId); + REQUIRE(value == expected); + REQUIRE_FALSE(f.timing.Advance().has_value()); + f.Advance(); + REQUIRE(f.syncpoints.GetGuestSyncpointValue(ReleaseId) == value); +} + +TEST_CASE("Syncpoint exhaustion never reserves ID zero and freed IDs can be reused", + "[nx2_present]") { + Core::System system; + system.Initialize(); + auto host = std::make_unique(system); + Service::Nvidia::NvCore::SyncpointManager allocator{*host}; + std::vector allocated; + while (const auto id = allocator.TryAllocateSyncpoint(true)) { + REQUIRE(*id != 0); + allocated.push_back(*id); + } + REQUIRE_FALSE(allocated.empty()); + REQUIRE_FALSE(allocator.TryAllocateSyncpoint(true).has_value()); + REQUIRE_FALSE(allocator.IsSyncpointAllocated(0)); + allocator.FreeSyncpoint(allocated.back()); + REQUIRE(allocator.TryAllocateSyncpoint(true) == allocated.back()); + REQUIRE_FALSE(allocator.TryAllocateSyncpoint(true).has_value()); + REQUIRE_FALSE(allocator.IsSyncpointAllocated(0)); +} From 87d9d59aec88aedeec475becfc39c4ede537fb45 Mon Sep 17 00:00:00 2001 From: Alleexxi <66967403+Alleexxi@users.noreply.github.com> Date: Fri, 6 Nov 2026 19:03:25 +0100 Subject: [PATCH 3/3] fix all the rabbit complains --- src/tests/core/nx2_present_queue.cpp | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/tests/core/nx2_present_queue.cpp b/src/tests/core/nx2_present_queue.cpp index 9b15a9433c..969ff078d3 100644 --- a/src/tests/core/nx2_present_queue.cpp +++ b/src/tests/core/nx2_present_queue.cpp @@ -5,6 +5,7 @@ #include #include +#include "common/scope_exit.h" #include "core/core.h" #include "core/core_timing.h" #include "core/hle/service/nvdrv/core/syncpoint_manager.h" @@ -32,6 +33,7 @@ struct Fixture { }; } // namespace +/// Checks both completion orders so neither the deadline nor GPU completion releases alone. TEST_CASE("Native present release needs both the deadline and GPU fence", "[nx2_present]") { Fixture f; Service::Nx2PresentReleaseQueue queue{f.timing, f.syncpoints, ReleaseId}; @@ -55,6 +57,7 @@ TEST_CASE("Native present release needs both the deadline and GPU fence", "[nx2_ REQUIRE_FALSE(f.timing.Advance().has_value()); } +/// Checks FIFO release gating and timer rearming after the queue becomes empty. TEST_CASE("Native present releases remain ordered when a later frame is ready", "[nx2_present]") { Fixture f; Service::Nx2PresentReleaseQueue queue{f.timing, f.syncpoints, ReleaseId}; @@ -73,6 +76,7 @@ TEST_CASE("Native present releases remain ordered when a later frame is ready", REQUIRE_FALSE(f.timing.Advance().has_value()); } +/// Verifies saturation rejects submissions until completed frames restore queue capacity. TEST_CASE("Native present queue bounds pending releases and recovers capacity", "[nx2_present]") { Fixture f; Service::Nx2PresentReleaseQueue queue{f.timing, f.syncpoints, ReleaseId}; @@ -90,6 +94,7 @@ TEST_CASE("Native present queue bounds pending releases and recovers capacity", REQUIRE(f.syncpoints.GetGuestSyncpointValue(ReleaseId) == 16); } +/// Verifies shutdown cancels callbacks without advancing counters already at or past the target. TEST_CASE("Native present shutdown cancels retries and resolves only pending releases", "[nx2_present]") { Fixture f; @@ -118,9 +123,13 @@ TEST_CASE("Native present shutdown cancels retries and resolves only pending rel REQUIRE(f.syncpoints.GetGuestSyncpointValue(ReleaseId) == value); } +/// Exhausts the allocator to check ID zero stays unreserved and freed IDs remain reusable. TEST_CASE("Syncpoint exhaustion never reserves ID zero and freed IDs can be reused", "[nx2_present]") { Core::System system; + SCOPE_EXIT { + system.CoreTiming().Reset(); + }; system.Initialize(); auto host = std::make_unique(system); Service::Nvidia::NvCore::SyncpointManager allocator{*host};