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:
Takoroka 2026-10-28 01:55:10 -04:00
commit 7e5b415e08
7 changed files with 242 additions and 2 deletions

View file

@ -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));

View file

@ -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);

View file

@ -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);

View file

@ -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

View file

@ -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,

View file

@ -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{};

View file

@ -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;