Switch 2: back GPU-only regions, pace presents, interleaved split methods
- gpu 214 creates GPU-only memory the graphics server backs itself (title 4 renders everything into six 300 MB regions at 0x81_0000_0000). Back each with application-pool pages mapped into the alias code region and map that for the GPU. This is what made title 4's frames appear. - vi2 229 waits (bounded) for the presented frame's GPU fence: the reply reports every buffer free, so without this the game laps Eden's GPU thread and rewrites command memory it has not read yet. - Split methods: keep several parked methods keyed by resume address (NVN interleaves command buffers) and drop them when the submission ends. - hid 214 (per-frame vibration send) answered quietly. - Debug aids: EDEN_NX2_WINDOW_TRACE / EDEN_NX2_RT_TRACE (render targets, clears, textures), EDEN_NX2_AUDIO_LEVEL (output peak), EDEN_NX2_PB_DUMP (command list for offline replay), parsed-entry change check.
This commit is contained in:
parent
9bee875c48
commit
d754fff776
8 changed files with 358 additions and 21 deletions
|
|
@ -4,8 +4,10 @@
|
|||
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
#include <cstdlib>
|
||||
#include <memory>
|
||||
#include <span>
|
||||
#include <vector>
|
||||
|
|
@ -213,6 +215,19 @@ void SinkStream::ProcessAudioOutAndRender(std::span<s16> output_buffer, std::siz
|
|||
|
||||
std::memcpy(last_frame.data(), &output_buffer[(frames_written - 1) * frame_size], frame_size_bytes);
|
||||
|
||||
// EDEN_NX2_AUDIO_LEVEL: log the output peak about every two seconds (is a game audible?).
|
||||
if (static const bool meter = std::getenv("EDEN_NX2_AUDIO_LEVEL") != nullptr; meter) {
|
||||
for (size_t i = 0; i < num_frames * frame_size; ++i) {
|
||||
level_peak = std::max<s32>(level_peak, std::abs(static_cast<s32>(output_buffer[i])));
|
||||
}
|
||||
level_frames += num_frames;
|
||||
if (level_frames >= TargetSampleRate * 2) {
|
||||
LOG_INFO(Audio, "Audio output peak over {} frames: {} of 32767", level_frames, level_peak);
|
||||
level_frames = 0;
|
||||
level_peak = 0;
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
std::scoped_lock lk{sample_count_lock};
|
||||
last_sample_count_update_time = system.CoreTiming().GetGlobalTimeNs();
|
||||
|
|
|
|||
|
|
@ -245,6 +245,9 @@ private:
|
|||
SinkBuffer playing_buffer{};
|
||||
/// The last played (or received) frame of audio, used when the callback underruns
|
||||
std::array<s16, MaxChannels> last_frame{};
|
||||
/// EDEN_NX2_AUDIO_LEVEL meter state.
|
||||
s32 level_peak{};
|
||||
u64 level_frames{};
|
||||
/// Number of buffers waiting to be played
|
||||
std::atomic<u32> queued_buffers{};
|
||||
/// The ring size for audio out buffers (usually 4, rarely 2 or 8)
|
||||
|
|
|
|||
|
|
@ -132,6 +132,7 @@ IHidServer::IHidServer(Core::System& system_, std::shared_ptr<ResourceManager> r
|
|||
{210, C<&IHidServer::EndPermitVibrationSession>, "EndPermitVibrationSession"},
|
||||
{211, C<&IHidServer::IsVibrationDeviceMounted>, "IsVibrationDeviceMounted"},
|
||||
{212, C<&IHidServer::SendVibrationValueInBool>, "SendVibrationValueInBool"},
|
||||
{214, C<&IHidServer::SendNx2VibrationValues>, "SendNx2VibrationValues (guessed)"},
|
||||
{300, C<&IHidServer::ActivateConsoleSixAxisSensor>, "ActivateConsoleSixAxisSensor"},
|
||||
{301, C<&IHidServer::StartConsoleSixAxisSensor>, "StartConsoleSixAxisSensor"},
|
||||
{302, C<&IHidServer::StopConsoleSixAxisSensor>, "StopConsoleSixAxisSensor"},
|
||||
|
|
@ -1069,6 +1070,11 @@ Result IHidServer::IsVibrationDeviceMounted(
|
|||
R_SUCCEED();
|
||||
}
|
||||
|
||||
Result IHidServer::SendNx2VibrationValues() {
|
||||
LOG_DEBUG(Service_HID, "(STUBBED) called");
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
Result IHidServer::SendVibrationValueInBool(
|
||||
bool is_vibrating, Core::HID::VibrationDeviceHandle vibration_device_handle,
|
||||
ClientAppletResourceUserId aruid) {
|
||||
|
|
|
|||
|
|
@ -189,6 +189,9 @@ private:
|
|||
Result SendVibrationValueInBool(bool is_vibrating,
|
||||
Core::HID::VibrationDeviceHandle vibration_device_handle,
|
||||
ClientAppletResourceUserId aruid);
|
||||
// Switch 2 sends vibration every frame with 214 (pointer buffers of device handles and
|
||||
// values, like 206); Eden doesn't emulate vibration, so accept and drop it.
|
||||
Result SendNx2VibrationValues();
|
||||
Result ActivateConsoleSixAxisSensor(ClientAppletResourceUserId aruid);
|
||||
Result StartConsoleSixAxisSensor(Core::HID::ConsoleSixAxisSensorHandle console_sixaxis_handle,
|
||||
ClientAppletResourceUserId aruid);
|
||||
|
|
|
|||
|
|
@ -5,12 +5,16 @@
|
|||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <thread>
|
||||
#include <chrono>
|
||||
#include <string>
|
||||
#include <cstdlib>
|
||||
#include <cstdio>
|
||||
#include <initializer_list>
|
||||
#include <atomic>
|
||||
#include <map>
|
||||
#include <optional>
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
#include "common/alignment.h"
|
||||
|
|
@ -23,6 +27,10 @@
|
|||
#include "core/core_timing.h"
|
||||
#include "core/hle/kernel/k_client_session.h"
|
||||
#include "core/hle/kernel/k_process.h"
|
||||
#include "core/hle/kernel/kernel.h"
|
||||
#include "core/hle/kernel/k_system_resource.h"
|
||||
#include "core/hle/kernel/k_memory_manager.h"
|
||||
#include "core/hle/kernel/k_page_group.h"
|
||||
#include "core/hle/kernel/k_resource_limit.h"
|
||||
#include "core/hle/kernel/k_server_session.h"
|
||||
#include "core/hle/kernel/k_event.h"
|
||||
|
|
@ -249,7 +257,7 @@ public:
|
|||
{102, &INx2GpuSession::SubSlotJob<3>, "Sub102 (job, slot in word 3)"},
|
||||
{211, &INx2GpuSession::SubSubmitJob, "NormalMapGpuMemory4K"},
|
||||
{213, &INx2GpuSession::SubSlotJob<8>, "NormalUnmapGpuMemory4K"},
|
||||
{214, &INx2GpuSession::SubSlotJob<11>, "CreateSparseRegion"},
|
||||
{214, &INx2GpuSession::SubCreateGpuOnlyRegion, "CreateGpuOnlyRegion (guessed)"},
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -362,8 +370,8 @@ private:
|
|||
template <size_t Slot>
|
||||
void SubSlotJob(HLERequestContext& ctx) {
|
||||
const auto* w = ctx.CommandBuffer();
|
||||
LOG_DEBUG(Service, "(STUBBED) nx2:gpu {} [2]={:#x} [3]={:#x} [4]={:#x} [5]={:#x} [6]={:#x} slot={}",
|
||||
ctx.GetCommand(), w[2], w[3], w[4], w[5], w[6], w[Slot]);
|
||||
LOG_INFO(Service, "(STUBBED) nx2:gpu {} words {:08x} slot={}", ctx.GetCommand(),
|
||||
fmt::join(std::span<const u32>{w + 2, 18}, " "), w[Slot]);
|
||||
EventFor(w[Slot])->Signal(system.Kernel());
|
||||
IPC::ResponseBuilder rb{ctx, 2};
|
||||
rb.Push(ResultSuccess);
|
||||
|
|
@ -380,8 +388,8 @@ private:
|
|||
const u64 c = u64{cmd_buf[8]} | (u64{cmd_buf[9]} << 32);
|
||||
// Words 3-4: first GPU address, 5-6: last, 7: memory handle, 8-9: offset into it.
|
||||
const u32 handle = cmd_buf[7];
|
||||
LOG_DEBUG(Service, "nx2:gpu 211 map [{:#x}, {:#x}] handle={} offset={:#x} slot={}", a, b,
|
||||
handle, c, slot);
|
||||
LOG_INFO(Service, "nx2:gpu 211 map [{:#x}, {:#x}] handle={} offset={:#x} slot={}", a, b,
|
||||
handle, c, slot);
|
||||
if (const auto it = m_memory.find(handle); it != m_memory.end() && b > a) {
|
||||
const u64 cpu = it->second.address + c;
|
||||
if (cpu != a) {
|
||||
|
|
@ -395,6 +403,68 @@ private:
|
|||
rb.Push(ResultSuccess);
|
||||
}
|
||||
|
||||
// Sub 214: GPU-only memory [start, last] (words 3-6, job slot in word 11). The graphics
|
||||
// server backs it itself: the client never maps anything there but renders into it (title 4
|
||||
// keeps its render targets in six 300 MB regions at 0x81_0000_0000). Back each region with
|
||||
// kernel memory placed in the process's alias code region and map that for the GPU.
|
||||
void SubCreateGpuOnlyRegion(HLERequestContext& ctx) {
|
||||
const auto* w = ctx.CommandBuffer();
|
||||
const u64 start = u64{w[3]} | (u64{w[4]} << 32);
|
||||
const u64 last = u64{w[5]} | (u64{w[6]} << 32);
|
||||
const u32 slot = w[11];
|
||||
if (last > start && ((last + 1 - start) % Core::Memory::YUZU_PAGESIZE) == 0) {
|
||||
const u64 size = last + 1 - start;
|
||||
if (const auto address = BackGpuOnlyRegion(size)) {
|
||||
Nvidia::Devices::Nx2GpuMemoryRegistry::Get().MapAt(start, *address, size);
|
||||
LOG_INFO(Service, "nx2:gpu 214 GPU-only region [{:#x}, {:#x}] backed at {:#x}",
|
||||
start, last, *address);
|
||||
}
|
||||
} else {
|
||||
LOG_WARNING(Service, "nx2:gpu 214 unexpected region [{:#x}, {:#x}]", start, last);
|
||||
}
|
||||
EventFor(slot)->Signal(system.Kernel());
|
||||
IPC::ResponseBuilder rb{ctx, 2};
|
||||
rb.Push(ResultSuccess);
|
||||
}
|
||||
|
||||
std::optional<u64> BackGpuOnlyRegion(u64 size) {
|
||||
using Core::Memory::YUZU_PAGESIZE;
|
||||
auto& kernel_core = system.Kernel();
|
||||
auto* const process = system.ApplicationProcess();
|
||||
if (process == nullptr) {
|
||||
return std::nullopt;
|
||||
}
|
||||
auto pg = std::make_unique<Kernel::KPageGroup>(
|
||||
kernel_core, std::addressof(kernel_core.GetSystemSystemResource().GetBlockInfoManager()));
|
||||
// Not cleared: the GPU writes these before reading them, and touching 300 MB would commit
|
||||
// it on the host.
|
||||
if (R_FAILED(kernel_core.MemoryManager().AllocateAndOpen(
|
||||
pg.get(), size / YUZU_PAGESIZE,
|
||||
Kernel::KMemoryManager::EncodeOption(Kernel::KMemoryManager::Pool::Application,
|
||||
Kernel::KMemoryManager::Direction::FromBack)))) {
|
||||
LOG_ERROR(Service, "nx2:gpu 214 could not allocate {:#x} bytes", size);
|
||||
return std::nullopt;
|
||||
}
|
||||
auto& page_table = process->GetPageTable();
|
||||
const u64 region_start = GetInteger(page_table.GetAliasCodeRegionStart());
|
||||
const u64 region_size = page_table.GetAliasCodeRegionSize();
|
||||
constexpr u64 Alignment = 0x200000; // 2 MiB
|
||||
for (u64 offset = 0; offset + size <= region_size; offset += Alignment) {
|
||||
const Common::ProcessAddress address{region_start + offset};
|
||||
if (R_SUCCEEDED(page_table.MapPageGroup(address, *pg, Kernel::KMemoryState::IoMemory,
|
||||
Kernel::KMemoryPermission::UserReadWrite))) {
|
||||
m_gpu_only_memory.push_back(std::move(pg));
|
||||
return GetInteger(address);
|
||||
}
|
||||
}
|
||||
LOG_ERROR(Service, "nx2:gpu 214 no room for {:#x} bytes in the alias code region "
|
||||
"[{:#x}, +{:#x})", size, region_start, region_size);
|
||||
pg->Close(kernel_core);
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::vector<std::unique_ptr<Kernel::KPageGroup>> m_gpu_only_memory;
|
||||
|
||||
struct GpuMemory {
|
||||
u64 address;
|
||||
u64 size;
|
||||
|
|
@ -498,12 +568,38 @@ 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]);
|
||||
}
|
||||
WaitForFrame(~w[22], w[24]);
|
||||
Composite(w[5], ~w[22], w[24]);
|
||||
constexpr u64 AvailableSlots = 0x7fff;
|
||||
TipcReplyWords(ctx, {static_cast<u32>(AvailableSlots),
|
||||
static_cast<u32>(AvailableSlots >> 32), 0, 0, 0, 0});
|
||||
}
|
||||
|
||||
// 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
|
||||
// Eden's GPU thread has processed the frame. Otherwise the game laps the GPU thread and
|
||||
// rewrites command memory it hasn't read yet.
|
||||
void WaitForFrame(u32 fence_id, u32 fence_value) {
|
||||
if (fence_id >= 192) {
|
||||
return;
|
||||
}
|
||||
auto& syncpoints = system.Host1x().GetSyncpointManager();
|
||||
const auto start = std::chrono::steady_clock::now();
|
||||
const auto deadline = start + std::chrono::milliseconds{250};
|
||||
while (!syncpoints.IsReadyGuest(fence_id, fence_value) &&
|
||||
std::chrono::steady_clock::now() < deadline && system.IsPoweredOn()) {
|
||||
std::this_thread::sleep_for(std::chrono::microseconds{200});
|
||||
}
|
||||
if (m_presents <= 8 || (m_presents % 600) == 0) {
|
||||
LOG_INFO(Service, "nx2:vi2 present #{} fence ({}, {}) now {}, waited {} us",
|
||||
m_presents, fence_id, fence_value, syncpoints.GetGuestSyncpointValue(fence_id),
|
||||
std::chrono::duration_cast<std::chrono::microseconds>(
|
||||
std::chrono::steady_clock::now() - start)
|
||||
.count());
|
||||
}
|
||||
}
|
||||
|
||||
// Shows a presented native image: 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.
|
||||
|
|
|
|||
|
|
@ -4,7 +4,10 @@
|
|||
// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
#include <cstdlib>
|
||||
#include <cstdio>
|
||||
|
||||
#include "common/settings.h"
|
||||
#include "core/core.h"
|
||||
|
|
@ -109,11 +112,13 @@ bool DmaPusher::Step() {
|
|||
if (use_safe) {
|
||||
Tegra::Memory::GpuGuestMemory<Tegra::CommandHeader, Tegra::Memory::GuestMemoryFlags::SafeRead>headers(memory_manager, dma_state.dma_get, header.size, &command_headers);
|
||||
const std::span<const CommandHeader> commands{headers.data(), headers.size()};
|
||||
RecordParsedEntry(header, commands);
|
||||
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);
|
||||
const std::span<const CommandHeader> commands{headers.data(), headers.size()};
|
||||
RecordParsedEntry(header, commands);
|
||||
command_resume_offset +=
|
||||
ProcessCommands(commands.subspan(command_resume_offset), command_resume_offset);
|
||||
}
|
||||
|
|
@ -127,6 +132,9 @@ bool DmaPusher::Step() {
|
|||
}
|
||||
|
||||
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.
|
||||
parked_methods.clear();
|
||||
dma_pushbuffer.pop();
|
||||
dma_pushbuffer_subindex = 0;
|
||||
} else {
|
||||
|
|
@ -211,34 +219,42 @@ size_t DmaPusher::ProcessCommands(std::span<const CommandHeader> commands, size_
|
|||
// 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<ParkedMethod> 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_method) {
|
||||
if (parked_methods.size() == MaxParkedMethods) {
|
||||
static std::atomic<u32> 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})",
|
||||
parked_method->method, parked_method->subchannel,
|
||||
parked_method->method_count, parked_method->resume_address);
|
||||
oldest.method, oldest.subchannel, oldest.method_count,
|
||||
oldest.resume_address);
|
||||
}
|
||||
parked_methods.erase(parked_methods.begin());
|
||||
}
|
||||
parked_method = ParkedMethod{
|
||||
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;
|
||||
} 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();
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -314,7 +330,50 @@ void DmaPusher::CheckSubmitHash(const CommandListHeader& header, u64 submit_hash
|
|||
}
|
||||
}
|
||||
|
||||
void DmaPusher::RecordParsedEntry(const CommandListHeader& header,
|
||||
std::span<const CommandHeader> commands) {
|
||||
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<u32>(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<u32> 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=<file>: 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<u32>(header.size);
|
||||
std::vector<u32> 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);
|
||||
|
|
|
|||
|
|
@ -179,6 +179,7 @@ public:
|
|||
/// 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);
|
||||
void RecordParsedEntry(const CommandListHeader& header, std::span<const CommandHeader> commands);
|
||||
|
||||
private:
|
||||
|
||||
|
|
@ -205,8 +206,8 @@ public:
|
|||
|
||||
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.
|
||||
// Switch 2: methods whose data continues in a later GPFIFO entry, set aside while other
|
||||
// command buffers run (NVN interleaves several, so more than one can be pending).
|
||||
struct ParkedMethod {
|
||||
u32 method;
|
||||
u32 subchannel;
|
||||
|
|
@ -216,7 +217,17 @@ public:
|
|||
GPUVAddr resume_address; ///< Where the entry that continues the method starts
|
||||
};
|
||||
void ResolveSplitMethod(GPUVAddr entry_address);
|
||||
std::optional<ParkedMethod> parked_method;
|
||||
static constexpr size_t MaxParkedMethods = 16;
|
||||
|
||||
// Debug aid: hashes of the words of the last entries as they were parsed.
|
||||
struct ParsedEntry {
|
||||
GPUVAddr address;
|
||||
u32 size;
|
||||
u64 hash;
|
||||
};
|
||||
std::array<ParsedEntry, 64> parsed_entries{};
|
||||
size_t parsed_entry_count{};
|
||||
std::vector<ParkedMethod> parked_methods;
|
||||
GPUVAddr split_method_end{}; ///< End of the last entry that ended inside a method
|
||||
|
||||
std::array<Engines::EngineInterface*, max_subchannels> subchannels{};
|
||||
|
|
|
|||
|
|
@ -7,6 +7,10 @@
|
|||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <memory>
|
||||
#include <cstdlib>
|
||||
#include <tuple>
|
||||
#include <set>
|
||||
#include <fstream>
|
||||
#include <mutex>
|
||||
|
||||
#include <fmt/format.h>
|
||||
|
|
@ -14,12 +18,14 @@
|
|||
#include "video_core/renderer_vulkan/renderer_vulkan.h"
|
||||
|
||||
#include "common/assert.h"
|
||||
#include "shader_recompiler/exception.h"
|
||||
#include "common/logging.h"
|
||||
#include "common/scope_exit.h"
|
||||
#include "common/settings.h"
|
||||
#include "video_core/buffer_cache/buffer_cache.h"
|
||||
#include "video_core/gpu_logging/gpu_logging.h"
|
||||
#include "video_core/control/channel_state.h"
|
||||
#include "video_core/native_query_trace.h"
|
||||
#include "video_core/engines/maxwell_3d.h"
|
||||
#include "video_core/engines/kepler_compute.h"
|
||||
#include "video_core/engines/maxwell_3d.h"
|
||||
|
|
@ -238,6 +244,14 @@ void RasterizerVulkan::PrepareDraw(bool is_indexed, Func&& draw_func) {
|
|||
SCOPE_EXIT {
|
||||
gpu.TickWork();
|
||||
};
|
||||
bool submitted = false;
|
||||
SCOPE_EXIT {
|
||||
auto& native = maxwell3d->GetNativeMacroEngine();
|
||||
if (native.native_turing && !submitted) {
|
||||
native.native_execution_failed = true;
|
||||
LOG_ERROR(HW_GPU, "Native graphics draw failed; refusing submission completion");
|
||||
}
|
||||
};
|
||||
FlushWork();
|
||||
gpu_memory->FlushCaching();
|
||||
|
||||
|
|
@ -257,9 +271,124 @@ void RasterizerVulkan::PrepareDraw(bool is_indexed, Func&& draw_func) {
|
|||
HandleTransformFeedback();
|
||||
query_cache.CounterEnable(VideoCommon::QueryType::ZPassPixelCount64, maxwell3d->regs.zpass_pixel_count_enable);
|
||||
draw_func();
|
||||
submitted = true;
|
||||
static u32 native_capture_count{};
|
||||
if (maxwell3d->GetNativeMacroEngine().native_turing &&
|
||||
std::getenv("EDEN_NATIVE_DRAW_CAPTURE") && native_capture_count < 16) {
|
||||
const u32 capture = native_capture_count++;
|
||||
const auto prefix = fmt::format("native-draw-{}", capture);
|
||||
const auto dump = [&](const std::string& name, GPUVAddr address, size_t size) {
|
||||
if (!size || !gpu_memory->IsFullyMappedRange(address, size)) return;
|
||||
std::vector<u8> bytes(size);
|
||||
gpu_memory->ReadBlockUnsafe(address, bytes.data(), bytes.size());
|
||||
std::ofstream file(prefix + name + ".bin", std::ios::binary);
|
||||
file.write(reinterpret_cast<const char*>(bytes.data()), bytes.size());
|
||||
LOG_INFO(HW_GPU, "Native draw capture {}{}: address={:#x}, bytes={}, nonzero={}",
|
||||
capture, name, address, size,
|
||||
std::count_if(bytes.begin(), bytes.end(), [](u8 byte) { return byte != 0; }));
|
||||
};
|
||||
const auto& regs = maxwell3d->regs;
|
||||
const auto& rt = regs.rt[0];
|
||||
const size_t size = static_cast<size_t>(rt.array_pitch) * 4;
|
||||
if (size && size <= 16 * 1024 * 1024) {
|
||||
if (const auto address = gpu_memory->GpuToCpuAddress(rt.Address(), size)) {
|
||||
LOG_INFO(HW_GPU, "Native draw {} render target physical address={:#x}", capture, *address);
|
||||
texture_cache.DownloadMemory(*address, size);
|
||||
dump("-rt0", rt.Address(), size);
|
||||
}
|
||||
}
|
||||
std::ofstream registers(prefix + "-registers.bin", std::ios::binary);
|
||||
registers.write(reinterpret_cast<const char*>(®s), sizeof(regs));
|
||||
for (size_t stream = 0; stream < 4; ++stream) {
|
||||
if (regs.vertex_streams[stream].IsEnabled())
|
||||
dump(fmt::format("-vertex-{}", stream), regs.vertex_streams[stream].Address(), 128);
|
||||
}
|
||||
for (size_t stage : {0U, 4U}) {
|
||||
for (size_t bank : {2U, 3U}) {
|
||||
const auto& cb = maxwell3d->state.shader_stages[stage].const_buffers[bank];
|
||||
if (cb.enabled) dump(fmt::format("-cbuf-{}-{}", stage, bank), cb.address, std::min<size_t>(cb.size, 128));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
namespace {
|
||||
// EDEN_NX2_WINDOW_TRACE: describe clears and draws into screen-sized colour targets (debug aid).
|
||||
void TraceWindowTarget(const Tegra::Engines::Maxwell3D& maxwell3d, Tegra::MemoryManager& memory,
|
||||
const char* kind, u32 count) {
|
||||
static const bool trace = std::getenv("EDEN_NX2_WINDOW_TRACE") != nullptr;
|
||||
static u32 traced{};
|
||||
const auto& regs = maxwell3d.regs;
|
||||
if (!trace || regs.rt[0].width != 3840 || traced >= 400) {
|
||||
return;
|
||||
}
|
||||
++traced;
|
||||
const auto& vp = regs.viewport_transform[0];
|
||||
const auto& sc = regs.scissor_test[0];
|
||||
LOG_INFO(HW_GPU,
|
||||
"nx2 window {} rt0={:#x} count={} clear={:#x} color=({},{},{},{}) mask_common={} "
|
||||
"mask0={:#x} rt_count={} vp scale=({},{}) translate=({},{}) scissor en={} x={}..{} "
|
||||
"y={}..{} blend0={} vs={:#x} ps={:#x} rasterize={}",
|
||||
kind, regs.rt[0].Address(), count, regs.clear_surface.raw, regs.clear_color[0],
|
||||
regs.clear_color[1], regs.clear_color[2], regs.clear_color[3], regs.color_mask_common,
|
||||
regs.color_mask[0].raw, regs.rt_control.count.Value(), vp.scale_x, vp.scale_y,
|
||||
vp.translate_x, vp.translate_y, sc.enable, sc.min_x.Value(), sc.max_x.Value(),
|
||||
sc.min_y.Value(), sc.max_y.Value(), regs.blend.enable[0],
|
||||
(u64{regs.pipelines[1].reserved[0] & 0xff} << 32) | regs.pipelines[1].reserved[1],
|
||||
(u64{regs.pipelines[5].reserved[0] & 0xff} << 32) | regs.pipelines[5].reserved[1],
|
||||
regs.rasterize_enable);
|
||||
if (kind[0] != 'd') {
|
||||
return;
|
||||
}
|
||||
// Pixel-stage constant buffer 3: colour constants of the loading-screen passes.
|
||||
if (const auto& cb3 = maxwell3d.state.shader_stages[4].const_buffers[3]; cb3.enabled) {
|
||||
std::array<f32, 8> values{};
|
||||
memory.ReadBlockUnsafe(cb3.address, values.data(), sizeof(values));
|
||||
LOG_INFO(HW_GPU, "nx2 window ps c[3]@{:#x} size={}: {}", cb3.address, cb3.size,
|
||||
fmt::join(values, " "));
|
||||
}
|
||||
static u32 texture_traces{};
|
||||
if (texture_traces++ > 40) {
|
||||
return;
|
||||
}
|
||||
// Texture used by the pixel shader's first bound TEX (handle in c[2][0x20]) and its TIC.
|
||||
const auto& cb = maxwell3d.state.shader_stages[4].const_buffers[2];
|
||||
const u32 handle = cb.enabled ? memory.Read<u32>(cb.address + 0x20) : 0;
|
||||
const u32 tic_index = handle & 0xfffff;
|
||||
const GPUVAddr tic_address = regs.tex_header.Address() + u64{tic_index} * 32;
|
||||
std::array<u32, 8> tic{};
|
||||
memory.ReadBlockUnsafe(tic_address, tic.data(), sizeof(tic));
|
||||
LOG_INFO(HW_GPU,
|
||||
"nx2 window texture: cb2={:#x} en={} handle={:#x} tic pool={:#x} limit={} "
|
||||
"sampler pool={:#x} tic[{}]={:08x}",
|
||||
cb.address, cb.enabled, handle, regs.tex_header.Address(), regs.tex_header.limit,
|
||||
regs.tex_sampler.Address(), tic_index, fmt::join(tic, " "));
|
||||
// First bytes of the texture's memory (block-linear, so just a sample).
|
||||
const GPUVAddr texture = (GPUVAddr{tic[2] & 0xffff} << 32) | tic[1];
|
||||
std::array<u32, 16> texels{};
|
||||
memory.ReadBlockUnsafe(texture, texels.data(), sizeof(texels));
|
||||
LOG_INFO(HW_GPU, "nx2 window texture {:#x} {}x{}: {:08x}", texture, (tic[4] & 0xffff) + 1,
|
||||
(tic[5] & 0xffff) + 1, fmt::join(texels, " "));
|
||||
}
|
||||
} // Anonymous namespace
|
||||
|
||||
void RasterizerVulkan::Draw(bool is_indexed, u32 instance_count) {
|
||||
TraceWindowTarget(*maxwell3d, *gpu_memory, is_indexed ? "draw-indexed" : "draw",
|
||||
is_indexed ? maxwell3d->draw_manager.draw_state.index_buffer.count
|
||||
: maxwell3d->draw_manager.draw_state.vertex_buffer.count);
|
||||
// EDEN_NX2_RT_TRACE: log the first draw to every distinct colour target (debug aid).
|
||||
if (static const bool trace = std::getenv("EDEN_NX2_RT_TRACE") != nullptr; trace) {
|
||||
static std::set<std::tuple<GPUVAddr, u32, u32, u32>> seen;
|
||||
const auto& rt = maxwell3d->regs.rt[0];
|
||||
const auto key = std::make_tuple(rt.Address(), rt.width, rt.height,
|
||||
static_cast<u32>(rt.format));
|
||||
if (rt.width >= 640 && seen.size() < 400 && seen.insert(key).second) {
|
||||
LOG_INFO(HW_GPU, "nx2 draw target {:#x} {}x{} format={:#x} tile={:#x} (targets so far {})",
|
||||
rt.Address(), rt.width, rt.height, static_cast<u32>(rt.format),
|
||||
static_cast<u32>(maxwell3d->regs.rt[0].tile_mode.block_height.Value()), seen.size());
|
||||
}
|
||||
}
|
||||
PrepareDraw(is_indexed, [this, is_indexed, instance_count] {
|
||||
const auto& draw_state = maxwell3d->draw_manager.draw_state;
|
||||
const u32 num_instances{instance_count};
|
||||
|
|
@ -398,6 +527,7 @@ void RasterizerVulkan::DrawTexture() {
|
|||
}
|
||||
|
||||
void RasterizerVulkan::Clear(u32 layer_count) {
|
||||
TraceWindowTarget(*maxwell3d, *gpu_memory, "clear", layer_count);
|
||||
FlushWork();
|
||||
gpu_memory->FlushCaching();
|
||||
|
||||
|
|
@ -588,11 +718,13 @@ void RasterizerVulkan::DispatchCompute() {
|
|||
|
||||
ComputePipeline* const pipeline{pipeline_cache.CurrentComputePipeline()};
|
||||
if (!pipeline) {
|
||||
if (kepler_compute->IsNativeMode()) throw Shader::RuntimeError("Native compute pipeline unavailable");
|
||||
return;
|
||||
}
|
||||
std::scoped_lock lock{texture_cache.mutex, buffer_cache.mutex};
|
||||
if (!pipeline->Configure(*kepler_compute, *gpu_memory, scheduler, buffer_cache,
|
||||
texture_cache)) {
|
||||
if (kepler_compute->IsNativeMode()) throw Shader::RuntimeError("Native compute resources unavailable");
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -942,6 +1074,12 @@ Tegra::Engines::AccelerateDMAInterface& RasterizerVulkan::AccessAccelerateDMA()
|
|||
void RasterizerVulkan::AccelerateInlineToMemory(GPUVAddr address, size_t copy_size,
|
||||
std::span<const u8> memory) {
|
||||
auto cpu_addr = gpu_memory->GpuToCpuAddress(address);
|
||||
if (gpu_memory->UsesNativeTiling()) {
|
||||
u32 first_word{};
|
||||
if (memory.size() >= sizeof(first_word)) std::memcpy(&first_word, memory.data(), sizeof(first_word));
|
||||
VideoCommon::TraceNativeQuery(11, gpu_memory->GetID(), address, cpu_addr.value_or(0),
|
||||
0, first_word, static_cast<u32>(copy_size));
|
||||
}
|
||||
if (!cpu_addr) [[unlikely]] {
|
||||
gpu_memory->WriteBlock(address, memory.data(), copy_size);
|
||||
return;
|
||||
|
|
@ -969,6 +1107,12 @@ std::optional<FramebufferTextureInfo> RasterizerVulkan::AccelerateDisplay(
|
|||
std::scoped_lock lock{texture_cache.mutex};
|
||||
const auto [image_view, scaled] =
|
||||
texture_cache.TryFindFramebufferImageView(config, framebuffer_addr);
|
||||
static u32 display_trace_count{};
|
||||
if (std::getenv("EDEN_NATIVE_FRAME_CAPTURE") && display_trace_count++ < 64) {
|
||||
LOG_INFO(HW_GPU, "Native display source: address={:#x}, offset={:#x}, dimensions={}x{}, stride={}, accelerated={}, mask={:#x}",
|
||||
framebuffer_addr, config.offset, config.width, config.height, pixel_stride,
|
||||
image_view != nullptr, config.layer_stack_mask);
|
||||
}
|
||||
if (!image_view) {
|
||||
return {};
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue