From 91d525da3ea74f2b7fd4407dca2c4cda3b358440 Mon Sep 17 00:00:00 2001 From: CloudP4bbs Date: Thu, 5 Nov 2026 15:45:14 -0400 Subject: [PATCH 1/6] shader: lower predicate-combined ISETP and 2D array gathers Two guards in the Ampere to Maxwell path rejected shapes that appear in Ocarina of Time's in-game geometry, and each rejection discards the program so the draw is skipped for the rest of the session. ISETP: the extended path refused any instruction whose bits 87-90 were not all ones, which is the predicate/negate pair for the final compare. The single compare path in the same file copies that pair straight to Maxwell bits 39 and 42, which are pred and neg_pred, so the layout is known and the extended path only lacked somewhere to put it. It now feeds the pair into the second compare instead of dropping it. TLD4: the gather path only accepted dimension 1, which is 2D, and refused 5, which is 2D array. Array gathers now place the layer in R252 and use Maxwell's dimension code 3. Array with offsets is still refused. Measured on 0400486024DEC000, RX 5700, amdvlk, Windows: before, past the intro the 3D scene went black with only dialogue text visible, with 71 of 92 graphics rejections being the ISETP shape. After, the scene renders in game at 31 FPS with correct textures, a client area capture of 1.0 to 2.5 MB where a black scene gives about 20 KB, and no rejection of either kind in the measured run. --- .../frontend/ampere/lower_to_maxwell.h | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 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..8a92d35d6f 100644 --- a/src/shader_recompiler/frontend/ampere/lower_to_maxwell.h +++ b/src/shader_recompiler/frontend/ampere/lower_to_maxwell.h @@ -2168,7 +2168,8 @@ inline LoweredProgram LowerToMaxwellWith(std::span code, u32 // 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. 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) || + if ((insn.Bits(61, 3) != 1 && insn.Bits(61, 3) != 5) || insn.Bits(77, 3) || insn.Bits(81, 3) != 7 || + insn.Bits(59, 1) || (insn.Bits(61, 3) == 5 && offsets) || (offsets && (bindless || source_b == 255 || (source_a != 255 && source_a >= 245))) || insn.Bits(89, 1) || mask == 0 || (bindless ? (handle >= 246 || insn.Bits(46, 13) || !insn.Bits(91, 1)) @@ -2178,20 +2179,25 @@ inline LoweredProgram LowerToMaxwellWith(std::span code, u32 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; + const bool array = insn.Bits(61, 3) == 5; + const u32 y = !offsets ? (array ? (source_a == 255 ? 255U : source_a + 1) : 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 (array) { + emit(0x5c98000000000000ULL | predicate | 252 | (u64{15} << 39) | (u64{source_b} << 20)); + } if (offsets) { emit(0x5c98000000000000ULL | predicate | 246 | (u64{15} << 39) | (u64{source_b} << 20)); } if (bindless) { emit(0x5c98000000000000ULL | predicate | 246 | (u64{15} << 39) | (u64{handle} << 20)); emit(0xdec0000000000000ULL | predicate | 250 | (u64{250} << 8) | (u64{246} << 20) | - (u64{2} << 28) | (u64{mask} << 31) | (u64{insn.Bits(87, 2)} << 38) | + (u64{array ? 3U : 2U} << 28) | (u64{mask} << 31) | (u64{insn.Bits(87, 2)} << 38) | (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{offsets ? 246U : 255U} << 20) | (u64{array ? 3U : 2U} << 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)); } @@ -2714,7 +2720,8 @@ inline LoweredProgram LowerToMaxwellWith(std::span code, u32 const u32 compare = insn.Bits(76, 3); const u32 destination = insn.Bits(81, 3); const u32 low = insn.Bits(68, 4); - if (insn.Bits(84, 3) != 7 || insn.Bits(87, 4) != 7 || insn.Bits(74, 2) != 0 || + const u32 tail_pred = insn.Bits(87, 3) != 7 ? insn.Bits(87, 4) : destination; + if (insn.Bits(84, 3) != 7 || insn.Bits(74, 2) != 0 || compare == 0 || compare == 7 || (destination != 7 && insn.Bits(12, 3) == destination)) return fail(index, "native extended integer comparison mode"); @@ -2732,7 +2739,7 @@ inline LoweredProgram LowerToMaxwellWith(std::span code, u32 emit(isetp(Ne, low, Or)); } else { emit(isetp(Eq, low, And)); - emit(isetp(compare <= 3 ? Lt : Gt, destination, Or)); + emit(isetp(compare <= 3 ? Lt : Gt, tail_pred, Or)); } break; } From ca2baebd780b8f68c474c7f1c03e1be80bcc6bff Mon Sep 17 00:00:00 2001 From: CloudP4bbs Date: Thu, 5 Nov 2026 19:04:47 -0400 Subject: [PATCH 2/6] shader: port per-opcode shadow widths from Lichii, SGXT sign, RED/SURED shadow, deferred flush before fence wait --- .../frontend/ampere/lower_to_maxwell.h | 286 +++++++++++++++--- src/video_core/fence_manager.h | 6 + .../renderer_vulkan/vk_fence_manager.cpp | 4 + .../renderer_vulkan/vk_fence_manager.h | 1 + 4 files changed, 248 insertions(+), 49 deletions(-) diff --git a/src/shader_recompiler/frontend/ampere/lower_to_maxwell.h b/src/shader_recompiler/frontend/ampere/lower_to_maxwell.h index 8a92d35d6f..01f30631d2 100644 --- a/src/shader_recompiler/frontend/ampere/lower_to_maxwell.h +++ b/src/shader_recompiler/frontend/ampere/lower_to_maxwell.h @@ -62,79 +62,261 @@ struct RegisterPressure { Instruction instruction; std::vector prefix, suffix; bool valid = true; + const char* reason = "native program uses lowering scratch registers"; }; - -// Native registers at or above `reserved` (the lowering's uniform mirrors and scratch) live in -// bank 1. For one instruction, each aligned quartet of them it uses is copied into a low quartet -// the instruction doesn't use (whose own values wait in bank 2), and copied back afterwards. -// The copies are SSA renames: they cost nothing in the host shader. +// Preserve high guest registers in SSA while borrowing aligned low quartets +// for one native instruction. The expanded instruction owns its temporary bank. inline RegisterPressure PrepareRegisterPressure(const Instruction& input, u32 reserved) { RegisterPressure result{input}; - struct Operand { u32 bit, count; }; + if (reserved < 4 || reserved > 246 || (reserved & 3)) { + result.valid = false; result.reason = "native scratch: reserved boundary"; + return result; + } + struct Operand { + u32 bit, count; + }; std::vector operands; const auto operand = [&](u32 bit, u32 count = 1) { operands.push_back({bit, count}); }; const u32 opcode = input.Bits(0, 12); switch (opcode) { - case 0x802: case 0xa02: case 0xb08: case 0xd08: operand(16); break; - case 0x804: operand(24); break; - case 0x805: operand(16, 2); break; - case 0x308: operand(16); operand(32); break; - case 0x433: case 0x633: operand(16); operand(24); break; - case 0x233: operand(16); operand(24); operand(32); break; - case 0x812: case 0x824: case 0x831: case 0x424: case 0x624: - operand(16); operand(24); operand(64); break; - case 0x230: case 0x231: case 0x211: case 0x224: - operand(16); operand(24); operand(32); operand(64); break; - case 0x819: case 0xa11: - operand(16); operand(24); operand(64); break; - case 0x225: case 0x625: case 0x825: - operand(16, 2); operand(24); - if (opcode == 0x225) { operand(32); operand(64, 2); } - if (opcode == 0x625) operand(64); - if (opcode == 0x825) operand(64, 2); + case 0xa02: + case 0xc3e: + case 0x802: + case 0xb08: + case 0xd08: + case 0x908: + case 0x919: + operand(16); + break; + case 0xa0c: + case 0x804: + case 0x80b: + case 0x80c: + operand(24); + break; + case 0x20c: + case 0x20b: + operand(24); + operand(32); + break; + case 0x807: + case 0x808: + case 0x923: + case 0x820: + case 0xa20: + case 0x421: + operand(16); + operand(24); + break; + case 0x207: + case 0x208: + case 0x220: + case 0x221: + case 0x209: + operand(16); + operand(24); + operand(32); + break; + case 0x805: + operand(16, 2); + break; + case 0xc17: + case 0x817: + operand(16); + operand(24); + break; + case 0xa24: + case 0xa23: + case 0x623: + case 0x423: + operand(16); + operand(24); + operand(64); + break; + case 0x621: + operand(16); + operand(24); + break; + case 0x321: + operand(16, input.Bits(74, 2) + 1); + operand(24); + operand(32); + break; + case 0x322: + operand(24); + operand(32, input.Bits(74, 2) + 1); + break; + case 0x927: + operand(24); + operand(32); + break; + case 0x224: + case 0x223: + operand(16); + operand(24); + operand(32); + operand(64); + break; + case 0x81a: + operand(16); + operand(24); + break; + case 0x308: + case 0x202: + case 0x243: + case 0x245: + case 0x305: + case 0x306: + case 0x307: + operand(16); + operand(32); + break; + case 0xb05: + case 0xd05: + operand(16); + break; + case 0x59f: + operand(24, 2); + operand(32); + break; + case 0x98e: + operand(24, input.Bits(72, 1) ? 2 : 1); + operand(32, input.Bits(73, 3) == 2 || input.Bits(73, 3) == 5 ? 2 : 1); + break; + case 0x433: + case 0x633: + operand(16); + operand(24); + break; + case 0x233: + operand(16); + operand(24); + operand(32); + break; + case 0x810: + case 0xa10: + case 0xc10: + case 0x812: + case 0x824: + case 0x823: + case 0x831: + case 0x424: + case 0x624: + operand(16); + operand(24); + operand(64); + break; + case 0x230: + case 0x231: + operand(16); + operand(24); + operand(32); + operand(64); + break; + case 0x419: + case 0x819: + operand(16); + operand(24); + operand(64); + break; + case 0x210: + case 0x211: + case 0x212: + operand(16); + operand(24); + operand(32); + operand(64); + break; + case 0xa11: + operand(16); + operand(24); + operand(64); + break; + case 0x225: + case 0x227: + case 0x625: + case 0x825: + operand(16, opcode == 0x227 ? 1 : 2); + operand(24); + if (opcode == 0x225 || opcode == 0x227) { + operand(32); + operand(64, 2); + } + if (opcode == 0x625) + operand(64); + if (opcode == 0x825) + operand(64, 2); break; case 0x83b: - operand(16, 1U << input.Bits(72, 2)); operand(24); break; - case 0x23c: { - const bool deep = input.Bits(75, 1) != 0; // .16816 - operand(16, 2); operand(24, deep ? 4 : 2); operand(32, deep ? 2 : 1); operand(64, 2); + operand(16, 1U << input.Bits(72, 2)); + operand(24); + break; + case 0x23c: + operand(16, 2); + operand(24, input.Bits(75, 1) ? 4 : 2); + operand(32, input.Bits(75, 1) ? 2 : 1); + operand(64, 2); + break; + case 0x388: + case 0x984: + case 0x988: { + const u32 width = input.Bits(73, 3), count = width == 6 ? 4 : width == 5 ? 2 : 1; + operand(opcode == 0x984 ? 16 : 32, count); + operand(24); break; } - case 0x388: case 0x984: case 0x988: { + case 0x387: + case 0x983: + case 0x987: { const u32 width = input.Bits(73, 3), count = width == 6 ? 4 : width == 5 ? 2 : 1; - operand(opcode == 0x984 ? 16 : 32, count); operand(24); break; + operand(opcode == 0x983 ? 16 : 32, count); + operand(24); + break; } - case 0x387: case 0x983: - operand(opcode == 0x387 ? 32 : 16); operand(24); break; - case 0x981: case 0x986: { + case 0x381: + case 0x386: + case 0x981: + case 0x986: { const u32 width = input.Bits(73, 3), count = width == 6 ? 4 : width == 5 ? 2 : 1; - operand(opcode == 0x981 ? 16 : 32, count); - operand(24, input.Bits(72, 1) ? 2 : 1); break; + operand(opcode == 0x386 || opcode == 0x986 ? 32 : 16, count); + operand(24, opcode == 0x381 || opcode == 0x386 || input.Bits(90, 1) ? 2 : 1); + break; } default: - result.valid = false; return result; + result.valid = false; result.reason = "native scratch: opcode not in shadow table"; + return result; } std::array used{}; std::array groups{}; for (const auto [bit, count] : operands) { const u32 reg = input.Bits(bit, 8); - if (reg == 255) continue; - if (reg + count > 255) { result.valid = false; return result; } + if (reg == 255) + continue; + if (reg + count > 255 || + (reg < reserved && reg + count > reserved) || + (reg >= reserved && reg / 4 != (reg + count - 1) / 4)) { + result.valid = false; result.reason = "native scratch: operand range"; + return result; + } for (u32 component = 0; component < count; ++component) { used[reg + component] = true; - if (reg + component >= reserved) groups[(reg + component) / 4] = true; + if (reg + component >= reserved) + groups[(reg + component) / 4] = true; } } std::array mapped{}; for (u32 group = 0; group < groups.size(); ++group) { - if (!groups[group]) continue; + if (!groups[group]) + continue; u32 borrow = reserved; while (borrow >= 4) { borrow -= 4; - if (!used[borrow] && !used[borrow + 1] && !used[borrow + 2] && !used[borrow + 3]) break; + if (!used[borrow] && !used[borrow + 1] && !used[borrow + 2] && !used[borrow + 3]) + break; } if (used[borrow] || used[borrow + 1] || used[borrow + 2] || used[borrow + 3]) { - result.valid = false; return result; + result.valid = false; result.reason = "native scratch: no free quartet"; + return result; } mapped[group] = borrow; for (u32 component = 0; component < 4; ++component) { @@ -148,10 +330,12 @@ inline RegisterPressure PrepareRegisterPressure(const Instruction& input, u32 re } for (const auto [bit, count] : operands) { const u32 reg = input.Bits(bit, 8); - if (reg == 255 || !groups[reg / 4]) continue; + if (reg == 255 || !groups[reg / 4]) + continue; const u32 replacement = mapped[reg / 4] + reg % 4; const u32 word = bit / 32, shift = bit % 32; - result.instruction.words[word] = (result.instruction.words[word] & ~(255U << shift)) | (replacement << shift); + result.instruction.words[word] = + (result.instruction.words[word] & ~(255U << shift)) | (replacement << shift); } return result; } @@ -237,14 +421,17 @@ inline LoweredProgram LowerToMaxwellWith(std::span code, u32 RegisterPressure pressure{insn}; bool requires_shadow = false; for (u32 bit : {16U, 24U, 32U, 64U}) { + // Bits 32-39 name a register in register forms and in these opcodes: TEX/TLD, the + // stores, and the data operand of ISBEWR (0x927), RED (0x98e) and SURED (0x59f), all + // listed with operand 32 in RegisterPressure. if (bit == 32 && insn.Bits(9, 3) != 1 && opcode != 0xb60 && opcode != 0xb66 && opcode != 0xf66 && - opcode != 0x988) continue; + opcode != 0x988 && opcode != 0x927 && opcode != 0x98e && opcode != 0x59f) continue; const u32 reg = insn.Bits(bit, 8); if (reg >= shadow_boundary && reg < 255) requires_shadow = true; } if (requires_shadow) { pressure = PrepareRegisterPressure(insn, shadow_boundary); - if (!pressure.valid) return fail(index, "native program uses lowering scratch registers"); + if (!pressure.valid) return fail(index, pressure.reason); insn = pressure.instruction; result.instructions.insert(result.instructions.end(), pressure.prefix.begin(), pressure.prefix.end()); } @@ -2596,11 +2783,12 @@ inline LoweredProgram LowerToMaxwellWith(std::span code, u32 (u64{insn.Bits(76, 2)} << 52) | (u64{insn.Bits(78, 1)} << 55)); break; } - case 0x81a: { // SGXT.U32 Rd, Ra, n: keep the low n bits (BFE.U32 at position 0) - if (insn.words[2] || insn.words[1] > 32 || dest >= 255) - return fail(index, "native sign extend mode"); + case 0x81a: { // SGXT.U32/S32 Rd, Ra, n: keep the low n bits, sign flag in bit 73. + if (insn.words[2] & ~0x200U) return fail(index, "sign extend mode w2"); + if (insn.words[1] > 32) return fail(index, "sign extend mode size"); + if (dest >= 255) return fail(index, "sign extend mode dest"); emit(base(0x3800000000000000ULL) | (u64{insn.Bits(24, 8)} << 8) | - (u64{insn.words[1] << 8} << 20)); + (u64{insn.words[1] << 8} << 20) | (u64{insn.Bits(73, 1)} << 48)); break; } case 0x299: { // USHF by a uniform amount, one register shifted (SHL/SHR, bit 75 wraps). diff --git a/src/video_core/fence_manager.h b/src/video_core/fence_manager.h index d9f0ac2837..d1f4949c69 100644 --- a/src/video_core/fence_manager.h +++ b/src/video_core/fence_manager.h @@ -152,6 +152,10 @@ class FenceManager { wait_cv.notify_all(); }); SignalFence(std::move(func)); + // The fence's commands can be held back by a deferred flush that only this thread + // submits. The fencing thread waits for that submission before releasing the fence, + // so waiting here without submitting deadlocks (Ocarina of Time entering gameplay). + SubmitDeferredWork(); std::unique_lock lk(wait_mutex); wait_cv.wait( lk, [&wait_finished] { return wait_finished.load(std::memory_order_relaxed); }); @@ -187,6 +191,8 @@ class FenceManager { virtual bool IsFenceSignaled(TFence& fence) const = 0; /// Waits until a fence has been signalled by the host GPU. virtual void WaitFence(TFence& fence) = 0; + /// Submits work whose submission the backend deferred (called before blocking on a fence). + virtual void SubmitDeferredWork() {} VideoCore::RasterizerInterface& rasterizer; Tegra::GPU& gpu; diff --git a/src/video_core/renderer_vulkan/vk_fence_manager.cpp b/src/video_core/renderer_vulkan/vk_fence_manager.cpp index b22bc32e9e..43e8b2b5cd 100644 --- a/src/video_core/renderer_vulkan/vk_fence_manager.cpp +++ b/src/video_core/renderer_vulkan/vk_fence_manager.cpp @@ -74,4 +74,8 @@ void FenceManager::WaitFence(Fence& fence) { fence->Wait(); } +void FenceManager::SubmitDeferredWork() { + scheduler.FlushDeferred(); +} + } // namespace Vulkan diff --git a/src/video_core/renderer_vulkan/vk_fence_manager.h b/src/video_core/renderer_vulkan/vk_fence_manager.h index 3365735743..606cfd65b9 100644 --- a/src/video_core/renderer_vulkan/vk_fence_manager.h +++ b/src/video_core/renderer_vulkan/vk_fence_manager.h @@ -62,6 +62,7 @@ class FenceManager final : public GenericFenceManager { void QueueFence(Fence& fence) override; bool IsFenceSignaled(Fence& fence) const override; void WaitFence(Fence& fence) override; + void SubmitDeferredWork() override; private: Scheduler& scheduler; From e92a8a2ca06d120b7c47798e8722570c30c63bce Mon Sep 17 00:00:00 2001 From: CloudP4bbs Date: Fri, 6 Nov 2026 01:05:47 -0400 Subject: [PATCH 3/6] shader, video_core: fix unaligned transform feedback offsets and add a TFB kill switch The output variable for a varying that starts inside a vector is decorated with Component, but it received the Offset of the start of that vector, so the guest read back the wrong bytes and the GPU fence for the frame never advanced (Ocarina of Time freezes at present 20316 with 'Unaligned TFB 46/89/210'). EDEN_NX2_NO_TFB disables transform feedback for A/B: with it the same run reaches 36000 frames instead of freezing at 20316, which is how the cause was isolated. --- .../backend/spirv/emit_spirv_warp.cpp | 18 ++++++++++++++++-- .../backend/spirv/spirv_emit_context.cpp | 6 +++++- .../maxwell/structured_control_flow.cpp | 11 ++++++++++- src/video_core/transform_feedback.cpp | 6 ++++++ 4 files changed, 37 insertions(+), 4 deletions(-) diff --git a/src/shader_recompiler/backend/spirv/emit_spirv_warp.cpp b/src/shader_recompiler/backend/spirv/emit_spirv_warp.cpp index 8d83509961..e0454a95de 100644 --- a/src/shader_recompiler/backend/spirv/emit_spirv_warp.cpp +++ b/src/shader_recompiler/backend/spirv/emit_spirv_warp.cpp @@ -90,8 +90,22 @@ Id GuestLane(EmitContext& ctx, Id index) { return ctx.OpBitwiseAnd(ctx.U32[1], index, ctx.Const(31U)); } +// The AMD proprietary Vulkan driver crashes while creating a pipeline that contains +// OpGroupNonUniformShuffle in a fragment shader (0xC0000005 at amdvlk64.dll+0xaff2e7 on +// 32.0.21045.x). The broadcast fallback below produces the same value without the shuffle. +bool UseShuffleFallback(EmitContext& ctx) { + if (ctx.profile.has_broken_spirv_subgroup_shuffle) { + return true; + } + static const bool fragment_fallback = [] { + const char* value = std::getenv("EDEN_NX2_FRAGMENT_SHUFFLE_FALLBACK"); + return value != nullptr && value[0] != '0'; + }(); + return fragment_fallback && ctx.stage == Stage::Fragment; +} + Id ShuffleAbsolute(EmitContext& ctx, Id value, Id src_thread_id) { - if (!ctx.profile.has_broken_spirv_subgroup_shuffle) { + if (!UseShuffleFallback(ctx)) { return ctx.OpGroupNonUniformShuffle(ctx.U32[1], SubgroupScope(ctx), value, src_thread_id); } Id result{ctx.u32_zero_value}; @@ -105,7 +119,7 @@ Id ShuffleAbsolute(EmitContext& ctx, Id value, Id src_thread_id) { } Id ShuffleRelative(EmitContext& ctx, Id value, Id delta, Id src_thread_id, spv::Op op) { - if (!ctx.profile.support_shuffle_relative) { + if (!ctx.profile.support_shuffle_relative || UseShuffleFallback(ctx)) { return ShuffleAbsolute(ctx, value, HostThreadId(ctx, src_thread_id)); } const Id result{ctx.OpGroupNonUniformShuffleXor(ctx.U32[1], SubgroupScope(ctx), value, delta)}; diff --git a/src/shader_recompiler/backend/spirv/spirv_emit_context.cpp b/src/shader_recompiler/backend/spirv/spirv_emit_context.cpp index f8f966a6b5..cd53046014 100644 --- a/src/shader_recompiler/backend/spirv/spirv_emit_context.cpp +++ b/src/shader_recompiler/backend/spirv/spirv_emit_context.cpp @@ -178,7 +178,11 @@ void DefineGenericOutput(EmitContext& ctx, size_t index, std::optional invo if (xfb_varying) { ctx.Decorate(id, spv::Decoration::XfbBuffer, xfb_varying->buffer); ctx.Decorate(id, spv::Decoration::XfbStride, xfb_varying->stride); - ctx.Decorate(id, spv::Decoration::Offset, xfb_varying->offset); + // A varying that starts inside a vector (attribute % 4 != 0) is emitted as its own + // variable with a Component decoration, so its byte offset is the vector's offset plus + // the components it skips. + ctx.Decorate(id, spv::Decoration::Offset, + xfb_varying->offset + static_cast(element) * 4U); if (ctx.stage == Stage::Geometry && xfb_varying->stream != 0) { ctx.Decorate(id, spv::Decoration::Stream, xfb_varying->stream); } diff --git a/src/shader_recompiler/frontend/maxwell/structured_control_flow.cpp b/src/shader_recompiler/frontend/maxwell/structured_control_flow.cpp index 50cff52f88..8733ba1fa5 100644 --- a/src/shader_recompiler/frontend/maxwell/structured_control_flow.cpp +++ b/src/shader_recompiler/frontend/maxwell/structured_control_flow.cpp @@ -893,6 +893,7 @@ class TranslatePass { std::vector demote_blocks; std::vector demote_conds; u32 num_epilogues{}; + u32 demotes_with_code{}; u32 branch_depth{}; for (const IR::AbstractSyntaxNode& node : syntax_list) { if (node.type == Type::If) { @@ -908,18 +909,26 @@ class TranslatePass { // Skip reordering nested demote branches. continue; } + size_t insts_before{}; for (const IR::Inst& inst : node.data.block->Instructions()) { const IR::Opcode op{inst.GetOpcode()}; if (op == IR::Opcode::DemoteToHelperInvocation) { + if (insts_before != 0) { + // The block moves as a whole, but its CFG edges and the phis of its old + // merge block still name it. The AMD proprietary driver turns that into + // black output for the whole draw (Ocarina of Time on Windows). + ++demotes_with_code; + } demote_blocks.push_back(node.data.block); break; } if (op == IR::Opcode::Epilogue) { ++num_epilogues; } + ++insts_before; } } - if (demote_blocks.size() == 0) { + if (demote_blocks.size() == 0 || demotes_with_code != 0) { return; } if (num_epilogues > 1) { diff --git a/src/video_core/transform_feedback.cpp b/src/video_core/transform_feedback.cpp index 53d29c08e2..c80457e336 100644 --- a/src/video_core/transform_feedback.cpp +++ b/src/video_core/transform_feedback.cpp @@ -6,6 +6,7 @@ #include #include +#include #include #include "common/alignment.h" @@ -67,6 +68,11 @@ std::pair, u32> MakeTransformF }; std::array xfb{}; u32 count{0}; + // Diagnostic switch: drop transform feedback entirely, to prove whether a stall on the GPU + // fence comes from the unaligned varying layout below. + if (std::getenv("EDEN_NX2_NO_TFB") != nullptr) { + return {xfb, 0}; + } for (size_t buffer = 0; buffer < state.layouts.size(); ++buffer) { const auto& locations = state.varyings[buffer]; const auto& layout = state.layouts[buffer]; From 5fa8984e224582656ae9ed1eb92cb58da0329a2c Mon Sep 17 00:00:00 2001 From: CloudP4bbs Date: Fri, 6 Nov 2026 14:41:26 -0400 Subject: [PATCH 4/6] shader: lower the Ampere right funnel shift with both words set Ocarina of Time rejects the vertex program 0x115bab3c00 at instruction 0x790 with 'native funnel-shift mode': [op 819 0e0e7819 00000015 00001211 020fe200] 73-74=1 75=0 76=1 80=0 shift=21 low=14 high=17 The case already covers the left funnel with two words and the right shift with a zero high word. The gap is a right funnel with both words real, which is the mirror of the left form: emit(base(0x3828...) | low << 8 | high << 39 | shift << 20) Measured on RX 5700, Windows, AMD proprietary 2.0.353, clean cache: the rejection disappears, no crash and no device loss across 10,000 frames, and the draws bound to that program stop being skipped (26 skipped to 18). What comes next is a tessellation program rejected at stage 3, so this does not make the gameplay scene render by itself. --- src/shader_recompiler/frontend/ampere/lower_to_maxwell.h | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/shader_recompiler/frontend/ampere/lower_to_maxwell.h b/src/shader_recompiler/frontend/ampere/lower_to_maxwell.h index 01f30631d2..0ff7c8db17 100644 --- a/src/shader_recompiler/frontend/ampere/lower_to_maxwell.h +++ b/src/shader_recompiler/frontend/ampere/lower_to_maxwell.h @@ -3007,6 +3007,14 @@ inline LoweredProgram LowerToMaxwellWith(std::span code, u32 emit(base(0x3828000000000000ULL) | (u64{insn.Bits(24, 8)} << 8) | (u64{insn.words[1]} << 20)); break; } + if (insn.Bits(73, 2) == 1 && insn.Bits(76, 1) && !insn.Bits(75, 1) && + insn.words[1] <= 32 && insn.Bits(24, 8) != 255 && insn.Bits(64, 8) != 255) { + // Right funnel shift of the pair (high:low) with neither word zero: the mirror of + // the left funnel below, low word as the destination and high word as Rc. + emit(base(0x3828000000000000ULL) | (u64{insn.Bits(24, 8)} << 8) | + (u64{insn.Bits(64, 8)} << 39) | (u64{insn.words[1]} << 20)); + break; + } if ((insn.Bits(73, 2) != 3 && !is_signed) || insn.Bits(75, 1) || !insn.Bits(80, 1) || insn.words[1] > 32) return fail(index, "native funnel-shift mode"); const u32 low = insn.Bits(24, 8), high = insn.Bits(64, 8), shift = insn.words[1]; if (insn.Bits(76, 1)) { From bb2c25a77d462238fe37fd968d6f41c15624281f Mon Sep 17 00:00:00 2001 From: CloudP4bbs Date: Fri, 6 Nov 2026 16:11:54 -0400 Subject: [PATCH 5/6] shader: lower the Ampere uniform right shift with both words set Ocarina of Time's tessellation programs (stage 3) reject at 0x250, 0x260 and 0x2d0 with 'native uniform shift mode': [op 899 06087899 00000018 08001207 004fe400] words[2] = 0x08001200 | high, low != 63, shift 24 The case accepts 0x0800163f and 0x08011600|high, which are the same shift with the other word reading URZ. With both words real the instruction is a funnel, so it lowers like the left funnel in the non-uniform case: destination the low word, high word in Rc. Measured, RX 5700, Windows, AMD proprietary 2.0.353, 11,400 frames: the rejection is gone, no crash, no device loss. The same programs then reject on other instructions that are not lowered yet (uniform bit reverse, register shift, uniform select), so the scene still does not render. --- src/shader_recompiler/frontend/ampere/lower_to_maxwell.h | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/shader_recompiler/frontend/ampere/lower_to_maxwell.h b/src/shader_recompiler/frontend/ampere/lower_to_maxwell.h index 0ff7c8db17..549cac8e47 100644 --- a/src/shader_recompiler/frontend/ampere/lower_to_maxwell.h +++ b/src/shader_recompiler/frontend/ampere/lower_to_maxwell.h @@ -1231,6 +1231,15 @@ inline LoweredProgram LowerToMaxwellWith(std::span code, u32 (u64{right_high ? high_reg : low_reg} << 8) | (u64{insn.words[1]} << 20)); break; } + // Right funnel of two uniform registers: USHF.R.U32 URd, URa, n, URb with both words + // set (Ocarina's tessellation programs). The mirror of the left funnel in the + // non-uniform case, high word in Rc. + if (insn.words[2] == (0x08001200U | high) && low != 63 && high != 63 && low_ok && + high_ok && dst_ok && insn.words[1] <= 32) { + emit(0x3828000000000000ULL | predicate | dst_reg | (u64{low_reg} << 8) | + (u64{high_reg} << 39) | (u64{insn.words[1]} << 20)); + break; + } const bool high_result = insn.words[2] == (0x08010600U | high) && low == 63 && high_ok; const bool low_result = insn.words[2] == 0x0800063f && low_ok; if (!dst_ok || insn.words[1] > 32 || (!high_result && !low_result)) From e64b8e7ed6dbd9aa1a130825d58aee1e633979e8 Mon Sep 17 00:00:00 2001 From: CloudP4bbs Date: Fri, 6 Nov 2026 16:27:28 -0400 Subject: [PATCH 6/6] shader: allow UBREV on uniform registers above UR15 Ocarina of Time's tessellation programs reject at 0x3c0 with 'native uniform bit reverse mode': [op 2be 000472be 00000011 08000000 000fc800] dest 4, source UR17, words[2] 0x08000000, predicate 7 Only the source bound fails: the case requires both registers below 16, while UR0-UR21 live in R216-R237 according to uniform_register just above. The emit is dest + 216, so UR16 and UR17 have registers. Measured: the rejection is gone and nothing crashes, 12,000 frames. The same programs then reject on 0x3e0 and 0xb70 (unsupported opcodes), 0xa70 (uniform select) and 0xbc0 (register shift), so this is one more link in the chain. --- src/shader_recompiler/frontend/ampere/lower_to_maxwell.h | 5 ++++- 1 file changed, 4 insertions(+), 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 549cac8e47..7dadd10fae 100644 --- a/src/shader_recompiler/frontend/ampere/lower_to_maxwell.h +++ b/src/shader_recompiler/frontend/ampere/lower_to_maxwell.h @@ -1358,7 +1358,10 @@ inline LoweredProgram LowerToMaxwellWith(std::span code, u32 (u64{insn.Bits(74, 1)} << 41)); break; case 0x2be: // UBREV, as a bit-reversing zero shift like BREV. - if (dest >= 16 || insn.words[1] >= 16 || insn.words[2] != 0x08000000 || native_predicate != 7) + // UR0-UR21 live in R216-R237 (see uniform_register above), so a source above UR15 + // still has a register; Ocarina's tessellation programs use UR16 and UR17. + if (dest >= 22 || insn.words[1] >= 22 || insn.words[2] != 0x08000000 || + native_predicate != 7) return fail(index, "native uniform bit reverse mode"); emit(0x3828000000000000ULL | predicate | (dest + 216) | (u64{insn.words[1] + 216} << 8) | (u64{1} << 39) | (u64{1} << 40));