Skip to content

Let Skia cache the mask of a blurred Android inset box shadow - #59083

Open
YevheniiKotyrlo wants to merge 4 commits into
react:mainfrom
YevheniiKotyrlo:perf/android-inset-box-shadow-blur-mask-cache
Open

YevheniiKotyrlo wants to merge 4 commits into
react:mainfrom
YevheniiKotyrlo:perf/android-inset-box-shadow-blur-mask-cache

Conversation

@YevheniiKotyrlo

@YevheniiKotyrlo YevheniiKotyrlo commented Oct 14, 2026 •

Copy link
Copy Markdown
Contributor

Summary:

InsetBoxShadowDrawable draws its shadow with drawDoubleRoundRect. 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: drawDoubleRoundRect sorts 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:

BoxShadowDrawableTest gains nine cases: a blurred rounded and a blurred rectangular inset shadow hand the canvas one even-odd Path, 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 failures
  • java -jar node_modules/ktfmt/lib/ktfmt.jar --do-not-remove-unused-imports --dry-run --set-exit-if-changed on the changed files: clean

On 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 Ganesh OpsTasks 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.

@YevheniiKotyrlo

YevheniiKotyrlo commented Oct 14, 2026 •

Copy link
Copy Markdown
Contributor Author

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.

#59081 this PR
Galaxy S23 (Android 14, API 34, Adreno 740) — frame 21.0 ms 13.6 ms
Galaxy S23 — Skia render passes per frame 530 37
Mi MIX 3 (Android 10, API 29, Adreno 630) — frame 395.6 ms 19.5 ms
control: no shadow, S23 / Mi MIX 3 8.9 / 18.5 ms 8.9 / 19.0 ms

RNTester release builds for arm64-v8a of #59081's head and of this branch, median of five 6 s dumpsys gfxinfo framestats windows per arm; render passes are Ganesh OpsTask executions in a 3 s Perfetto capture with debug.hwui.skia_atrace_enabled. The static frame differs only inside the shadows' blur, by at most 4 of 255 per channel on the S23 and 1 on the Mi MIX 3.

Scene

The BoxShadowStress scene from #59081's measurement, with boxShadow set to 'inset 0 2px 6px #7c3aed99', or null for the control.

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.

before — #59081 after — this PR

Galaxy S23 (Android 14, API 34), 1080×2340 at density 480, the scene above with inset 0 2px 6px #7c3aed99 and no animation, cropped to 16 of the 150 views. Below the status bar, 553,203 pixels of the full frame differ, by at most 4 of 255; on the Mi MIX 3, 44,368 differ, by at most 1.

@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. label Oct 14, 2026
@YevheniiKotyrlo
YevheniiKotyrlo force-pushed the perf/android-inset-box-shadow-blur-mask-cache branch from 5d65b93 to e32e6b2 Compare October 18, 2026 22:50
@YevheniiKotyrlo
YevheniiKotyrlo marked this pull request as ready for review October 18, 2026 23:04
@facebook-github-tools facebook-github-tools Bot added the Shared with Meta Applied via automation to indicate that an Issue or Pull Request has been shared with the team. label Oct 18, 2026
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.
@YevheniiKotyrlo
YevheniiKotyrlo force-pushed the perf/android-inset-box-shadow-blur-mask-cache branch from e32e6b2 to 59b6ad1 Compare November 11, 2026 18:05
@YevheniiKotyrlo
YevheniiKotyrlo marked this pull request as draft November 13, 2026 17:21
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.
@YevheniiKotyrlo
YevheniiKotyrlo force-pushed the perf/android-inset-box-shadow-blur-mask-cache branch from 59b6ad1 to aedd9a6 Compare November 13, 2026 19:33
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.
@github-actions

github-actions Bot commented Nov 13, 2026 •

Copy link
Copy Markdown

Visual regression

No visual differences at 7dafeabe8066.

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.
@YevheniiKotyrlo
YevheniiKotyrlo force-pushed the perf/android-inset-box-shadow-blur-mask-cache branch from aedd9a6 to 7dafeab Compare November 13, 2026 22:08
@YevheniiKotyrlo
YevheniiKotyrlo marked this pull request as ready for review November 13, 2026 23:31

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. Shared with Meta Applied via automation to indicate that an Issue or Pull Request has been shared with the team.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant