Skip to content

Fix infinite recursion in bezier stroke hit-testing at extreme zoom - #58

Open
Gamedirection wants to merge 1 commit into
noahc3:mainfrom
Gamedirection:fix-zoom-freeze-bezier-hittest
Open

Fix infinite recursion in bezier stroke hit-testing at extreme zoom#58
Gamedirection wants to merge 1 commit into
noahc3:mainfrom
Gamedirection:fix-zoom-freeze-bezier-hittest

Conversation

@Gamedirection

Copy link
Copy Markdown

Summary

Fixes infinite recursion in bezier stroke hit-testing at extreme zoom levels, reported in seapear/AffinityOnLinux#134.

Root cause

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.

DenisJosifoski root-caused this precisely in the issue thread: they measured the exact freeze point at 8,232,116% zoom, matching the 2^23 float32 mantissa limit almost exactly, and pointed at the d2d1.dll matrix transform as the likely cause.

The fix

Adds the same depth cap the sibling function already uses (20 levels here, since this recursion halves the curve each time rather than converting cubic to quadratic). Past that depth, the curve is treated as not on the stroke rather than looping indefinitely, the same fallback direction (FALSE) the function's own two fast-exit checks already use for definitely-not-on-curve cases.

Test plan

  • geometry.c compiles cleanly with this change, 0 new errors or warnings (verified via make TARGET=x86_64-unix in WineFix/lib/d2d1).
  • 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 (confirmed via git stash), a pre-existing, unrelated issue.
  • Not yet tested at the actual reported zoom level in a running Affinity instance, I don't have a build of the full d2d1.dll to test with on this machine. Would appreciate someone with a working WineFix build environment confirming this actually resolves the freeze.

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.
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.

1 participant