nx2: stand in for title 4's tensor-core upscaler and its blit constants
- When a compute program is rejected for tensor-core math, blit its input image (handle at cbuf0+0x90) to its output (cbuf0+0xa0) with bilinear filtering. Title 4 renders at 2560x1440 and upscales to 3840x2160 with a DLSS-style network; this keeps the 4K output fed. EDEN_NX2_TENSOR=stub keeps the program with HMMA as D = C (for experiments). - BlitConstantsPass: vertex shaders reading bank 24 (lowered to 17) get the console-provided blit block synthesized from the render area (texel size, size, UV scale 1, rectangle 0..W-1,H-1). - Debug: EDEN_NX2_CB24_TRACE, upscaler resource description and render target sampling at the fallback. The scene behind the intro dialogue is still black: the tonemap output that feeds the upscaler has no colour although earlier targets do.
This commit is contained in:
parent
26abba68f5
commit
7e5b415e08
7 changed files with 242 additions and 2 deletions
|
|
@ -891,12 +891,20 @@ inline LoweredProgram LowerToMaxwell(std::span<const Instruction> code) {
|
|||
// g / g+8 at columns 2i, 2i+1. Rebuilt from shuffles with HFMA2, accumulating in R252/R253.
|
||||
// The expansion is correct but large: title 4's 372-HMMA network takes the host driver
|
||||
// minutes to compile, so it stays opt-in (the QMD still completes when rejected).
|
||||
static const bool enabled = std::getenv("EDEN_NX2_TENSOR") != nullptr;
|
||||
if (!enabled) return fail(index, "native matrix multiply (set EDEN_NX2_TENSOR to emulate)");
|
||||
// EDEN_NX2_TENSOR=stub keeps the rest of the program but makes every HMMA D = C.
|
||||
static const char* const mode = std::getenv("EDEN_NX2_TENSOR");
|
||||
if (!mode) return fail(index, "native matrix multiply (set EDEN_NX2_TENSOR to emulate)");
|
||||
const u32 a = insn.Bits(24, 8), b = insn.Bits(32, 8), c = insn.Bits(64, 8);
|
||||
if ((insn.words[1] & ~0xffU) || (insn.words[2] & ~0xffU) != 0x800 || dest + 2 > 246 ||
|
||||
a + 4 > 246 || b + 2 > 246 || (c != 255 && c + 2 > 246))
|
||||
return fail(index, "native matrix multiply mode");
|
||||
if (std::string_view{mode} == "stub") {
|
||||
for (u32 row = 0; row < 2; ++row) {
|
||||
if (c == 255) mov_imm(static_cast<u32>(dest) + row, 0);
|
||||
else if (c != dest) emit(0x5c98000000000000ULL | predicate | (dest + row) | (u64{15} << 39) | (u64{c + row} << 20));
|
||||
}
|
||||
break;
|
||||
}
|
||||
const auto shuffle = [&](u32 target, u32 source_reg) {
|
||||
emit(0xef10000000000000ULL | predicate | target | (u64{source_reg} << 8) | (u64{246} << 20) |
|
||||
(u64{1} << 29) | (u64{31} << 34) | (u64{7} << 48));
|
||||
|
|
|
|||
|
|
@ -391,6 +391,7 @@ IR::Program TranslateProgram(ObjectPool<IR::Inst>& inst_pool, ObjectPool<IR::Blo
|
|||
Optimization::ConstantPropagationPass(env, program);
|
||||
|
||||
Optimization::PositionPass(env, program);
|
||||
Optimization::BlitConstantsPass(env, program);
|
||||
|
||||
Optimization::GlobalMemoryToStorageBufferPass(program, normalized_host_info);
|
||||
Optimization::GeometryCompactionPass(program, normalized_host_info);
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ void LowerInt64ToInt32(IR::Program& program);
|
|||
void RescalingPass(IR::Program& program);
|
||||
void SsaRewritePass(IR::Program& program);
|
||||
void PositionPass(Environment& env, IR::Program& program);
|
||||
void BlitConstantsPass(Environment& env, IR::Program& program);
|
||||
void TexturePass(Environment& env, IR::Program& program, const HostTranslateInfo& host_info);
|
||||
void LayerPass(IR::Program& program, const HostTranslateInfo& host_info);
|
||||
void VendorWorkaroundPass(IR::Program& program);
|
||||
|
|
|
|||
|
|
@ -74,4 +74,67 @@ void PositionPass(Environment& env, IR::Program& program) {
|
|||
}
|
||||
}
|
||||
}
|
||||
// Switch 2 fullscreen blit vertex shaders read a parameter block from constant bank 24 (lowered to
|
||||
// bank 17, which games never bind) that the console provides itself. For a blit covering the whole
|
||||
// render target it holds: texel size (0x0/0x4, again at 0x10/0x14), size (0x8/0xc), a UV scale of
|
||||
// 1 (0x18/0x1c) and the integer source rectangle 0,0..W-1,H-1 (0x50-0x5c).
|
||||
void BlitConstantsPass(Environment& env, IR::Program& program) {
|
||||
if (env.ShaderStage() != Stage::VertexB) {
|
||||
return;
|
||||
}
|
||||
bool used{};
|
||||
for (IR::Block* const block : program.post_order_blocks) {
|
||||
for (IR::Inst& inst : block->Instructions()) {
|
||||
const IR::Opcode opcode{inst.GetOpcode()};
|
||||
if (opcode != IR::Opcode::GetCbufU32 && opcode != IR::Opcode::GetCbufF32 &&
|
||||
opcode != IR::Opcode::GetCbufU32x2) {
|
||||
continue;
|
||||
}
|
||||
if (!inst.Arg(0).IsImmediate() || inst.Arg(0).U32() != 17 || !inst.Arg(1).IsImmediate()) {
|
||||
continue;
|
||||
}
|
||||
IR::IREmitter ir{*block, IR::Block::InstructionList::s_iterator_to(inst)};
|
||||
const IR::F32 width{ir.RenderAreaWidth()};
|
||||
const IR::F32 height{ir.RenderAreaHeight()};
|
||||
const auto word = [&](u32 offset) -> IR::U32 {
|
||||
switch (offset) {
|
||||
case 0x0:
|
||||
case 0x10:
|
||||
return ir.BitCast<IR::U32>(IR::F32{ir.FPRecip(width)});
|
||||
case 0x4:
|
||||
case 0x14:
|
||||
return ir.BitCast<IR::U32>(IR::F32{ir.FPRecip(height)});
|
||||
case 0x8:
|
||||
return ir.BitCast<IR::U32>(width);
|
||||
case 0xc:
|
||||
return ir.BitCast<IR::U32>(height);
|
||||
case 0x18:
|
||||
case 0x1c:
|
||||
return ir.Imm32(0x3f800000U);
|
||||
case 0x58:
|
||||
return IR::U32{ir.ConvertFToU(32, ir.FPAdd(width, ir.Imm32(-1.0f)))};
|
||||
case 0x5c:
|
||||
return IR::U32{ir.ConvertFToU(32, ir.FPAdd(height, ir.Imm32(-1.0f)))};
|
||||
default:
|
||||
return ir.Imm32(0U);
|
||||
}
|
||||
};
|
||||
const u32 offset{inst.Arg(1).U32()};
|
||||
IR::Value value;
|
||||
if (opcode == IR::Opcode::GetCbufU32x2) {
|
||||
value = ir.CompositeConstruct(word(offset), word(offset + 4));
|
||||
} else if (opcode == IR::Opcode::GetCbufF32) {
|
||||
value = ir.BitCast<IR::F32>(word(offset));
|
||||
} else {
|
||||
value = word(offset);
|
||||
}
|
||||
inst.ReplaceUsesWith(value);
|
||||
used = true;
|
||||
}
|
||||
}
|
||||
if (used) {
|
||||
program.info.uses_render_area = true;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace Shader::Optimization
|
||||
|
|
|
|||
|
|
@ -8,10 +8,12 @@
|
|||
#include <bitset>
|
||||
#include <cstdlib>
|
||||
#include <fstream>
|
||||
#include <optional>
|
||||
#include "common/assert.h"
|
||||
#include "common/logging.h"
|
||||
#include "shader_recompiler/exception.h"
|
||||
#include "core/core.h"
|
||||
#include "video_core/engines/fermi_2d.h"
|
||||
#include "video_core/engines/kepler_compute.h"
|
||||
#include "video_core/engines/maxwell_3d.h"
|
||||
#include "video_core/memory_manager.h"
|
||||
|
|
@ -396,11 +398,145 @@ bool KeplerCompute::DispatchNativeCompute(const NativeQmdScheduler::Descriptor&
|
|||
rasterizer->DispatchCompute();
|
||||
} catch (const Shader::Exception& error) {
|
||||
LOG_ERROR(HW_GPU, "Native compute shader rejected: {}", error.what());
|
||||
if (std::string_view{error.what()}.find("matrix multiply") != std::string_view::npos) {
|
||||
DescribeNativeResources(qmd);
|
||||
// The console runs this on tensor cores; stand in with a plain upscale of its input.
|
||||
return UpscaleInsteadOfNativeProgram();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Logs the images a rejected program's handles (constant bank 0, bytes 0x80-0x11c) point to, so a
|
||||
// substitute (e.g. an upscale blit for a tensor-core pass) can find its inputs and output.
|
||||
void KeplerCompute::DescribeNativeResources(const NativeQmdScheduler::Descriptor& qmd) {
|
||||
static u32 described{};
|
||||
if (described++ >= 2) return;
|
||||
const auto& cbuf = launch_description.const_buffer_config[0];
|
||||
const GPUVAddr cbuf_address = (GPUVAddr{cbuf.address_high} << 32) | cbuf.address_low;
|
||||
LOG_INFO(HW_GPU, "Native resources: cbuf0 {:#x} size {:#x}, grid {}x{}x{}, block {}x{}x{}, tic pool {:#x}",
|
||||
cbuf_address, cbuf.size.Value(), qmd[12], qmd[13] & 0xffff, qmd[14] & 0xffff,
|
||||
launch_description.block_dim_x.Value(), launch_description.block_dim_y.Value(),
|
||||
launch_description.block_dim_z.Value(), regs.tic.Address());
|
||||
for (u32 offset = 0x80; offset < 0x120; offset += 4) {
|
||||
const u32 handle = memory_manager.Read<u32>(cbuf_address + offset);
|
||||
const u32 tic_index = handle & 0xfffff;
|
||||
if (handle == 0 || tic_index > regs.tic.limit) continue;
|
||||
const auto tic = GetTICEntry(tic_index);
|
||||
LOG_INFO(HW_GPU, " handle @{:#x} = {:#x}: tic {} address {:#x} {}x{} format {:#x} types {}/{}/{}/{}",
|
||||
offset, handle, tic_index, tic.Address(), tic.Width(), tic.Height(),
|
||||
static_cast<u32>(tic.format.Value()), static_cast<u32>(tic.r_type.Value()),
|
||||
static_cast<u32>(tic.g_type.Value()), static_cast<u32>(tic.b_type.Value()),
|
||||
static_cast<u32>(tic.a_type.Value()));
|
||||
}
|
||||
}
|
||||
|
||||
// Title 4's upscaler reads the tonemapped frame through the texture handle at cbuf0+0x90 and
|
||||
// writes the output resolution image through the surface handle at cbuf0+0xa0 (from its
|
||||
// TLD/SUST operands). Blit one to the other with bilinear filtering when both are plain 2D images
|
||||
// of the same format and the output is larger.
|
||||
bool KeplerCompute::UpscaleInsteadOfNativeProgram() {
|
||||
const auto& cbuf = launch_description.const_buffer_config[0];
|
||||
const GPUVAddr cbuf_address = (GPUVAddr{cbuf.address_high} << 32) | cbuf.address_low;
|
||||
const auto image = [&](u32 offset) -> std::optional<Texture::TICEntry> {
|
||||
const u32 tic_index = memory_manager.Read<u32>(cbuf_address + offset) & 0xfffff;
|
||||
if (tic_index > regs.tic.limit) return std::nullopt;
|
||||
return GetTICEntry(tic_index);
|
||||
};
|
||||
const auto input = image(0x90), output = image(0xa0);
|
||||
if (!input || !output || input->format != output->format || input->Address() == 0 ||
|
||||
output->Address() == 0 || output->Width() <= input->Width() || !input->IsBlockLinear() ||
|
||||
!output->IsBlockLinear())
|
||||
return false;
|
||||
using RTFormat = Tegra::RenderTargetFormat;
|
||||
RTFormat format{};
|
||||
switch (input->format) {
|
||||
case Texture::TextureFormat::A2B10G10R10: format = RTFormat::A2B10G10R10_UNORM; break;
|
||||
case Texture::TextureFormat::A8B8G8R8: format = RTFormat::A8B8G8R8_UNORM; break;
|
||||
case Texture::TextureFormat::B10G11R11: format = RTFormat::B10G11R11_FLOAT; break;
|
||||
case Texture::TextureFormat::R16G16B16A16: format = RTFormat::R16G16B16A16_FLOAT; break;
|
||||
default: return false;
|
||||
}
|
||||
const auto surface = [&](const Texture::TICEntry& tic) {
|
||||
Fermi2D::Surface result{};
|
||||
result.format = format;
|
||||
result.linear = Fermi2D::MemoryLayout::BlockLinear;
|
||||
result.block_width.Assign(tic.block_width);
|
||||
result.block_height.Assign(tic.block_height);
|
||||
result.block_depth.Assign(tic.block_depth);
|
||||
result.depth = 1;
|
||||
result.width = tic.Width();
|
||||
result.height = tic.Height();
|
||||
result.addr_upper = static_cast<u32>(tic.Address() >> 32);
|
||||
result.addr_lower = static_cast<u32>(tic.Address());
|
||||
return result;
|
||||
};
|
||||
const Fermi2D::Config config{
|
||||
.operation = Fermi2D::Operation::SrcCopy,
|
||||
.filter = Fermi2D::Filter::Bilinear,
|
||||
.must_accelerate = true,
|
||||
.dst_x0 = 0,
|
||||
.dst_y0 = 0,
|
||||
.dst_x1 = static_cast<s32>(output->Width()),
|
||||
.dst_y1 = static_cast<s32>(output->Height()),
|
||||
.src_x0 = 0,
|
||||
.src_y0 = 0,
|
||||
.src_x1 = static_cast<s32>(input->Width()),
|
||||
.src_y1 = static_cast<s32>(input->Height()),
|
||||
};
|
||||
// Debug: does the input hold anything? Download it once and count non-zero words.
|
||||
if (static u32 sampled{}; sampled++ == 20) {
|
||||
const auto sample = [&](const Texture::TICEntry& tic) {
|
||||
const u64 size = u64{tic.Width()} * tic.Height() * 4;
|
||||
if (const auto device = memory_manager.GpuToCpuAddress(tic.Address())) {
|
||||
rasterizer->FlushRegion(*device, size, VideoCommon::CacheType::All);
|
||||
}
|
||||
u32 nonzero{}, samples{};
|
||||
u64 sum{};
|
||||
for (u64 offset = 0; offset < size; offset += 4096 + 4) {
|
||||
const u32 word = memory_manager.Read<u32>(tic.Address() + offset);
|
||||
++samples;
|
||||
nonzero += word != 0;
|
||||
sum += (word & 0x3ff) + ((word >> 10) & 0x3ff) + ((word >> 20) & 0x3ff);
|
||||
}
|
||||
LOG_INFO(HW_GPU, "Upscale fallback image {:#x}: {}/{} sampled words non-zero, mean channel {}",
|
||||
tic.Address(), nonzero, samples, samples ? sum / (samples * 3) : 0);
|
||||
};
|
||||
sample(*input);
|
||||
sample(*output);
|
||||
// Every 2560x1440 target the in-game passes draw to (from EDEN_NX2_RT_TRACE).
|
||||
for (const GPUVAddr address : {0x8116a03000ULL, 0x8117ae5000ULL, 0x8114b62000ULL,
|
||||
0x8115a63000ULL, 0x8117904000ULL, 0x8114a02000ULL}) {
|
||||
Texture::TICEntry fake = *input;
|
||||
const u64 size = 2560ULL * 1440 * 4;
|
||||
if (const auto device = memory_manager.GpuToCpuAddress(address)) {
|
||||
rasterizer->FlushRegion(*device, size, VideoCommon::CacheType::All);
|
||||
}
|
||||
u32 nonzero{}, samples{}, distinct{};
|
||||
u32 last{};
|
||||
for (u64 offset = 0; offset < size; offset += 4096 + 4) {
|
||||
const u32 word = memory_manager.Read<u32>(address + offset);
|
||||
++samples;
|
||||
nonzero += word != 0;
|
||||
distinct += word != last;
|
||||
last = word;
|
||||
}
|
||||
LOG_INFO(HW_GPU, "Upscale fallback target {:#x}: {}/{} non-zero, {} changes", address, nonzero,
|
||||
samples, distinct);
|
||||
(void)fake;
|
||||
}
|
||||
}
|
||||
const bool done = rasterizer->AccelerateSurfaceCopy(surface(*input), surface(*output), config);
|
||||
static u32 logged{};
|
||||
if (logged++ < 4) {
|
||||
LOG_INFO(HW_GPU, "Upscale fallback {:#x} {}x{} -> {:#x} {}x{}: {}", input->Address(),
|
||||
input->Width(), input->Height(), output->Address(), output->Width(),
|
||||
output->Height(), done ? "blitted" : "not accelerated");
|
||||
}
|
||||
return done;
|
||||
}
|
||||
|
||||
void KeplerCompute::ProcessLaunch() {
|
||||
const GPUVAddr launch_desc_loc = regs.launch_desc_loc.Address();
|
||||
memory_manager.ReadBlockUnsafe(launch_desc_loc, &launch_description,
|
||||
|
|
|
|||
|
|
@ -234,6 +234,8 @@ public:
|
|||
private:
|
||||
bool DispatchNativeCompute(const NativeQmdScheduler::Descriptor& descriptor);
|
||||
void ProcessNativeReportSemaphore(u32 argument);
|
||||
void DescribeNativeResources(const NativeQmdScheduler::Descriptor& qmd);
|
||||
bool UpscaleInsteadOfNativeProgram();
|
||||
bool ProcessNativeMethod(Core::System& system, u32 method, u32 argument, bool is_last_call);
|
||||
NativeQmdScheduler native_qmd;
|
||||
bool native_mode{};
|
||||
|
|
|
|||
|
|
@ -399,6 +399,35 @@ 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_CB24_TRACE: for draws whose vertex shader reads bank 24, show the inline
|
||||
// constant buffer upload target (CB_ADDRESS) and its first words.
|
||||
if (static const bool trace = std::getenv("EDEN_NX2_CB24_TRACE") != nullptr; trace) {
|
||||
const auto& regs = maxwell3d->regs;
|
||||
const u64 vs = (u64{regs.pipelines[1].reserved[0] & 0xff} << 32) | regs.pipelines[1].reserved[1];
|
||||
static u32 logged{};
|
||||
if ((vs == 0x11471c9a00 || vs == 0x11471ca600 || vs == 0x11471cd300) && logged++ < 3) {
|
||||
std::array<u32, 24> words{};
|
||||
gpu_memory->ReadBlockUnsafe(regs.const_buffer.Address(), words.data(), sizeof(words));
|
||||
LOG_INFO(HW_GPU, "bank24 draw vs={:#x}: CB_ADDRESS {:#x} size {:#x} offset {:#x}: {:08x}", vs,
|
||||
regs.const_buffer.Address(), regs.const_buffer.size, regs.const_buffer.offset,
|
||||
fmt::join(words, " "));
|
||||
// Search every bound vertex-stage buffer for render-target-size values.
|
||||
const auto& stage = maxwell3d->state.shader_stages[0];
|
||||
for (u32 slot = 0; slot < Tegra::Engines::Maxwell3D::Regs::MaxConstBuffers; ++slot) {
|
||||
const auto& buffer = stage.const_buffers[slot];
|
||||
if (!buffer.enabled || buffer.size == 0) continue;
|
||||
std::vector<u32> data(std::min<u32>(buffer.size, 0x10000) / 4);
|
||||
gpu_memory->ReadBlockUnsafe(buffer.address, data.data(), data.size() * 4);
|
||||
for (size_t i = 0; i < data.size(); ++i) {
|
||||
const u32 w = data[i];
|
||||
if (w == 0x45200000 || w == 0x44b40000 || w == 0x39cccccd || w == 0x3a360b61 ||
|
||||
w == 0x44a00000 || w == 0x44340000 || w == 2559 || w == 1439) {
|
||||
LOG_INFO(HW_GPU, " slot {} +{:#x} = {:#x}", slot, i * 4, w);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// 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, u64, u64>> seen;
|
||||
|
|
|
|||
Loading…
Reference in a new issue