// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project // SPDX-License-Identifier: GPL-3.0-or-later // SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later #include #include #include #include #include "common/scope_exit.h" #include "common/settings.h" #include "core/core.h" #include "core/memory.h" #include "video_core/nx2_profile.h" #include "video_core/dma_pusher.h" #include "video_core/control/channel_state.h" #include "video_core/engines/kepler_compute.h" #include "video_core/engines/maxwell_3d.h" #include "video_core/gpu.h" #include "video_core/gpu_split.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 { constexpr u32 MacroRegistersStart = 0xE00; constexpr u32 ComputeInline = 0x6D; DmaPusher::DmaPusher(Core::System& system_, MemoryManager& memory_manager_, Control::ChannelState& channel_state_) : system{system_} , memory_manager{memory_manager_} , channel_state{channel_state_} , signal_sync{false} , synced{false} {} DmaPusher::~DmaPusher() = default; void DmaPusher::DispatchCalls() { // 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()) { break; } } if (auto& split = VideoCommon::GpuSplit::Get(); split.Active()) { split.Defer([gpu = &system.GPU()] { gpu->FlushCommands(); gpu->OnCommandListEnd(); }); split.Publish(); return; } system.GPU().FlushCommands(); system.GPU().OnCommandListEnd(); } bool DmaPusher::Step() { if (!ib_enable || dma_pushbuffer.empty()) { return false; } if (!puller.ResumeAcquire(*this)) { return false; } CommandList& command_list = dma_pushbuffer.front(); const size_t prefetch_size = command_list.prefetch_command_list.size(); const size_t command_list_size = command_list.command_lists.size(); if (prefetch_size == 0 && command_list_size == 0) { dma_pushbuffer.pop(); dma_pushbuffer_subindex = 0; return true; } if (prefetch_size > 0) { const std::span 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; } auto& current_command = command_list.command_lists[dma_pushbuffer_subindex]; const CommandListHeader& header = current_command; dma_state.dma_get = header.addr; if (signal_sync && !synced) { std::unique_lock lk(sync_mutex); sync_cv.wait(lk, [this]() { return synced; }); signal_sync = false; synced = false; } if (header.size > 0) { if (subchannels[dma_state.subchannel] && dma_state.method_count) { const auto engine = subchannel_type[dma_state.subchannel]; const bool kepler_payload = engine == Engines::EngineTypes::KeplerCompute && dma_state.method == ComputeInline && dma_state.non_incrementing; const bool macro_payload = engine == Engines::EngineTypes::Maxwell3D && dma_state.method >= MacroRegistersStart; if (kepler_payload || macro_payload) { const size_t words = std::min(dma_state.method_count, header.size); auto* const engine_state = subchannels[dma_state.subchannel]; if (auto& split = VideoCommon::GpuSplit::Get(); split.Active()) { // The caches behind IsMemoryDirty belong to the split's back thread. split.Defer([engine_state, memory = &memory_manager, address = GPUVAddr{dma_state.dma_get}, bytes = words * sizeof(u32)] { engine_state->current_dirty = memory->IsMemoryDirty(address, bytes); }); } else { engine_state->current_dirty = memory_manager.IsMemoryDirty(dma_state.dma_get, words * sizeof(u32)); } } } if (command_resume_offset == 0) { // A method's data continues across GP entries, whatever their level: title 4 ends a // main entry after a macro's first word and supplies the remaining parameters in a // 5-word subroutine entry. EDEN_NX2_PARK_SPLIT_METHODS restores the old guess (an // entry elsewhere is a call that starts clean; resume where the split one ended). static const bool park_split_methods = std::getenv("EDEN_NX2_PARK_SPLIT_METHODS") != nullptr; if (g_nx2_ampere) { if (static u32 split_logs{}; dma_state.method_count != 0 && split_logs < 64) { ++split_logs; LOG_DEBUG(HW_GPU, "Split method {:#x} ({} left) on ch {}: next entry {:#x} size {} raw {:#x} (level bit {}, priv bit {}), expected {:#x}, parked {}", dma_state.method, dma_state.method_count, channel_state.bind_id, u64{header.addr}, u64{header.size}, header.raw, u64{header.is_push_buffer}, u64{header.allow_flush}, split_method_end, parked_methods.size()); std::array next_words{}; std::array prev_words{}; memory_manager.ReadBlockUnsafe(header.addr, next_words.data(), std::min(header.size, 8) * sizeof(u32)); memory_manager.ReadBlockUnsafe(split_method_end - sizeof(prev_words), prev_words.data(), sizeof(prev_words)); LOG_DEBUG(HW_GPU, " previous entry ends {:08x}; next entry starts {:08x}", fmt::join(prev_words, " "), fmt::join(next_words, " ")); } if (park_split_methods) { 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. // A safe read flushes the caches, which belong to the split's back thread while it runs // (the split only starts with unsafe reads; the settings can change mid-game). const bool use_safe = !VideoCommon::GpuSplit::Get().Active() && (Settings::IsDMALevelDefault() ? Settings::IsGPULevelHigh() : Settings::IsDMALevelSafe()); // The words as submitted, unless this is a resumed entry or the GPU has newer ones the // safe read below would flush (with the split, the front can't tell: it reads memory // unflushed anyway). Entries on pages the GPU writes (culling) always take the flushing // read: their words at submit time are an older frame's. const bool submitted = command_resume_offset == 0 && dma_pushbuffer_subindex < command_list.submitted_offsets.size() && (command_list.snapshot_suspensions & 1) == 0 && command_list.snapshot_suspensions == suspensions.load(std::memory_order_acquire) && !IsGpuWrittenCommands(dma_state.dma_get, size_t{header.size} * sizeof(u32)) && !(use_safe && memory_manager.IsMemoryDirty(dma_state.dma_get, header.size * sizeof(u32))); if (submitted) { const std::span commands{ command_list.submitted_words.data() + command_list.submitted_offsets[dma_pushbuffer_subindex], header.size}; RecordParsedEntry(header, commands); command_resume_offset += ProcessCommands(commands, 0); } else if (use_safe) { Tegra::Memory::GpuGuestMemoryheaders(memory_manager, dma_state.dma_get, header.size, &command_headers); const std::span commands{headers.data(), headers.size()}; RecordParsedEntry(header, commands); command_resume_offset += ProcessCommands(commands.subspan(command_resume_offset), command_resume_offset); } else if (ReadGpuWrittenCommands(dma_state.dma_get, static_cast(header.size))) { const std::span commands{command_headers.data(), header.size}; RecordParsedEntry(header, commands); command_resume_offset += ProcessCommands(commands.subspan(command_resume_offset), command_resume_offset); } else { FlushGpuWrittenCommands(dma_state.dma_get, size_t{header.size} * sizeof(u32)); Tegra::Memory::GpuGuestMemoryheaders(memory_manager, dma_state.dma_get, header.size, &command_headers); const std::span commands{headers.data(), headers.size()}; RecordParsedEntry(header, commands); 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); } } if (++dma_pushbuffer_subindex >= command_list_size) { // A split method resumes within the submission that split it; ring addresses repeat // every frame, so a stale one could otherwise swallow the headers of a later entry. for (const auto& parked : parked_methods) { static std::atomic reported{}; if (reported.fetch_add(1) < 16) { LOG_WARNING(HW_GPU, "Submission ended with parked method {:#x} (sub {}, {} words left, " "resume {:#x})", parked.method, parked.subchannel, parked.method_count, parked.resume_address); } } parked_methods.clear(); dma_pushbuffer.pop(); dma_pushbuffer_subindex = 0; } else { signal_sync = command_list.command_lists[dma_pushbuffer_subindex].sync && Settings::values.sync_memory_operations.GetValue(); } if (signal_sync) { VideoCommon::GpuSplit::Get().Drain(); rasterizer->SignalFence([this]() { std::scoped_lock lk(sync_mutex); synced = true; sync_cv.notify_all(); }); } return true; } size_t DmaPusher::ProcessCommands(std::span 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 = u64((word_offset + index) * sizeof(u32)); u32 max_write = u32(std::min(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 = 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; dma_state.non_incrementing |= dma_increment_once; dma_state.method_count--; index++; } else { auto const command_header = commands[index]; //can copy // No command active - this is the first word of a new one switch (command_header.mode) { case SubmissionMode::Increasing: SetState(command_header); dma_state.non_incrementing = false; dma_increment_once = false; break; case SubmissionMode::NonIncreasing: SetState(command_header); dma_state.non_incrementing = true; dma_increment_once = false; break; case SubmissionMode::Inline: dma_state.method = command_header.method; dma_state.subchannel = command_header.subchannel; dma_state.dma_word_offset = u64(-s64(dma_state.dma_get)); // negate to set address as 0 CallMethod(command_header.arg_count); dma_state.non_incrementing = true; dma_increment_once = false; break; case SubmissionMode::IncreaseOnce: SetState(command_header); dma_state.non_incrementing = false; dma_increment_once = true; break; default: break; } 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) { const auto continued = std::ranges::find(parked_methods, entry_address, &ParkedMethod::resume_address); std::optional resume; if (continued != parked_methods.end()) { resume = *continued; parked_methods.erase(continued); } if (dma_state.method_count != 0 && entry_address != split_method_end) { if (parked_methods.size() == MaxParkedMethods) { static std::atomic reported{}; if (reported.fetch_add(1) < 8) { const auto& oldest = parked_methods.front(); LOG_WARNING(HW_GPU, "Dropping parked method {:#x} (sub {}, {} words left, resume {:#x})", oldest.method, oldest.subchannel, oldest.method_count, oldest.resume_address); } parked_methods.erase(parked_methods.begin()); } parked_methods.push_back(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; } if (resume && dma_state.method_count == 0) { dma_state.method = resume->method; dma_state.subchannel = resume->subchannel; dma_state.method_count = resume->method_count; dma_state.non_incrementing = resume->non_incrementing; dma_increment_once = resume->increment_once; } } void DmaPusher::SetState(const CommandHeader& command_header) { dma_state.method = command_header.method; dma_state.subchannel = command_header.subchannel; dma_state.method_count = command_header.method_count; } // EDEN_NX2_SEM_WATCH=: log every method whose argument is the low word of an // address in the 16 bytes there (finds the engine and method that release a semaphore). static const u32 g_semaphore_watch = [] { const char* value = std::getenv("EDEN_NX2_SEM_WATCH"); return value ? static_cast(std::strtoull(value, nullptr, 16)) : 0U; }(); // EDEN_NX2_PB_CHECK also keeps the last parsed entries, for DumpCurrentEntry. static const bool g_record_parsed_entries = std::getenv("EDEN_NX2_PB_CHECK") != nullptr; void DmaPusher::WatchArgument(u32 method, u32 argument) const { const u32 watch = g_semaphore_watch; if ((argument & ~0xfU) == watch) { LOG_INFO(HW_GPU, "Semaphore watch: channel {} subchannel {} method {:#x} argument {:#x}", channel_state.bind_id, dma_state.subchannel, method, argument); } } bool DmaPusher::WantsSegment() const { // Only macro parameters and compute uploads read the engine's current_dma_segment. const auto engine = subchannel_type[dma_state.subchannel]; return engine == Engines::EngineTypes::KeplerCompute || (engine == Engines::EngineTypes::Maxwell3D && dma_state.method >= MacroRegistersStart); } void DmaPusher::CallMethod(u32 argument) { if (g_semaphore_watch != 0) [[unlikely]] { WatchArgument(dma_state.method, argument); } auto& split = VideoCommon::GpuSplit::Get(); if (dma_state.method < non_puller_methods) { puller.CallPullerMethod(*this, Engines::Puller::MethodCall{ dma_state.method, argument, dma_state.subchannel, dma_state.method_count, }); } else { auto subchannel = subchannels[dma_state.subchannel]; if (subchannel == nullptr) { ReportUnboundSubchannel(); return; } // Compute dispatches and DMA copies write the command lists ReadGpuWrittenCommands reads. const auto engine_type = subchannel_type[dma_state.subchannel]; const bool writer = engine_type == Engines::EngineTypes::KeplerCompute || engine_type == Engines::EngineTypes::MaxwellDMA; SCOPE_EXIT { if (writer) { split.NoteWriter(); } }; if (split.Active()) { const GPUVAddr segment = dma_state.dma_get + dma_state.dma_word_offset; if (split.FrontMacros()) { const auto type = subchannel_type[dma_state.subchannel]; if (type == Engines::EngineTypes::Maxwell3D) { auto* const maxwell3d = static_cast(subchannel); if (maxwell3d->GetNativeMacroEngine().native_turing) { maxwell3d->FrontCallMethod(system, split, dma_state.method, argument, dma_state.is_last_call); return; } } else if (type == Engines::EngineTypes::KeplerCompute) { auto* const compute = static_cast(subchannel); if (compute->FrontRunsMacros()) { compute->FrontCallMethod(system, split, dma_state.method, argument, dma_state.is_last_call, &segment); return; } } } split.Method(subchannel, dma_state.method, argument, dma_state.is_last_call, WantsSegment() ? &segment : nullptr); return; } if (!subchannel->execution_mask[dma_state.method]) { subchannel->method_sink.emplace_back(dma_state.method, argument); } else { VideoCommon::Nx2Profile::Scope method_scope{VideoCommon::Nx2Profile::Zone::Method}; subchannel->ConsumeSink(system); subchannel->current_dma_segment = dma_state.dma_get + dma_state.dma_word_offset; subchannel->CallMethod(system, dma_state.method, argument, dma_state.is_last_call); } } } void DmaPusher::CallMultiMethod(const u32* base_start, u32 num_methods) { if (g_semaphore_watch != 0) [[unlikely]] { for (u32 i = 0; i < num_methods; ++i) { WatchArgument(dma_state.method + (dma_state.non_incrementing ? 0 : i), base_start[i]); } } auto& split = VideoCommon::GpuSplit::Get(); if (dma_state.method < non_puller_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; } // Compute dispatches and DMA copies write the command lists ReadGpuWrittenCommands reads. const auto engine_type = subchannel_type[dma_state.subchannel]; const bool writer = engine_type == Engines::EngineTypes::KeplerCompute || engine_type == Engines::EngineTypes::MaxwellDMA; SCOPE_EXIT { if (writer) { split.NoteWriter(); } }; if (split.Active()) { const GPUVAddr segment = dma_state.dma_get + dma_state.dma_word_offset; if (split.FrontMacros()) { const auto type = subchannel_type[dma_state.subchannel]; if (type == Engines::EngineTypes::Maxwell3D) { auto* const maxwell3d = static_cast(subchannel); if (maxwell3d->GetNativeMacroEngine().native_turing) { maxwell3d->FrontCallMultiMethod(system, split, dma_state.method, base_start, num_methods, dma_state.method_count); return; } } else if (type == Engines::EngineTypes::KeplerCompute) { auto* const compute = static_cast(subchannel); if (compute->FrontRunsMacros()) { compute->FrontCallMultiMethod(system, split, dma_state.method, base_start, num_methods, dma_state.method_count, &segment); return; } } } split.MultiMethod(subchannel, dma_state.method, base_start, num_methods, dma_state.method_count, WantsSegment() ? &segment : nullptr); return; } VideoCommon::Nx2Profile::Scope method_scope{VideoCommon::Nx2Profile::Zone::Method}; 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); } } bool DmaPusher::IsGpuWrittenCommands(GPUVAddr address, size_t size) { if (!Tegra::g_nx2_ampere.load(std::memory_order_relaxed) || size == 0) { return false; } for (GPUVAddr page = address & ~GPUVAddr{0xfff}; page < address + size; page += 0x1000) { if (const auto device = memory_manager.GpuToCpuAddress(page); device && Tegra::Nx2IsGpuWritten(*device)) { return true; } } return false; } bool DmaPusher::ReadGpuWrittenCommands(GPUVAddr address, u32 count) { // The pages are marked as command list pages: from the next write on, the buffer cache copies // them to host memory right after the dispatch or copy that writes them and submits. The // front then waits only for the back to run that work and for the GPU to finish the copy, not // for everything queued and a cache flush (FlushGpuWrittenCommands: ~400 GPU round trips a // second in a Mario Kart World race, during which the back had nothing to do). Pages // FlushGpuWrittenCommands flushed whole and no GPU work wrote since are read from guest // memory. if (!Tegra::g_nx2_ampere.load(std::memory_order_relaxed) || count == 0 || rasterizer == nullptr) { return false; } const GPUVAddr end = address + u64{count} * sizeof(u32); bool written = false; for (GPUVAddr page = address & ~GPUVAddr{0xfff}; page < end; page += 0x1000) { if (const auto device = memory_manager.GpuToCpuAddress(page); device && Tegra::Nx2IsGpuWritten(*device)) { Tegra::Nx2MarkCommandListPage(*device); written = true; } } if (!written) { return false; } if (auto& split = VideoCommon::GpuSplit::Get(); split.Active()) { split.WaitForWriters(); } using GpuWrittenList = VideoCore::RasterizerInterface::GpuWrittenList; command_headers.resize_destructive(count); u8* const out = reinterpret_cast(command_headers.data()); for (GPUVAddr chunk = address; chunk < end;) { const GPUVAddr chunk_end = (std::min)(end, (chunk & ~GPUVAddr{0xfff}) + 0x1000); const std::span part{out + (chunk - address), static_cast(chunk_end - chunk)}; const auto device = memory_manager.GpuToCpuAddress(chunk); if (!device) { return false; } GpuWrittenList where = GpuWrittenList::Synced; if (Tegra::Nx2IsGpuWritten(*device)) { where = rasterizer->ReadGpuWrittenCommandList(*device, part); } if (where == GpuWrittenList::Unknown) { return false; } if (where == GpuWrittenList::Synced) { memory_manager.ReadBlockUnsafe(chunk, part.data(), part.size()); } chunk = chunk_end; } return true; } void DmaPusher::FlushGpuWrittenCommands(GPUVAddr address, size_t size) { // Title 1 runs small command lists its culling dispatches fill in each frame. Accurate GPU // mode reads every command list through the caches (safe reads); here only the ones on pages // the GPU has written are, or their draws run with an older frame's words (trees vanish). // These are the lists ReadGpuWrittenCommands can't take from a copy or guest memory (a page's // first list, or pages draws write too). if (!IsGpuWrittenCommands(address, size)) { return; } // The caches belong to the split's back: it first runs what the front queued (the dispatch // that writes these words among it). The whole pages are flushed: their next lists read // guest memory until GPU work writes them again. VideoCommon::GpuSplit::Get().Drain(); const GPUVAddr first_page = address & ~GPUVAddr{0xfff}; const GPUVAddr end_page = (address + size + 0xfff) & ~GPUVAddr{0xfff}; if (memory_manager.IsMemoryDirty(first_page, end_page - first_page, VideoCommon::CacheType::BufferCache)) { memory_manager.FlushRegion(first_page, end_page - first_page, VideoCommon::CacheType::BufferCache); } if (rasterizer == nullptr) { return; } for (GPUVAddr page = first_page; page < end_page; page += 0x1000) { if (const auto device = memory_manager.GpuToCpuAddress(page); device && Tegra::Nx2IsCommandListPage(*device)) { rasterizer->NoteGpuWrittenCommandListSynced(*device); } } } void DmaPusher::ReportUnboundSubchannel() { // Methods sent to a subchannel with no engine (no SetObject yet) are dropped. static std::atomic 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 words(header.size); memory_manager.ReadBlockUnsafe(header.addr, words.data(), words.size() * sizeof(u32)); static std::atomic checked{}; static std::atomic 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::RecordParsedEntry(const CommandListHeader& header, std::span commands) { if (!g_record_parsed_entries) { return; } u64 hash = 0xcbf29ce484222325ULL; for (const CommandHeader& command : commands) { hash = (hash ^ command.argument) * 0x100000001b3ULL; } parsed_entries[parsed_entry_count++ % parsed_entries.size()] = {header.addr, static_cast(header.size), hash}; } void DmaPusher::DumpCurrentEntry(const char* reason) { // Entries whose memory changed after they were parsed (the CPU rewrote them meanwhile). for (size_t i = 0; i < std::min(parsed_entry_count, parsed_entries.size()); ++i) { const auto& entry = parsed_entries[(parsed_entry_count - 1 - i) % parsed_entries.size()]; std::vector words(entry.size); memory_manager.ReadBlockUnsafe(entry.address, words.data(), words.size() * sizeof(u32)); u64 hash = 0xcbf29ce484222325ULL; for (const u32 word : words) { hash = (hash ^ word) * 0x100000001b3ULL; } LOG_WARNING(HW_GPU, " parsed -{}: {:#x}+{:#x} {}", i, entry.address, entry.size, hash == entry.hash ? "unchanged" : "CHANGED since parsed"); } // EDEN_NX2_PB_DUMP=: also write the whole command list (u64 address, u32 size and the // current words of every entry, then the current entry index) for offline replay. if (static const char* const path = std::getenv("EDEN_NX2_PB_DUMP"); path != nullptr && !dma_pushbuffer.empty()) { if (std::FILE* file = std::fopen(path, "wb")) { const auto& lists = dma_pushbuffer.front().command_lists; const u64 current = dma_pushbuffer_subindex; const u64 count = lists.size(); std::fwrite(&count, sizeof(count), 1, file); std::fwrite(¤t, sizeof(current), 1, file); for (const CommandListHeader& header : lists) { const u64 address = header.addr; const u32 size = static_cast(header.size); std::vector words(size); memory_manager.ReadBlockUnsafe(address, words.data(), words.size() * sizeof(u32)); std::fwrite(&address, sizeof(address), 1, file); std::fwrite(&size, sizeof(size), 1, file); std::fwrite(words.data(), sizeof(u32), words.size(), file); } std::fclose(file); } } 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(header.size, 0x80); word += 8) { std::string gpu_line; std::string cpu_line; for (u64 j = word; j < std::min(header.size, word + 8); ++j) { const GPUVAddr address = header.addr + j * sizeof(u32); const u32 gpu_value = memory_manager.Read(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_; } } // namespace Tegra