Skip to content

Fix ofRectangle::operator!= disagreeing with operator== - #8567

Merged
danoli3 merged 1 commit into
openframeworks:masterfrom
Arthur031221:bugfix-ofrectangle-equality-operators
Oct 3, 2026
Merged

danoli3 merged 1 commit into
openframeworks:masterfrom
Arthur031221:bugfix-ofrectangle-equality-operators

Conversation

@Arthur031221

Copy link
Copy Markdown
Contributor

operator== compares x/y/width/height with ofIsFloatEqual (epsilon tolerant), but operator!= compared the same fields with raw !=. For two rectangles whose fields differ by less than the epsilon (e.g. a width of 1.0f vs std::nextafter(1.0f, 2.0f), a one ULP difference), both a == b and a != b evaluate true for the same pair. Anything that relies on == and != being complements (containers, dedup logic, assertions) gets a contradictory answer for ofRectangle objects built from accumulated floating point math, which is a routine case (scaling, translation, layout).

An older closed issue about ofVec3f/ofVec4f operator==/operator!= relying on raw float comparisons shows how this happened: a 2016 pull request switched operator== for ofRectangle to ofIsFloatEqual for the same reason, and the maintainer's comment closing that issue says the equivalent fix was judged not worth making in ofVec3f/ofVec4f because those types were being deprecated, but that "it has been fixed in ofRectangle and other core classes." operator!= was never updated to match operator==, so it kept comparing the raw fields.

operator!= now returns !(*this == rect) in libs/openFrameworks/types/ofRectangle.cpp, so the two operators can't disagree by construction.

Added tests/types/ofRectangleTests/, modeled on the existing tests/types/parameters project, using the ofxUnitTests addon convention used elsewhere under tests/. It checks:

  • identical rectangles compare equal and not-unequal
  • a rectangle vs. a copy with width bumped by one float ULP: equal under ==, not unequal under != (the regression case)
  • a clearly different rectangle compares unequal and not equal

Tested two ways. First, a standalone compile of the real ofRectangle.cpp in isolation (no engine build, no mocks) through a before/fix/revert cycle on the ULP pair: before the fix the binary printed a == b : true / a != b : true, after the fix a == b : true / a != b : false, and reverting the source reproduced the original output. Second, the exact six assertions in tests/types/ofRectangleTests/src/main.cpp were run against the same isolated build of ofRectangle.cpp: all six pass against the fixed source, and the ULP-case assertion ("operator!= must be the logical negation of operator==") is the only one that fails against the original source, with the other five passing in both cases. The new test project itself was not run through the full openFrameworks engine build locally; it follows the structure of the CI-validated tests/types/parameters project and will be built and run by the project's own CI, which builds and runs every directory under tests/.

Platform tested: Linux (isolated unit compile of ofRectangle.cpp with g++, not the full OF build).

operator== compares x, y, width and height with ofIsFloatEqual
(epsilon tolerance), but operator!= compared the same fields with the
raw != operator instead of negating operator==. For a pair of
rectangles whose fields differ by less than the epsilon tolerance (for
example by a single float ULP), operator== returned true while
operator!= also returned true for the same pair, violating the basic
invariant that the two operators must be logical complements.

An older closed issue about ofVec3f/ofVec4f operator==/operator!=
relying on raw float comparisons shows how this happened: a 2016 pull
request switched ofRectangle's operator== to ofIsFloatEqual for the
same reason, and the equivalent fix was later judged not worth making
in ofVec3f/ofVec4f because those types were being deprecated, but
operator!= in ofRectangle was never updated to match operator==,
leaving it comparing the raw fields.

Fix operator!= to simply negate operator==, and add a regression test
exercising both the ULP case and a clearly different pair of
rectangles.
@danoli3
danoli3 self-requested a review October 3, 2026 04:57
@danoli3

danoli3 commented Oct 3, 2026

Copy link
Copy Markdown
Member

You are correct the floating point math compare is a problem in this case. Interesting using the ptr path, I'll analyse the code and run some further tests

@danoli3 danoli3 added the core label Oct 3, 2026
@danoli3

danoli3 commented Oct 3, 2026

Copy link
Copy Markdown
Member

I verified PR #8567: the fix is correct. On master, == and != give contradictory answers in about 166k of 4M comparisons; with the PR they never do.

Tested: I compiled the real ofRectangle.cpp from master and from the PR against the actual oF headers. I stubbed only the logging and ofMap symbols it links against. Then I ran the same harness on both builds, checking two things for every pair:

  • != is exactly the opposite of ==;
  • the result is what I'd expect.

Screenshot 2026-10-03 at 3 03 35 pm

What I covered:

  • Identical and clearly different rectangles: all correct on both.
  • 1-ULP float differences: values just above and below 1, 100, 1920, -3.5 and FLT_MAX. All of them contradict on master and are consistent with the PR.
  • Integers: 0, ±1, 255, 1080, 1920, 3840, 65535, ±1e6, INT_MAX, INT_MIN, plus "int vs int+1". All correct on both.
  • Large integers: past 2^24 (16,777,216), neighbouring ints are no longer distinct floats. On master, 2^24 vs 2^24+2 and 1e8 vs 1e8+8 each came out both equal and unequal; with the PR they're consistently equal.
  • Zero and tiny values: +0 vs -0, the smallest float, FLT_MIN, 1e-7. All correct on both.
  • Accumulated math: after scale(1.1) then scale(1/1.1), master says the result is both equal and unequal to the original; with the PR it's consistently equal.
  • Random fuzz: 2M pairs, made of exact copies, copies nudged by 1–3 ULP, random floats in ±1e6 and random ints. 166,418 contradictions on master, 0 with the PR.

Behaviour changes to know about. These all come from the existing ofIsFloatEqual that == uses; the PR doesn't introduce them, but != now inherits them:

  1. Infinity. A rectangle with an infinite field is not == to itself, because inf − inf is NaN. So with the PR, r != r becomes true (on master it was false). Also +inf == -inf is true on both.
  2. The tolerance is very small. It's eps × |a|, about one ULP. Ten translate(0.1) calls followed by translate(-1) drift about 4 ULP, and both versions call that unequal. So the PR description's point about "accumulated floating point math" holds only for 1–2 ULP of error.
  3. Large coordinates. != now has the same tolerance as ==, so very large rectangles that differ by a single float step (e.g. 1e8 vs 1e8+8) are no longer reported as different. That's intended consistency, but it is a change in behaviour.

@danoli3

danoli3 commented Oct 3, 2026

Copy link
Copy Markdown
Member

I think this is good to go, tests really verified it and historical fix to fix the same

@danoli3
danoli3 merged commit 86a27df into openframeworks:master Oct 3, 2026
17 of 20 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants