Repository navigation
Let Skia cache the mask of a blurred Android inset box shadow - #59083
YevheniiKotyrlo wants to merge 4 commits into
Conversation
Device measurement — before / after#59081 alone — every frame of an animated background renders each blurred inset shadow's mask offscreen and blurs it: 530 render passes per frame for 150 views on a Galaxy S23. This PR — 37: Skia draws all but seven of the masks from its cache each frame.
RNTester release builds for SceneThe Device evidence — before / after#59081 alone — a blurred inset shadow drawn as a double round rect. This PR — the same shadow drawn from its cached even-odd path: the pixels that differ all lie inside the blur, by at most 4 of 255 per channel.
Galaxy S23 (Android 14, API 34), 1080×2340 at density 480, the scene above with |
5d65b93 to
e32e6b2
Compare
InsetBoxShadowDrawable and OutsetBoxShadowDrawable resolved the border radii and built new RectF, Path and radii arrays inside draw(), so every redraw of a view's background, every frame of an animation that touches it, allocated them again and handed the canvas a new Path. Both drawables now keep that geometry in fields and rebuild it only when its inputs change: on a bounds or layout direction change, and when borderRadius or borderInsets is assigned. An alpha or color filter change only redraws.
e32e6b2 to
59b6ad1
Compare
The box-shadow drawables cache their geometry, which depends on the screen density and, in RTL, on the left/right swap preference, but rebuilt it only on a bounds or layout direction change and when borderRadius or borderInsets was assigned. Neither the density, which changes in place for an activity that handles density configuration changes, nor I18nManager.swapLeftAndRightInRTL() notifies a drawable, so one drawn again afterwards kept the old geometry. Each drawable now records the density and swap it built its geometry with and rebuilds it when either differs.
59b6ad1 to
aedd9a6
Compare
The geometry check read the left/right swap preference on every RTL draw, also for a shadow with no border radius and no border insets, whose geometry never reads it. It now reads it only when the drawable has a border radius, or border insets for an inset shadow, as resolving them does.
Visual regressionNo visual differences at ios: no capture |
A blurred inset shadow is drawn with drawDoubleRoundRect, and Skia turns a double round rect with a mask filter into a new volatile path on every draw, which it never caches: the blurred mask is rendered again, offscreen, every time the view is drawn. Draw a blurred inset shadow from a cached even-odd path instead, so Skia can key its blurred mask on the path, the blur and the matrix. An unblurred shadow keeps drawDoubleRoundRect, which Skia draws analytically. A spread past half the padding box's height now empties the shadow's inner shape, as one past half its width did: the inner rect was left inverted, which drawDoubleRoundRect sorts into a hole the shape does not have and the path drew without one.
aedd9a6 to
7dafeab
Compare


Summary:
InsetBoxShadowDrawabledraws its shadow withdrawDoubleRoundRect. With a blur, Skia's GPU backend draws a double round rect as a new volatile even-odd path, and it never caches the blurred mask of a volatile path, so a blurred inset shadow renders its mask offscreen, and blurs it, every time the view draws.A blurred inset shadow now draws an even-odd path the drawable builds with the rest of its geometry, so Skia keys the blurred mask on the path, the blur and the matrix, and draws it from its cache on the next frame. An unblurred inset shadow keeps
drawDoubleRoundRect, which Skia draws analytically. The path is rebuilt with the rest of the geometry, when the bounds, the border radius, the layout direction, the density or the RTL left/right swap preference changes.A spread larger than half the padding box's height now empties the shadow's inner shape, as one larger than half its width already did. There the inner rect came out with its top below its bottom:
drawDoubleRoundRectsorts it and cuts it out as a hole, where the even-odd path would not, so the blurred and the unblurred shadow would differ. Both now fill the padding box, as Blink does for an empty inset shadow shape.This builds on the cached geometry from #59081 and includes its commits; #59097 builds on it.
Changelog:
[ANDROID] [FIXED] - Blurred inset box shadows no longer render their mask offscreen on every draw
Test Plan:
BoxShadowDrawableTestgains nine cases: a blurred rounded and a blurred rectangular inset shadow hand the canvas one even-oddPath, the same instance on every draw (on #59081 they draw no path at all); an unblurred inset shadow still draws a double round rect and no path; a blurred inset shadow follows bounds growing from empty, a radius changed in place and reassigned, a density change and the swap preference turned off in RTL, compared with a freshly built one; and an inset shadow whose spread passes half its height draws like one whose spread passes its width, blurred and unblurred (the unblurred case fails without the height check)../gradlew :packages:react-native:ReactAndroid:testDebugUnitTest -x :packages:react-native:ReactAndroid:buildCodegenCLI -Preact.internal.useHermesStable=true -PenableWarningsAsErrors=true --rerun --tests com.facebook.react.uimanager.drawable.BoxShadowDrawableTest: 30 tests, 0 failuresjava -jar node_modules/ktfmt/lib/ktfmt.jar --do-not-remove-unused-imports --dry-run --set-exit-if-changedon the changed files: cleanOn device (evidence), RNTester release builds of #59081 and of this branch, 150 views with an animated background and
inset 0 2px 6px: Skia's render passes per frame, counted as GaneshOpsTasks in a Perfetto trace, fall from 530 to 37 on a Galaxy S23 (Android 14, API 34, Adreno 740), and the median frame from 21.0 to 13.6 ms there and from 395.6 to 19.5 ms on a Mi MIX 3 (Android 10, API 29, Adreno 630). The no-shadow control stays level, and a still frame differs only inside the blur, by at most 4 of 255 per channel (the before/after pair is in the same comment). Those builds predate two later changes: #59081's density and RTL swap check rebuilds nothing while neither changes, and the height check applies only to a spread, which these shadows don't have.I didn't run the full Android
build, the template and HelloWorld app builds, or the Android e2e jobs locally.