aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorSssnowyyy <78863999+Sssnowyyy@users.noreply.github.com>2026-11-02 11:34:01 -0500
committerSssnowyyy <78863999+Sssnowyyy@users.noreply.github.com>2026-11-02 14:53:47 -0500
commit690f80e15f411d8fa46a2eecdfc50ddadc8ed5e0 (patch)
tree800473afe225f5ab7516e739f212961b41e06c46 /src
parenta1f781fa24cc16e6d636213a5596cfed4123f493 (diff)
nx2: don't stall presents on a current-frame wait the GPU can't finish
0f14696044 made PresentClientBuffer also wait for the current frame's fence whenever the previous frame's wasn't reached. Mario Kart World's GPU waits on semaphores its CPU releases only after the present returns, so in that state the current fence can't be reached until the present returns, and each such present stalled up to the 4 s deadline. Wait for the current frame by default, but give up after a few ms once a GPU channel was seen suspended on a semaphore acquire and wait for the previous frame instead. EDEN_NX2_FRAMES_AHEAD=1 always waits for the previous frame (with 0f14696044's current-frame wait when the GPU is behind), =0 always for the current one. The scheduler gains a lock-free flag so the present thread can see suspended channels. Assisted-by: Claude Sonnet 5.5 (implementation and commit message)
Diffstat (limited to 'src')
-rw-r--r--src/core/hle/service/nx2_gsv.h65
-rw-r--r--src/video_core/control/scheduler.cpp2
-rw-r--r--src/video_core/control/scheduler.h6
-rw-r--r--src/video_core/gpu.cpp4
-rw-r--r--src/video_core/gpu.h3
5 files changed, 58 insertions, 22 deletions
diff --git a/src/core/hle/service/nx2_gsv.h b/src/core/hle/service/nx2_gsv.h
index bc354b4d8f..259681a2a2 100644
--- a/src/core/hle/service/nx2_gsv.h
+++ b/src/core/hle/service/nx2_gsv.h
@@ -1711,37 +1711,41 @@ private:
LOG_INFO(Service, "nx2:vi2 229 present #{} token={:#x} slot={} image={}", m_presents,
u64{w[2]} | (u64{w[3]} << 32), w[4], w[5]);
}
- // Wait for the previous frame's fence instead of this one, so the game prepares frame N+1
- // while the GPU thread runs frame N (the console keeps several frames in flight). Waiting
- // for this frame serialized the two: 8-56 ms of every 34-105 ms frame on title 1.
- // EDEN_NX2_FRAMES_AHEAD=0 restores the wait for the current frame.
- static const bool frames_ahead = [] {
+ // Wait for this frame's fence, so the game doesn't write the next frame's data (constant
+ // buffers holding texture handles, vertex data, Pokopia's glyph atlas) while the GPU
+ // thread still reads this frame's. But title 1's GPU waits on semaphores its CPU releases
+ // only after this present returns: while a channel stays suspended on one, wait for the
+ // previous frame's fence instead and let the game prepare frame N+1 meanwhile (waiting
+ // for this frame there cost 8-56 ms of every 34-105 ms frame, or never finished).
+ // EDEN_NX2_FRAMES_AHEAD=1 always waits for the previous frame, =0 always for this one.
+ static const int frames_ahead = [] {
const char* value = std::getenv("EDEN_NX2_FRAMES_AHEAD");
- return value == nullptr || value[0] != '0';
+ return value == nullptr ? -1 : value[0] == '1' ? 1 : 0;
}();
const auto entered = std::chrono::steady_clock::now();
- if (frames_ahead) {
+ if (frames_ahead == 1 || (frames_ahead == -1 && !WaitForFrame(~w[22], w[24], true))) {
LOG_DEBUG(Service, "nx2:vi2 present #{} fence ({}, {:#x}), waiting for ({}, {:#x}); now {:#x}",
m_presents, ~w[22], w[24], m_previous_fence_id, m_previous_fence_value,
~w[22] < 192 ? system.Host1x().GetSyncpointManager().GetGuestSyncpointValue(~w[22]) : 0U);
- // Only run a frame ahead while the GPU keeps up. With the previous frame still on the
- // GPU here, the GPU is the limit and running ahead gains nothing; and Splatoon Raiders,
- // which reuses its command memory as if the GPU were at most a frame behind, rewrote
- // command lists Eden had not read yet (misread commands, then crashes, with a slow
- // GPU). Wait for this frame too then.
- const bool gpu_behind =
- m_previous_fence_id < 192 &&
- !system.Host1x().GetSyncpointManager().IsReadyGuest(m_previous_fence_id,
- m_previous_fence_value);
+ // With EDEN_NX2_FRAMES_AHEAD=1, only run a frame ahead while the GPU keeps up. With
+ // the previous frame still on the GPU here, the GPU is the limit and running ahead
+ // gains nothing; and Splatoon Raiders, which reuses its command memory as if the GPU
+ // were at most a frame behind, rewrote command lists Eden had not read yet (misread
+ // commands, then crashes, with a slow GPU). Wait for this frame too then. Not when a
+ // suspended channel brought us here: this frame can't finish before the present
+ // returns.
+ const bool gpu_behind = frames_ahead == 1 && m_previous_fence_id < 192 &&
+ !system.Host1x().GetSyncpointManager().IsReadyGuest(
+ m_previous_fence_id, m_previous_fence_value);
WaitForFrame(m_previous_fence_id, m_previous_fence_value);
if (gpu_behind) {
WaitForFrame(~w[22], w[24]);
}
- m_previous_fence_id = ~w[22];
- m_previous_fence_value = w[24];
- } else {
+ } else if (frames_ahead == 0) {
WaitForFrame(~w[22], w[24]);
}
+ m_previous_fence_id = ~w[22];
+ m_previous_fence_value = w[24];
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();
@@ -1838,9 +1842,12 @@ private:
m_next_vblank += interval;
}
- void WaitForFrame(u32 fence_id, u32 fence_value) {
+ /// Waits until the fence is reached. With `unless_gpu_waits`, gives up after a few ms once a
+ /// GPU channel was seen suspended on a semaphore (it may be one this thread releases later).
+ /// Returns whether the fence was reached.
+ bool WaitForFrame(u32 fence_id, u32 fence_value, bool unless_gpu_waits = false) {
if (fence_id >= 192) {
- return;
+ return true;
}
auto& syncpoints = system.Host1x().GetSyncpointManager();
const auto start = std::chrono::steady_clock::now();
@@ -1865,11 +1872,24 @@ private:
wake->cv.notify_all();
});
std::unique_lock lk{wake->mutex};
+ // Title 1's channels resume and suspend again while they wait on this thread, so any
+ // suspension counts, not only an unbroken one.
+ bool gpu_waited = false;
while (!wake->done && system.IsPoweredOn() &&
std::chrono::steady_clock::now() < deadline) {
- wake->cv.wait_for(lk, std::chrono::milliseconds{50});
+ if (unless_gpu_waits) {
+ gpu_waited |= system.GPU().HasSuspendedChannels();
+ if (gpu_waited &&
+ std::chrono::steady_clock::now() - start > std::chrono::milliseconds{3}) {
+ return false;
+ }
+ }
+ wake->cv.wait_for(lk, std::chrono::milliseconds{unless_gpu_waits ? 1 : 50});
}
}
+ if (unless_gpu_waits && !syncpoints.IsReadyGuest(fence_id, fence_value)) {
+ return false;
+ }
if (!syncpoints.IsReadyGuest(fence_id, fence_value) && m_wait_timeouts++ < 64) {
LOG_WARNING(Service, "nx2:vi2 present #{} fence ({}, {}) not reached in 4 s (now {})",
m_presents, fence_id, fence_value, syncpoints.GetGuestSyncpointValue(fence_id));
@@ -1881,6 +1901,7 @@ private:
std::chrono::steady_clock::now() - start)
.count());
}
+ 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
diff --git a/src/video_core/control/scheduler.cpp b/src/video_core/control/scheduler.cpp
index dc42906752..97d2dcd482 100644
--- a/src/video_core/control/scheduler.cpp
+++ b/src/video_core/control/scheduler.cpp
@@ -48,6 +48,7 @@ void Scheduler::Push(GPU& gpu, s32 channel, CommandList&& entries) {
std::find(pending_channels.begin(), pending_channels.end(), channel_state) ==
pending_channels.end()) {
pending_channels.push_back(channel_state);
+ any_pending.store(true, std::memory_order_release);
}
if (!pending_channels.empty()) {
ResumePending(gpu);
@@ -77,6 +78,7 @@ void Scheduler::ResumePending(GPU& gpu) {
std::erase_if(pending_channels, [](const auto& state) {
return !state->payload->dma_pusher.HasPendingAcquire();
});
+ any_pending.store(!pending_channels.empty(), std::memory_order_release);
}
void Scheduler::DeclareChannel(std::shared_ptr<ChannelState> new_channel) {
diff --git a/src/video_core/control/scheduler.h b/src/video_core/control/scheduler.h
index 9a0f1b919f..b1aaf46b60 100644
--- a/src/video_core/control/scheduler.h
+++ b/src/video_core/control/scheduler.h
@@ -6,6 +6,7 @@
#pragma once
+#include <atomic>
#include <memory>
#include <mutex>
#include <vector>
@@ -30,6 +31,10 @@ public:
[[nodiscard]] bool HasPendingChannels() const {
return !pending_channels.empty();
}
+ /// HasPendingChannels for threads other than the GPU thread.
+ [[nodiscard]] bool HasPendingChannelsAnyThread() const {
+ return any_pending.load(std::memory_order_acquire);
+ }
void DeclareChannel(std::shared_ptr<ChannelState> new_channel);
@@ -39,6 +44,7 @@ private:
::Common::unordered_map<s32, std::shared_ptr<ChannelState>> channels;
std::mutex scheduling_guard;
std::vector<std::shared_ptr<ChannelState>> pending_channels; ///< Suspended on an acquire
+ std::atomic<bool> any_pending{};
};
} // namespace Control
diff --git a/src/video_core/gpu.cpp b/src/video_core/gpu.cpp
index 53f8f063f7..a6f31359a3 100644
--- a/src/video_core/gpu.cpp
+++ b/src/video_core/gpu.cpp
@@ -505,6 +505,10 @@ bool GPU::IsAsync() const {
return impl->is_async;
}
+bool GPU::HasSuspendedChannels() const {
+ return impl->scheduler.HasPendingChannelsAnyThread();
+}
+
bool GPU::UseNvdec() const {
return impl->use_nvdec;
}
diff --git a/src/video_core/gpu.h b/src/video_core/gpu.h
index 3a6546b41b..4afc89f0e6 100644
--- a/src/video_core/gpu.h
+++ b/src/video_core/gpu.h
@@ -217,6 +217,9 @@ public:
[[nodiscard]] bool IsAsync() const;
+ /// Whether a channel is suspended on a semaphore acquire (callable from any thread).
+ [[nodiscard]] bool HasSuspendedChannels() const;
+
[[nodiscard]] bool UseNvdec() const;
void RendererFrameEndNotify();