From 8e34e4569e86ff016d5258ec08f520c36352b460 Mon Sep 17 00:00:00 2001 From: GameDirection Date: Sat, 22 Aug 2026 18:52:01 -0400 Subject: [PATCH] Fix infinite recursion in bezier stroke hit-testing at extreme zoom d2d_point_on_bezier_segment() recurses on itself to subdivide a stroked bezier curve until its approximation error drops below tolerance. It had no recursion depth limit, unlike the sibling function d2d_figure_add_cubic_bezier_recursive(), which already caps at depth 16. At extreme zoom levels, the transform matrix scale grows large enough that the transformed coordinates this function computes exceed float32's mantissa precision (2^23). Past that point the error estimate no longer shrinks as the curve subdivides, so the function recurses without bound instead of terminating. Root caused and reported by DenisJosifoski in seapear/AffinityOnLinux issue #134 (affinity apps freeze completely at extremely high zoom on Wine, first observed on an NVIDIA hybrid Fedora Wayland setup). They measured the exact freeze point at 8,232,116% zoom, matching the 2^23 float32 mantissa limit almost exactly. Adds the same depth cap the sibling function already uses. Past that depth the curve is treated as not on the stroke rather than looping indefinitely, the same fallback direction (false) as the function's own two fast-exit checks already use for definitely-not-on-curve cases. Verified geometry.c compiles cleanly with this change (0 new errors or warnings). Could not verify the full d2d1.dll build end to end, device.c fails to compile against this machine's Wine headers with or without this change, a pre-existing, unrelated issue. --- WineFix/lib/d2d1/src/geometry.c | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/WineFix/lib/d2d1/src/geometry.c b/WineFix/lib/d2d1/src/geometry.c index 1a8d01c..af7ab0b 100644 --- a/WineFix/lib/d2d1/src/geometry.c +++ b/WineFix/lib/d2d1/src/geometry.c @@ -577,12 +577,24 @@ static BOOL d2d_point_on_line_segment(const D2D1_POINT_2F *q, const D2D1_POINT_2 * also done. If neither of those is the case, we subdivide the Bézier segment * and try again. */ static BOOL d2d_point_on_bezier_segment(const D2D1_POINT_2F *q, const D2D1_POINT_2F *p0, - const D2D1_BEZIER_SEGMENT *b, const D2D1_MATRIX_3X2_F *transform, float stroke_width, float tolerance) + const D2D1_BEZIER_SEGMENT *b, const D2D1_MATRIX_3X2_F *transform, float stroke_width, float tolerance, + unsigned int depth) { float d1, d2, d3, d4, d, l, m, w, w2; D2D1_POINT_2F t[7], start, end, v_p; D2D1_BEZIER_SEGMENT b0, b1; + /* At extreme zoom levels the transformed coordinates below can exceed the float32 + * mantissa's precision (2^23), so the error estimate never shrinks below tolerance + * and this function would otherwise recurse forever. Cap the depth like the sibling + * cubic-to-quadratic subdivision function does, and treat a still-ambiguous result + * at max depth as "not on the curve" rather than looping indefinitely. */ + if (depth > 20) + { + WARN("Maximum recursion depth reached for bezier hit-testing - treating as not on curve.\n"); + return FALSE; + } + m = 1.0f; w = stroke_width * 0.5f; @@ -733,8 +745,8 @@ static BOOL d2d_point_on_bezier_segment(const D2D1_POINT_2F *q, const D2D1_POINT d2d_point_lerp(&b0.point2, &t[0], &b0.point1, 0.5f); d2d_point_lerp(&b0.point3, &b0.point2, &b1.point1, 0.5f); - return d2d_point_on_bezier_segment(q, p0, &b0, transform, stroke_width, tolerance) - || d2d_point_on_bezier_segment(q, &b0.point3, &b1, transform, stroke_width, tolerance); + return d2d_point_on_bezier_segment(q, p0, &b0, transform, stroke_width, tolerance, depth + 1) + || d2d_point_on_bezier_segment(q, &b0.point3, &b1, transform, stroke_width, tolerance, depth + 1); } static void d2d_rect_union(D2D1_RECT_F *l, const D2D1_RECT_F *r) @@ -3856,7 +3868,7 @@ static HRESULT STDMETHODCALLTYPE d2d_path_geometry_StrokeContainsPoint(ID2D1Path b.point1 = figure->original_bezier_controls[bezier_idx++]; b.point2 = figure->original_bezier_controls[bezier_idx++]; b.point3 = figure->vertices[j]; - *contains = d2d_point_on_bezier_segment(&point, &p, &b, transform, stroke_width, tolerance); + *contains = d2d_point_on_bezier_segment(&point, &p, &b, transform, stroke_width, tolerance, 0); p = b.point3; break;