aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorTakoroka <137927095+Takoroka@users.noreply.github.com>2026-10-27 20:53:15 -0400
committerTakoroka <137927095+Takoroka@users.noreply.github.com>2026-10-27 20:53:15 -0400
commitbf1de8f40130af5d3b490ee40dcc2fb20d460a14 (patch)
treef57762bcc67491aa61288bd2eec107348756fb79 /src
parent3f5bee600839a7593e98e0cb1231f09c29373ac8 (diff)
Switch 2: suspend channels on semaphore acquires; fix pushbuffer desyncs
NVN2 gates command buffers with semaphores the CPU releases after writing them. Eden read past unmet SEMAPHORED acquires, so it executed stale commands and desynchronised. Channels now suspend on an unmet acquire and resume from the same word once it is met (scheduler keeps the waiting channels, the GPU thread polls them), as in changes.patch. Also: methods split across GPFIFO entries around a called command buffer are set aside and resumed; subchannel 2 defaults to inline-to-memory; unbound subchannels and out-of-range compute/DMA methods are dropped instead of crashing; 4-byte semaphore releases no longer write 16 bytes; Ampere SEM_PAYLOAD_HI/SEM_EXECUTE no longer spin as Kepler acquires; unmapped GPU reads log instead of asserting. Debug aids: EDEN_NX2_PB_CHECK (pushbuffer changed after submit), EDEN_NX2_QUERY_TRACE, a one-time pushbuffer dump.
Diffstat (limited to 'src')
-rw-r--r--src/core/hle/service/nvdrv/devices/nvhost_gpu.cpp12
-rw-r--r--src/video_core/control/channel_state.cpp6
-rw-r--r--src/video_core/control/scheduler.cpp32
-rw-r--r--src/video_core/control/scheduler.h8
-rw-r--r--src/video_core/dma_pusher.cpp157
-rw-r--r--src/video_core/dma_pusher.h49
-rw-r--r--src/video_core/engines/kepler_compute.cpp15
-rw-r--r--src/video_core/engines/maxwell_dma.cpp6
-rw-r--r--src/video_core/engines/puller.cpp133
-rw-r--r--src/video_core/engines/puller.h23
-rw-r--r--src/video_core/gpu_thread.cpp14
-rw-r--r--src/video_core/memory_manager.cpp5
-rw-r--r--src/video_core/nx2.h35
-rw-r--r--src/video_core/query_cache/query_cache.h5
14 files changed, 473 insertions, 27 deletions
diff --git a/src/core/hle/service/nvdrv/devices/nvhost_gpu.cpp b/src/core/hle/service/nvdrv/devices/nvhost_gpu.cpp
index a72ccd9b4f..cf3948c321 100644
--- a/src/core/hle/service/nvdrv/devices/nvhost_gpu.cpp
+++ b/src/core/hle/service/nvdrv/devices/nvhost_gpu.cpp
@@ -4,7 +4,9 @@
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
+#include <cstdlib>
#include <cstring>
+#include <vector>
#include "common/assert.h"
#include "common/logging.h"
#include "core/core.h"
@@ -422,6 +424,16 @@ NvResult nvhost_gpu::SubmitGPFIFOImpl(IoctlSubmitGpfifo& params, Tegra::CommandL
LOG_DEBUG(Service_NVDRV, "nx2 GPFIFO entry raw={:#018x} addr={:#x} size={:#x}", entry.raw,
entry.addr.Value(), entry.size.Value());
}
+ static const bool check = std::getenv("EDEN_NX2_PB_CHECK") != nullptr;
+ if (check) {
+ std::vector<u32> words;
+ for (const auto& entry : entries.command_lists) {
+ words.resize(entry.size);
+ system.ApplicationMemory().ReadBlock(entry.addr.Value(), words.data(),
+ words.size() * sizeof(u32));
+ entries.submit_hashes.push_back(Tegra::HashPushbufferWords(words));
+ }
+ }
}
auto& flags = params.flags;
diff --git a/src/video_core/control/channel_state.cpp b/src/video_core/control/channel_state.cpp
index 12a452dfc0..a1a8110510 100644
--- a/src/video_core/control/channel_state.cpp
+++ b/src/video_core/control/channel_state.cpp
@@ -21,6 +21,7 @@ namespace {
// Match NVK/Nouveau's initial pushbuffer subchannel layout.
constexpr u32 Nvk3DSubchannel = 0;
constexpr u32 NvkComputeSubchannel = 1;
+constexpr u32 NvkM2mfSubchannel = 2;
constexpr u32 Nvk2DSubchannel = 3;
constexpr u32 NvkCopySubchannel = 4;
@@ -29,7 +30,10 @@ void BindNvkDefaultSubchannels(ChannelState::Payload& payload) {
dma_pusher.BindSubchannel(&payload.maxwell_3d, Nvk3DSubchannel, Engines::EngineTypes::Maxwell3D);
dma_pusher.BindSubchannel(&payload.kepler_compute, NvkComputeSubchannel,
Engines::EngineTypes::KeplerCompute);
- // Subchannel 2 is M2MF there; Eden does not expose a 0x9039 engine yet.
+ // Subchannel 2 is M2MF/inline-to-memory there (NVN uses it without binding a class);
+ // KeplerMemory has the same LINE_LENGTH_IN/OFFSET_OUT/LAUNCH_DMA/LOAD_INLINE_DATA layout.
+ dma_pusher.BindSubchannel(&payload.kepler_memory, NvkM2mfSubchannel,
+ Engines::EngineTypes::KeplerMemory);
dma_pusher.BindSubchannel(&payload.fermi_2d, Nvk2DSubchannel, Engines::EngineTypes::Fermi2D);
dma_pusher.BindSubchannel(&payload.maxwell_dma, NvkCopySubchannel,
Engines::EngineTypes::MaxwellDMA);
diff --git a/src/video_core/control/scheduler.cpp b/src/video_core/control/scheduler.cpp
index 8af13e415c..24f1db1f7c 100644
--- a/src/video_core/control/scheduler.cpp
+++ b/src/video_core/control/scheduler.cpp
@@ -4,7 +4,9 @@
// SPDX-FileCopyrightText: 2021 yuzu Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
+#include <algorithm>
#include <memory>
+#include <vector>
#include "common/assert.h"
#include "video_core/control/channel_state.h"
@@ -26,6 +28,36 @@ void Scheduler::Push(GPU& gpu, s32 channel, CommandList&& entries) {
// Multiple channels can prepare their commands in parallel.
channel_state->payload->dma_pusher.Push(std::move(entries));
channel_state->payload->dma_pusher.DispatchCalls();
+ if (channel_state->payload->dma_pusher.HasPendingAcquire() &&
+ std::find(pending_channels.begin(), pending_channels.end(), channel_state) ==
+ pending_channels.end()) {
+ pending_channels.push_back(channel_state);
+ }
+ if (!pending_channels.empty()) {
+ ResumePending(gpu);
+ }
+}
+
+void Scheduler::ResumePending(GPU& gpu) {
+ // Running one channel can release the semaphore another waits for: retry until none moves.
+ for (bool progress = true; progress;) {
+ progress = false;
+ for (const auto& state : pending_channels) {
+ auto& dma_pusher = state->payload->dma_pusher;
+ if (!dma_pusher.HasPendingAcquire() || !dma_pusher.CanResumeAcquire()) {
+ continue;
+ }
+ {
+ std::scoped_lock lk{scheduling_guard};
+ gpu.BindChannel(state->bind_id);
+ }
+ dma_pusher.DispatchCalls();
+ progress = true;
+ }
+ }
+ std::erase_if(pending_channels, [](const auto& state) {
+ return !state->payload->dma_pusher.HasPendingAcquire();
+ });
}
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 342bea5025..3a3dcc58cf 100644
--- a/src/video_core/control/scheduler.h
+++ b/src/video_core/control/scheduler.h
@@ -8,6 +8,7 @@
#include <memory>
#include <mutex>
+#include <vector>
#include "common/container/unordered_map.h"
#include "video_core/dma_pusher.h"
@@ -24,11 +25,18 @@ class Scheduler {
public:
void Push(GPU& gpu, s32 channel, CommandList&& entries);
+ /// Runs channels whose pending semaphore acquire is now met.
+ void ResumePending(GPU& gpu);
+ [[nodiscard]] bool HasPendingChannels() const {
+ return !pending_channels.empty();
+ }
+
void DeclareChannel(std::shared_ptr<ChannelState> new_channel);
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
};
} // namespace Control
diff --git a/src/video_core/dma_pusher.cpp b/src/video_core/dma_pusher.cpp
index 2c0ba007ef..0889964337 100644
--- a/src/video_core/dma_pusher.cpp
+++ b/src/video_core/dma_pusher.cpp
@@ -4,13 +4,17 @@
// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
+#include <atomic>
+
#include "common/settings.h"
#include "core/core.h"
+#include "core/memory.h"
#include "video_core/dma_pusher.h"
#include "video_core/engines/maxwell_3d.h"
#include "video_core/gpu.h"
#include "video_core/guest_memory.h"
#include "video_core/memory_manager.h"
+#include "video_core/nx2.h"
#include "video_core/rasterizer_interface.h"
namespace Tegra {
@@ -29,7 +33,7 @@ DmaPusher::DmaPusher(Core::System& system_, MemoryManager& memory_manager_, Cont
DmaPusher::~DmaPusher() = default;
void DmaPusher::DispatchCalls() {
- dma_pushbuffer_subindex = 0;
+ // The sub-index is kept: a channel suspended on a semaphore resumes inside its command list.
dma_state.is_last_call = true;
while (system.IsPoweredOn()) {
if (!Step()) {
@@ -44,6 +48,9 @@ bool DmaPusher::Step() {
if (!ib_enable || dma_pushbuffer.empty()) {
return false;
}
+ if (!puller.ResumeAcquire(*this)) {
+ return false;
+ }
CommandList& command_list = dma_pushbuffer.front();
@@ -57,7 +64,13 @@ bool DmaPusher::Step() {
}
if (prefetch_size > 0) {
- ProcessCommands(command_list.prefetch_command_list);
+ const std::span<const CommandHeader> commands{command_list.prefetch_command_list};
+ command_resume_offset +=
+ ProcessCommands(commands.subspan(command_resume_offset), command_resume_offset);
+ if (puller.HasPendingAcquire()) {
+ return false;
+ }
+ command_resume_offset = 0;
dma_pushbuffer.pop();
return true;
}
@@ -83,13 +96,33 @@ bool DmaPusher::Step() {
subchannels[dma_state.subchannel]->current_dirty = memory_manager.IsMemoryDirty(dma_state.dma_get, words * sizeof(u32));
}
}
+ if (command_resume_offset == 0) {
+ if (g_nx2_ampere) {
+ ResolveSplitMethod(header.addr);
+ }
+ if (dma_pushbuffer_subindex < command_list.submit_hashes.size()) {
+ CheckSubmitHash(header, command_list.submit_hashes[dma_pushbuffer_subindex]);
+ }
+ }
+ // A resumed entry is read again: the words after the acquire may be new by now.
const bool use_safe = Settings::IsDMALevelDefault() ? Settings::IsGPULevelHigh() : Settings::IsDMALevelSafe();
if (use_safe) {
Tegra::Memory::GpuGuestMemory<Tegra::CommandHeader, Tegra::Memory::GuestMemoryFlags::SafeRead>headers(memory_manager, dma_state.dma_get, header.size, &command_headers);
- ProcessCommands(headers);
+ const std::span<const CommandHeader> commands{headers.data(), headers.size()};
+ command_resume_offset +=
+ ProcessCommands(commands.subspan(command_resume_offset), command_resume_offset);
} else {
Tegra::Memory::GpuGuestMemory<Tegra::CommandHeader, Tegra::Memory::GuestMemoryFlags::UnsafeRead>headers(memory_manager, dma_state.dma_get, header.size, &command_headers);
- ProcessCommands(headers);
+ const std::span<const CommandHeader> commands{headers.data(), headers.size()};
+ command_resume_offset +=
+ ProcessCommands(commands.subspan(command_resume_offset), command_resume_offset);
+ }
+ if (puller.HasPendingAcquire()) {
+ return false;
+ }
+ command_resume_offset = 0;
+ if (dma_state.method_count != 0) {
+ split_method_end = header.addr + header.size * sizeof(u32);
}
}
@@ -111,20 +144,23 @@ bool DmaPusher::Step() {
return true;
}
-void DmaPusher::ProcessCommands(std::span<const CommandHeader> commands) {
+size_t DmaPusher::ProcessCommands(std::span<const CommandHeader> commands, size_t word_offset) {
for (size_t index = 0; index < commands.size();) {
// Data word of methods command
if (dma_state.method_count && dma_state.non_incrementing) {
auto const& command_header = commands[index]; //must ref (MUltiMethod re)
- dma_state.dma_word_offset = u32(index * sizeof(u32));
- const u32 max_write = u32(std::min<std::size_t>(index + dma_state.method_count, commands.size()) - index);
+ dma_state.dma_word_offset = u64((word_offset + index) * sizeof(u32));
+ u32 max_write = u32(std::min<std::size_t>(index + dma_state.method_count, commands.size()) - index);
+ if (g_nx2_ampere && dma_state.method < non_puller_methods) {
+ max_write = 1; // a semaphore acquire may suspend the channel after any word
+ }
CallMultiMethod(&command_header.argument, max_write);
dma_state.method_count -= max_write;
dma_state.is_last_call = true;
index += max_write;
} else if (dma_state.method_count) {
auto const command_header = commands[index]; //can copy
- dma_state.dma_word_offset = u32(index * sizeof(u32));
+ dma_state.dma_word_offset = u64((word_offset + index) * sizeof(u32));
dma_state.is_last_call = dma_state.method_count <= 1;
CallMethod(command_header.argument);
dma_state.method += !dma_state.non_incrementing ? 1 : 0;
@@ -163,6 +199,46 @@ void DmaPusher::ProcessCommands(std::span<const CommandHeader> commands) {
}
index++;
}
+ if (puller.HasPendingAcquire()) {
+ return index;
+ }
+ }
+ return commands.size();
+}
+
+// NVN on Switch 2 ends GPFIFO entries in the middle of a method's data and calls another command
+// buffer (e.g. a compute dispatch) before the entry that carries the rest, which starts right
+// where the first one ended. An entry starting anywhere else is such a call: run it with a clean
+// method state and resume the unfinished method when the stream continues at that address.
+void DmaPusher::ResolveSplitMethod(GPUVAddr entry_address) {
+ if (dma_state.method_count != 0 && entry_address != split_method_end) {
+ if (parked_method) {
+ static std::atomic<u32> reported{};
+ if (reported.fetch_add(1) < 8) {
+ LOG_WARNING(HW_GPU,
+ "Dropping parked method {:#x} (sub {}, {} words left, resume {:#x})",
+ parked_method->method, parked_method->subchannel,
+ parked_method->method_count, parked_method->resume_address);
+ }
+ }
+ parked_method = ParkedMethod{
+ .method = dma_state.method,
+ .subchannel = dma_state.subchannel,
+ .method_count = dma_state.method_count,
+ .non_incrementing = dma_state.non_incrementing,
+ .increment_once = dma_increment_once,
+ .resume_address = split_method_end,
+ };
+ dma_state.method_count = 0;
+ dma_increment_once = false;
+ } else if (dma_state.method_count == 0 && parked_method &&
+ entry_address == parked_method->resume_address) {
+ dma_state.method = parked_method->method;
+ dma_state.subchannel = parked_method->subchannel;
+ dma_state.method_count = parked_method->method_count;
+ dma_state.non_incrementing = parked_method->non_incrementing;
+ dma_increment_once = parked_method->increment_once;
+ parked_method.reset();
}
}
@@ -182,6 +258,10 @@ void DmaPusher::CallMethod(u32 argument) {
});
} else {
auto subchannel = subchannels[dma_state.subchannel];
+ if (subchannel == nullptr) {
+ ReportUnboundSubchannel();
+ return;
+ }
if (!subchannel->execution_mask[dma_state.method]) {
subchannel->method_sink.emplace_back(dma_state.method, argument);
} else {
@@ -197,12 +277,73 @@ void DmaPusher::CallMultiMethod(const u32* base_start, u32 num_methods) {
puller.CallMultiMethod(*this, dma_state.method, dma_state.subchannel, base_start, num_methods, dma_state.method_count);
} else {
auto subchannel = subchannels[dma_state.subchannel];
+ if (subchannel == nullptr) {
+ ReportUnboundSubchannel();
+ return;
+ }
subchannel->ConsumeSink(system);
subchannel->current_dma_segment = dma_state.dma_get + dma_state.dma_word_offset;
subchannel->CallMultiMethod(system, dma_state.method, base_start, num_methods, dma_state.method_count);
}
}
+void DmaPusher::ReportUnboundSubchannel() {
+ // Methods sent to a subchannel with no engine (no SetObject yet) are dropped.
+ static std::atomic<u32> reported{};
+ if (reported.fetch_add(1) < 8) {
+ LOG_WARNING(HW_GPU, "Dropping method {:#x} on unbound subchannel {}", dma_state.method,
+ dma_state.subchannel);
+ }
+}
+
+void DmaPusher::CheckSubmitHash(const CommandListHeader& header, u64 submit_hash) {
+ std::vector<u32> words(header.size);
+ memory_manager.ReadBlockUnsafe(header.addr, words.data(), words.size() * sizeof(u32));
+ static std::atomic<u64> checked{};
+ static std::atomic<u64> changed{};
+ ++checked;
+ if (HashPushbufferWords(words) != submit_hash) {
+ if (changed.fetch_add(1) < 16) {
+ LOG_WARNING(HW_GPU, "pushbuffer entry {:#x}+{:#x} changed between submit and execution",
+ header.addr.Value(), header.size.Value());
+ }
+ }
+ if ((checked % 20000) == 0) {
+ LOG_WARNING(HW_GPU, "pushbuffer check: {} of {} entries changed after submit",
+ changed.load(), checked.load());
+ }
+}
+
+void DmaPusher::DumpCurrentEntry(const char* reason) {
+ LOG_WARNING(HW_GPU, "pushbuffer dump ({}): method={:#x} sub={} count={} non_inc={} get={:#x}",
+ reason, dma_state.method, dma_state.subchannel, dma_state.method_count,
+ dma_state.non_incrementing, dma_state.dma_get);
+ if (dma_pushbuffer.empty()) {
+ return;
+ }
+ const auto& lists = dma_pushbuffer.front().command_lists;
+ const size_t first = dma_pushbuffer_subindex >= 2 ? dma_pushbuffer_subindex - 2 : 0;
+ for (size_t i = first; i <= dma_pushbuffer_subindex && i < lists.size(); ++i) {
+ const CommandListHeader& header = lists[i];
+ LOG_WARNING(HW_GPU, " entry {} of {}: addr={:#x} size={:#x}", i, lists.size(),
+ header.addr.Value(), header.size.Value());
+ for (u64 word = 0; word < std::min<u64>(header.size, 0x80); word += 8) {
+ std::string gpu_line;
+ std::string cpu_line;
+ for (u64 j = word; j < std::min<u64>(header.size, word + 8); ++j) {
+ const GPUVAddr address = header.addr + j * sizeof(u32);
+ const u32 gpu_value = memory_manager.Read<u32>(address);
+ const u32 cpu_value = system.ApplicationMemory().Read32(address);
+ gpu_line += fmt::format(" {:08x}", gpu_value);
+ cpu_line += gpu_value == cpu_value ? std::string(" ........")
+ : fmt::format(" {:08x}", cpu_value);
+ }
+ LOG_WARNING(HW_GPU, " +{:03x}:{}", word * 4, gpu_line);
+ LOG_WARNING(HW_GPU, " cpu :{}", cpu_line);
+ }
+ }
+}
+
void DmaPusher::BindRasterizer(VideoCore::RasterizerInterface* rasterizer_) {
rasterizer = rasterizer_;
}
diff --git a/src/video_core/dma_pusher.h b/src/video_core/dma_pusher.h
index 62af628511..e735839d30 100644
--- a/src/video_core/dma_pusher.h
+++ b/src/video_core/dma_pusher.h
@@ -8,6 +8,7 @@
#include <array>
#include <condition_variable>
+#include <optional>
#include <span>
#include <vector>
#include <boost/container/small_vector.hpp>
@@ -114,8 +115,19 @@ struct CommandList final {
boost::container::small_vector<CommandListHeader, 512> command_lists;
boost::container::small_vector<CommandHeader, 512> prefetch_command_list;
+ /// EDEN_NX2_PB_CHECK: hash of each entry's words when it was submitted.
+ std::vector<u64> submit_hashes;
};
+/// FNV-1a over pushbuffer words (EDEN_NX2_PB_CHECK).
+inline u64 HashPushbufferWords(std::span<const u32> words) {
+ u64 hash = 0xcbf29ce484222325ULL;
+ for (const u32 word : words) {
+ hash = (hash ^ word) * 0x100000001b3ULL;
+ }
+ return hash;
+}
+
/// @brief The DmaPusher class implements DMA submission to FIFOs, providing an area of memory that the
/// emulated app fills with commands and tells PFIFO to process. The pushbuffers are then assembled
/// into a "command stream" consisting of 32-bit words that make up "commands".
@@ -132,6 +144,14 @@ public:
void DispatchCalls();
+ /// Switch 2: true while the channel is suspended on a semaphore acquire.
+ [[nodiscard]] bool HasPendingAcquire() const {
+ return puller.HasPendingAcquire();
+ }
+ [[nodiscard]] bool CanResumeAcquire() {
+ return puller.CanResumeAcquire(*this);
+ }
+
void BindSubchannel(Engines::EngineInterface* engine, u32 subchannel_id, Engines::EngineTypes engine_type) {
subchannels[subchannel_id] = engine;
subchannel_type[subchannel_id] = engine_type;
@@ -143,12 +163,24 @@ private:
static constexpr u32 non_puller_methods = 0x40;
static constexpr u32 max_subchannels = 8;
bool Step();
- void ProcessCommands(std::span<const CommandHeader> commands);
+ /// Returns how many words were processed: fewer than given when a semaphore acquire
+ /// suspended the channel.
+ size_t ProcessCommands(std::span<const CommandHeader> commands, size_t word_offset = 0);
+ size_t command_resume_offset{}; ///< Words of the current entry done before a suspension
void SetState(const CommandHeader& command_header);
void CallMethod(u32 argument);
void CallMultiMethod(const u32* base_start, u32 num_methods);
+ void ReportUnboundSubchannel();
+
+public:
+ /// Logs the GPFIFO entry being processed (and the one before it), read through the GPU MMU
+ /// and straight from guest memory, to find pushbuffer desyncs. Debug aid for Switch 2.
+ void DumpCurrentEntry(const char* reason);
+ void CheckSubmitHash(const CommandListHeader& header, u64 submit_hash);
+
+private:
public:
Common::ScratchBuffer<CommandHeader> command_headers; ///< Buffer for list of commands fetched at once
@@ -172,6 +204,21 @@ public:
Control::ChannelState& channel_state;
DmaState dma_state{};
+
+ // Switch 2: a method whose data continues in a later GPFIFO entry, set aside while another
+ // command buffer called in between runs.
+ struct ParkedMethod {
+ u32 method;
+ u32 subchannel;
+ u32 method_count;
+ bool non_incrementing;
+ bool increment_once;
+ GPUVAddr resume_address; ///< Where the entry that continues the method starts
+ };
+ void ResolveSplitMethod(GPUVAddr entry_address);
+ std::optional<ParkedMethod> parked_method;
+ GPUVAddr split_method_end{}; ///< End of the last entry that ended inside a method
+
std::array<Engines::EngineInterface*, max_subchannels> subchannels{};
std::array<Engines::EngineTypes, max_subchannels> subchannel_type;
diff --git a/src/video_core/engines/kepler_compute.cpp b/src/video_core/engines/kepler_compute.cpp
index 96a12539a2..166bbd6d53 100644
--- a/src/video_core/engines/kepler_compute.cpp
+++ b/src/video_core/engines/kepler_compute.cpp
@@ -35,13 +35,24 @@ void KeplerCompute::BindRasterizer(VideoCore::RasterizerInterface* rasterizer_)
void KeplerCompute::ConsumeSinkImpl(Core::System& system) {
for (auto [method, value] : method_sink) {
- regs.reg_array[method] = value;
+ if (method < Regs::NUM_REGS) {
+ regs.reg_array[method] = value;
+ }
}
method_sink.clear();
}
void KeplerCompute::CallMethod(Core::System& system, u32 method, u32 method_argument, bool is_last_call) {
- ASSERT_MSG(method < Regs::NUM_REGS, "Invalid KeplerCompute register, increase the size of the Regs structure");
+ if (method >= Regs::NUM_REGS) {
+ // Ampere compute (Switch 2) uses methods past Kepler's register file; writing them would
+ // run off reg_array, so drop them.
+ static bool reported = false;
+ if (!reported) {
+ reported = true;
+ LOG_WARNING(HW_GPU, "Ignoring KeplerCompute method {:#x} past the register file", method);
+ }
+ return;
+ }
regs.reg_array[method] = method_argument;
diff --git a/src/video_core/engines/maxwell_dma.cpp b/src/video_core/engines/maxwell_dma.cpp
index ed4675ed12..5ab953e510 100644
--- a/src/video_core/engines/maxwell_dma.cpp
+++ b/src/video_core/engines/maxwell_dma.cpp
@@ -42,7 +42,11 @@ void MaxwellDMA::ConsumeSinkImpl(Core::System& system) {
}
void MaxwellDMA::CallMethod(Core::System& system, u32 method, u32 method_argument, bool is_last_call) {
- ASSERT_MSG(method < NUM_REGS, "Invalid MaxwellDMA register");
+ if (method >= NUM_REGS) {
+ // Ampere copy (Switch 2) can use methods past this register file; don't write past it.
+ LOG_DEBUG(HW_GPU, "Ignoring MaxwellDMA method {:#x} past the register file", method);
+ return;
+ }
regs.reg_array[method] = method_argument;
diff --git a/src/video_core/engines/puller.cpp b/src/video_core/engines/puller.cpp
index 9e78d586d1..97a58903fe 100644
--- a/src/video_core/engines/puller.cpp
+++ b/src/video_core/engines/puller.cpp
@@ -4,6 +4,7 @@
// SPDX-FileCopyrightText: 2022 yuzu Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
+#include <atomic>
#include <cstdlib>
#include "common/assert.h"
@@ -30,8 +31,58 @@ namespace {
constexpr u32 Gf100BindClassMask = 0xffff;
constexpr u32 Gf100BindValidMask = 0x1f0000 | Gf100BindClassMask;
+// Reads a semaphore word, flushing a GPU-written copy out of the buffer cache first.
+u32 ReadSemaphoreWord(DmaPusher& dma_pusher, GPUVAddr address) {
+ if (const auto device = dma_pusher.memory_manager.GpuToCpuAddress(address);
+ device && dma_pusher.rasterizer->MustFlushRegion(*device, sizeof(u32),
+ VideoCommon::CacheType::BufferCache)) {
+ dma_pusher.rasterizer->FlushRegion(*device, sizeof(u32),
+ VideoCommon::CacheType::BufferCache);
+ }
+ return dma_pusher.memory_manager.Read<u32>(address);
+}
+
} // Anonymous namespace
+bool Puller::PendingAcquire::Matches(u32 current) const {
+ switch (static_cast<GpuSemaphoreOperation>(operation)) {
+ case GpuSemaphoreOperation::AcquireEqual:
+ return current == value;
+ case GpuSemaphoreOperation::AcquireGequal:
+ return current >= value;
+ case GpuSemaphoreOperation::AcquireMask:
+ return (current & value) != 0;
+ default:
+ return true;
+ }
+}
+
+bool Puller::CanResumeAcquire(DmaPusher& dma_pusher) {
+ if (!pending_acquire) {
+ return true;
+ }
+ const u32 current = ReadSemaphoreWord(dma_pusher, pending_acquire->address);
+ if (pending_acquire->Matches(current)) {
+ return true;
+ }
+ if (!pending_acquire->reported &&
+ std::chrono::steady_clock::now() - pending_acquire->since > std::chrono::seconds{2}) {
+ pending_acquire->reported = true;
+ LOG_WARNING(HW_GPU, "Channel {} waits on semaphore {:#x} (op {}, {:#x}, now {:#x}) for 2 s",
+ dma_pusher.channel_state.bind_id, pending_acquire->address,
+ pending_acquire->operation, pending_acquire->value, current);
+ }
+ return false;
+}
+
+bool Puller::ResumeAcquire(DmaPusher& dma_pusher) {
+ if (!CanResumeAcquire(dma_pusher)) {
+ return false;
+ }
+ pending_acquire.reset();
+ return true;
+}
+
void Puller::ProcessBindMethod(DmaPusher& dma_pusher, const MethodCall& method_call) {
LOG_DEBUG(HW_GPU, "Binding subchannel {} to engine {:#x}", method_call.subchannel, method_call.argument);
u32 engine = method_call.argument;
@@ -109,12 +160,39 @@ void Puller::ProcessSemaphoreTriggerMethod(DmaPusher& dma_pusher) {
if (op == GpuSemaphoreOperation::WriteLong) {
const GPUVAddr sequence_address{regs.semaphore_address.SemaphoreAddress()};
const u32 payload = regs.semaphore_sequence;
- dma_pusher.rasterizer->Query(sequence_address, VideoCommon::QueryType::Payload, VideoCommon::QueryPropertiesFlags::HasTimeout, payload, 0);
- const u64 source_page = g_nx2_semaphore_source_page;
- if (source_page != 0 && (sequence_address & ~GPUVAddr{0xFFF}) == source_page) {
- const GPUVAddr mirror = g_nx2_semaphore_mirror_page + (sequence_address & 0xFFF);
+ // RELEASE_SIZE (bit 24) set means a 4-byte release without the timestamp; writing 16
+ // bytes there clobbers the words next to it (Switch 2 packs semaphores tightly).
+ const auto properties = g_nx2_ampere && (regs.semaphore_trigger & (1U << 24)) != 0
+ ? VideoCommon::QueryPropertiesFlags::IsAFence
+ : VideoCommon::QueryPropertiesFlags::HasTimeout;
+ dma_pusher.rasterizer->Query(sequence_address, VideoCommon::QueryType::Payload, properties, payload, 0);
+ if (const GPUVAddr mirror = g_nx2_ampere ? Nx2SemaphoreMirror(sequence_address) : 0;
+ mirror != 0) {
LOG_DEBUG(HW_GPU, "Switch 2 semaphore {:#x}={:#x} mirrored to {:#x}", sequence_address, payload, mirror);
- dma_pusher.rasterizer->Query(mirror, VideoCommon::QueryType::Payload, VideoCommon::QueryPropertiesFlags::HasTimeout, payload, 0);
+ dma_pusher.rasterizer->Query(mirror, VideoCommon::QueryType::Payload, properties, payload, 0);
+ }
+ } else if (g_nx2_ampere) {
+ // Switch 2 waits on semaphores the CPU (or another channel) releases, e.g. once it has
+ // written the commands this channel runs next: suspend the channel until the value is
+ // met instead of reading ahead (approach from changes.patch).
+ PendingAcquire acquire{
+ .address = regs.semaphore_address.SemaphoreAddress(),
+ .value = regs.semaphore_sequence,
+ .operation = static_cast<u32>(op),
+ .since = std::chrono::steady_clock::now(),
+ .reported = false,
+ };
+ if (op != GpuSemaphoreOperation::AcquireEqual && op != GpuSemaphoreOperation::AcquireGequal &&
+ op != GpuSemaphoreOperation::AcquireMask) {
+ LOG_ERROR(HW_GPU, "Invalid semaphore operation {}", static_cast<u32>(op));
+ return;
+ }
+ if (acquire.Matches(ReadSemaphoreWord(dma_pusher, acquire.address))) {
+ return;
+ }
+ dma_pusher.rasterizer->ReleaseFences();
+ if (!acquire.Matches(ReadSemaphoreWord(dma_pusher, acquire.address))) {
+ pending_acquire = acquire;
}
} else {
do {
@@ -167,6 +245,36 @@ void Puller::ProcessSemaphoreAcquire(DmaPusher& dma_pusher) {
}
}
+// Ampere SEM_EXECUTE (method 0x1B): operation in bits 2:0 on the semaphore at
+// SEM_ADDR_HI:SEM_ADDR_LO (methods 0x18/0x17) with payload SEM_PAYLOAD_LO (0x19).
+void Puller::ProcessSemaphoreExecute(DmaPusher& dma_pusher, u32 argument) {
+ const GPUVAddr address = (GPUVAddr{regs.reg_array[0x18] & 0x1FFFFFF} << 32) |
+ (regs.reg_array[0x17] & ~3U);
+ const u32 payload = regs.reg_array[0x19];
+ const u32 operation = argument & 7;
+ if (operation == 1) { // RELEASE
+ dma_pusher.rasterizer->Query(address, VideoCommon::QueryType::Payload,
+ VideoCommon::QueryPropertiesFlags::IsAFence, payload, 0);
+ return;
+ }
+ // Acquires: check once after flushing pending fences; never block the GPU thread on them.
+ dma_pusher.rasterizer->ReleaseFences();
+ const u32 word = dma_pusher.memory_manager.Read<u32>(address);
+ const bool ready = operation == 0 ? word == payload
+ : operation == 2 ? word >= payload
+ : operation == 3 ? static_cast<s32>(word - payload) >= 0
+ : operation == 4 ? (word & payload) != 0
+ : operation == 5 ? (word & payload) == 0
+ : true;
+ if (!ready) {
+ static std::atomic<u32> reported{};
+ if (reported.fetch_add(1) < 8) {
+ LOG_WARNING(HW_GPU, "SEM_EXECUTE op {} at {:#x}: {:#x} vs {:#x} not met, continuing",
+ operation, address, word, payload);
+ }
+ }
+}
+
/// Calls a GPU puller method.
void Puller::CallPullerMethod(DmaPusher& dma_pusher, const MethodCall& method_call) {
static const bool nx2_trace = std::getenv("EDEN_NX2_PULLER_TRACE") != nullptr;
@@ -221,11 +329,18 @@ void Puller::CallPullerMethod(DmaPusher& dma_pusher, const MethodCall& method_ca
break;
}
case BufferMethods::SemaphoreAcquire: {
- ProcessSemaphoreAcquire(dma_pusher);
+ // On Ampere host classes 0x17..0x1A are SEM_ADDR_LO/HI and SEM_PAYLOAD_LO/HI.
+ if (!g_nx2_ampere) {
+ ProcessSemaphoreAcquire(dma_pusher);
+ }
break;
}
case BufferMethods::SemaphoreRelease: {
- ProcessSemaphoreRelease(dma_pusher);
+ if (g_nx2_ampere) {
+ ProcessSemaphoreExecute(dma_pusher, method_call.argument);
+ } else {
+ ProcessSemaphoreRelease(dma_pusher);
+ }
break;
}
case BufferMethods::Yield: {
@@ -235,6 +350,10 @@ void Puller::CallPullerMethod(DmaPusher& dma_pusher, const MethodCall& method_ca
}
default:
LOG_ERROR(HW_GPU, "Special puller engine method {:X} not implemented", method);
+ if (static bool dumped = false; g_nx2_ampere && !dumped) {
+ dumped = true;
+ dma_pusher.DumpCurrentEntry("unknown host method");
+ }
break;
}
}
diff --git a/src/video_core/engines/puller.h b/src/video_core/engines/puller.h
index e8994f5640..2b9e2c5df3 100644
--- a/src/video_core/engines/puller.h
+++ b/src/video_core/engines/puller.h
@@ -7,7 +7,9 @@
#pragma once
#include <array>
+#include <chrono>
#include <cstddef>
+#include <optional>
#include <vector>
#include "common/bit_field.h"
#include "common/common_funcs.h"
@@ -70,6 +72,24 @@ public:
BitField<8, 24, u32> syncpoint_id;
};
+ /// Switch 2: a semaphore acquire this channel is suspended on.
+ struct PendingAcquire {
+ GPUVAddr address;
+ u32 value;
+ u32 operation;
+ std::chrono::steady_clock::time_point since;
+ bool reported;
+
+ [[nodiscard]] bool Matches(u32 current) const;
+ };
+
+ [[nodiscard]] bool HasPendingAcquire() const {
+ return pending_acquire.has_value();
+ }
+ [[nodiscard]] bool CanResumeAcquire(DmaPusher& dma_pusher);
+ /// Clears the pending acquire once it is met; false while the channel has to keep waiting.
+ bool ResumeAcquire(DmaPusher& dma_pusher);
+
void CallMethod(DmaPusher& dma_pusher, const MethodCall& method_call);
void CallMultiMethod(DmaPusher& dma_pusher, u32 method, u32 subchannel, const u32* base_start, u32 amount, u32 methods_pending);
void BindRasterizer(DmaPusher& dma_pusher, VideoCore::RasterizerInterface* rasterizer);
@@ -125,8 +145,11 @@ private:
void ProcessSemaphoreAcquire(DmaPusher& dma_pusher);
void ProcessSemaphoreRelease(DmaPusher& dma_pusher);
void ProcessSemaphoreTriggerMethod(DmaPusher& dma_pusher);
+ void ProcessSemaphoreExecute(DmaPusher& dma_pusher, u32 argument);
[[nodiscard]] bool ExecuteMethodOnEngine(DmaPusher& dma_pusher, u32 method);
+ std::optional<PendingAcquire> pending_acquire;
+
/// Mapping of command subchannels to their bound engine ids
std::array<EngineID, 8> bound_engines{};
diff --git a/src/video_core/gpu_thread.cpp b/src/video_core/gpu_thread.cpp
index a01375e7be..97e18df499 100644
--- a/src/video_core/gpu_thread.cpp
+++ b/src/video_core/gpu_thread.cpp
@@ -4,6 +4,9 @@
// SPDX-FileCopyrightText: Copyright 2019 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
+#include <chrono>
+#include <thread>
+
#include "common/assert.h"
#include "common/scope_exit.h"
#include "common/settings.h"
@@ -36,7 +39,16 @@ void ThreadManager::StartThread(VideoCore::RendererBase& renderer, Core::Fronten
auto current_context = context.Acquire();
CommandDataContainer next;
while (!stop_token.stop_requested()) {
- state.queue.PopWait(next, stop_token);
+ if (scheduler.HasPendingChannels()) {
+ // Suspended channels wait on semaphores the CPU releases: poll them meanwhile.
+ scheduler.ResumePending(system.GPU());
+ if (!state.queue.TryPop(next)) {
+ std::this_thread::sleep_for(std::chrono::milliseconds{1});
+ continue;
+ }
+ } else {
+ state.queue.PopWait(next, stop_token);
+ }
if (stop_token.stop_requested()) {
break;
}
diff --git a/src/video_core/memory_manager.cpp b/src/video_core/memory_manager.cpp
index 0290500e7c..52e9db189d 100644
--- a/src/video_core/memory_manager.cpp
+++ b/src/video_core/memory_manager.cpp
@@ -251,7 +251,10 @@ T MemoryManager::Read(GPUVAddr addr) const {
return value;
}
- ASSERT(false);
+ static std::atomic<u32> reported{};
+ if (reported.fetch_add(1) < 16) {
+ LOG_ERROR(HW_GPU, "Read{} of unmapped GPU address {:#x}", sizeof(T) * 8, addr);
+ }
return {};
}
diff --git a/src/video_core/nx2.h b/src/video_core/nx2.h
index 9fdf0a7c68..ef6619df3f 100644
--- a/src/video_core/nx2.h
+++ b/src/video_core/nx2.h
@@ -3,6 +3,7 @@
#pragma once
+#include <array>
#include <atomic>
#include "common/common_types.h"
@@ -13,10 +14,34 @@ namespace Tegra {
// Ampere classes and the Turing+ macro ISA, which Eden's Maxwell engines don't implement.
inline std::atomic<bool> g_nx2_ampere{false};
-// NVN2 queues release their progress values into one GPU page and wait (GPU acquires and CPU
-// polls) on the same offsets in the page below it. Something on the console copies one into
-// the other; until that is known, the puller mirrors releases that land in the source page.
-inline std::atomic<u64> g_nx2_semaphore_source_page{0}; // GPU VA of the released-to page
-inline std::atomic<u64> g_nx2_semaphore_mirror_page{0}; // GPU VA of the waited-on page
+// NVN2 queues release their progress values into the second page of a two-page GPU-accessible
+// region and GPU acquires read the same offsets in the first page (the CPU polls the second).
+// Something on the console copies one into the other; until that is known, the puller mirrors
+// releases that land in a second page. Every queue has such a region, so keep a small table.
+inline constexpr size_t Nx2MaxSemaphoreRegions = 32;
+inline std::array<std::atomic<u64>, Nx2MaxSemaphoreRegions> g_nx2_semaphore_regions{}; // starts
+
+inline void Nx2AddSemaphoreRegion(u64 start) {
+ for (auto& region : g_nx2_semaphore_regions) {
+ u64 expected = 0;
+ if (region.load() == start || region.compare_exchange_strong(expected, start)) {
+ return;
+ }
+ }
+}
+
+/// GPU VA in the first page that mirrors a release at `address`, or 0.
+inline u64 Nx2SemaphoreMirror(u64 address) {
+ for (const auto& region : g_nx2_semaphore_regions) {
+ const u64 start = region.load(std::memory_order_relaxed);
+ if (start == 0) {
+ break;
+ }
+ if (address >= start + 0x1000 && address < start + 0x2000) {
+ return address - 0x1000;
+ }
+ }
+ return 0;
+}
} // namespace Tegra
diff --git a/src/video_core/query_cache/query_cache.h b/src/video_core/query_cache/query_cache.h
index b8c1bc4f45..11b2f631a5 100644
--- a/src/video_core/query_cache/query_cache.h
+++ b/src/video_core/query_cache/query_cache.h
@@ -7,6 +7,7 @@
#pragma once
#include <array>
+#include <cstdlib>
#include <deque>
#include <memory>
#include <mutex>
@@ -233,6 +234,10 @@ void QueryCacheBase<Traits>::CounterReport(GPUVAddr addr, QueryType counter_type
QueryPropertiesFlags flags, u32 payload, u32 subreport) {
const bool has_timestamp = True(flags & QueryPropertiesFlags::HasTimeout);
const bool is_fence = True(flags & QueryPropertiesFlags::IsAFence);
+ if (static const bool trace = std::getenv("EDEN_NX2_QUERY_TRACE") != nullptr; trace) {
+ LOG_INFO(HW_GPU, "nx2 report addr={:#x} type={} flags={:#x} payload={:#x}", addr,
+ static_cast<u32>(counter_type), static_cast<u32>(flags), payload);
+ }
size_t streamer_id = static_cast<size_t>(counter_type);
auto* streamer = impl->streamers[streamer_id];
if (streamer == nullptr) [[unlikely]] {