diff options
| author | Duck912 <350528965+Duck912@users.noreply.github.com> | 2026-11-01 16:18:10 -0500 |
|---|---|---|
| committer | Duck912 <350528965+Duck912@users.noreply.github.com> | 2026-11-01 21:54:48 -0500 |
| commit | 18b0671e1b640fc3cbca565c39f5a69a287ede9b (patch) | |
| tree | 8ff328cfb15e5800e0cb9b642dc1400191fbaa18 /src/shader_recompiler | |
| parent | e7973dae7e2ea44665ff4ec83d7214a5d2daa366 (diff) | |
shader: read extra texture descriptor buffers bindless
Maxwell's bound texture instructions read every descriptor from one
constant buffer per program, but Fire Emblem: Fortune's Weave's shaders
sample from more than one (bindings 4-7, 14 and 15 next to an earlier one). The
lowering rejected the second binding, so most of its graphics programs
failed and 3D scenes stayed black.
A binding that conflicts with the program's buffer is now read bindless:
the handle is loaded from its buffer with an unpredicated MOV and passed
as the first meta register of TEX.B, TLD.B or SUST.B. The TEXS fast path
and the array depth form check the buffer instead of setting it to 2.
Also lowered: TEX with the handle in a register (0x361, packed or not) or
a uniform register (0xf60), TXQ on any binding and the mip count query,
TXQ.B (0x370), TXD with any channel mask and .NODEP, and SUST.STRONG.GPU
fenced with MEMBAR.GL on both sides.
Assisted-by: Claude Opus 5.5 (debugging, implementation and drafting)
Signed-off-by: duck912 <350528965+Duck912@users.noreply.github.com>
Signed-off-by: Duck912 <350528965+Duck912@users.noreply.github.com>
Diffstat (limited to 'src/shader_recompiler')
| -rw-r--r-- | src/shader_recompiler/frontend/ampere/lower_to_maxwell.h | 151 |
1 files changed, 108 insertions, 43 deletions
diff --git a/src/shader_recompiler/frontend/ampere/lower_to_maxwell.h b/src/shader_recompiler/frontend/ampere/lower_to_maxwell.h index 031f7fac75..2d994ad366 100644 --- a/src/shader_recompiler/frontend/ampere/lower_to_maxwell.h +++ b/src/shader_recompiler/frontend/ampere/lower_to_maxwell.h @@ -56,7 +56,7 @@ inline LoweredProgram LowerToMaxwell(std::span<const Instruction> code) { for (const auto& instruction : code) { const u32 op = instruction.Bits(0, 12); uses_uniform_bank |= op == 0x3c4 || op == 0x2bd || op == 0xc0c || op == 0x986 || op == 0x9a8 || op == 0xc12 || op == 0xc17 || op == 0xe24 || op == 0xc24 || op == 0xd00 || op == 0xd09 || op == 0x9c3 || op == 0xc45 || op == 0x2a4 || op == 0x891 || op == 0x890 || op == 0x290 || op == 0x28c || op == 0x88c || op == 0x287 || op == 0x887 || op == 0x958 || op == 0xab9 || op == 0xabb || op == 0x899 || op == 0xc20 || op == 0xe21 || op == 0xe23 || op == 0xc02 || - op == 0x886 || op == 0x3c2 || op == 0xc82 || op == 0x882 || op == 0x292 || op == 0x892 || op == 0x291 || op == 0x981 || op == 0xc13 || op == 0xc10 || op == 0xc23 || op == 0x526 || op == 0x2be || op == 0x8a4 || op == 0xd05 || (op == 0x984 && instruction.Bits(91, 1)) || op == 0x89c || op == 0x883 || op == 0x2a5 || op == 0xd06 || op == 0x299 || op == 0xc09 || (op == 0x81c && instruction.Bits(67, 1)); + op == 0x886 || op == 0x3c2 || op == 0xc82 || op == 0x882 || op == 0x292 || op == 0x892 || op == 0x291 || op == 0x981 || op == 0xc13 || op == 0xc10 || op == 0xc23 || op == 0x526 || op == 0x2be || op == 0x8a4 || op == 0xd05 || (op == 0x984 && instruction.Bits(91, 1)) || op == 0x89c || op == 0x883 || op == 0x2a5 || op == 0xd06 || op == 0x299 || op == 0xc09 || (op == 0x81c && instruction.Bits(67, 1)) || op == 0xf60; } const auto address_of = [](u32 index) { return 0x50U + index * 8 + ((index + 1) / 3) * 8; }; const auto fail = [&](u32 index, std::string_view reason) { @@ -99,7 +99,8 @@ inline LoweredProgram LowerToMaxwell(std::span<const Instruction> code) { // Scratch values and emulated uniform state occupy reserved high GPRs. // Reject native accesses that could alias that state. for (u32 bit : {16U, 24U, 32U, 64U}) { - if (bit == 32 && insn.Bits(9, 3) != 1 && opcode != 0xb60 && opcode != 0xb66) continue; + if (bit == 32 && insn.Bits(9, 3) != 1 && opcode != 0xb60 && opcode != 0xb66 && opcode != 0x361 && + opcode != 0xf60) continue; const u32 reg = insn.Bits(bit, 8); if (reg >= (uses_uniform_bank ? 216U : 246U) && reg < 255) return fail(index, "native program uses lowering scratch registers"); } @@ -163,6 +164,13 @@ inline LoweredProgram LowerToMaxwell(std::span<const Instruction> code) { }; std::string_view operand_error; const auto emit = [&](u64 word) { result.instructions.push_back(word); }; + // Bound Maxwell texture instructions share one descriptor constant buffer per program. + const auto bind_texture_buffer = [&](u32 binding) { + if (result.required_texture_buffer != ~0U && result.required_texture_buffer != binding) + return false; + result.required_texture_buffer = binding; + return true; + }; // P(guard slot) while a uniform guard is swapped in; P6 for lowerings that use it as // scratch (R2UR, MATCH, HFMA2.RELU), which save and restore it with the defaults below. const u32 guard_mask = guard_slot < 7 ? 1U << guard_slot : 0U; @@ -759,16 +767,18 @@ inline LoweredProgram LowerToMaxwell(std::span<const Instruction> code) { (u64{insn.Bits(64, 8)} << 20) | (u64{maxwell} << 28)); break; } - case 0xb6c: { // TXD 2D or ARRAY_2D, four channels; the two destination pairs anywhere. + case 0xb6c: { // TXD 2D or ARRAY_2D, any channels; the two destination pairs anywhere. const u32 binding = insn.Bits(54, 5), offset = insn.Bits(40, 14); const u32 coordinates = insn.Bits(24, 8), derivatives = insn.Bits(32, 8); // Ampere dimensions: 1 = 2D, 5 = ARRAY_2D; Maxwell TXD's are 2 and 3. The coordinates // (x, y[, layer]) and the four gradients sit in the same registers for both. const u32 dimension = insn.Bits(61, 3); const u32 high = insn.Bits(64, 8); // destination of the third and fourth channels - if ((insn.words[2] & ~0xffU) != 0x001e0f00 || insn.Bits(59, 2) || + const u32 mask = insn.Bits(72, 4), channels = static_cast<u32>(std::popcount(mask)); + if ((insn.words[2] & ~(0xfffU | (1U << 26))) != 0x001e0000 || mask == 0 || insn.Bits(59, 2) || (dimension != 1 && dimension != 5) || binding >= 18 || offset > 0x1fff || - (dest & 1) || dest >= 248 || (high & 1) || (high >= 248 && high != 255) || + (channels > 1 && (dest & 1)) || dest >= 248 || + (channels > 2 ? (high & 1) || high >= 248 : high != 255) || coordinates >= 248 || derivatives >= 247) return fail(index, "native gradient texture mode"); if (result.required_texture_buffer != ~0U && result.required_texture_buffer != binding) @@ -776,16 +786,15 @@ inline LoweredProgram LowerToMaxwell(std::span<const Instruction> code) { result.required_texture_buffer = binding; // Maxwell writes four consecutive registers: when the second pair is elsewhere (Splatoon // Raiders: R12/R18, R18/R44), write them to scratch and move each pair into place. - const bool consecutive = high == dest + 2; + const bool consecutive = channels <= 2 || high == dest + 2; const u64 target = consecutive ? dest : 248; emit(0xde00000000000000ULL | predicate | target | (u64{coordinates} << 8) | - (u64{derivatives} << 20) | (u64{dimension == 5 ? 3U : 2U} << 28) | (u64{15} << 31) | + (u64{derivatives} << 20) | (u64{dimension == 5 ? 3U : 2U} << 28) | (u64{mask} << 31) | (u64{offset} << 36) | (u64{insn.Bits(90, 1)} << 49) | (u64{7} << 51)); if (!consecutive) { - for (u32 i = 0; i < 2; ++i) { - emit(0x5c98000000000000ULL | predicate | (dest + i) | (u64{15} << 39) | (u64{248 + i} << 20)); - if (high != 255) - emit(0x5c98000000000000ULL | predicate | (high + i) | (u64{15} << 39) | (u64{250 + i} << 20)); + for (u32 i = 0; i < channels; ++i) { + emit(0x5c98000000000000ULL | predicate | (i < 2 ? dest + i : high + i - 2) | + (u64{15} << 39) | (u64{248 + i} << 20)); } } break; @@ -944,14 +953,20 @@ inline LoweredProgram LowerToMaxwell(std::span<const Instruction> code) { const u32 binding = insn.Bits(54, 5), offset = insn.Bits(40, 14); const u32 data = insn.Bits(32, 8), coordinate = insn.Bits(24, 8); const u32 dimension = insn.Bits(61, 3); - if (insn.words[2] != 0x00100f00 || dimension < 3 || dimension > 5 || + const bool strong = insn.words[2] == 0x0010af00; // .STRONG.GPU: fenced on both sides + if ((insn.words[2] != 0x00100f00 && !strong) || dimension < 3 || dimension > 5 || binding >= 18 || offset > 0x1fff || insn.Bits(59, 2) || data > 252 || coordinate > (dimension >= 4 ? 252U : 253U)) return fail(index, "native formatted surface store mode"); - if (result.required_texture_buffer != ~0U && result.required_texture_buffer != binding) - return fail(index, "native surface descriptor buffer conflict"); - result.required_texture_buffer = binding; + // A second descriptor buffer: SUST.B with the handle, loaded from that buffer, in R250. + const bool bindless = !bind_texture_buffer(binding); + if (bindless) { + emit(0x4c98000000070000ULL | 250 | (u64{15} << 39) | (u64{offset} << 20) | (u64{binding} << 34)); + } + if (strong) emit(0xef98000000000000ULL | predicate | (u64{1} << 8)); // MEMBAR.GL emit(0xeb20000000000000ULL | predicate | data | (u64{coordinate} << 8) | - (u64{15} << 20) | (u64{dimension} << 33) | (u64{offset} << 36) | (u64{1} << 51)); + (u64{15} << 20) | (u64{dimension} << 33) | + (bindless ? u64{250} << 39 : (u64{offset} << 36) | (u64{1} << 51))); + if (strong) emit(0xef98000000000000ULL | predicate | (u64{1} << 8)); break; } case 0x301: // BREV register. @@ -1363,17 +1378,28 @@ inline LoweredProgram LowerToMaxwell(std::span<const Instruction> code) { } break; } - case 0xb6f: { // TXQ texture dimensions, two packed scalar outputs. - if (insn.Bits(59, 5) || insn.Bits(64, 8) != 255 || insn.Bits(72, 4) == 0 || insn.Bits(72, 4) > 3 || - insn.Bits(76, 14) || insn.Bits(91, 1) || insn.Bits(54, 5) != 2 || - insn.Bits(40, 14) > 0x1fff || (insn.Bits(72, 4) == 3 && (dest & 1)) || dest >= 254) + case 0xb6f: { // TXQ texture dimensions (mask 1-3) or mip level count (mask 8). + const u32 mask = insn.Bits(72, 4); + if (insn.Bits(59, 5) || insn.Bits(64, 8) != 255 || (mask == 0 || (mask > 3 && mask != 8)) || + insn.Bits(76, 14) || insn.Bits(91, 1) || insn.Bits(54, 5) >= 18 || + insn.Bits(40, 14) > 0x1fff || (mask == 3 && (dest & 1)) || dest >= 254) return fail(index, "native texture dimension query mode"); - result.required_texture_buffer = 2; + if (!bind_texture_buffer(insn.Bits(54, 5))) + return fail(index, "native texture descriptor buffer conflict"); emit(0xdf48000000000000ULL | predicate | dest | (u64{insn.Bits(24, 8)} << 8) | - (u64{1} << 22) | (u64{insn.Bits(72, 4)} << 31) | (u64{insn.Bits(40, 14)} << 36) | + (u64{1} << 22) | (u64{mask} << 31) | (u64{insn.Bits(40, 14)} << 36) | (u64{insn.Bits(90, 1)} << 49)); break; } + case 0x370: { // TXQ.B dimensions or mip count: handle in Ra, mip level in Ra+1. + const u32 mask = insn.Bits(72, 4), source = insn.Bits(24, 8); + if (insn.words[1] != 0x08000000 || insn.Bits(64, 8) != 255 || (mask == 0 || (mask > 3 && mask != 8)) || + (insn.words[2] & ~0x04000f00U) != 255 || source > 245 || (mask == 3 && (dest & 1)) || dest >= 254) + return fail(index, "native bindless texture dimension query mode"); + emit(0xdf50000000000000ULL | predicate | dest | (u64{source} << 8) | + (u64{1} << 22) | (u64{mask} << 31) | (u64{insn.Bits(90, 1)} << 49)); + break; + } case 0xb66: { // TLD.SCR.LZ, direct texel fetch const u32 mask = insn.Bits(72, 4); const u32 dimension = insn.Bits(61, 3); @@ -1402,9 +1428,25 @@ inline LoweredProgram LowerToMaxwell(std::span<const Instruction> code) { return fail(index, "native texel fetch mode or descriptor binding"); } const u32 binding = insn.Bits(54, 5); - if (result.required_texture_buffer != ~0U && result.required_texture_buffer != binding) - return fail(index, "native texture descriptor buffer conflict"); - result.required_texture_buffer = binding; + if (!bind_texture_buffer(binding)) { + // A second descriptor buffer: TLD.B (LZ) with the coordinates in R250.. (array + // index or Z after Ra) and the handle, loaded from that buffer, in R253. + const auto copy = [&](u32 target, u32 source) { + emit(0x5c98000000000000ULL | predicate | target | (u64{15} << 39) | (u64{source} << 20)); + }; + copy(250, source_a); + if (array || volume) copy(251, source_a + 1); + if (dimension != 0) copy(array || volume ? 252 : 251, source_b); + emit(0x4c98000000070000ULL | 253 | (u64{15} << 39) | (u64{insn.Bits(40, 14)} << 20) | + (u64{binding} << 34)); + emit(0xdd00000000000000ULL | predicate | 250 | (u64{250} << 8) | (u64{253} << 20) | + (u64{array ? 3U : volume ? 4U : dimension == 0 ? 0U : 2U} << 28) | (u64{mask} << 31) | + (u64{insn.Bits(90, 1)} << 49) | (u64{7} << 51)); + const u32 components = static_cast<u32>(std::popcount(mask)); + for (u32 i = 0; i < components; ++i) + copy(i < 2 || !split ? static_cast<u32>(dest) + i : second_dest + (i - 2), 250 + i); + break; + } if (array) { emit(0x5c98000000000000ULL | predicate | 250 | (u64{15} << 39) | (u64{source_a + 1} << 20)); emit(0x5c98000000000000ULL | predicate | 251 | (u64{15} << 39) | (u64{source_b} << 20)); @@ -1475,7 +1517,10 @@ inline LoweredProgram LowerToMaxwell(std::span<const Instruction> code) { } break; } + case 0x361: // TEX with the descriptor handle in a register (after the coordinates) + case 0xf60: // TEX with the descriptor handle in a uniform register case 0xb60: { // TEX, scalar 2D implicit-LOD sample + const bool register_handle = opcode == 0x361, uniform_handle = opcode == 0xf60; const u32 mask = insn.Bits(72, 4); const u32 second_dest = insn.Bits(64, 8); constexpr std::array<u32, 8> rg_masks{1, 2, 4, 8, 3, 9, 10, 12}; @@ -1485,12 +1530,13 @@ inline LoweredProgram LowerToMaxwell(std::span<const Instruction> code) { const bool depth_compare = insn.Bits(78, 1); 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 (dimension == 5 && depth_compare && lod == 0 && !split && mask == 1) { + if (dimension == 5 && depth_compare && lod == 0 && !split && mask == 1 && !register_handle && + !uniform_handle) { 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 || (source_a & 1) || (source_b & 1) || source_a >= 248 || source_b >= 248 || dest >= 248) return fail(index, "native array depth comparison sample mode"); - result.required_texture_buffer = 2; + if (!bind_texture_buffer(2)) 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) | @@ -1510,14 +1556,24 @@ inline LoweredProgram LowerToMaxwell(std::span<const Instruction> code) { constexpr std::array<u32, 8> coordinate_count{1, 2, 3, 3, 2, 3, 0, 4}; constexpr std::array<u32, 8> maxwell_type{0, 2, 4, 6, 1, 3, 0, 7}; const bool offsets = insn.Bits(76, 1) != 0; - if (dimension == 6 || insn.Bits(77, 1) || insn.Bits(81, 3) != 7 || insn.Bits(59, 1) || - insn.Bits(40, 14) > 0x1fff || insn.Bits(54, 5) >= 18 || (lod > 3 && lod != 6 && lod != 7) || - (depth_compare && mask != 1) || mask == 0) + // Handle forms: 0x361 has no descriptor fields (bit 59 set), 0xf60 names a UR + // in bits 40-45 (bit 91 set) and is only known with packed operands. + const u32 handle_register = uniform_handle ? uniform_register(insn.Bits(40, 6)) : ~0U; + if (register_handle ? !insn.Bits(59, 1) || insn.Bits(40, 19) + : uniform_handle ? !insn.Bits(60, 1) || insn.Bits(59, 1) || insn.Bits(46, 13) || + !insn.Bits(91, 1) || handle_register == ~0U || handle_register == 255 + : insn.Bits(59, 1) || insn.Bits(40, 14) > 0x1fff || insn.Bits(54, 5) >= 18) + return false; + if (dimension == 6 || insn.Bits(77, 1) || insn.Bits(81, 3) != 7 || + (lod > 3 && lod != 6 && lod != 7) || (depth_compare && mask != 1) || mask == 0) return false; const u32 coordinates = coordinate_count[dimension]; const bool has_lod = lod == 2 || lod == 3 || lod == 6 || lod == 7; - const u32 count = coordinates + (has_lod ? 1 : 0) + (offsets ? 1 : 0) + (depth_compare ? 1 : 0); - const u32 in_a = count <= 4 ? (count + 1) / 2 : 4; + const u32 count = coordinates + (register_handle ? 1 : 0) + (has_lod ? 1 : 0) + + (offsets ? 1 : 0) + (depth_compare ? 1 : 0); + // Unpacked 0x361 (bit 60 clear): Ra holds the coordinates, Rb the handle and the rest. + const u32 in_a = register_handle && !insn.Bits(60, 1) ? coordinates + : count <= 4 ? (count + 1) / 2 : 4; const auto value = [&](u32 i) -> u32 { if (i < in_a) return source_a == 255 ? 255U : source_a + i; return source_b == 255 ? 255U : source_b + (i - in_a); @@ -1525,25 +1581,34 @@ inline LoweredProgram LowerToMaxwell(std::span<const Instruction> code) { if ((source_a != 255 && source_a + in_a > 246) || (count > in_a && source_b != 255 && source_b + (count - in_a) > 246)) return false; - const u32 binding = insn.Bits(54, 5); - if (result.required_texture_buffer != ~0U && result.required_texture_buffer != binding) - return false; - result.required_texture_buffer = binding; + // Handle forms and a second descriptor buffer are read bindless: TEX.B takes the + // handle (the register's, or loaded from that buffer) as its first meta register. + const u32 binding = insn.Bits(54, 5), offset = insn.Bits(40, 14); + const bool bindless = register_handle || uniform_handle || !bind_texture_buffer(binding); const auto copy = [&](u32 target, u32 source) { emit(0x5c98000000000000ULL | predicate | target | (u64{15} << 39) | (u64{source} << 20)); }; - // Coordinates to R250.., then LOD/bias, offsets and reference to R246... + // Coordinates to R250.., then the handle, LOD/bias, offsets and reference to R246... for (u32 i = 0; i < coordinates; ++i) copy(250 + i, value(i)); u32 meta = 246; u32 next = coordinates; + // The handle is copied unpredicated, so it is the same value on every path. + if (register_handle || uniform_handle) { + emit(0x5c98000000070000ULL | meta++ | (u64{15} << 39) | + (u64{register_handle ? value(next++) : handle_register} << 20)); + } else if (bindless) { + emit(0x4c98000000070000ULL | meta++ | (u64{15} << 39) | (u64{offset} << 20) | + (u64{binding} << 34)); + } if (has_lod) copy(meta++, value(next++)); if (offsets) copy(meta++, value(next++)); if (depth_compare) copy(meta++, value(next++)); - emit(0xc000000000000000ULL | predicate | 250 | (u64{250} << 8) | + emit((bindless ? 0xde80000000000000ULL : 0xc000000000000000ULL) | predicate | 250 | (u64{250} << 8) | (u64{meta == 246 ? 255U : 246U} << 20) | (u64{maxwell_type[dimension]} << 28) | - (u64{mask} << 31) | (u64{insn.Bits(40, 14)} << 36) | - (u64{insn.Bits(90, 1)} << 49) | (u64{depth_compare} << 50) | (u64{7} << 51) | - (u64{offsets} << 54) | (u64{lod} << 55)); + (u64{mask} << 31) | (u64{insn.Bits(90, 1)} << 49) | (u64{depth_compare} << 50) | + (u64{7} << 51) | + (bindless ? (u64{offsets} << 36) | (u64{lod} << 37) + : (u64{offset} << 36) | (u64{offsets} << 54) | (u64{lod} << 55))); const u32 components = static_cast<u32>(std::popcount(mask)); if (insn.Bits(79, 1)) { // FP16 results: (c0, c1) packed into the first destination, (c2, c3) into @@ -1587,11 +1652,11 @@ inline LoweredProgram LowerToMaxwell(std::span<const Instruction> code) { dest == 255 || (mask & (mask - 1) && ((dest & 1) || dest >= 254)) || (split && mask == 15 && ((second_dest & 1) || second_dest >= 254)) || (depth_compare && (dimension != 1 || split || mask != 1 || (source_a & 1) || source_a >= 249)) || - (dimension == 5 && ((source_a & 1) || source_a >= 249))) { + (dimension == 5 && ((source_a & 1) || source_a >= 249)) || register_handle || uniform_handle || + !bind_texture_buffer(2)) { if (!general_tex()) return fail(index, "native texture mode or descriptor binding"); break; } - result.required_texture_buffer = 2; emit((0xd800000000000000ULL & ~(u64{insn.Bits(79, 1)} << 59)) | predicate | dest | (u64{insn.Bits(24, 8)} << 8) | (u64{insn.Bits(32, 8)} << 20) | (u64{insn.Bits(64, 8)} << 28) | (u64{insn.Bits(40, 14)} << 36) | (u64{insn.Bits(90, 1)} << 49) | |
