From e7ab34df6764a4c1b224a09b95e3d416fcc3083e Mon Sep 17 00:00:00 2001 From: Jonas Date: Fri, 6 Nov 2026 17:27:33 +0600 Subject: [PATCH 01/12] shader: lower Ampere TLD4 per-tap offsets Welcome Tour's TLD4.PTP carries per-tap offsets: bit 77 of the native instruction, with the two coordinates in Ra, Ra+1 and the four taps' packed offsets in Rb, Rb+1. Maxwell TLD4 has the same thing as PTP, so copy both offset registers to the meta registers and set the offset mode to 2 instead of rejecting the program (the pixel shader of its first 3D draw was skipped). --- .../frontend/ampere/lower_to_maxwell.h | 29 ++++++++++++------- src/tests/video_core/ampere_lowering.cpp | 17 +++++++++++ 2 files changed, 36 insertions(+), 10 deletions(-) diff --git a/src/shader_recompiler/frontend/ampere/lower_to_maxwell.h b/src/shader_recompiler/frontend/ampere/lower_to_maxwell.h index 66748f2bcd..832cdedfe3 100644 --- a/src/shader_recompiler/frontend/ampere/lower_to_maxwell.h +++ b/src/shader_recompiler/frontend/ampere/lower_to_maxwell.h @@ -2158,18 +2158,24 @@ inline LoweredProgram LowerToMaxwellWith(std::span code, u32 } break; } - case 0xb63: case 0xf63: { // TLD4 (gather), 2D without offsets or depth compare; component at bits 87-88. + case 0xb63: case 0xf63: { // TLD4 (gather), 2D without depth compare; component at bits 87-88. // 0xf63: the handle in a uniform register (bit 91), Maxwell TLD4.B with the handle in Rb. const bool bindless = opcode == 0xf63; const u32 handle = bindless ? uniform_register(insn.Bits(40, 6)) : 255; const u32 mask = insn.Bits(72, 4), second_dest = insn.Bits(64, 8); const u32 source_a = insn.Bits(24, 8), source_b = insn.Bits(32, 8); const u32 binding = insn.Bits(54, 5); - // Bit 76: a texel offset after the coordinates (x, y in Ra, Ra+1, offsets in Rb, as - // for TEX); Maxwell TLD4 takes it as AOFFI (bits 54-55) from Rb. + // Bit 76: a texel offset after the coordinates (x, y in Ra, Ra+1, offset in Rb, as + // for TEX); Maxwell TLD4 takes it as AOFFI (bits 54-55) from Rb. Bit 77: per-tap + // offsets (Welcome Tour's TLD4.PTP), the same coordinates with the four taps' packed + // offsets in Rb, Rb+1, which Maxwell TLD4 takes as PTP from the same registers. const bool offsets = insn.Bits(76, 1) != 0; - if (insn.Bits(61, 3) != 1 || insn.Bits(77, 3) || insn.Bits(81, 3) != 7 || insn.Bits(59, 1) || - (offsets && (bindless || source_b == 255 || (source_a != 255 && source_a >= 245))) || + const bool per_tap_offsets = insn.Bits(77, 1) != 0; + const bool offset_registers = offsets || per_tap_offsets; + if (insn.Bits(61, 3) != 1 || insn.Bits(78, 2) || insn.Bits(81, 3) != 7 || insn.Bits(59, 1) || + (offset_registers && (bindless || source_b == 255 || + (per_tap_offsets && source_b >= 245) || + (source_a != 255 && source_a >= 245))) || insn.Bits(89, 1) || mask == 0 || (bindless ? (handle >= 246 || insn.Bits(46, 13) || !insn.Bits(91, 1)) : (insn.Bits(40, 14) > 0x1fff || binding >= 18 || @@ -2177,13 +2183,16 @@ inline LoweredProgram LowerToMaxwellWith(std::span code, u32 return fail(index, "native texture gather mode"); if (!bindless) result.required_texture_buffer = binding; // Two coordinates: x in Ra, y in Rb (the coordinate list splits evenly across them); - // with an offset, x and y in Ra, Ra+1 and the offset in Rb. - const u32 y = !offsets ? source_b : source_a == 255 ? 255U : source_a + 1; + // with offsets, x and y in Ra, Ra+1 and the offsets in Rb (and Rb+1 per tap). + const u32 y = !offset_registers ? source_b : source_a == 255 ? 255U : source_a + 1; emit(0x5c98000000000000ULL | predicate | 250 | (u64{15} << 39) | (u64{source_a} << 20)); emit(0x5c98000000000000ULL | predicate | 251 | (u64{15} << 39) | (u64{y} << 20)); - if (offsets) { + if (offset_registers) { emit(0x5c98000000000000ULL | predicate | 246 | (u64{15} << 39) | (u64{source_b} << 20)); } + if (per_tap_offsets) { + emit(0x5c98000000000000ULL | predicate | 247 | (u64{15} << 39) | (u64{source_b + 1} << 20)); + } if (bindless) { emit(0x5c98000000000000ULL | predicate | 246 | (u64{15} << 39) | (u64{handle} << 20)); emit(0xdec0000000000000ULL | predicate | 250 | (u64{250} << 8) | (u64{246} << 20) | @@ -2191,9 +2200,9 @@ inline LoweredProgram LowerToMaxwellWith(std::span code, u32 (u64{insn.Bits(90, 1)} << 49) | (u64{7} << 51)); } else { emit(0xc800000000000000ULL | predicate | 250 | (u64{250} << 8) | - (u64{offsets ? 246U : 255U} << 20) | (u64{2} << 28) | (u64{mask} << 31) | + (u64{offset_registers ? 246U : 255U} << 20) | (u64{2} << 28) | (u64{mask} << 31) | (u64{insn.Bits(40, 14)} << 36) | (u64{insn.Bits(90, 1)} << 49) | (u64{7} << 51) | - (u64{offsets ? 1U : 0U} << 54) | (u64{insn.Bits(87, 2)} << 56)); + (u64{per_tap_offsets ? 2U : offsets ? 1U : 0U} << 54) | (u64{insn.Bits(87, 2)} << 56)); } const u32 components = static_cast(std::popcount(mask)); const bool split = second_dest != 255; diff --git a/src/tests/video_core/ampere_lowering.cpp b/src/tests/video_core/ampere_lowering.cpp index 3d2ea62600..ef625951ac 100644 --- a/src/tests/video_core/ampere_lowering.cpp +++ b/src/tests/video_core/ampere_lowering.cpp @@ -147,4 +147,21 @@ TEST_CASE("Ampere programs go on past an EXIT that a branch jumps over", "[video REQUIRE(static_cast(lowered)); REQUIRE(lowered.source_size == sizeof(code)); } + +TEST_CASE("Ampere TLD4 lowers per-pixel offsets", "[video_core][ampere]") { + // Welcome Tour's TLD4.PTP instruction: x/y in R4/R5 and packed per-tap offsets in R8/R9. + const std::array code{{ + {{0x040a7b63, 0x33800408, 0x041e2f08, 0x100fc200}}, + EXIT, + }}; + const auto lowered = LowerToMaxwell(code); + INFO(lowered.error); + REQUIRE(static_cast(lowered)); + + const u64 gather = FindMaxwell(lowered, 0xc8, 56); + REQUIRE(gather != 0); + CHECK(((gather >> 28) & 7) == 2); // 2D + CHECK(((gather >> 20) & 0xff) == 246); // packed PTP offsets in the meta register + CHECK(((gather >> 54) & 3) == 2); // per-pixel offsets +} } // namespace From 2e771322b1bf245d8f6ce05ff5fe16f8513299fc Mon Sep 17 00:00:00 2001 From: Jonas Date: Fri, 6 Nov 2026 17:27:35 +0600 Subject: [PATCH 02/12] nx2: gsv 1121 returns a session The SDK takes a handle from 1121 and handshakes on it right away. The zeroed stub reply had no handle, so Welcome Tour aborted with 2001-0114 right after its second present. Return a session that echoes the TIPC 0 token and answers command 11 with 0, as Eden-NS2 does. --- src/core/hle/service/nx2_gsv.h | 37 +++++++++++++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/src/core/hle/service/nx2_gsv.h b/src/core/hle/service/nx2_gsv.h index e2060c90a7..bd146ac445 100644 --- a/src/core/hle/service/nx2_gsv.h +++ b/src/core/hle/service/nx2_gsv.h @@ -2943,6 +2943,32 @@ class INx2VideoSession final : public ServiceFramework { std::thread m_worker; }; +// Session returned by gsv 1121. The SDK sends TIPC 0 (a tick-like u64 it requires echoed) right +// after getting the handle, then 11 with two u64 values. The server behind 11 is unknown, so it +// logs its arguments and replies 0. Left to the zeroed stub, 1121 had no handle and Welcome Tour +// aborted with 2001-0114 during boot. +class INx2Session1121 final : public ServiceFramework { +public: + explicit INx2Session1121(Core::System& system_) : ServiceFramework{system_, "nx2:gsv:1121"} { + RegisterHandlersTipc({ + {0, &INx2Session1121::Handshake, "Handshake"}, + {11, &INx2Session1121::Command11, "Unknown11"}, + }); + } + +private: + void Handshake(HLERequestContext& ctx) { + TipcHandshake(ctx, "nx2:gsv:1121"); + } + + void Command11(HLERequestContext& ctx) { + const auto* w = ctx.CommandBuffer(); + LOG_DEBUG(Service, "(STUBBED) nx2:gsv:1121 TIPC 11 args={:#x}, {:#x}", + u64{w[2]} | (u64{w[3]} << 32), u64{w[4]} | (u64{w[5]} << 32)); + TipcReplyWords(ctx, {0, 0}); + } +}; + // Placeholder for the Switch 2 graphics-system server that nn::gsv::BootWithRemoteMemory talks // to over TIPC. Nothing behind it is emulated: commands succeed and are logged, so boot can show // what comes next. Commands 202 and 1402 hand out the syncpt and nvsrv sessions the SDK uses. @@ -3063,7 +3089,7 @@ class INx2GraphicsSystemSession final : public ServiceFramework(system)); + IPC::ResponseBuilder rb{ctx, 2, 0, 1}; + rb.Push(ResultSuccess); + rb.PushMoveObjects(ctx, *client); + } + std::set m_address_spaces; ///< Ids attached with 434 bool m_client_handle_registered = false; ///< Set by 11 From b1f0db37775476f6ba4f1ad9f8c4fe385f36bbe5 Mon Sep 17 00:00:00 2001 From: Jonas Date: Fri, 6 Nov 2026 17:53:50 +0600 Subject: [PATCH 03/12] shader: bind the descriptor buffer array depth comparison TEX names The special case for ARRAY_2D depth-compare TEX was written against binding 2 and rejected every other binding, which skipped the pixel shaders that sample Welcome Tour's shadow maps. Take the binding from the instruction instead. --- src/shader_recompiler/frontend/ampere/lower_to_maxwell.h | 5 +++-- 1 file changed, 3 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 832cdedfe3..75c9b144af 100644 --- a/src/shader_recompiler/frontend/ampere/lower_to_maxwell.h +++ b/src/shader_recompiler/frontend/ampere/lower_to_maxwell.h @@ -2234,11 +2234,12 @@ inline LoweredProgram LowerToMaxwellWith(std::span code, u32 const u32 dimension = insn.Bits(61, 3), lod = insn.Bits(87, 3); const u32 source_a = insn.Bits(24, 8), source_b = insn.Bits(32, 8); if (!bindless && dimension == 5 && depth_compare && lod == 0 && !split && mask == 1) { + const u32 binding = insn.Bits(54, 5); if (!insn.Bits(60, 1) || insn.Bits(76, 2) || insn.Bits(81, 3) != 7 || - insn.Bits(59, 1) || insn.Bits(40, 14) > 0x1fff || insn.Bits(54, 5) != 2 || + insn.Bits(59, 1) || insn.Bits(40, 14) > 0x1fff || binding >= 18 || (source_a & 1) || (source_b & 1) || source_a >= 248 || source_b >= 248 || dest >= 248) return fail(index, "native array depth comparison sample mode"); - if (!bind_texture_buffer(2)) return fail(index, "native texture descriptor buffer conflict"); + if (!bind_texture_buffer(binding)) return fail(index, "native texture descriptor buffer conflict"); for (u32 i = 0; i < 3; ++i) { const u32 source = i < 2 ? source_a + i : source_b; emit(0x5c98000000000000ULL | predicate | (250 + i) | From 8af73fcbc9731543f6a210be69ae1d9c26ae5df2 Mon Sep 17 00:00:00 2001 From: Jonas Date: Fri, 6 Nov 2026 17:53:50 +0600 Subject: [PATCH 04/12] shader_environment: dump a TIC that reads as format 0 Format 0 is not a Maxwell texture format, so either the handle points at the wrong entry or the game uses a format the table does not know. The descriptor words tell the two apart. --- src/video_core/shader_environment.cpp | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/video_core/shader_environment.cpp b/src/video_core/shader_environment.cpp index bf1cef1928..c73149fc51 100644 --- a/src/video_core/shader_environment.cpp +++ b/src/video_core/shader_environment.cpp @@ -346,6 +346,17 @@ Tegra::Texture::TICEntry GenericEnvironment::ReadTextureInfo(GPUVAddr tic_addr, const GPUVAddr descriptor_addr{tic_addr + index * sizeof(Tegra::Texture::TICEntry)}; Tegra::Texture::TICEntry entry; gpu_memory->ReadBlock(descriptor_addr, &entry, sizeof(entry)); + if (static_cast(entry.format.Value()) == 0) { + // Format 0 is not a Maxwell format: dump the descriptor to tell a native format from a + // handle that points at the wrong entry. + std::array words{}; + gpu_memory->ReadBlock(descriptor_addr, words.data(), sizeof(words)); + LOG_WARNING(Shader, + "TIC {:#x} (index {}, limit {}) has format 0: {:08x} {:08x} {:08x} {:08x} " + "{:08x} {:08x} {:08x} {:08x}", + descriptor_addr, index, tic_limit, words[0], words[1], words[2], words[3], + words[4], words[5], words[6], words[7]); + } return entry; } From 4bd876a103afd9cf23cc370db858e5c9451b1fd8 Mon Sep 17 00:00:00 2001 From: Jonas Date: Fri, 6 Nov 2026 18:29:41 +0600 Subject: [PATCH 05/12] shader: read a second descriptor buffer for the array depth comparison TEX A program's texture instructions share one descriptor constant buffer. The ARRAY_2D depth-compare case rejected a program that also samples through a second buffer, which skipped every pixel shader that samples Welcome Tour's shadow maps. Load that instruction's handle from its own buffer and emit TEX.B, as the general TEX path does. --- .../frontend/ampere/lower_to_maxwell.h | 28 +++++++++++++------ 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/src/shader_recompiler/frontend/ampere/lower_to_maxwell.h b/src/shader_recompiler/frontend/ampere/lower_to_maxwell.h index 75c9b144af..6d3edbc36b 100644 --- a/src/shader_recompiler/frontend/ampere/lower_to_maxwell.h +++ b/src/shader_recompiler/frontend/ampere/lower_to_maxwell.h @@ -2239,16 +2239,26 @@ inline LoweredProgram LowerToMaxwellWith(std::span code, u32 insn.Bits(59, 1) || insn.Bits(40, 14) > 0x1fff || binding >= 18 || (source_a & 1) || (source_b & 1) || source_a >= 248 || source_b >= 248 || dest >= 248) return fail(index, "native array depth comparison sample mode"); - if (!bind_texture_buffer(binding)) return fail(index, "native texture descriptor buffer conflict"); - for (u32 i = 0; i < 3; ++i) { - const u32 source = i < 2 ? source_a + i : source_b; - emit(0x5c98000000000000ULL | predicate | (250 + i) | - (u64{15} << 39) | (u64{source} << 20)); + // A second descriptor buffer is read bindless: TEX.B takes the handle, loaded from + // that buffer, as its first meta register, then the depth reference. + const bool second_buffer = !bind_texture_buffer(binding); + const auto copy = [&](u32 target, u32 source) { + emit(0x5c98000000000000ULL | predicate | target | (u64{15} << 39) | (u64{source} << 20)); + }; + for (u32 i = 0; i < 3; ++i) copy(250 + i, i < 2 ? source_a + i : source_b); + if (second_buffer) { + emit(0x4c98000000070000ULL | 246 | (u64{15} << 39) | (u64{insn.Bits(40, 14)} << 20) | + (u64{binding} << 34)); + copy(247, source_b + 1); + emit(0xde80000000000000ULL | predicate | dest | (u64{250} << 8) | (u64{246} << 20) | + (u64{3} << 28) | (u64{1} << 31) | (u64{insn.Bits(90, 1)} << 49) | + (u64{1} << 50) | (u64{7} << 51)); + } else { + emit(0xc000000000000000ULL | predicate | dest | (u64{250} << 8) | + (u64{source_b + 1} << 20) | (u64{3} << 28) | (u64{1} << 31) | + (u64{insn.Bits(40, 14)} << 36) | (u64{insn.Bits(90, 1)} << 49) | + (u64{1} << 50) | (u64{7} << 51)); } - emit(0xc000000000000000ULL | predicate | dest | (u64{250} << 8) | - (u64{source_b + 1} << 20) | (u64{3} << 28) | (u64{1} << 31) | - (u64{insn.Bits(40, 14)} << 36) | (u64{insn.Bits(90, 1)} << 49) | - (u64{1} << 50) | (u64{7} << 51)); break; } // General form: gather the native operand list (array index first, coordinates, From 63bb5b2790c88747bbc2ceaf2bab4ebdc1435495 Mon Sep 17 00:00:00 2001 From: Jonas Date: Fri, 6 Nov 2026 19:24:07 +0600 Subject: [PATCH 06/12] debug: dump the TIC descriptors programs read, and the traced target format Welcome Tour's 3D layer comes out with no blue at all while its HUD has blue, and the scene draws write all four channels into an A8B8G8R8_SRGB target, so the colour loss is upstream of the blend. The first 150 texture descriptors now log their format, component types and channel swizzle, and EDEN_NX2_WINDOW_TRACE prints the render target format. --- .../renderer_vulkan/vk_rasterizer.cpp | 5 ++-- src/video_core/shader_environment.cpp | 24 ++++++++++++------- 2 files changed, 19 insertions(+), 10 deletions(-) diff --git a/src/video_core/renderer_vulkan/vk_rasterizer.cpp b/src/video_core/renderer_vulkan/vk_rasterizer.cpp index bb8c7f57a3..8d57abc324 100644 --- a/src/video_core/renderer_vulkan/vk_rasterizer.cpp +++ b/src/video_core/renderer_vulkan/vk_rasterizer.cpp @@ -488,10 +488,11 @@ void TraceWindowTarget(const Tegra::Engines::Maxwell3D& maxwell3d, Tegra::Memory 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={} " + "nx2 window {} rt0={:#x} fmt={:#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], + kind, regs.rt[0].Address(), static_cast(regs.rt[0].format), 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(), diff --git a/src/video_core/shader_environment.cpp b/src/video_core/shader_environment.cpp index c73149fc51..ada407738d 100644 --- a/src/video_core/shader_environment.cpp +++ b/src/video_core/shader_environment.cpp @@ -6,6 +6,7 @@ #include #include +#include #include #include #include @@ -346,16 +347,23 @@ Tegra::Texture::TICEntry GenericEnvironment::ReadTextureInfo(GPUVAddr tic_addr, const GPUVAddr descriptor_addr{tic_addr + index * sizeof(Tegra::Texture::TICEntry)}; Tegra::Texture::TICEntry entry; gpu_memory->ReadBlock(descriptor_addr, &entry, sizeof(entry)); - if (static_cast(entry.format.Value()) == 0) { - // Format 0 is not a Maxwell format: dump the descriptor to tell a native format from a - // handle that points at the wrong entry. + // Diagnostic: the first descriptors the programs read, with the fields that decide the + // sampled colour (format, component types, channel swizzle) and the raw words. + static std::atomic described{}; + if (described.fetch_add(1, std::memory_order_relaxed) < 150) { std::array words{}; gpu_memory->ReadBlock(descriptor_addr, words.data(), sizeof(words)); - LOG_WARNING(Shader, - "TIC {:#x} (index {}, limit {}) has format 0: {:08x} {:08x} {:08x} {:08x} " - "{:08x} {:08x} {:08x} {:08x}", - descriptor_addr, index, tic_limit, words[0], words[1], words[2], words[3], - words[4], words[5], words[6], words[7]); + LOG_INFO(Shader, + "TIC {:#x} idx {} fmt {:#x} types {}/{}/{}/{} swz {}/{}/{}/{} type {} {}x{}x{} " + "srgb {}: {:08x} {:08x} {:08x} {:08x}", + descriptor_addr, index, static_cast(entry.format.Value()), + static_cast(entry.r_type.Value()), static_cast(entry.g_type.Value()), + static_cast(entry.b_type.Value()), static_cast(entry.a_type.Value()), + static_cast(entry.x_source.Value()), static_cast(entry.y_source.Value()), + static_cast(entry.z_source.Value()), static_cast(entry.w_source.Value()), + static_cast(entry.texture_type.Value()), entry.Width(), entry.Height(), + entry.Depth(), static_cast(entry.srgb_conversion.Value()), words[0], words[1], + words[2], words[3]); } return entry; } From 5280bca1e698ad03deaa87788cc608200cf0f5ad Mon Sep 17 00:00:00 2001 From: Jonas Date: Fri, 6 Nov 2026 19:43:56 +0600 Subject: [PATCH 07/12] debug: trace every screen-sized draw, with its target size Welcome Tour renders its scene below 3840 wide, so the window trace never showed the scene draws themselves. Trace anything at least 1280 wide and log the target's size next to its format. --- src/video_core/renderer_vulkan/vk_rasterizer.cpp | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/video_core/renderer_vulkan/vk_rasterizer.cpp b/src/video_core/renderer_vulkan/vk_rasterizer.cpp index 8d57abc324..6ed233321c 100644 --- a/src/video_core/renderer_vulkan/vk_rasterizer.cpp +++ b/src/video_core/renderer_vulkan/vk_rasterizer.cpp @@ -480,7 +480,7 @@ void TraceWindowTarget(const Tegra::Engines::Maxwell3D& maxwell3d, Tegra::Memory }(); const u64 pixel_shader = (u64{regs.pipelines[5].reserved[0] & 0xff} << 32) | regs.pipelines[5].reserved[1]; - if (!trace || regs.rt[0].width != 3840 || traced >= 400 || seen_events++ < skip || + if (!trace || regs.rt[0].width < 1280 || traced >= 600 || seen_events++ < skip || (only_ps != 0 && pixel_shader != only_ps)) { return; } @@ -488,10 +488,11 @@ void TraceWindowTarget(const Tegra::Engines::Maxwell3D& maxwell3d, Tegra::Memory const auto& vp = regs.viewport_transform[0]; const auto& sc = regs.scissor_test[0]; LOG_INFO(HW_GPU, - "nx2 window {} rt0={:#x} fmt={:#x} count={} clear={:#x} color=({},{},{},{}) mask_common={} " + "nx2 window {} rt0={:#x} fmt={:#x} {}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(), static_cast(regs.rt[0].format), count, + kind, regs.rt[0].Address(), static_cast(regs.rt[0].format), regs.rt[0].width, + regs.rt[0].height, 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, From fd0d392542c697e602c51d4c412960de3e201dd3 Mon Sep 17 00:00:00 2001 From: Jonas Date: Fri, 6 Nov 2026 19:59:58 +0600 Subject: [PATCH 08/12] debug: trace the indirect draws too Welcome Tour renders its scene with GPU-driven indirect draws, which never reached the window trace, so the scene passes and their targets were invisible. Log them as draw-indirect. --- src/video_core/renderer_vulkan/vk_rasterizer.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/video_core/renderer_vulkan/vk_rasterizer.cpp b/src/video_core/renderer_vulkan/vk_rasterizer.cpp index 6ed233321c..d09e9cbdaf 100644 --- a/src/video_core/renderer_vulkan/vk_rasterizer.cpp +++ b/src/video_core/renderer_vulkan/vk_rasterizer.cpp @@ -716,6 +716,7 @@ void RasterizerVulkan::DrawIndirect() { VideoCommon::Nx2Profile::Scope profile_scope{VideoCommon::Nx2Profile::Zone::Draw}; const auto& params = maxwell3d->draw_manager.indirect_state; buffer_cache.SetDrawIndirect(¶ms); + TraceWindowTarget(*maxwell3d, *gpu_memory, "draw-indirect", params.max_draw_counts); PrepareDraw(params.is_indexed, [this, ¶ms] { if (mesh_draw) { // Indirect task/mesh draws would need vkCmdDrawMeshTasksIndirectEXT's layout. From 613e25e001d27af94290e07879014c6e4f3a0507 Mon Sep 17 00:00:00 2001 From: Jonas Date: Fri, 6 Nov 2026 20:39:38 +0600 Subject: [PATCH 09/12] shader: pack TEX FP16 results with the first component in the low half The ISCADD merged the halves as (c0 << 16) + c1, swapping R/G and B/A. With a three-component mask the high source is RZ, so blue came out as zero: Welcome Tour's FP16 post-process passes turned the scene yellow. Assisted-by: Claude Opus 5.5 (debugging and implementation) --- src/shader_recompiler/frontend/ampere/lower_to_maxwell.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/shader_recompiler/frontend/ampere/lower_to_maxwell.h b/src/shader_recompiler/frontend/ampere/lower_to_maxwell.h index 6d3edbc36b..8db7df0bdd 100644 --- a/src/shader_recompiler/frontend/ampere/lower_to_maxwell.h +++ b/src/shader_recompiler/frontend/ampere/lower_to_maxwell.h @@ -2335,7 +2335,7 @@ inline LoweredProgram LowerToMaxwellWith(std::span code, u32 if (target == 255) return; emit(0x5ca8000000000000ULL | predicate | 246 | (u64{1} << 8) | (u64{2} << 10) | (u64{low} << 20)); emit(0x5ca8000000000000ULL | predicate | 247 | (u64{1} << 8) | (u64{2} << 10) | (u64{high} << 20)); - emit(0x5c18000000000000ULL | predicate | target | (u64{246} << 8) | (u64{247} << 20) | (u64{16} << 39)); + emit(0x5c18000000000000ULL | predicate | target | (u64{247} << 8) | (u64{246} << 20) | (u64{16} << 39)); }; pack(static_cast(dest), 250, components > 1 ? 251U : 255U); if (components > 2) { From 42fd03b23e8806321f999278c9af42bae426a2e0 Mon Sep 17 00:00:00 2001 From: Jonas Date: Fri, 6 Nov 2026 22:41:17 +0600 Subject: [PATCH 10/12] nx2: relayout decoder frames written before the first GPU query A frame decoded before the client's first 428/426 was written pitch-linear (and BGRA for code 5), but the client then samples it block-linear. Welcome Tour renders its "How Rumble Works" slide once from the first frame of the clip, so it came out sheared. Rewrite such a frame in the GPU layout when the image is first queried, or right after writing it if the query raced the write. Assisted-by: Claude Opus 5.5 (debugging and implementation) --- src/core/hle/service/nx2_gsv.h | 49 ++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/src/core/hle/service/nx2_gsv.h b/src/core/hle/service/nx2_gsv.h index bd146ac445..8117586019 100644 --- a/src/core/hle/service/nx2_gsv.h +++ b/src/core/hle/service/nx2_gsv.h @@ -1460,6 +1460,7 @@ struct Nx2NativeObjects { bool decoder{}; // a video decoder output image (gsv 911) // A decoder image the client reads on the GPU (gsv 428/426): written block-linear. bool gpu_read{}; + bool pitch_frame{}; // holds a frame written before gpu_read was set u32 gpu_maps{}; // outstanding 426 maps bool mapped_here{}; // 426 added its GPU mapping (no other mapping covered it) }; @@ -1617,6 +1618,31 @@ inline Nx2DecoderImageLayout Nx2CalculateDecoderImageLayout(const u32* preferenc return layout; } +// Rewrites a decoder image holding a pitch-linear frame in the layout GPU reads use (RGBA, +// native block-linear, 16-GOB blocks). For frames decoded before the client's first 428/426. +inline void Nx2RelayoutDecoderImage(Core::System& system, u64 address, + const Nx2DecoderImageLayout& layout) { + if (layout.format == Tegra::Host1x::Nx2PictureFormat::Nv12 || + system.ApplicationProcess() == nullptr) { + return; + } + auto& memory = system.ApplicationMemory(); + std::vector linear(layout.size); + memory.ReadBlock(address, linear.data(), linear.size()); + if (layout.format == Tegra::Host1x::Nx2PictureFormat::Bgra8) { + for (size_t i = 0; i + 3 < linear.size(); i += 4) { + std::swap(linear[i], linear[i + 2]); + } + } + std::vector tiled(layout.size); + Tegra::Texture::SwizzleTexture(tiled, linear, 4, static_cast(layout.pitch / 4), + layout.height, 1, 4, 0, 1, true); + memory.WriteBlock(address, tiled.data(), tiled.size()); + if (const auto device = Nvidia::Devices::Nx2GpuMemoryRegistry::Get().DeviceAddressOf(address)) { + system.GPU().InvalidateRegion(*device, tiled.size()); + } +} + // Placeholder for the Switch 2 display session (nn::vi2::sdk), handed out by gsv 305. Reply // shapes read from the SDK: TIPC 1 -> u32 < 0x400, TIPC 3 -> u32 < 8, TIPC 11/31(u32) and // 32(handle, u32) -> one handle at word 3 then the result. Handles returned here are events (a @@ -2882,6 +2908,19 @@ class INx2VideoSession final : public ServiceFramework { if (const auto device = Nvidia::Devices::Nx2GpuMemoryRegistry::Get().DeviceAddressOf(address)) { system.GPU().InvalidateRegion(*device, layout.size); } + // A 428/426 that came in while this frame was written didn't see it: relayout it here. + bool relayout = false; + if (layout.format != Tegra::Host1x::Nx2PictureFormat::Nv12) { + std::scoped_lock lk{m_objects->mutex}; + if (const auto image = m_objects->images.find(image_id); + image != m_objects->images.end()) { + relayout = image->second.gpu_read; + image->second.pitch_frame = !relayout; + } + } + if (relayout) { + Nx2RelayoutDecoderImage(system, address, layout); + } return {width, height}; } @@ -3552,6 +3591,8 @@ class INx2GraphicsSystemSession final : public ServiceFrameworkmutex}; const auto image = m_objects->images.find(handle); @@ -3563,6 +3604,10 @@ class INx2GraphicsSystemSession final : public ServiceFrameworksecond.gpu_read = true; + if (std::exchange(image->second.pitch_frame, false)) { + relayout_address = memory->second.address + image->second.offset; + relayout_layout = Nx2CalculateDecoderImageLayout(image->second.preference.data()); + } offset = image->second.offset; size = image->second.size; dimensions = image->second.preference[2]; @@ -3576,6 +3621,10 @@ class INx2GraphicsSystemSession final : public ServiceFramework words{}; From b939271cd3a5a34b2081a2154cdf2f81da85ec14 Mon Sep 17 00:00:00 2001 From: Jonas Date: Sat, 7 Nov 2026 07:43:52 +0600 Subject: [PATCH 11/12] nx2: address Welcome Tour review findings Assisted-by: GPT-6.1 (review fixes and regression tests) --- src/core/hle/service/nx2_gsv.h | 68 +++++++++++++++++-- .../frontend/ampere/lower_to_maxwell.h | 6 +- src/tests/video_core/ampere_lowering.cpp | 60 ++++++++++++++++ .../renderer_vulkan/vk_rasterizer.cpp | 55 +++++++++++---- src/video_core/shader_environment.cpp | 3 +- 5 files changed, 170 insertions(+), 22 deletions(-) diff --git a/src/core/hle/service/nx2_gsv.h b/src/core/hle/service/nx2_gsv.h index 8117586019..caa4bec3ee 100644 --- a/src/core/hle/service/nx2_gsv.h +++ b/src/core/hle/service/nx2_gsv.h @@ -46,6 +46,7 @@ #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_scoped_resource_reservation.h" #include "core/hle/kernel/k_server_session.h" #include "core/hle/kernel/k_event.h" #include "core/hle/kernel/k_hardware_timer.h" @@ -78,13 +79,22 @@ namespace Service { // works from a domain too, for callers that want a raw session handle. template inline Kernel::KClientSession* CreateRawSession(Core::System& system, HLERequestContext& ctx, - std::shared_ptr iface) { + std::shared_ptr iface, Result& result) { auto& kernel = system.Kernel(); - ASSERT(Kernel::GetCurrentProcess(kernel).GetResourceLimit()->Reserve( - kernel, Kernel::LimitableResource::SessionCountMax, 1)); + Kernel::KScopedResourceReservation reservation{ + kernel, &Kernel::GetCurrentProcess(kernel), Kernel::LimitableResource::SessionCountMax}; + if (!reservation.Succeeded()) { + result = Kernel::ResultLimitReached; + return nullptr; + } auto* session = Kernel::KSession::Create(kernel); + if (session == nullptr) { + result = Kernel::ResultOutOfResource; + return nullptr; + } session->Initialize(kernel, nullptr, 0); + reservation.Commit(); Kernel::KSession::Register(kernel, session); auto manager = @@ -120,7 +130,17 @@ inline void TipcHandshake(HLERequestContext& ctx, std::string_view session) { // Replies with success and a copied handle to a new session served by `iface`. template inline void ReturnRawSession(Core::System& system, HLERequestContext& ctx, std::shared_ptr iface) { - auto* client = CreateRawSession(system, ctx, std::move(iface)); + Result result = ResultSuccess; + auto* client = CreateRawSession(system, ctx, std::move(iface), result); + if (client == nullptr) { + IPC::ResponseBuilder rb{ctx, 2}; + if (ctx.IsTipc()) { + rb.PushRaw(result.raw); + } else { + rb.Push(result); + } + return; + } IPC::ResponseBuilder rb{ctx, 2, 1, 0}; rb.Push(ResultSuccess); rb.PushCopyObjects(ctx, *client); @@ -1461,6 +1481,7 @@ struct Nx2NativeObjects { // A decoder image the client reads on the GPU (gsv 428/426): written block-linear. bool gpu_read{}; bool pitch_frame{}; // holds a frame written before gpu_read was set + std::shared_ptr decoder_mutex; u32 gpu_maps{}; // outstanding 426 maps bool mapped_here{}; // 426 added its GPU mapping (no other mapping covered it) }; @@ -1469,6 +1490,12 @@ struct Nx2NativeObjects { std::map images; u32 next_memory{1}; u32 next_image{1}; + + std::shared_ptr DecoderMutex(u32 id) { + std::scoped_lock lk{mutex}; + const auto image = images.find(id); + return image != images.end() ? image->second.decoder_mutex : nullptr; + } }; // Block-linear layout of an InterModuleUserImagePreference (16 words): word 2 holds @@ -2855,13 +2882,18 @@ class INx2VideoSession final : public ServiceFramework { // Writes a decoded frame into an output image; returns the size the frame reports. std::pair WriteImage(Tegra::Host1x::Nx2VideoDecoder* decoder, u32 image_id, const Decoded& frame, std::vector& bytes) { + const auto image_mutex = m_objects->DecoderMutex(image_id); + if (!image_mutex) { + return {m_width, m_height}; + } + std::scoped_lock image_lock{*image_mutex}; u64 address{}; Nx2DecoderImageLayout layout{}; bool block_linear{}; { std::scoped_lock lk{m_objects->mutex}; const auto image = m_objects->images.find(image_id); - if (image == m_objects->images.end()) { + if (image == m_objects->images.end() || image->second.decoder_mutex != image_mutex) { return {m_width, m_height}; } layout = Nx2CalculateDecoderImageLayout(image->second.preference.data()); @@ -3205,7 +3237,13 @@ class INx2GraphicsSystemSession final : public ServiceFramework(system)); + Result result = ResultSuccess; + auto* client = CreateRawSession(system, ctx, std::make_shared(system), result); + if (client == nullptr) { + IPC::ResponseBuilder rb{ctx, 2}; + rb.PushRaw(result.raw); + return; + } IPC::ResponseBuilder rb{ctx, 2, 0, 1}; rb.Push(ResultSuccess); rb.PushMoveObjects(ctx, *client); @@ -3585,6 +3623,11 @@ class INx2GraphicsSystemSession final : public ServiceFrameworkDecoderMutex(handle); + if (!image_mutex) { + return false; + } + std::scoped_lock image_lock{*image_mutex}; const bool map = ctx.GetCommand() == 426; u64 address{}; u64 size{}; @@ -3596,7 +3639,7 @@ class INx2GraphicsSystemSession final : public ServiceFrameworkmutex}; const auto image = m_objects->images.find(handle); - if (image == m_objects->images.end() || !image->second.decoder) { + if (image == m_objects->images.end() || image->second.decoder_mutex != image_mutex) { return false; } const auto memory = m_objects->memories.find(image->second.memory); @@ -3655,6 +3698,11 @@ class INx2GraphicsSystemSession final : public ServiceFrameworkDecoderMutex(handle); + std::unique_lock image_lock; + if (image_mutex) { + image_lock = std::unique_lock{*image_mutex}; + } std::optional> unmap; { std::scoped_lock lk{m_objects->mutex}; @@ -3707,6 +3755,7 @@ class INx2GraphicsSystemSession final : public ServiceFramework(); } LOG_INFO(Service, "nx2:gsv 911 decoder images {}..{} code={} {}x{} in memory {}", first, first + count - 1, layout.code, layout.width, layout.height, w[19]); @@ -3748,6 +3797,11 @@ class INx2GraphicsSystemSession final : public ServiceFrameworkDecoderMutex(id); + std::unique_lock image_lock; + if (image_mutex) { + image_lock = std::unique_lock{*image_mutex}; + } std::optional> unmap; { std::scoped_lock lk{m_objects->mutex}; diff --git a/src/shader_recompiler/frontend/ampere/lower_to_maxwell.h b/src/shader_recompiler/frontend/ampere/lower_to_maxwell.h index 8db7df0bdd..c8f1892d20 100644 --- a/src/shader_recompiler/frontend/ampere/lower_to_maxwell.h +++ b/src/shader_recompiler/frontend/ampere/lower_to_maxwell.h @@ -2173,9 +2173,11 @@ inline LoweredProgram LowerToMaxwellWith(std::span code, u32 const bool per_tap_offsets = insn.Bits(77, 1) != 0; const bool offset_registers = offsets || per_tap_offsets; if (insn.Bits(61, 3) != 1 || insn.Bits(78, 2) || insn.Bits(81, 3) != 7 || insn.Bits(59, 1) || + (offsets && per_tap_offsets) || + (source_b != 255 && source_b >= shadow_boundary) || (offset_registers && (bindless || source_b == 255 || - (per_tap_offsets && source_b >= 245) || - (source_a != 255 && source_a >= 245))) || + (per_tap_offsets && source_b + 1 >= shadow_boundary) || + (source_a != 255 && source_a + 1 >= shadow_boundary))) || insn.Bits(89, 1) || mask == 0 || (bindless ? (handle >= 246 || insn.Bits(46, 13) || !insn.Bits(91, 1)) : (insn.Bits(40, 14) > 0x1fff || binding >= 18 || diff --git a/src/tests/video_core/ampere_lowering.cpp b/src/tests/video_core/ampere_lowering.cpp index ef625951ac..f0dfc122cc 100644 --- a/src/tests/video_core/ampere_lowering.cpp +++ b/src/tests/video_core/ampere_lowering.cpp @@ -3,6 +3,7 @@ #include #include +#include #include #include "shader_recompiler/frontend/ampere/lower_to_maxwell.h" @@ -164,4 +165,63 @@ TEST_CASE("Ampere TLD4 lowers per-pixel offsets", "[video_core][ampere]") { CHECK(((gather >> 20) & 0xff) == 246); // packed PTP offsets in the meta register CHECK(((gather >> 54) & 3) == 2); // per-pixel offsets } + +TEST_CASE("Ampere TLD4 rejects shadowed per-tap offset registers", "[video_core][ampere]") { + std::array code{{ + {{0x040a7b63, 0x338004f2, 0x041e2f08, 0x100fc200}}, + EXIT, + }}; + REQUIRE(static_cast(LowerToMaxwell(code))); + code[0].words[1] = 0x338004f3; + CHECK_FALSE(static_cast(LowerToMaxwell(code))); + code[0].words[1] = 0x338004f4; + CHECK_FALSE(static_cast(LowerToMaxwell(code))); +} + +TEST_CASE("Ampere TLD4 lowers a single offset register", "[video_core][ampere]") { + std::array code{{ + {{0x040a7b63, 0x33800408, 0x041e1f08, 0x100fc200}}, + EXIT, + }}; + const auto lowered = LowerToMaxwell(code); + REQUIRE(static_cast(lowered)); + const u64 gather = FindMaxwell(lowered, 0xc8, 56); + REQUIRE(gather != 0); + CHECK(((gather >> 54) & 3) == 1); + code[0].words[2] |= 1U << 13; + CHECK_FALSE(static_cast(LowerToMaxwell(code))); +} + +TEST_CASE("Ampere TLD4 packs half results in component order", "[video_core][ampere]") { + const std::array code{{ + {{0x040a7b63, 0x33800408, 0x041eaf08, 0x100fc200}}, + EXIT, + }}; + const auto lowered = LowerToMaxwell(code); + REQUIRE(static_cast(lowered)); + std::vector conversions; + for (const u64 word : lowered.instructions) { + if ((word >> 48) == 0x5ca8) { + conversions.push_back(word); + } + } + REQUIRE(conversions.size() == 4); + CHECK(((conversions[0] >> 20) & 0xff) == 249); + CHECK(((conversions[1] >> 20) & 0xff) == 248); + CHECK(((conversions[2] >> 20) & 0xff) == 247); + CHECK(((conversions[3] >> 20) & 0xff) == 246); +} + +TEST_CASE("Ampere array depth samples use a second descriptor buffer", "[video_core][ampere]") { + const std::array code{{ + {{0x040a7b60, 0xb0000008, 0x000e41ff, 0}}, + {{0x040a7b60, 0xb0400008, 0x000e41ff, 0}}, + EXIT, + }}; + const auto lowered = LowerToMaxwell(code); + REQUIRE(static_cast(lowered)); + CHECK(lowered.required_texture_buffer == 0); + CHECK(FindMaxwell(lowered, 0xc0, 56) != 0); + CHECK(FindMaxwell(lowered, 0xde, 56) != 0); +} } // namespace diff --git a/src/video_core/renderer_vulkan/vk_rasterizer.cpp b/src/video_core/renderer_vulkan/vk_rasterizer.cpp index d09e9cbdaf..f82b2c2e93 100644 --- a/src/video_core/renderer_vulkan/vk_rasterizer.cpp +++ b/src/video_core/renderer_vulkan/vk_rasterizer.cpp @@ -480,7 +480,18 @@ void TraceWindowTarget(const Tegra::Engines::Maxwell3D& maxwell3d, Tegra::Memory }(); const u64 pixel_shader = (u64{regs.pipelines[5].reserved[0] & 0xff} << 32) | regs.pipelines[5].reserved[1]; - if (!trace || regs.rt[0].width < 1280 || traced >= 600 || seen_events++ < skip || + if (!trace) { + return; + } + u32 width = regs.rt[0].width; + if (regs.rt[0].tile_mode.is_pitch_linear) { + const auto format = VideoCore::Surface::TryPixelFormatFromRenderTargetFormat(regs.rt[0].format); + if (!format) { + return; + } + width /= VideoCore::Surface::BytesPerBlock(*format); + } + if (width < 1280 || traced >= 600 || seen_events++ < skip || (only_ps != 0 && pixel_shader != only_ps)) { return; } @@ -491,7 +502,7 @@ void TraceWindowTarget(const Tegra::Engines::Maxwell3D& maxwell3d, Tegra::Memory "nx2 window {} rt0={:#x} fmt={:#x} {}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(), static_cast(regs.rt[0].format), regs.rt[0].width, + kind, regs.rt[0].Address(), static_cast(regs.rt[0].format), width, regs.rt[0].height, 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, @@ -504,12 +515,23 @@ void TraceWindowTarget(const Tegra::Engines::Maxwell3D& maxwell3d, Tegra::Memory if (kind[0] != 'd') { return; } + const auto read_trace = [&](GPUVAddr address, void* data, size_t size) { + if (size == 0 || address > std::numeric_limits::max() - (size - 1) || + !memory.IsWithinGPUAddressRange(address) || + !memory.IsWithinGPUAddressRange(address + size - 1) || + !memory.IsFullyMappedRange(address, size)) { + return false; + } + memory.ReadBlockUnsafe(address, data, size); + return true; + }; // 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 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, " ")); + if (cb3.size >= sizeof(values) && read_trace(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++ > 200) { @@ -517,11 +539,17 @@ void TraceWindowTarget(const Tegra::Engines::Maxwell3D& maxwell3d, Tegra::Memory } // 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(cb.address + 0x20) : 0; + u32 handle{}; + if (!cb.enabled || cb.size < 0x24 || cb.address > std::numeric_limits::max() - 0x20 || + !read_trace(cb.address + 0x20, &handle, sizeof(handle))) { + return; + } const u32 tic_index = handle & 0xfffff; const GPUVAddr tic_address = regs.tex_header.Address() + u64{tic_index} * 32; std::array tic{}; - memory.ReadBlockUnsafe(tic_address, tic.data(), sizeof(tic)); + if (tic_index > regs.tex_header.limit || !read_trace(tic_address, tic.data(), sizeof(tic))) { + return; + } LOG_INFO(HW_GPU, "nx2 window texture: cb2={:#x} en={} handle={:#x} tic pool={:#x} limit={} " "sampler pool={:#x} tic[{}]={:08x}", @@ -530,16 +558,19 @@ void TraceWindowTarget(const Tegra::Engines::Maxwell3D& maxwell3d, Tegra::Memory // 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 texels{}; - memory.ReadBlockUnsafe(texture, texels.data(), sizeof(texels)); + if (!read_trace(texture, texels.data(), sizeof(texels))) { + return; + } LOG_INFO(HW_GPU, "nx2 window texture {:#x} {}x{}: {:08x}", texture, (tic[4] & 0xffff) + 1, (tic[5] & 0xffff) + 1, fmt::join(texels, " ")); // EDEN_NX2_TEXTURE_DUMP=: save the first 4 MiB of the last traced texture's memory. if (const char* path = std::getenv("EDEN_NX2_TEXTURE_DUMP"); path != nullptr) { std::vector bytes(2048u * 10240u); - memory.ReadBlockUnsafe(texture, bytes.data(), bytes.size()); - if (std::FILE* file = std::fopen(path, "wb")) { - std::fwrite(bytes.data(), 1, bytes.size(), file); - std::fclose(file); + if (read_trace(texture, bytes.data(), bytes.size())) { + if (std::FILE* file = std::fopen(path, "wb")) { + std::fwrite(bytes.data(), 1, bytes.size(), file); + std::fclose(file); + } } } } diff --git a/src/video_core/shader_environment.cpp b/src/video_core/shader_environment.cpp index ada407738d..a0dd9359f3 100644 --- a/src/video_core/shader_environment.cpp +++ b/src/video_core/shader_environment.cpp @@ -349,8 +349,9 @@ Tegra::Texture::TICEntry GenericEnvironment::ReadTextureInfo(GPUVAddr tic_addr, gpu_memory->ReadBlock(descriptor_addr, &entry, sizeof(entry)); // Diagnostic: the first descriptors the programs read, with the fields that decide the // sampled colour (format, component types, channel swizzle) and the raw words. + static const bool diagnostics = std::getenv("EDEN_NATIVE_DRAW_DIAGNOSTICS") != nullptr; static std::atomic described{}; - if (described.fetch_add(1, std::memory_order_relaxed) < 150) { + if (diagnostics && described.fetch_add(1, std::memory_order_relaxed) < 150) { std::array words{}; gpu_memory->ReadBlock(descriptor_addr, words.data(), sizeof(words)); LOG_INFO(Shader, From ce2a1596c70ad51a6fcfb9eaff435a5c536cab36 Mon Sep 17 00:00:00 2001 From: Jonas Date: Sat, 7 Nov 2026 08:41:21 +0600 Subject: [PATCH 12/12] nx2: tighten Welcome Tour review checks Assisted-by: GPT-6.1 (review fixes and regression tests) --- src/core/hle/service/nx2_gsv.h | 2 +- src/tests/video_core/ampere_lowering.cpp | 25 +++++++++++++++++-- .../renderer_vulkan/vk_rasterizer.cpp | 8 +++++- 3 files changed, 31 insertions(+), 4 deletions(-) diff --git a/src/core/hle/service/nx2_gsv.h b/src/core/hle/service/nx2_gsv.h index 7489c07cb6..9c98f1248b 100644 --- a/src/core/hle/service/nx2_gsv.h +++ b/src/core/hle/service/nx2_gsv.h @@ -3346,7 +3346,7 @@ class INx2GraphicsSystemSession final : public ServiceFramework(ResultSuccess.raw); rb.PushMoveObjects(ctx, *client); } diff --git a/src/tests/video_core/ampere_lowering.cpp b/src/tests/video_core/ampere_lowering.cpp index 50b0ec180e..809a233302 100644 --- a/src/tests/video_core/ampere_lowering.cpp +++ b/src/tests/video_core/ampere_lowering.cpp @@ -513,7 +513,28 @@ TEST_CASE("Ampere array depth samples use a second descriptor buffer", "[video_c const auto lowered = LowerToMaxwell(code); REQUIRE(static_cast(lowered)); CHECK(lowered.required_texture_buffer == 0); - CHECK(FindMaxwell(lowered, 0xc0, 56) != 0); - CHECK(FindMaxwell(lowered, 0xde, 56) != 0); + constexpr u64 sample_mask = 0xffc0000000000000ULL; + const auto count_sample = [&](u64 opcode) { + return std::count_if(lowered.instructions.begin(), lowered.instructions.end(), + [=](u64 word) { return (word & sample_mask) == opcode; }); + }; + CHECK(count_sample(0xc000000000000000ULL) == 1); + CHECK(count_sample(0xde00000000000000ULL) == 0); + CHECK(count_sample(0xde80000000000000ULL) == 1); + CHECK(count_sample(0xdec0000000000000ULL) == 0); + const u64 bound = FindMaxwell(lowered, 0xc0, 56); + const u64 bindless = FindMaxwell(lowered, 0xde, 56); + REQUIRE(bound != 0); + REQUIRE(bindless != 0); + CHECK(((bound >> 36) & 0x1fff) == 0); + CHECK(((bound >> 20) & 0xff) == 9); + CHECK(((bindless >> 20) & 0xff) == 246); + const u64 handle_load = FindMaxwell(lowered, 0x4c98, 48); + REQUIRE(handle_load != 0); + CHECK((handle_load & 0xff) == 246); + CHECK(((handle_load >> 34) & 0x1f) == 1); + CHECK(((handle_load >> 20) & 0x3fff) == 0); + CHECK(std::find(lowered.instructions.begin(), lowered.instructions.end(), + 0x5c980780009700f7ULL) != lowered.instructions.end()); } } // namespace diff --git a/src/video_core/renderer_vulkan/vk_rasterizer.cpp b/src/video_core/renderer_vulkan/vk_rasterizer.cpp index 4f1310a85c..216725d034 100644 --- a/src/video_core/renderer_vulkan/vk_rasterizer.cpp +++ b/src/video_core/renderer_vulkan/vk_rasterizer.cpp @@ -604,6 +604,12 @@ void TraceWindowTarget(const Tegra::Engines::Maxwell3D& maxwell3d, Tegra::Memory if (regs.rt[0].tile_mode.is_pitch_linear) { const auto format = VideoCore::Surface::TryPixelFormatFromRenderTargetFormat(regs.rt[0].format); if (!format) { + static u32 unknown_formats{}; + if (unknown_formats++ < 8) { + LOG_INFO(HW_GPU, "nx2 window {} undecodable pitch-linear rt0={:#x} fmt={:#x} pitch={} height={}", + kind, regs.rt[0].Address(), static_cast(regs.rt[0].format), + regs.rt[0].width, regs.rt[0].height); + } return; } width /= VideoCore::Surface::BytesPerBlock(*format); @@ -682,7 +688,7 @@ void TraceWindowTarget(const Tegra::Engines::Maxwell3D& maxwell3d, Tegra::Memory (tic[5] & 0xffff) + 1, fmt::join(texels, " ")); // EDEN_NX2_TEXTURE_DUMP=: save the first 4 MiB of the last traced texture's memory. if (const char* path = std::getenv("EDEN_NX2_TEXTURE_DUMP"); path != nullptr) { - std::vector bytes(2048u * 10240u); + std::vector bytes(4 * 1024 * 1024); if (read_trace(texture, bytes.data(), bytes.size())) { if (std::FILE* file = std::fopen(path, "wb")) { std::fwrite(bytes.data(), 1, bytes.size(), file);