Clipper2: FP exception in GetIntersectPoint()
Executing the following code results in a floating point exception in function Point64 GetIntersectPoint(const Active& e1, const Active& e2). Namely, when a double value is cast to int64_t, it appears to be outside the range of int64_t.
The coordinates of the polygon vertices are within the range -4.6e18 … 4.6e18. According to the documentation, that is a valid input.
#include <stdio.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
#include <fenv.h>
#include <glob.h>
#include "clipper2/clipper.h"
using namespace Clipper2Lib;
#define ADD(A,X,Y) A.push_back(Point64(int64_t(X), int64_t(Y)))
int main(int, char**) {
feenableexcept( FE_DIVBYZERO | FE_INVALID | FE_OVERFLOW );
Path64 a, b;
ADD(a, -7009388980LL, 1671079308836362LL);
ADD(a, -576460711222920704LL, 1671063793410802LL);
ADD(a, -864690986154904320LL, -286977111392363424LL);
ADD(a, -1152921411648951168LL, -575625207815834176LL);
ADD(a, -864691057648438912LL, -864273328105026304LL);
ADD(a, -576460762799345152LL, -1152921434302619648LL);
ADD(a, -12880478468LL, -1152921451122536832LL);
ADD(a, 576460787720869760LL, -1152921504606846976LL);
ADD(a, 864691268131862400LL, -864273433561362176LL);
ADD(a, 1152921504606846848LL, -575625211080225088LL);
ADD(a, 864691062448491520LL, -286977010832613792LL);
ADD(a, 576460654512679296LL, 1671130833237401LL);
ADD(b, -54234486065476976LL, 1151250415789406208LL);
ADD(b, -612617068314194048LL, 1152921504606846976LL);
ADD(b, -864691197085273984LL, 867615548203339008LL);
ADD(b, -1152921504606846848LL, 578967376389736064LL);
ADD(b, -864691140504451968LL, 290319223463759488LL);
ADD(b, -576460711222912256LL, 1671063793410802LL);
ADD(b, -7009388980LL, 1671079308836362LL);
ADD(b, 576460654512679296LL, 1671130833237401LL);
ADD(b, 864690908098943872LL, 290319324023499392LL);
ADD(b, 1100313577101746304LL, 576428327042791872LL);
ADD(b, 785779376874207360LL, 863806873623168128LL);
ADD(b, 487696647658790656LL, 1150382388220066304LL);
const int m = 10;
for(size_t i=0; i<a.size(); i++) { a[i].x /= m; a[i].y /= m; b[i].x /= m, b[i].y /= m; }
Paths64 AA; AA.push_back(a);
Paths64 BB; BB.push_back(b);
Paths64 solution = Intersect(Paths64(AA), Paths64(BB), FillRule::NonZero);
}
Division by ten is not essential.
About this issue
- Original URL
- State: closed
- Created 2 years ago
- Comments: 144 (75 by maintainers)
Commits related to this issue
- Fixed overflow error in GetIntersectPoint (#317) — committed to AngusJohnson/Clipper2 by AngusJohnson 2 years ago
- Fixed integer overflows (#317) Other minor tweaks :) — committed to AngusJohnson/Clipper2 by AngusJohnson 2 years ago
- bugfixes for GetIntersectPoint() (#317) __cpp_exceptions to enable exceptions (#305) — committed to AngusJohnson/Clipper2 by AngusJohnson 2 years ago
- further bugfixes for GetIntersectPoint() (#317) — committed to AngusJohnson/Clipper2 by AngusJohnson 2 years ago
- additional tweaks to GetIntersectPoint() (#317) — committed to AngusJohnson/Clipper2 by AngusJohnson 2 years ago
@AngusJohnson I checked your last version of GetIntersectPoint. It throws an exception in the following example.
I’m getting seriously off-topic, but thanks for everything you do.
I actually admire that kind of dedication; I also would have a ton of code to share, but I guess I’m just scared of getting stuck doing tech support (or I don’t want that extra source of stress on top?). Either way, thanks, it’s well appreciated. I applaud you.
I had to look that up, it appears the Microsoft compiler doesn’t support a 128 bits integers type. But they have the intrinsics _mul128(), _udiv128(), __shiftright128(), _addcarry_u64(), _subborrow_u64(), __lzcnt64() which could be used to implement everything.
So it could be done… although with a
#if
for MSVC, another for gcc/clang, and a generic fallback.Here’s BP5 and BP6:
The difference between the two is just rounding occurring after the division (PB5) or before (PB6).
Hey, I improved your version a little bit:
It’s not longer checking if the intersection point is within the two edges, I suspect Clipper2 wants the intersection point between the two vectors; it already decided that they intersect when that function is called.
The extra rounding corrects these tiny errors:
Hey, don’t laugh at my 1024 bits solution! It’s fast, I’m telling you. 😁
As this saga still doesn’t want to end, as I was really tired of never knowing what the proper answers were, I wrote an intersection test using my 1024 bits math library (all written in amd64 assembly, yarr).
The BN1024 results are absolutely correct, the two cases above:
Only PB3 gets it right. The difference of 1 is due to a lack of rounding in PB3(), easily fixed.
The intersection point between the two vectors is actually outside of the line0 segment, the segments don’t intersect. PB3 returned a failure because this check fails:
and that’s quite correct, the hit is actually outside the line0 range (alpha_num is negative).
My last example yields
Rounding the input coordinates can’t affect so much. Assuming that the last result is correct, the error of the first intersection area is 6e+22. For the initial data within +/- 1e+14 range, I consider this result to be inaccurate.
Well, I guess that’s the fastest version we got, with perfect accuracy:
It requires input to be within INT64_MAX/2 range, and assumes that detecting output hit overflow is pointless as it can’t happen. My only concern is that some compilers might output a ton of branches instead of conditional moves.
I do have a SSE 4.2 version, but it’s only 38% faster (_mm_cmpgt_epi64/_mm_blendv_epi8 is not really better than cmp/cmov/cmov), assuming you compile with -ffast-math (to output one divsd instead of two).
EDIT: And the second place is only 5% slower, without any risk of a compiler emitting branches:
Robustness is a given regardless of the degree of precision required. And that’s why I was chasing so hard the overflow errors above. And precision is important too for everyone, up to a point it unacceptably compromises performance. And yes I am now considering a compiler switch as evidently some (?very few) like you do require extremely high (> 1.0e12) degrees of precision.
Hey Angus. Well, perfect accuracy is achievable without special floating point types, it just needs code that’s a bit more complicated… and slower, yes.
Perhaps offer a compilation-time switch then? Some people need speed, other people need accuracy and robustness above all (bahvalo above seems to require accuracy ~ so do I; my computations take hours/days, the result is useless if incorrect, and I sure want to switch to Clipper2 eventually).
Well, I think I’ll cook up a quick SSE optimized version, and without requiring AVX-512 (int64_t<->double is awkward without _mm_cvtepi64_pd() and _mm_cvtpd_epi64(), but I’ll figure out something). Perhaps a 2-3x speed boost will make you change your mind? 😉
Thanks. I will have a good look again, I just need a day or two to enjoy the weekend and recharge.
@alexisnaveros
It is true for 387 ISA and not for SSE2. I believe you hardly would pick Pentium2 nowadays for these tasks.
Compiled with
g++ -mfpmath=sse -std=c++17 -O1
Output:Yep, see my edit above.😁
Thank you Alexis. That’s very helpful and might solve this problem.