Skip to content

riscv_fpu: static/FMA rounding modes, exact integral rounding, FMA underflow tininess#246

Open
SolAstrius wants to merge 10 commits into
LekKit:stagingfrom
pufit:fix/fpu-conformance
Open

riscv_fpu: static/FMA rounding modes, exact integral rounding, FMA underflow tininess#246
SolAstrius wants to merge 10 commits into
LekKit:stagingfrom
pufit:fix/fpu-conformance

Conversation

@SolAstrius

@SolAstrius SolAstrius commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

Summary

Reworked resubmission of the remaining #239 commits, addressing @purplesyringa's review. Four commits, each standalone on staging; no dependency on #242 (RMM), and the two deliberately overlap nowhere except the shared riscv_fpu_host_rm helper.

1. fpu_lib: round to integral values on the encoding, not via biasing adds

Replaces the NACK'd INEXACT patch. The review showed the old f + 0.5 trick was broken beyond the flag leak (this comment): the biasing add itself rounds, so rne(prev(0.5)) returned 1 and rne(2.5) returned 3 (ties-away, not ties-to-even). fpu_round_fXX_internal now decides on the fraction bits of the encoding: no host FP arithmetic, so no spurious flags, no double rounding, and no dependence on a host rounding mode (relevant for the future USE_SOFT_FENV story). RMM is a first-class case — only DYN falls back to the tracked mode, so fcvt/fround with a static rmm field round correctly.

Verified bit-exact against host rint()/round() in all four modes over the full fractional exponent range of both widths (2.0e9 cases, zero mismatches).

2. riscv_fpu: honor the static rounding-mode field

Reworked per review: the mode change is now gated per-instruction (riscv_f_op_is_rounding) — funct3 is an rm field only on rounding-capable ops, so fsgnj/fcmp/fclass/fmv never trigger it. Ops that consume rm as an argument (fcvt-to-int, fround) need no wrap. A static rmm field runs in RNE for now (differs only on exact ties; wired to the exact fixups after #242 lands). The dynamic-RMM preparation is gated to rm == DYN so a static field is no longer clobbered by it.

3. riscv_fpu: honor the rounding mode in the FMA ops

The fnmadd operand-negation part already landed via #241; this is the rm plumbing, now sharing riscv_fpu_host_rm with the OP-FP dispatch instead of duplicating the effective-mode logic (review ask).

4. fpu_lib: set the FMA underflow flag by IEEE after-rounding tininess

Reworked per review, and moved into fpu_fma32/64 proper; fpu_fma32's own rounding is unchanged (see the review thread for why the direct downcast mis-rounds):

  • the NX fast-path gate is gone (it conflated "set by this op" with "ever set");
  • the verdict merges over a flag snapshot from before the op, so a sticky UF from earlier ops survives;
  • the fixup runs while the effective mode is still set (it lives inside the op now);
  • f64 scales by 2^52 instead of 2^200, with the operand-bound proof from the review included as a comment; the b == 0 overflow-to-NaN case reads correctly as not-tiny;
  • f32 decides tininess by comparing the widened 53-bit sum against the mode's threshold in f64, with the exact TwoSum error breaking the tie when the sum lands exactly on the to-nearest threshold.

On aarch64 (before-rounding tininess), fma(2^-62*(1-2^-23), 2^-64*(1+2^-23), 0) returned the correct 2^-126 but flagged NX|UF instead of NX; targeted tests cover both boundary directions, an exact boundary, a genuinely-tiny-at-boundary case, and sticky-UF preservation — all pass with the fix, three fail without it.

Conformance (riscv-arch-test ACT4, Spike reference)

build F D I M total
staging 13/82 24/114 51/51 13/13 101/260
this PR 21/82 51/114 51/51 13/13 136/260

No regressions. The remaining F/D failures are, by sampled diagnosis, exactly the two intentionally-excluded categories:

  • RMM vectors — the old riscv_prepare_rmm directed hack (e.g. it reads a NaN-boxed f32 through the f64 default: path and rounds fsqrt the wrong way). Deleted and replaced by riscv_fpu: RMM (roundTiesToAway) for the scalar OP-FP ops #242.
  • NaN result canonicalization — arithmetic results emit host NaN payloads where the spec wants the canonical NaN (fadd.s0x7fd825fe vs 0x7fc00000). That's the separate flag-gated patch we agreed on; needs a decision on compile-time vs runtime flag first.

A local combined build (this PR + #242 + the canonicalization patch) reaches 244/260, and every one of its 16 residual failures is an FMA test failing on an frm=rmm tie sub-case — that's the one #239 commit not yet resubmitted anywhere ("round the FMA family to nearest, ties-away under RMM"); it needs #242's fixup machinery, so it goes on top of #242 once that merges, closing the gap to the 260/260 the old chain reported.

cc @purplesyringa — draft until reviewed, to keep GitHub from merging things on its own again.

fpu_round_fXX_internal biased the value by +/-0.5 (or +/-1.0) with a real
FP add and let the caller's int cast truncate. The add is itself a
rounded op, which broke it two ways: it raised a spurious INEXACT for
the discarded fraction (leaking NX alongside e.g. the NV of an
out-of-range fcvt), and it mis-rounded whenever the biasing add was
inexact or exact on the wrong side -- rne(prev(0.5)) returned 1 because
prev(0.5) + 0.5 rounds up to 1.0, and rne(2.5) returned 3 because
2.5 + 0.5 is exactly 3.0, i.e. the trick implements ties-away, not
ties-to-even.

Decide on the fraction bits of the encoding instead: no host FP
arithmetic, so no flag leaks, no double rounding, and no dependence on
the host rounding mode (which USE_SOFT_FENV hosts don't have). RMM is
now a first-class case -- only DYN falls back to the tracked mode, so an
explicit rmm request is honored instead of being swapped for the
dynamic mode.

Verified bit-exact against host rint()/round() in all four modes over
the full fractional exponent range of both widths (2.0e9 cases).

Signed-off-by: Sol Astrius Phoenix <sol@astrius.ink>
Arithmetic and conversion ops only ever ran in the mode the frm CSR left
on the host FPU; a static rm field (fadd.s ...,rtz etc.) was silently
ignored. Apply a differing static mode around the op, gated on the op
actually being rounding-capable: funct3 is an rm field only there, while
on fsgnj/fmin/fmax/fcmp/fclass/fmv it encodes the operation itself and
must not drive the host mode. Ops that take rm as an explicit argument
(fcvt to integer, fround) consume the field directly and need no wrap.

A static rmm field runs in RNE for now, which differs from
roundTiesToAway only on exact halfway ties; those are covered once the
exact RMM fixups land (LekKit#242). The dynamic-RMM preparation is gated to
rm == DYN so it no longer fires under a static field.

Signed-off-by: Sol Astrius Phoenix <sol@astrius.ink>
fpu_fma rounds in the host mode, so a static rm field on
fmadd/fmsub/fnmsub/fnmadd was silently computed in whatever mode frm
left set. Route the FMA ops through riscv_fma32/64, which apply a
differing static mode around the op using the same riscv_fpu_host_rm
rule as the OP-FP dispatch. As there, a static rmm field runs in RNE
until the exact ties-away fixups (LekKit#242) extend to FMA.

Signed-off-by: Sol Astrius Phoenix <sol@astrius.ink>
IEEE (and RISC-V) underflow means tiny after rounding: the result,
rounded with an unbounded exponent range, lies below the minimum normal.
Hosts that detect tininess before rounding (e.g. aarch64) disagree
exactly when an FMA result lands on +/- the minimum normal, flagging a
spurious UF for a result that rounded up out of the tiny range. On that
boundary, recompute the after-rounding verdict and force the flag:

- f32: the exact product widens into f64, the sum rounds once at 53
  bits, and the scaled conversion once at 24; 53 >= 2*24 + 2 makes the
  double rounding innocuous, so the scaled magnitude compared against
  1.0 is the unbounded-exponent verdict.
- f64: no wider type exists, so rescale the op by 2^52 into the normal
  range, where the single fused rounding is already unbounded-
  equivalent (safety bounds are derived in a comment; an overflowing
  a*2^52 for b == 0 yields NaN, which correctly reads as not-tiny).

The verdict merges over a flag snapshot from before the op, so a
sticky UF from earlier ops is preserved and nothing the recompute
raises can leak. fpu_fma64 is split into a bare fpu_fma64_raw plus the
flag handling so the f64 fixup can reuse the fused op without
re-entering it.

On aarch64, fma(2^-62*(1-2^-23), 2^-64*(1+2^-23), 0) previously
returned the correct 2^-126 but flagged NX|UF instead of NX; the f64
analog and the negative side misbehaved the same way.

Signed-off-by: Sol Astrius Phoenix <sol@astrius.ink>
Comment thread src/util/fpu_lib.c Outdated
Comment thread src/util/fpu_lib.c
Comment thread src/util/fpu_lib.c
Comment thread src/util/fpu_lib.c
Comment thread src/util/fpu_lib.c Outdated
Comment thread src/cpu/riscv_fpu.c Outdated
@purplesyringa

Copy link
Copy Markdown
Collaborator

I noticed that the first commit (the one about float-to-integer rounding) kept

    if (unlikely(fpu_is_zero64_soft(d))) {
        return 0;
    }

in fpu_round_f64_to_u32 (and similar methods). I'm not sure what role that if used to play, but judging from unlikely, maybe it's not needed anymore? @LekKit do you remember why that was needed?

Comment thread src/cpu/riscv_fpu.c Outdated
@purplesyringa

purplesyringa commented Jul 20, 2026

Copy link
Copy Markdown
Collaborator

Just throwing ideas out there: I think fp-to-int roundings can be optimized further. I don't expect you to do this here if you don't want to, I'm mostly just glad it's sound, but you can try this if you have time.

The old FP-based code tried to force correct roundings with formulas like f + 0.5, which have edge cases, because you're trying to fit complex rules in one formula. However, you can force the FPU to do the correct rounding with a different method. Let's say 0 <= f < 2^52 - 1 is a floating-point value. Then f + 2^52 will have an exponent of exactly 52, so the mantissa field is exactly round(f). Since addition uses the rounding mode from fenv, you can effectively control which rounding mode this method implements. See also.

This method introduces a few subtleties. For instance, there are some edge cases around f ~= 2^52, e.g. for f = 2^52 - 0.5, f + 2^52 can round up to 2^53, which has an empty mantissa field, but you can fix this by computing bitcast(f + 2^52) - bitcast(2^52) instead of masking bits. Point is, you'll still need some special casing for e >= 52, but you don't need a switch on the runtime type, you can just use the host mode for everything and avoid manually implementing FP logic.

Of course, RMM is the elephant in the room. That will still require some sort of fixup.

Comment thread src/cpu/riscv_fpu.h Outdated
Comment thread src/util/fpu_lib.h
Comment on lines 1048 to 1049
fpu_f64_t err = fpu_add_error64(sum, mul, add);
fpu_f64_t res = fpu_odd_round64(sum, err);

@purplesyringa purplesyringa Jul 20, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My original review mentioned that computing sum is already exact in the 32-bit case, so handling err/res/soft_fenv_... is unnecessary and you can directly downcast sum to f32 and obtain the correct result. The PR mentions that you fixed fpu_fma32, but I don't see that? Did you run into some problems while trying that?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried exactly that and it mis-rounds — sum isn't exact (the product is, but product + c can span more bits than f64 holds), and downcasting the 53-bit rounding erases 24-bit tie-breaks:

// cc -O2 tie_probe.c -lm && ./a.out
#include <stdio.h>
#include <stdint.h>
#include <string.h>
#include <math.h>
volatile float a, b, c;
int main(void)
{
    uint32_t ua = 0x6BE58000, ub = 0x3A123B00, uc = 0x117D0E3E, r1, r2;
    memcpy((void*)&a, &ua, 4); memcpy((void*)&b, &ub, 4); memcpy((void*)&c, &uc, 4);
    float f1 = fmaf(a, b, c), f2 = (float)((double)a * b + c);
    memcpy(&r1, &f1, 4); memcpy(&r2, &f2, 4);
    printf("fmaf     %08x\ndowncast %08x\n", r1, r2);
}
# arm64 and x86_64, identical:
fmaf     668317e5
downcast 668317e4

a*b sits exactly on a 24-bit tie and c breaks it upward; the intermediate rounding loses that. (2p+2 doesn't apply — the addends aren't 24-bit values.) So fpu_fma32's own rounding keeps the odd-round; the PR text claiming I "fixed" it was wrong, rewording.

@purplesyringa purplesyringa Jul 20, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, now I see, (float)(a * b + c) has double rounding, one after + and one due to (float), thanks. I'll re-review the code taking this into account in a bit.

Comment thread src/util/fpu_lib.h
#endif
#endif

if (unlikely((fpu_bit_f32_to_u32(ret) & FPU_LIB_FP32_NOSIGNED_MASK) == FPU_LIB_FP32_MINIMUM_NORM)) {

@purplesyringa purplesyringa Jul 20, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hopefully you can explain this to me.

If I understand it correctly, RISC-V says UF corresponds to the value after rounding for all operations. In particular, I'm assuming this includes fcvt.s.d, which is implemented via fpu_fcvt_f32_to_f64.

Since f32 denormals easily fit in f64, the only function in fpu_fma32 that can set UF is fpu_fcvt_f32_to_f64. So if fcvt.s.d sets UF correctly, then fpu_fcvt_f32_to_f64 should be sufficient here would FMA-specific logic, and this function could be much, much simpler (I think you wouldn't need to handle exceptions at all, actually, but I'm not certain about that). This would certainly remove fpu_fma32_fixup_uf at least.

And if fpu_fcvt_f32_to_f64 is broken, then that's where the fix should live, not here, I think. This also raises the topic of whether any other arithmetic operations need similar adjustments -- e.g. what specifically makes FMA so different from multiplication that only FMA needs special handling?

Could you explain where my logic fails?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The premise fails: fcvt.s.d doesn't set UF correctly on aarch64 either. (You mean the narrowing fpu_fcvt_f64_to_f32 — the widening one is exact.)

// cc -O2 uf_probe.c -lm && ./a.out
// Every case rounds up from below onto exactly +min-normal:
// after-rounding tininess says NX only.
#include <stdio.h>
#include <stdint.h>
#include <string.h>
#include <math.h>
#include <fenv.h>
volatile float fa, fb, fr;
volatile double da, db, dr;
static void flags(const char* op, uint64_t bits)
{
    int e = fetestexcept(FE_ALL_EXCEPT);
    printf("%-8s %016llx  %s%s\n", op, (unsigned long long)bits,
           (e & FE_INEXACT) ? "NX " : "", (e & FE_UNDERFLOW) ? "UF" : "");
    feclearexcept(FE_ALL_EXCEPT);
}
int main(void)
{
    uint32_t a32 = 0x207FFFFE, b32 = 0x1F800001, r32; // 2^-62*(1-2^-23), 2^-64*(1+2^-23)
    uint64_t a64 = 0x200FFFFFFFFFFFFE, b64 = 0x1FF0000000000001, r64;
    memcpy((void*)&fa, &a32, 4); memcpy((void*)&fb, &b32, 4);
    memcpy((void*)&da, &a64, 8); memcpy((void*)&db, &b64, 8);
    feclearexcept(FE_ALL_EXCEPT);
    fr = fa * fb;         memcpy(&r32, (void*)&fr, 4); flags("mul.s", r32);
    fr = fmaf(fa, fb, 0); memcpy(&r32, (void*)&fr, 4); flags("fma.s", r32);
    dr = da * db;         memcpy(&r64, (void*)&dr, 8); flags("mul.d", r64);
    dr = fma(da, db, 0);  memcpy(&r64, (void*)&dr, 8); flags("fma.d", r64);
    dr = ldexp(1.0 - ldexp(1.0, -46), -126); feclearexcept(FE_ALL_EXCEPT);
    fr = (float)dr;       memcpy(&r32, (void*)&fr, 4); flags("cvt.s.d", r32);
}
# x86_64:                        # arm64 (M1):
mul.s    0000000000800000  NX    mul.s    0000000000800000  NX UF
fma.s    0000000000800000  NX    fma.s    0000000000800000  NX UF
mul.d    0010000000000000  NX    mul.d    0010000000000000  NX UF
fma.d    0010000000000000  NX    fma.d    0010000000000000  NX UF
cvt.s.d  0000000000800000  NX    cvt.s.d  0000000000800000  NX UF

ARM detects tininess before rounding on every instruction, x86 after. So nothing makes FMA special — mul/div/cvt all have the same bug; only add/sub are safe, since a sum near the boundary is always exact (shared quantum when both operands are small, Sterbenz's lemma on cancellation).

The right end state is fixing this once for all of them, agreed — but doing it correctly needs a flags snapshot from before the op, or a not-tiny verdict can't clear the bogus UF without also clearing a sticky one. fma already pays for that snapshot at entry; the hot ops don't. So: FMA here, cvt/mul/div/sqrt together as a follow-up.

Comment thread src/util/fpu_lib.h Outdated
@purplesyringa

purplesyringa commented Jul 20, 2026

Copy link
Copy Markdown
Collaborator

I think this about concludes my review for now. The logic I didn't comment on looks good.

@LekKit

LekKit commented Jul 20, 2026

Copy link
Copy Markdown
Owner

I noticed that the first commit (the one about float-to-integer rounding) kept

    if (unlikely(fpu_is_zero64_soft(d))) {
        return 0;
    }

in fpu_round_f64_to_u32 (and similar methods). I'm not sure what role that if used to play, but judging from unlikely, maybe it's not needed anymore? @LekKit do you remember why that was needed?

AFAIK the original rounding routine was written in a way that did not account for literal zero (which is already rounded anyways). Now this seems redundant, but i would instead verify if it actually produces expected result.

Review feedback on LekKit#246: spell out the 0.5 ties-to-even case, the e == 0
odd-integer test through the biased-exponent LSB, and the mantissa-to-
exponent carry on rounding away; use shifted constants for half/step and
annotate the 1.0 literals. No functional change (re-verified bit-exact
against host rint()/round(), 2.0e9 cases).

Signed-off-by: Sol Astrius Phoenix <sol@astrius.ink>
Review feedback on LekKit#246: hoist the static-rm application out of the
per-op FMA helpers into riscv_fpu_static_rm_enter/leave, used identically
by the OP-FP wrapper and the four FMA emulate functions -- one code path,
less forceinline bloat. Both paths reject rm 5/6 before entering (the
FMA functions via their existing riscv_fpu_rm_is_valid gate).

Gate the RMM preparation on the effective mode instead of only the
dynamic one: a static rmm field now behaves exactly like frm == RMM
rather than approximating RNE, so the two select the same (interim)
RMM path until the exact fixups land. Rename the predicate to
riscv_f_op_is_implicitly_rounding to distinguish the ops that consume
rm as an explicit argument.

Signed-off-by: Sol Astrius Phoenix <sol@astrius.ink>
The boundary recompute rounded the widened sum once at 53 bits and then
converted at 24; when the 53-bit rounding lands exactly on a 24-bit tie
midpoint the conversion resolves the tie blindly and the tininess
verdict flips. Concretely, an fma32 with significand product 2^47 + t
(t < 2^18, quantum 2^-198, negative) and c = 2^-126 has an exact value
just below the midpoint 2^-126 - 2^-151: the delivered result is 2^-126
but the unbounded rounding is below the minimum normal, so UF must be
raised -- the plain recompute concluded not-tiny and cleared it.

Odd-round the sum against its exact TwoSum error before scaling, as
fpu_fma32 itself does: odd rounding at 53 bits composes correctly with
any narrower final rounding (53 >= 24 + 2), so the scaled conversion
yields the true unbounded-exponent result. Also tighten the f64 bounds
comment (|a| <= 2^158, per review).

Signed-off-by: Sol Astrius Phoenix <sol@astrius.ink>
Signed-off-by: Sol Astrius Phoenix <sol@astrius.ink>
@purplesyringa

purplesyringa commented Jul 20, 2026

Copy link
Copy Markdown
Collaborator

LGTM on everything except the fpu_fma32 UF handling, I still don't understand why we have some complicated logic here.

Say, consider the example in the commit message. We have an exact result of 2^-126 - 2^-151 = 2^-127 * (1 + (1 - 2^-24)). Round-to-nearest with an unbounded exponent produces 2^-127 * (1 + (1 - 2^23)), which is below the smallest denormal, and bounded rounding produces 2^-126, which is not tiny, so yes, setting tiny from the value already rounded to f32 is incorrect, as the commit description correctly mentions.

But we never used sum rounded to f32 for calculating tiny, we first multiplied it by 2^126, so there was never a situation where the exponent in this rounding would get bounded. And even if it did, we can just compare sum to 2^-126 while staying in f64 -- in fact, that should be faster and cleaner.

So I don't see why you're piling on more logic instead of removing it. See also: the two unresolved review comments on this topic, in particular I'm wondering if other operations, like addition or f64-to-f32 downcasting, should also get special handling for UF, and if so, if 32-bit FMA UF handling would look simpler if it was based on that logic.

Per review: instead of reconstructing the unbounded 24-bit rounding
through odd rounding, scaling and a conversion, compare the 53-bit sum
directly against the magnitude below which that rounding falls under
2^-126 -- the to-nearest midpoint 2^-126 - 2^-151 (exclusive; an exact
tie there resolves up), 2^-126 - 2^-150 inclusive when rounding away
from zero, and 2^-126 exclusive when rounding toward zero.

The sum alone is ambiguous only when it lands exactly on the to-nearest
threshold, where the exact TwoSum error breaks the tie -- and round-to-
nearest is precisely where 2Sum is exact, so the directed-rounding 2Sum
caveat is never in play: in the directed modes the sum errs in the same
direction as the rounding, making the plain compare already conclusive.

Covers the same below-midpoint counterexample as before, plus new
vectors for the exact midpoint (err == 0), the away-inclusive threshold
under RNE and RUP, and the from-above RDN case.

Signed-off-by: Sol Astrius Phoenix <sol@astrius.ink>
@SolAstrius

Copy link
Copy Markdown
Contributor Author

In b3a3f3a I have introduced a regression; fixing it.

== b17bfc5 (fpu_lib: set the FMA underflow flag by IEEE a)
  F-fcvt.l.s-00 PASS
  F-fcvt.s.w-00 PASS
  F-fcvt.wu.s-00 PASS
== 1587da8 (fpu_lib: clarify the integral-rounding edge c)
  F-fcvt.l.s-00 PASS
  F-fcvt.s.w-00 PASS
  F-fcvt.wu.s-00 PASS
== b3a3f3a (riscv_fpu: share static-rm handling between t)
  F-fcvt.l.s-00 FAIL
  F-fcvt.s.w-00 FAIL
  F-fcvt.wu.s-00 FAIL
== bd053f0 (fpu_lib: decide FMA32 tininess by an f64 thre)
  F-fcvt.l.s-00 FAIL
  F-fcvt.s.w-00 FAIL
  F-fcvt.wu.s-00 FAIL

Gating the RMM preparation on the effective mode let it fire for ops
outside its add/sub/mul/div cases, where the default branch derived the
rounding direction from rs1 reinterpreted as f64 and left that mode on
the host with nothing restoring it: a static-rmm fcvt would poison the
tracked mode for every later dynamic-rm op in the same frm block
(F-fcvt.l.s/s.w/wu.s regressions). Make the default case a no-op -- sqrt
has no exact ties to synthesize, and ops taking rm as an argument
handle RMM natively in the integral rounder -- and have the static-rm
enter always report a mode to restore, so a mode change made by the op
itself (the RMM preparation) is undone by leave.

Net effect beyond fixing the regression: fcvt and fsqrt under RMM (both
static and dynamic) now round exactly instead of inheriting the
directed hack; F goes 16->21 and D 45->51 on the ACT4 suite with no new
failures.

Signed-off-by: Sol Astrius Phoenix <sol@astrius.ink>
@SolAstrius

Copy link
Copy Markdown
Contributor Author

Switched to the f64 compare in bd053f0; the per-mode thresholds are in the comment, the rest is in the two threads. One correction to the walk-through: 2^-126 - 2^-151 in the commit message is the midpoint, not the exact result (ambiguous wording, mine). The failing input sits just below it — e.g. 2^-126 - 2^-151 - 2^-197, whose sum collapses onto the midpoint exactly — which is why comparing sum alone needs the TwoSum tie-break there, and only there. Done committing on this one.

@SolAstrius
SolAstrius marked this pull request as ready for review July 20, 2026 17:44
Comment thread src/cpu/riscv_fpu.c
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants