From 7e5b415e08082f1076a765b22e996afec5afde28 Mon Sep 17 00:00:00 2001 From: Takoroka <137927095+Takoroka@users.noreply.github.com> Date: Wed, 28 Oct 2026 01:55:10 -0400 Subject: [PATCH] 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. --- .../frontend/ampere/lower_to_maxwell.h | 12 +- .../frontend/maxwell/translate_program.cpp | 1 + src/shader_recompiler/ir_opt/passes.h | 1 + .../ir_opt/position_pass.cpp | 63 ++++++++ src/video_core/engines/kepler_compute.cpp | 136 ++++++++++++++++++ src/video_core/engines/kepler_compute.h | 2 + .../renderer_vulkan/vk_rasterizer.cpp | 29 ++++ 7 files changed, 242 insertions(+), 2 deletions(-) diff --git a/src/shader_recompiler/frontend/ampere/lower_to_maxwell.h b/src/shader_recompiler/frontend/ampere/lower_to_maxwell.h index d196a1d1fc..2bcf4a49ce 100644 --- a/src/shader_recompiler/frontend/ampere/lower_to_maxwell.h +++ b/src/shader_recompiler/frontend/ampere/lower_to_maxwell.h @@ -891,12 +891,20 @@ inline LoweredProgram LowerToMaxwell(std::span 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(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)); diff --git a/src/shader_recompiler/frontend/maxwell/translate_program.cpp b/src/shader_recompiler/frontend/maxwell/translate_program.cpp index 0cc4b3b844..cd4678cef6 100644 --- a/src/shader_recompiler/frontend/maxwell/translate_program.cpp +++ b/src/shader_recompiler/frontend/maxwell/translate_program.cpp @@ -391,6 +391,7 @@ IR::Program TranslateProgram(ObjectPool& inst_pool, ObjectPoolInstructions()) { + 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::F32{ir.FPRecip(width)}); + case 0x4: + case 0x14: + return ir.BitCast(IR::F32{ir.FPRecip(height)}); + case 0x8: + return ir.BitCast(width); + case 0xc: + return ir.BitCast(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(word(offset)); + } else { + value = word(offset); + } + inst.ReplaceUsesWith(value); + used = true; + } + } + if (used) { + program.info.uses_render_area = true; + } +} + } // namespace Shader::Optimization diff --git a/src/video_core/engines/kepler_compute.cpp b/src/video_core/engines/kepler_compute.cpp index f49dc26f82..136fcb9dda 100644 --- a/src/video_core/engines/kepler_compute.cpp +++ b/src/video_core/engines/kepler_compute.cpp @@ -8,10 +8,12 @@ #include #include #include +#include #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(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(tic.format.Value()), static_cast(tic.r_type.Value()), + static_cast(tic.g_type.Value()), static_cast(tic.b_type.Value()), + static_cast(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 { + const u32 tic_index = memory_manager.Read(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(tic.Address() >> 32); + result.addr_lower = static_cast(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(output->Width()), + .dst_y1 = static_cast(output->Height()), + .src_x0 = 0, + .src_y0 = 0, + .src_x1 = static_cast(input->Width()), + .src_y1 = static_cast(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(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(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, diff --git a/src/video_core/engines/kepler_compute.h b/src/video_core/engines/kepler_compute.h index 51d31b9c95..3bd41a2dfa 100644 --- a/src/video_core/engines/kepler_compute.h +++ b/src/video_core/engines/kepler_compute.h @@ -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{}; diff --git a/src/video_core/renderer_vulkan/vk_rasterizer.cpp b/src/video_core/renderer_vulkan/vk_rasterizer.cpp index 044c783a99..47a55c9580 100644 --- a/src/video_core/renderer_vulkan/vk_rasterizer.cpp +++ b/src/video_core/renderer_vulkan/vk_rasterizer.cpp @@ -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 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 data(std::min(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> seen;