Guard geometry state writes against sub-pixel bounds jitter to prevent idle recomposition loops - #499
Open
Aecasorg wants to merge 2 commits into
Open
Guard geometry state writes against sub-pixel bounds jitter to prevent idle recomposition loops#499Aecasorg wants to merge 2 commits into
Aecasorg wants to merge 2 commits into
Conversation
|
Thank you for your pull request and welcome to our community. We could not parse the GitHub identity of the following contributors: Henrik Gustavii.
|
…t idle recomposition loops Global-position callbacks can deliver bounds differing from the previous layout pass by sub-pixel amounts. GeometryReader, onGeometryChangeErased, and PresentationRoot write those raw Rects into remembered state that gates their composed content, closing a write -> recompose -> remeasure -> write loop that recomposes continuously at idle (skiptools#488). Adds Rect.isApproximatelyEqual(to:) (edges within 0.5px) and guards the three gating state writes. The shared onGloballyPositionedInRoot/InWindow helpers still deliver every callback so derived-value consumers (e.g. safe-area edge probing) keep their semantics; their docs now carry a warning for bounds-gating callers. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Aecasorg
force-pushed
the
geometry-epsilon-guard
branch
from
July 30, 2026 08:10
976ad32 to
d8d5f6a
Compare
|
Thank you for your pull request and welcome to the Skip community. We require contributors to sign our contributor license agreement (CLA), and we don't seem to have the user(s) @Aecasorg on file. In order for us to review and merge your code, for each noted user please add your GitHub username to Skip's .clabot file |
Author
|
recheck |
…les to a form Kotlin cannot compare against Float Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Aecasorg
force-pushed
the
geometry-epsilon-guard
branch
from
July 31, 2026 14:05
d8d5f6a to
f8ec125
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #488.
Problem
GeometryReader,onGeometryChange, andPresentationRootwrite raw floatRectbounds from global-position callbacks into remembered state that gates their composed content. Bounds can differ from the previous layout pass by sub-pixel amounts (float rounding, scroll settle, in-progress animations, or content that sizes itself against the reported geometry), andmutableStateOf's structural equality only deduplicates bit-identical rects — so a sub-pixel delta closes a write → recompose → remeasure → write loop that recomposes continuously while the screen is idle. We hit this as a sustained idle-recomposition loop in our production Fuse app (skip-ui 1.57-era): aGeometryReader-centered empty state recomposed continuously at rest, and scoping theGeometryReaderto a smaller subtree removed the trigger — which limits blast radius but leaves the unguarded write in place. Full mechanism citations in #488.Change
Adds
Rect.isApproximatelyEqual(to:)(all four edges withinboundsEpsilonPx = 0.5f— half a device pixel, below anything visually meaningful) and guards the three gating state writes with it:GeometryReader.Render—rememberedGlobalFramePxonGeometryChangeErased—globalFramePxPresentationRoot—presentationBoundsDeliberately not changed: the shared
onGloballyPositionedInRoot/InWindowhelpers still deliver every callback. Deduplicating there would silently change semantics for callers whose derived values depend on more than the bounds — e.g. the safe-area edge probing inComposeLayouts.swiftrecomputes edges from bounds plus the current safe area, and must not be suppressed when the safe area changes while bounds don't. Instead the helpers' doc comment now warns that callers gating content on remembered bounds should guard their writes.The comparison intentionally avoids
abs()— the transpiledskip.libform is not comparable against a KotlinFloat(caught by the Android Kotlin compile).Verification
Verified with CI-built APKs of a standalone counter-instrumented app (https://github.com/Aecasorg/skip-fuse-perf-repro), on an API 35 emulator, comparing stock skip-ui 1.57.0, stock 1.59.1, and 1.59.1 + this patch:
Happy to adjust the epsilon, naming, or approach — including pushing the guard into the shared helpers instead if you'd rather own the call-site audit.