Merge pull request #3040 from CouchRiv/fix-shader-ldexp-f16-denorm-flush
fix(shader): flush v_ldexp_f16 denormals per the f16 denormal mode
This commit is contained in:
commit
f6b421b73c
|
|
@ -725,6 +725,75 @@ void Check(std::uint32_t first, std::uint32_t count, std::span<const Vector> vec
|
|||
}
|
||||
}
|
||||
|
||||
alignas(256) constexpr std::array<std::uint32_t, 13> LdexpDenormalCode{
|
||||
0x34020082u, 0x34060084u, 0xe0302000u, 0x80000401u, 0xe0302004u, 0x80000501u, 0x7e1402ffu, 0xabcdabcdu,
|
||||
0xbf8c3f70u, 0x76140b04u, 0xe0702000u, 0x80010a03u, 0xbf810000u,
|
||||
};
|
||||
|
||||
struct LdexpDenormalVector {
|
||||
std::uint32_t half;
|
||||
std::uint32_t exponent;
|
||||
std::array<std::uint32_t, 4> expected;
|
||||
};
|
||||
|
||||
constexpr LdexpDenormalVector LdexpDenormalVectors[] = {
|
||||
{0x00000401u, 0xffffffffu, {0xabcd0000u, 0xabcd0000u, 0xabcd0200u, 0xabcd0200u}},
|
||||
{0x000003ffu, 0x00000000u, {0xabcd0000u, 0xabcd0000u, 0xabcd0000u, 0xabcd03ffu}},
|
||||
{0x000003ffu, 0x00000001u, {0xabcd0000u, 0xabcd07feu, 0xabcd0000u, 0xabcd07feu}},
|
||||
{0x00000400u, 0xffffffffu, {0xabcd0000u, 0xabcd0000u, 0xabcd0200u, 0xabcd0200u}},
|
||||
{0x00008400u, 0xffffffffu, {0xabcd8000u, 0xabcd8000u, 0xabcd8200u, 0xabcd8200u}},
|
||||
{0x000007ffu, 0xffffffffu, {0xabcd0000u, 0xabcd0000u, 0xabcd0400u, 0xabcd0400u}},
|
||||
{0x000087ffu, 0xffffffffu, {0xabcd8000u, 0xabcd8000u, 0xabcd8400u, 0xabcd8400u}},
|
||||
{0x00000001u, 0x00000000u, {0xabcd0000u, 0xabcd0000u, 0xabcd0000u, 0xabcd0001u}},
|
||||
{0x00000001u, 0x00000001u, {0xabcd0000u, 0xabcd0000u, 0xabcd0000u, 0xabcd0002u}},
|
||||
{0x00008001u, 0x00000000u, {0xabcd8000u, 0xabcd8000u, 0xabcd8000u, 0xabcd8001u}},
|
||||
{0x00000200u, 0x00000000u, {0xabcd0000u, 0xabcd0000u, 0xabcd0000u, 0xabcd0200u}},
|
||||
{0x00000200u, 0x00000001u, {0xabcd0000u, 0xabcd0400u, 0xabcd0000u, 0xabcd0400u}},
|
||||
{0x00003c01u, 0x00000000u, {0xabcd3c01u, 0xabcd3c01u, 0xabcd3c01u, 0xabcd3c01u}},
|
||||
{0x00003c01u, 0xffffffffu, {0xabcd3801u, 0xabcd3801u, 0xabcd3801u, 0xabcd3801u}},
|
||||
{0x00007c00u, 0x00000001u, {0xabcd7c00u, 0xabcd7c00u, 0xabcd7c00u, 0xabcd7c00u}},
|
||||
{0x00007e55u, 0x00000000u, {0xabcd7e55u, 0xabcd7e55u, 0xabcd7e55u, 0xabcd7e55u}},
|
||||
{0x00000000u, 0x00000040u, {0xabcd0000u, 0xabcd0000u, 0xabcd0000u, 0xabcd0000u}},
|
||||
{0x00008000u, 0xffffffffu, {0xabcd8000u, 0xabcd8000u, 0xabcd8000u, 0xabcd8000u}},
|
||||
};
|
||||
|
||||
void RunLdexpDenormal(AgcDriver::VulkanDevice& device, std::uint32_t denorm16) {
|
||||
Input.fill(0u);
|
||||
for (std::uint32_t lane = 0; lane < std::size(LdexpDenormalVectors); ++lane) {
|
||||
Input[lane * Inputs] = LdexpDenormalVectors[lane].half;
|
||||
Input[lane * Inputs + 1] = LdexpDenormalVectors[lane].exponent;
|
||||
}
|
||||
Output.fill(0xdeadbeefu);
|
||||
std::vector<std::uint32_t> userData(8, 0u);
|
||||
const auto input = BufferDescriptor(Input.data(), static_cast<std::uint32_t>(Input.size()));
|
||||
const auto output = BufferDescriptor(Output.data(), static_cast<std::uint32_t>(Output.size()));
|
||||
std::copy(input.begin(), input.end(), userData.begin());
|
||||
std::copy(output.begin(), output.end(), userData.begin() + 4);
|
||||
const std::span<const std::uint32_t> code(LdexpDenormalCode);
|
||||
const std::array<ShaderRecompiler::MemoryRegion, 1> memory{{{reinterpret_cast<std::uintptr_t>(code.data()), std::as_bytes(code)}}};
|
||||
const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0, {false, false, false}, false, 1};
|
||||
ShaderRecompiler::RecompileRequest request{
|
||||
{ShaderStage::Compute, reinterpret_cast<std::uintptr_t>(code.data()), code, 0, {}},
|
||||
{32, 0, userData, compute, std::nullopt, std::nullopt, memory},
|
||||
device.Target(),
|
||||
{0, 0, 0, 128}
|
||||
};
|
||||
request.context.floatMode = ShaderRecompiler::ShaderFloatMode{denorm16 << 6u, true, true, false};
|
||||
request.useCache = false;
|
||||
const auto result = ShaderRecompiler::Recompile(request);
|
||||
device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast<std::uintptr_t>(code.data()));
|
||||
device.WaitIdle();
|
||||
}
|
||||
|
||||
void CheckLdexpDenormal(std::uint32_t denorm16) {
|
||||
for (std::uint32_t lane = 0; lane < std::size(LdexpDenormalVectors); ++lane) {
|
||||
const auto& vector = LdexpDenormalVectors[lane];
|
||||
const auto expected = vector.expected[denorm16];
|
||||
const auto actual = Output[lane * Results];
|
||||
Require(Matches(actual, expected), std::string("f16 ldexp denormal: denorm16 ") + std::to_string(denorm16) + " vector " + std::to_string(lane) + " (" + Hex(vector.half) + ", " + Hex(vector.exponent) + ") is " + Hex(actual) + ", expected " + Hex(expected));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
int main() {
|
||||
|
|
@ -753,6 +822,10 @@ int main() {
|
|||
}
|
||||
}
|
||||
}
|
||||
for (std::uint32_t denorm16 = 0u; denorm16 < 4u; ++denorm16) {
|
||||
RunLdexpDenormal(*device, denorm16);
|
||||
CheckLdexpDenormal(denorm16);
|
||||
}
|
||||
std::puts("f16 rounding tests passed");
|
||||
return 0;
|
||||
} catch (const std::exception& error) {
|
||||
|
|
|
|||
|
|
@ -324,7 +324,12 @@ IrU32 TranslationContext::normF32(IrU32 bits, bool signedValue) {
|
|||
}
|
||||
|
||||
bool TranslationContext::vLdexpF16(const RdnaInstruction& inst) {
|
||||
const IrU32 bits = readF16Bits(sourceAt(inst, 0u));
|
||||
const IrU32 source = readF16Bits(sourceAt(inst, 0u));
|
||||
IrU32 bits = source;
|
||||
if (floatMode.has_value() && (floatMode->floatMode & 0x40u) == 0u) {
|
||||
const IrU1 tiny(ir.ULessThan(ir.BitwiseAnd(bits.Value(), ir.Constant(0x7fffu)), ir.Constant(0x0400u)));
|
||||
bits = IrU32(ir.Select(tiny.Value(), ir.BitwiseAnd(bits.Value(), ir.Constant(0x8000u)), bits.Value()));
|
||||
}
|
||||
const IrF16 half(ir.Emit(IrOpcode::BitCastF16U16, IrType::F16, {&ir.Emit(IrOpcode::ConvertU16U32, IrType::U16, {&bits.Value()})}));
|
||||
const IrF32 value(ir.Emit(IrOpcode::ConvertF32F16, IrType::F32, {&half.Value()}));
|
||||
const RdnaOperand& exponentSource = sourceAt(inst, 1u);
|
||||
|
|
@ -332,9 +337,14 @@ bool TranslationContext::vLdexpF16(const RdnaInstruction& inst) {
|
|||
const IrU32 exponent(ir.Emit(IrOpcode::BitFieldSExtract, IrType::U32, {&exponentBits.Value(), &ir.Constant(0u), &ir.Constant(16u)}));
|
||||
const IrU32 clamped(ir.Emit(IrOpcode::SMax32, IrType::U32, {&ir.Emit(IrOpcode::SMin32, IrType::U32, {&exponent.Value(), &ir.Constant(64u)}), &ir.Constant(static_cast<std::uint32_t>(-64))}));
|
||||
IrValue& power = ir.BitCastF32(ir.ShiftLeftLogical(ir.IAdd(clamped.Value(), ir.Constant(127u)), ir.Constant(23u)));
|
||||
const IrU32 result = convertF32ToF16Bits(IrF32(ir.Emit(IrOpcode::FPMul32, IrType::F32, {&value.Value(), &power})), {&value.Value()});
|
||||
const IrU1 nan(ir.UGreaterThan(ir.BitwiseAnd(bits.Value(), ir.Constant(0x7fffu)), ir.Constant(0x7c00u)));
|
||||
write16Bits(inst.destination, clampF16Bits(inst.destination, IrU32(ir.Select(nan.Value(), quietNan16(bits).Value(), result.Value()))));
|
||||
const IrF32 product(ir.Emit(IrOpcode::FPMul32, IrType::F32, {&value.Value(), &power}));
|
||||
IrU32 result = convertF32ToF16Bits(product, {&value.Value()});
|
||||
if (floatMode.has_value() && (floatMode->floatMode & 0x80u) == 0u) {
|
||||
const IrU1 tiny(ir.ULessThan(ir.BitwiseAnd(ir.BitCastU32(product.Value()), ir.Constant(0x7fffffffu)), ir.Constant(0x38800000u)));
|
||||
result = IrU32(ir.Select(tiny.Value(), ir.BitwiseAnd(bits.Value(), ir.Constant(0x8000u)), result.Value()));
|
||||
}
|
||||
const IrU1 nan(ir.UGreaterThan(ir.BitwiseAnd(source.Value(), ir.Constant(0x7fffu)), ir.Constant(0x7c00u)));
|
||||
write16Bits(inst.destination, clampF16Bits(inst.destination, IrU32(ir.Select(nan.Value(), quietNan16(source).Value(), result.Value()))));
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -439,6 +439,7 @@ Throughout the project, every function at every stage either **does exactly what
|
|||
- A [label or store](../../core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp) (`RELEASE_MEM`, `WRITE_DATA`, `DMA_DATA`) into guest memory the device has no host import of waits for all recorded work and is then written by the CPU, so the queue worker stalls once per such label. The console's CP stores it in stream order without that stall.
|
||||
- [SET_*_REG_INDIRECT](../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/Submission.cpp) (libSceAgcDriver) reads its register list when the command buffer is submitted, with the packets. The console's graphics PFP reads it when it reaches the packet, so a list written after the submission (by the CPU, or by GPU work ahead of the packet behind PFP_SYNC_ME) is read here before that write.
|
||||
- The [shader float mode](../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp) (`FLOAT_MODE`, `DX10_CLAMP`, `IEEE_MODE` and `FP16_OVFL` of `COMPUTE_PGM_RSRC1` and `SPI_SHADER_PGM_RSRC1_PS/GS/HS`, bit positions from Mesa's `gfx10.json` and `gfx103.json`) is decoded into `GuestContext::floatMode` and is part of the shader cache keys and the serialized request (#981). `IEEE_MODE` is read by `v_min_f32`, `v_max_f32`, `v_min3_f32`, `v_max3_f32` and `v_med3_f32`, their f16 forms, `v_min_f64` and `v_max_f64`, `IEEE_MODE` and the `FLOAT_MODE` output denormal bits by the output modifier, `DX10_CLAMP` by the clamp modifier, `FP16_OVFL` by every instruction with an f16 result (see its entry), and the whole mode by the 16-bit interpolation instructions (see their entries). With IEEE=1 a signaling NaN operand returns that NaN quieted (the first operand checked first, min3/max3 as two nested pairs, med3 as min3 when any operand is NaN), measured on RDNA2 (gfx1035) over every combination of ±0, ±1, ±inf, denormals, quiet and signaling NaNs with f32 denormals kept and flushed. IEEE=0 and unknown modes keep the previous f32 lowering, which matches IEEE=0. The f16 forms choose their NaN results on the bits: a NaN operand returns the other operand unchanged (src0 when both are NaN), and with IEEE=1 a signaling NaN operand returns that NaN quieted, in the same order as f32. `v_min_f64` and `v_max_f64` quiet a signaling NaN operand the same way with IEEE=1; with IEEE=0 they return src0 unchanged when src1 is NaN, otherwise src1 when src0 is NaN. Both were measured on RDNA2 (gfx1036) at IEEE=0 and IEEE=1 with f32 denormals kept and flushed and f16/f64 denormals kept; RDNA2 has no f64 min3, max3 or med3. Unknown modes follow IEEE=0. `v_min_f16` and `v_max_f16` flush each denormal input to its signed zero when bit 0 of the f16/f64 denormal field is clear (0 and 2); fields 1 and 3 preserve inputs, even when output flushing is enabled. Measured on RDNA2 (gfx1036) at IEEE=0 and IEEE=1, including signed zeros, NaNs and neg/abs modifiers. Unknown modes keep f16 denormals. With f16/f64 denormals flushed the hardware also flushes denormal inputs of f64 min/max, which those lowerings still do not. NaN quieting in f16 arithmetic (the unary instructions and the f32 arithmetic follow it, see below) and `FLOAT_MODE` (f32 denormals flushed before the min/max compare in Astro's Playroom's 0xc0) still follow a single mode, as the entries below record. The mode comes from the registered shader header's `sh_registers`, the same source at registration and at draw/dispatch so the prepared artifact keys match. A title that rewrites RSRC1 at run time, or a raw compute program with no header, compiles with that mode or none.
|
||||
- [v_ldexp_f16](../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) follows the `FLOAT_MODE` f16 denormal field: bit 0 preserves the half input and bit 1 the half result. Otherwise a denormal input is flushed to its signed zero, and a result whose exact scaled value is below the minimum normal (2^-14) is flushed to its signed zero before rounding, so `v_ldexp_f16 0x07ff, -1` gives +0 instead of 0x0400. Measured on RDNA2 (gfx1036) with round to nearest, IEEE 0 and 1 and denorm16 0..3, over every half magnitude 0x0000..0x0fff of both signs scaled by 2^-1 and 2^0 (16384 rows per mode): no mismatch. NaN and infinity handling and destination high-half preservation are unchanged. Shaders without a known float mode keep the previous lowering.
|
||||
- A [WAIT_REG_MEM](../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/WaitMemory.cpp) (libSceAgcDriver) that a compute queue passes on another queue's recorded label store, before the GPU has executed it, is landed before the queue's next dispatch, draw, COND_EXEC or predicated packet reads guest memory: the worker polls until memory satisfies the wait or no unfinished batch writes the label. The graphics queue (queue 0) is exempt, as landing its waits costs PPSA01341 about a quarter of its frame rate; a title that fills memory a graphics command buffer reads after it submits the buffer behind such a wait reads it early there. `APS5_LAND_WAITS_ALL_QUEUES=1` lands the graphics queue's waits too.
|
||||
- The [dispatches that follow a compute queue's wait](../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/DispatchAhead.cpp) (libSceAgcDriver), up to the next wait that memory does not satisfy, a packet that writes guest memory other than a label, or a conditional packet, are resolved (memory captured, shader recompiled) when the wait passes, as the CP reads them right after it; the graphics queue is exempt (`APS5_GROUP_CAPTURE_ALL_QUEUES=1` includes it). The device still reads guest memory when the dispatch is issued; when that memory no longer matches the capture, the dispatch is resolved again from current memory.
|
||||
- [v_mullit_f32](../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) follows the hardware measured with the MODE register at IEEE=1 and denormals kept (a signaling NaN src0 is quieted only with IEEE=1, as measured at IEEE=0 too; a positive denormal src2 counts as positive); with f32 denormals flushed it follows the f32 denormals entry below, and denormal modes 1 and 2 throw.
|
||||
|
|
|
|||
Loading…
Reference in New Issue