test(agc): skip only the device-dependent f64 denormal mode results

On a device whose target reports no f64 DenormPreserve, the f64
rounding and add/mul denormal mode tests skipped every mismatch in the
modes that keep f64 denormal inputs (1, 3 and no known float mode) and
checked modes 0 and 2 strictly. A recompiler regression in modes 1 and
3 never failed there, such as dropping the v_fract_f64 result flush
that only mode 1 shows, while mode 2 failed where a driver may flush a
denormal result of normal inputs (v_add_f64 and v_mul_f64 rows 4, 9,
11 and 15).

A result is now skipped only when its expected value changes if the
device flushes f64 denormal inputs, results or both, and only until the
device shows that it keeps them: when the run without a known float
mode, which uses the native instructions, matches every row, the other
modes are checked strictly.

Co-Authored-By: Claude <[email protected]>
This commit is contained in:
thuanquangandClaude committed 2026-10-20 15:04:34 +07:00
1 parent 7d876ad3ba
commit 1868043281
2 files changed
+50 -41

No files matched your search

@@ -167,15 +167,23 @@ void Run(AgcDriver::VulkanDevice& device, const Mode& mode) {
device.WaitIdle();
}
void Check(const Mode& mode) {
std::uint32_t Check(const Mode& mode, bool keepsDenormals) {
constexpr const char* Names[2] = {"v_add_f64", "v_mul_f64"};
const std::array<std::uint32_t, 3> deviceFlushes{mode.field & 2u, mode.field & 1u, 0u};
std::uint32_t skipped = 0;
for (std::uint32_t tid = 0; tid < Threads; ++tid) {
for (std::uint32_t form = 0; form < 2u; ++form) {
const std::uint64_t actual = Output[tid * Results + form * 2u] | (static_cast<std::uint64_t>(Output[tid * Results + form * 2u + 1u]) << 32u);
const std::uint64_t expected = Expected[mode.field][tid][form];
Require(actual == expected, std::string(Names[form]) + " " + mode.name + " of " + Hex(Pairs[tid][0]) + " and " + Hex(Pairs[tid][1]) + " is " + Hex(actual) + ", expected " + Hex(expected));
if (actual == expected) continue;
const std::string message = std::string(Names[form]) + " " + mode.name + " of " + Hex(Pairs[tid][0]) + " and " + Hex(Pairs[tid][1]) + " is " + Hex(actual) + ", expected " + Hex(expected);
const bool deviceDependent = std::any_of(deviceFlushes.begin(), deviceFlushes.end(), [&](std::uint32_t flushed) { return Expected[flushed][tid][form] != expected; });
Require(deviceDependent && !keepsDenormals, message);
std::printf("skipped %s, the device flushes f64 denormals without DenormPreserve support\n", message.c_str());
++skipped;
}
}
return skipped;
}
}
@@ -195,21 +203,15 @@ int main() {
{"f64 denormal mode 3, IEEE mode on", ShaderRecompiler::ShaderFloatMode{0xf0u, true, true, false}, 3u},
{"f64 denormal mode 3, IEEE mode off", ShaderRecompiler::ShaderFloatMode{0xf0u, true, false, false}, 3u},
};
const bool canPreserveDenormals = device->Target().float64DenormPreserve;
bool keepsDenormals = device->Target().float64DenormPreserve;
std::uint32_t skipped = 0;
for (const Mode& mode : modes) {
Run(*device, mode);
const bool keepsDenormals = !mode.value.has_value() || ((mode.value->floatMode >> 6u) & 1u) != 0u;
if (!keepsDenormals || canPreserveDenormals) {
Check(mode);
continue;
}
try {
Check(mode);
} catch (const std::exception& error) {
std::printf("skipped %s, the device flushes f64 denormals without DenormPreserve support: %s\n", mode.name, error.what());
}
const std::uint32_t modeSkipped = Check(mode, keepsDenormals);
keepsDenormals = keepsDenormals || (!mode.value.has_value() && modeSkipped == 0u);
skipped += modeSkipped;
}
std::puts("f64 add and mul denormal mode tests passed");
std::printf("f64 add and mul denormal mode tests passed, %u device-dependent results skipped\n", skipped);
return 0;
} catch (const std::exception& error) {
std::cerr << error.what() << '\n';
@@ -94,28 +94,41 @@ std::uint64_t Bits(double value) {
return std::bit_cast<std::uint64_t>(value);
}
void Check(const Mode& mode) {
std::array<std::uint64_t, 5> Expected(std::uint64_t source, std::uint32_t field) {
const double value = std::bit_cast<double>(Flush(source, (field & 1u) == 0u));
const double below = std::bit_cast<double>(0x3fefffffffffffffull);
int exponent = 0;
const double mantissa = std::frexp(value, &exponent);
return {
Bits(std::floor(value)),
Bits(std::ceil(value)),
Flush(Bits(std::min(value - std::floor(value), below)), (field & 2u) == 0u),
Bits(mantissa),
static_cast<std::uint32_t>(exponent),
};
}
std::uint32_t Check(const Mode& mode, bool keepsDenormals) {
const std::uint32_t field = mode.value.has_value() ? mode.value->floatMode >> 6u : 3u;
const std::array<std::uint32_t, 3> deviceFlushes{field & 2u, field & 1u, 0u};
constexpr const char* Names[5] = {"v_floor_f64", "v_ceil_f64", "v_fract_f64", "v_frexp_mant_f64", "v_frexp_exp_i32_f64"};
std::uint32_t skipped = 0;
for (std::uint32_t tid = 0; tid < Threads; ++tid) {
const std::uint64_t source = ValueAt(tid);
const double value = std::bit_cast<double>(Flush(source, (field & 1u) == 0u));
const double below = std::bit_cast<double>(0x3fefffffffffffffull);
int exponent = 0;
const double mantissa = std::frexp(value, &exponent);
const std::array<std::uint64_t, 4> expected{
Bits(std::floor(value)),
Bits(std::ceil(value)),
Flush(Bits(std::min(value - std::floor(value), below)), (field & 2u) == 0u),
Bits(mantissa),
};
constexpr const char* Names[4] = {"v_floor_f64", "v_ceil_f64", "v_fract_f64", "v_frexp_mant_f64"};
const auto expected = Expected(source, field);
const std::uint32_t* out = &Output[tid * Results];
for (std::uint32_t index = 0; index < expected.size(); ++index) {
const std::uint64_t actual = out[index * 2u] | (static_cast<std::uint64_t>(out[index * 2u + 1u]) << 32u);
Require(actual == expected[index], std::string(Names[index]) + " " + mode.name + " of " + Hex(source) + " is " + Hex(actual) + ", expected " + Hex(expected[index]));
const std::uint64_t actual = index < 4u ? out[index * 2u] | (static_cast<std::uint64_t>(out[index * 2u + 1u]) << 32u) : out[8];
if (actual == expected[index]) continue;
const auto text = [index](std::uint64_t bits) { return index < 4u ? Hex(bits) : std::to_string(static_cast<std::int32_t>(bits)); };
const std::string message = std::string(Names[index]) + " " + mode.name + " of " + Hex(source) + " is " + text(actual) + ", expected " + text(expected[index]);
const bool deviceDependent = std::any_of(deviceFlushes.begin(), deviceFlushes.end(), [&](std::uint32_t flushed) { return Expected(source, flushed)[index] != expected[index]; });
Require(deviceDependent && !keepsDenormals, message);
std::printf("skipped %s, the device flushes f64 denormals without DenormPreserve support\n", message.c_str());
++skipped;
}
Require(out[8] == static_cast<std::uint32_t>(exponent), std::string("v_frexp_exp_i32_f64 ") + mode.name + " of " + Hex(source) + " is " + std::to_string(static_cast<std::int32_t>(out[8])) + ", expected " + std::to_string(exponent));
}
return skipped;
}
}
@@ -135,21 +148,15 @@ int main() {
{"f64 denormal mode 3, IEEE mode on", ShaderRecompiler::ShaderFloatMode{0xf0u, true, true, false}},
{"f64 denormal mode 3, IEEE mode off", ShaderRecompiler::ShaderFloatMode{0xf0u, true, false, false}},
};
const bool canPreserveDenormals = device->Target().float64DenormPreserve;
bool keepsDenormals = device->Target().float64DenormPreserve;
std::uint32_t skipped = 0;
for (const Mode& mode : modes) {
Run(*device, mode);
const bool keepsDenormals = !mode.value.has_value() || ((mode.value->floatMode >> 6u) & 1u) != 0u;
if (!keepsDenormals || canPreserveDenormals) {
Check(mode);
continue;
}
try {
Check(mode);
} catch (const std::exception& error) {
std::printf("skipped %s, the device flushes f64 denormals without DenormPreserve support: %s\n", mode.name, error.what());
}
const std::uint32_t modeSkipped = Check(mode, keepsDenormals);
keepsDenormals = keepsDenormals || (!mode.value.has_value() && modeSkipped == 0u);
skipped += modeSkipped;
}
std::puts("f64 rounding denormal mode tests passed");
std::printf("f64 rounding denormal mode tests passed, %u device-dependent results skipped\n", skipped);
return 0;
} catch (const std::exception& error) {
std::cerr << error.what() << '\n';