Reading the competition's source, properly¶
competitive-landscape.md is the positioning audit: who exists, what they claim, where we stand.
This document is the other half, and the one that changes code. Eight repositories are cloned and
kept, and every claim below carries the file and line it came from, so the next question has
something to consult instead of being asked again.
Read on 2026-09-11 against the versions in the table. Nothing was copied; every file quoted is
read-only analysis. Where a technique is worth adopting it is described so it can be implemented
from the description, and provenance is recorded so credit lands in NOTICE.
| Project | Version read | Licence | Stars |
|---|---|---|---|
| Kyant0/AndroidLiquidGlass | 2.0.1 (65ab177) |
Apache-2.0 | 3736 |
| chrisbanes/haze | main @ 2026-09-10 | Apache-2.0 | 2518 |
| skydoves/Cloudy | main @ 2026-09-08 | Apache-2.0 | 1266 |
| iyinchao/liquid-glass-studio | main @ 2026-09-08 | MIT | 683 |
| elyesmansour/compose-floating-tab-bar | main @ 2026-06-07 | Apache-2.0 | 156 |
| QWEA0/Liquid-Glass-Android | v2.0.10 | MIT | 116 |
| styropyr0/PrismalAGSL | v1.0.3 | MIT | 26 |
| 3bdul7akim/liquify | main @ 2026-09-01 | Apache-2.0 | 4 |
All eight are permissively licensed, so there is no copyleft hazard in reading them.
Four things this pass settled¶
1. The field is three lineages, not eight implementations. PrismalAGSL and liquify are
Kyant0's shader, not merely similar to it. liquify says so in its header — "Derived from Kyant0's
AndroidLiquidGlass (Apache-2.0)", liquify/…/effects/Lens.kt:2 — and Prismal does not, but the
code settles it: the same height profile 1.0 - sqrt(1.0 - x * x)
(rect_refraction_dispersion.agsl:11, Shaders.kt:55), the same inflated normal radius
min(radius * 1.5, min(halfSize.x, halfSize.y)), and the same seven dispersion taps with the
same hand-tuned /3.5 and /7.0 weights. So Kyant0's family is one vote, not three, and
the audit's "four implementations all made the same choice" overstates the independent evidence.
2. Our central claim about sampling direction is correct, and now verified three ways. This is the claim the whole library rests on, so it was worth checking hard.
- Kyant0: exactly one negation exists in the repo, at the host —
setFloatUniform("refractionAmount", -refractionAmount)(effects/Lens.kt:49), with the public parameter guarded positive. The shader'sgradSdRoundedRectreturns the outward normal and the displacement is added, so a negative amount samples inward. Two independent corroborations: their lens effect deletes the padding it would need to sample outward (Lens.kt:25-27reduces padding byrefractionHeight, which zeroes it in every catalog configuration), and their SDF-texture variant subtracts an outward normal explicitly (catalog/utils/SdfShader.kt:66). Outward sampling is not merely unused there, it is structurally impossible. - QWEA0:
refractDiris-1by default (LiquidGlassView.kt:526), bound atGlassLensRenderer.kt:522, and their own comment glosses-1as "inward sampling (default, matching iOS: inner compressed mirror)". - Cloudy:
sampleXY = xy - bend * curve * minDim * normalwith an outward normal (LiquidGlassShaderSource.kt:77,:121).
We remain the only implementation sampling outward. That is a genuine differentiator and it is safe to say so. It is also the single claim most worth falsifying with the overhang capture, because if it is wrong it is wrong at the foundation.
3. QWEA0's "pixel-measured iOS replica" does not survive the code. This matters because it
is the only competitor whose measurement claim rivals ours. The reality: colours eyedropped from
a screenshot into constants in a demo Activity (ProfessionalDemoActivity.kt:204-220), layout
coordinates hand-typed in iOS points and scaled by widthPixels / 430f (:1383), one light-axis
angle read off a screenshot (LightSourceController.kt:29-31), and one wallpaper asset that is
the backdrop, not a picture of glass. The entire test suite is three files: two IDE templates
and one real suite that tests blur energy conservation only. There is no golden image, no
pixel diff, no screenshot test, no regression guard.
Our frame-crop-and-measure workflow is a stronger method than anything in the field. The gap is that we do not assert it in tests either. Closing that is worth more than any single optical change, because it is what stops the material regressing.
4. Blur-before-refraction is universal, and it is why nobody else has our rim. Kyant0 chains
vibrancy(); blur(...); lens(...) in that order in every call site, so the lens bends an already
blurred image. QWEA0 does the same, and says so: their chain builds the blur as the inner
effect (GlassLensRenderer.kt:550-558). Their rims therefore cannot show a legible compressed
image of the surroundings, because the detail was destroyed before the warp ran. Ours refracts
sharp and scatters afterwards, keyed to rim distance. This is the second real differentiator and
it should be said far more loudly than it currently is.
haze blurs first too, and their workaround is worth understanding because it is the most
sophisticated answer anyone has: they punch a hole in the optical result at the rim
(opticalColor *= 1.0 - detailAlpha, GlassShaders.kt:186) and refill it from a second pass
that samples the unblurred capture (GlassShaders.kt:410-510). So they do recover a sharp
rim, at the cost of an extra pass, and it is off by default (refractionDetailIntensity = 0f).
Their architecture is the right one if the blur must be large, because a real separable Gaussian
destroys text where our nineteen rotated taps do not. Ours is the right one for a legible rim.
That is a genuine trade rather than a mistake on either side.
What is worth taking, ranked¶
| # | Technique | From | Cost | Value |
|---|---|---|---|---|
| 1 | Render the whole glass graph at ~0.7 scale, adaptively | haze | Mechanical | Very high |
| 2 | Shadow drawn into the backdrop, so glass refracts its own contact shadow | studio | Host change | Very high |
| 3 | Highlights composited in a perceptual space with a chroma term | studio | ~20 ALU | Very high |
| 4 | GLSL ES translation for below API 33 | Cloudy | Large | Very high |
| 5 | Analytic blended normal, smooth across the medial axis and the corner joins | haze | Medium | High |
| 6 | Snapshot-observed effect re-evaluation | Kyant0 | Small | High |
| 7 | Re-render only a patch around the pointer during a press | haze | Medium | High |
| 8 | Progressive blur as blur + alpha-mask cross-fade | Kyant0 | ~30 lines | High |
| 9 | Two-texture depth-lerp instead of a 19-tap variable blur | studio | Medium | High |
| 10 | Corner-weighted dispersion | haze | 1 multiply | Medium |
| 11 | Quintic smootherstep in place of smoothstep at the fades | haze | 2 lines | Medium |
| 12 | A fold that reverses the rim derivative, replacing the mirrored tap | haze | Medium | Medium |
| 13 | Dither the specular body | Cloudy | 3 lines | Medium |
| 14 | Screen-blend the highlight instead of adding | Cloudy | 1 line | Medium |
| 15 | A debug cap that forces older API tiers on one device | QWEA0 | ~10 lines | Medium |
| 16 | Named trace sections for systrace | haze | Small | Medium |
| 17 | Guards on degenerate uniforms and fp16 traps | Cloudy, studio | Small | Medium |
| 18 | Scroll-driven minimize and a shared-element accessory | floating-tab-bar | Medium | High for the bar |
| 19 | Velocity-driven squash and stretch | Kyant0, studio | Small | Medium |
Render fewer pixels¶
The largest performance win available, and it is mechanical rather than clever. haze renders the
entire glass graph at sqrt(0.5), about 0.707, by default — half the pixels — and drops to 0.5
under load with 12.5% hysteresis (haze-glass/.../GlassInputScalePolicy.kt:52-73). Their own
figure for the step from 0.75 to 0.5 is a 39.7% cut in median frame time and 30.7% in peak GPU
memory (docs/adr/0005-*.md:48-60). They pair it with a render budget that caps dimensions at
4096 and retained pixels at 16.7M, bisecting the scale factor when over budget.
We render at full resolution, always. Implementing this means threading a scale factor into the padded layer size and pre-multiplying every pixel uniform. Nothing about it is subtle.
Put the shadow in the backdrop¶
liquid-glass-studio composites the drop shadow into the background texture before the blur
passes and before the main pass samples it (src/shaders/fragment-bg.glsl:104-120). Three
consequences we do not get:
- The shadow is inside the refracted image, so the rim shows a compressed, bent smear of the object's own contact shadow. That is what a real lens resting on a surface does, and it is the strongest single "sitting on something" cue there is.
- Shadow and backdrop share one defocus, because the same Gaussian blurs both.
- The falloff
exp(-|merged| · height / shadowExpand)uses the absolute distance, so the shadow exists under the shape as well as around it and darkens the refracted interior.
The shadow is offset by moving the SDF centres rather than by translating a blurred alpha
(fragment-bg.glsl:106-114), so it stays correct through a merge.
We cannot do any of this: we draw elevation shadow outside the shader, and edgeShadow and
innerShadow are rim contours, not a cast shadow. This is an architectural change in the host
recording path, not a shader change, and it is the biggest single idea in the eight repositories.
Composite highlights in a perceptual space¶
liquid-glass-studio converts to CIE LCH, raises L* by 20 for the fresnel band and 150 for the
glare, raises C* by 30, then converts back (fragment-main.glsl:522-532, :562-573).
Why it matters: adding white, which is what we do, desaturates every highlight. Raising
lightness keeps the hue, and raising chroma alongside makes the highlight more saturated than
the ground beneath it, which is what a specular on a tinted transparent medium actually looks
like. Their L* clamp is 120 against a legal maximum of 100, deliberately: the core of the lobe
clips to white while the shoulder stays chromatic. That asymmetry is most of the difference
between "glowing glass" and "white overlay", and "white overlay" is a fair description of a
complaint we have.
Full CIE Lab in AGSL costs a pow(x, 2.4) per channel each way plus a cube root. Oklab is the
cheap substitute: no piecewise transfer, one cube root, about 20 ALU operations round trip, and
closer to hue-linear than CIE Lab anyway. This is the highest value-per-instruction change
available to us.
GLSL ES translation is the route below Android 13¶
The roadmap's P4 assumed the only path below API 33 was QWEA0's native one. It is not, and the alternative is better.
Cloudy translates its AGSL kernels to #version 300 es GLSL ES and runs them on a dedicated GLES
context for API 29 to 32 (internal/MirageGlslEs.kt). Their summary is that AGSL is "very close
to GLSL ES 3.0 already" and the divergences are mechanical: half/halfN to float/vecN,
floatN to vecN, uniform shader to sampler2D plus a pixel-to-UV helper, layout(color) to
plain vec4, and the entry point rewritten to a void main() writing an out vec4. The one
load-bearing trap is the Y-flip: gl_FragCoord has a bottom-left origin where every Compose
kernel assumes top-left, so fragCoord.y becomes uResolution.y - gl_FragCoord.y and the content
sampler's V must derive from that same flipped coordinate. Get it wrong and everything renders
upside down.
Execution is one process-wide GLES environment on one dedicated thread owning one EGL context,
with per-size ImageReader targets and a zero-copy readback: eglSwapBuffers pushes into the
reader and Bitmap.wrapHardwareBuffer wraps the HardwareBuffer (internal/GlEnv.kt:41-58).
Two things they learned that we would not have to: acquireLatestImage() polled without the
listener returns null, so the listener bridge is required; and the render must be awaited under a
timeout, because a wedged GL thread cannot be interrupted from native code.
Compare QWEA0's native route, which is the alternative. Their C++ is real — about 2300 lines with
a Deriche recursive IIR Gaussian, scalar and NEON, whose cost is independent of sigma and which
vectorises beautifully because RGBA8888 is exactly one float32x4_t lane group. Their own figure
is 0.3 ms for 128 by 128 at sigma 12 on a Pixel 7 (cpp/CMakeLists.txt:8-10). The blur half is
worth reimplementing from the 1993 Deriche paper. The refraction half is not: it uses a
displacement map baked from a hardcoded normalised rounded rect that ignores the view's actual
corner radius (DisplacementMapGenerator.kt:119-123), radial-from-centre normals rather than the
real gradient, a magic constant the comment admits was raised 100-fold to make the effect visible,
and an aspect correction applied only to X. Every Liquid Glass 2.0 feature silently no-ops there,
and it ships arm64 and armv7 only, so emulators and Chromebooks get an unsatisfied link.
Why this matters today. The Huawei on the desk is a JKM-LX1 on Android 9, API 28, Kirin 710,
reporting OpenGL ES 3.2. It has the hardware for this and is missing only RuntimeShader.
Two caveats: Bitmap.wrapHardwareBuffer is API 29, so API 28 needs a glReadPixels copy and will
be slower; and the readback is asynchronous, so this is a route to a correct material on old
devices, not a 120 Hz one.
Three free lessons from their native work, whatever we choose: keep NDK debug builds at -O2,
because at -O0 their NEON path measured 76 ms against 7.7 ms and made debug builds look like
9 FPS (CMakeLists.txt:47-49); set ANDROID_SUPPORT_FLEXIBLE_PAGE_SIZES=ON on NDK r27 for
Android 16's 16 KB alignment; and if we ever want outward sampling under a RenderEffect in the
View system, their layerNode workaround (GlassLensRenderer.kt:313-321, :408-419) is the
answer to Android not exposing Skia's childSampleRadius.
Let the snapshot system decide when to re-upload uniforms¶
The cleanest idea in Kyant0's codebase, and the cheapest for us to adopt. Their effects lambda is
read inside observeReads { updateEffects() } on an ObserverModifierNode
(DrawBackdropModifier.kt:350-368), so an animating value read inside that lambda invalidates
exactly the uniform upload and nothing else. Their scope's update returns true only when
density, font scale, size or layout direction changed (BackdropEffectScope.kt:38-57). They also
set shouldAutoInvalidate = false on the highlight and shadow nodes and invalidate manually.
Given the frame-rate work already done here, where a single looping animation was costing everything, this is directly relevant.
Progressive blur, cheaply¶
Kyant0's progressive blur is one uniform blur plus a vertical alpha ramp
(catalog/destinations/ProgressiveBlurContent.kt:36-62): because a copy of the backdrop is
painted over the live content, wherever the mask alpha goes to zero the unblurred original shows
through. So it is a cross-fade between one blurred and one sharp image rather than a
spatially-varying kernel. The transition region is a double exposure and reads slightly ghosted
on high-contrast content, which is the honest cost.
Ours has no equivalent at all: GlassScrollEdge is a gradient wash to the ground colour with no
blur in it. This is about thirty lines and we already own every piece.
Two textures and a depth lerp, instead of a 19-tap disc¶
getTextureDispersion (fragment-main.glsl:135-157) samples the sharp and the
pre-blurred textures at the same three dispersed offsets and lerps per channel by depth from
the rim. Six taps, constant cost, and the blur radius can be anything because it was produced by
a separable Gaussian in an earlier pass.
Our standing objection to a pre-blurred backdrop — that the detail the rim exists to bend is already gone — does not apply to this design, because they keep both textures and the rim reads the sharp one. This is the first construction seen that gets rim-sharp and interior-soft without a per-pixel variable-radius blur, and it is roughly three times cheaper than our 19-tap rotated disc while giving a true Gaussian at any radius instead of noise. It costs an extra offscreen layer. Worth measuring against ours in the rendered harness.
An analytic normal that is smooth everywhere¶
The best construction seen for a problem we spend four field evaluations on. haze's
gradSdRoundedRect (GlassShaders.kt:680-713) never differences the distance field. It sums the
four edge normals weighted by reversedSmootherstep((maxDistance - edgeDistance) / blendWidth)
and mixes toward a corner gradient, with blendWidth = max(refractionHeight, 1.0) — the blend
scale is the refraction band itself, which is exactly the intuition behind our widened epsilon,
made analytic and branchless.
On the medial axis all four weights are equal, so the sum cancels smoothly to a short vector
rather than flipping sign, and they read that vector's length as a confidence term the same way
we do: centerFade = smootherstep(clamp(gradientLength / 0.5, 0, 1)) (GlassShaders.kt:882-888).
Our axisFade is the same idea with a different threshold.
Note the constraint before anyone starts: it assumes circular corners, and we would have to
generalise it for uCornerPower. That is real work, but it removes four evaluations per pixel
and it is smooth at the corner-to-flat joins, where Cloudy's diagonal blend and Kyant0's inflated
radius are both approximations.
Re-render only what the finger touches¶
During a press, haze re-renders a patch around the pointer sized
ceil(2 * (radius + samplingPadding)) + 1, feathered back in with a smoothstep over a quarter of
the radius (GlassInteractionPatch.kt:47-74). Our touch magnifier redraws the entire panel on
every frame of a press. On a large surface that is most of the cost of interaction, and this is
the fix.
Related and cheaper: their stage-level invalidation table has the rim ignore backdrop changes
entirely (GlassStageInvalidation.kt:49-63), so a scrolling backdrop never re-records the rim.
Dispersion belongs at the corners¶
cornerWeight = |x * y| / (halfWidth * halfHeight) (GlassShaders.kt:117-121) makes fringing
vanish on the straight runs and peak at the corners, which is where a real bevel is most curved
and therefore most dispersive. Ours applies dispersion uniformly along the normal. This is one
multiply.
Kyant0's family independently arrived at the same normalised x * y product, which is some
evidence it looks right rather than merely defensible.
Quintic instead of cubic at the fades¶
haze uses smootherstep, t³(t(6t - 15) + 10), everywhere we use smoothstep
(GlassShaders.kt:616-619). It is C2 rather than C1 at the joins, which matters precisely where
a fade meets the unfaded region and a C1 join can show as a faint ring. Our bend fade is one
such place. Two lines.
A fold instead of a mirrored tap¶
Our mirrored edge band costs a full extra backdrop sample per pixel. haze gets the edge reversal
by remapping the height field instead: foldedRefractionHeightNorm collapses the normalised
height toward a small residual inside a narrow envelope in the outer quarter of the band
(GlassShaders.kt:849-865), reversing the sampling derivative without pushing displacement
through zero. Zero extra samples.
It is not the same effect as ours and it should not silently replace it, but offering a fold as
an alternative to mirror would let Chrome drop a tap on the preset that most needs the
budget.
Instrument it¶
haze carries 21 named trace sections wired into macrobenchmark metrics
(GlassTraceSection.kt:7-53). We have none, and the frame-rate work already done here was done
by measuring frame counts from the outside because there was nothing to read from the inside.
The small ones¶
- Dither. Our shader has no dithering anywhere. Cloudy adds an ordered dither of one 255th to
the specular body to break eight-bit Mach banding (
LiquidGlassShaderSource.kt:212-215), bounding the coordinate withfractbefore the hash so it cannot blow up on a large panel. A largeThicksheet with a broad specular is exactly the case that bands. - Screen blend.
pixel.rgb += (1 - pixel.rgb) * clamp(highlight, 0, 1)(LiquidGlassShaderSource.kt:220) cannot exceed 1.0 and degrades gracefully over a bright backdrop instead of clipping. - A debug API cap. QWEA0 carries
debugApiLevelCap(LiquidGlassView.kt:937), a knob that makes the renderer pretend it is on an older API so every fallback tier is reachable on one device. We have three tiers and one of them is only reachable on a phone from 2018. - Guards. Zero-width
smoothstepis implementation-defined and becomes a hard step, so clamp the width;normalizeneeds an epsilon against a zero vector;powbands in fp16 and asqrtnear one loses its significance, so both wantfloatrather thanhalf. The studio's WebGPU port had to add asafeNormalizewith a 1e-8 floor, which is an admission that its GLSL path carries a latent NaN on the medial axis. Sweep ours for all of these.
The tab bar behaviour, with a working reference¶
The roadmap lists three things Apple's tab bar does and ours does not.
compose-floating-tab-bar implements two of them in one file. The scroll behaviour is a
NestedScrollConnection that accumulates delta, resets the accumulator on a direction change,
and flips state past a threshold, returning Offset.Zero so the scroll still reaches the list
(FloatingTabBar.kt:225-272). The direction-change reset is the part that matters: without it a
wandering scroll accumulates noise and the bar flickers. The accessory is a
SharedTransitionScope element with inline and expanded forms, so the mini player physically
moves between layouts rather than cross-fading.
This lands directly on the complaint that our bottom bar is not flagship. It also passes our own
test for what earns a component: it is behaviour a modifier cannot express, so it belongs inside
GlassTabBar.
Velocity into geometry¶
Two independent implementations do the same thing. Kyant0 divides scaleX and multiplies
scaleY by a velocity term clamped to a fifth (LiquidBottomTabs.kt:270-272), in a layerBlock
with no shader cost. The studio dilates the shape extents along the axis of motion from the
spring's instantaneous velocity (App.tsx:618-631). We read velocity nowhere. This is the
"liquid" part of the name and it is cheap.
What we should defend¶
Worth stating plainly, because several of these are things we are better at than the market leader and the README currently undersells all of them.
- Outward sampling with real padding. Nobody else can show anything from outside the element.
- Exact Snell deviation with no transcendentals. Kyant0's family uses
1 - sqrt(1 - x²), a hemispherical heuristic with a vertical tangent at the rim. The studio does solve Snell but spends three transcendentals per pixel on it (fragment-main.glsl:468-471) and pairs it with an ad-hoc(x/R)²surface profile. QWEA0's C++ carries the same solve plus the ad-hoc clamp our tangent-difference identity was written to avoid. Ours is self-bounding and needs no clamp. - Refraction before scatter, keyed to rim distance. Universal difference, discussed above.
- Medial-axis handling. Kyant0 has none: their flat-run tie-break flips direction
discontinuously across the axis, and for a wide capsule their corner branch reaches
normalize(float2(0.0, 0.0)), dodged only by half-pixel sampling. TheirdepthEffectboolean is a crude version of the same idea, which is why every large demo turns it on. The studio has none either; its unnormalised gradient fades refraction and highlights together near the axis, which is functionally ouraxisFadebut implicit, unbounded, and resolution-dependent — their gradient hardcodes a 1000-pixel canvas as unit scale, so every constant they ship is tuned for one resolution. - Squircle corners and arbitrary shapes. Kyant0 is L2 only and throws for anything that is not
a corner-based shape. Their per-corner radius selection is also broken:
radiusAtis called withcoordrather than the centred coordinate (Shaders.kt:70,:107,:173,:195), and since layer-local coordinates are always positive it always returns the bottom-right radius. Every catalog usage is a capsule or a uniform rounded rectangle, which is why nobody has noticed. - Tint as a medium, per-pixel, plus legibility and inversion. Kyant0 has no tint in the shader
at all; it is a
drawRecton top. QWEA0's absorb-and-scatter model is the one we already implement and already credit. - Fresnel. We ship Schlick. The studio wrote both Schlick and the full unpolarised Fresnel
equations, then commented both out in favour of a
pow(depth, 5)band. Their commented-out code is evidence they tried and disliked it, which is worth knowing before we assume ours is wrong. - Refraction physics at all. haze has no index of refraction anywhere in its codebase and no
Snell solve; its displacement is height times strength times scale along a negated gradient.
They compensate with a
refractionFoldparameter that fakes the edge reversal our formulation produces for free. - Accessibility. Nobody else has frost, contrast or reduced-motion handling that raises opacity. haze's reduce-transparency multiplies blur by 0.65 and changes no opacity at all, which is arguably backwards.
- A rendered test harness. Nobody else asserts optics on pixels at all.
Two corrections to our own materials fall out of this. Our audit says QWEA0 "multiplies by the tint and adds a scattering term" as though it were an alternative to a lerp; in fact they multiply, add scattering, and then lerp toward that result by the tint alpha, which is what we already do. And the studio's per-channel dispersion sign is inverted relative to real dispersion: their red is displaced further than their blue. Ours is the physical way round.
Two facts about the hardware on the desk¶
Both checked, not assumed.
The Huawei is not a mystery. JKM-LX1, Android 9, API 28, Kirin 710, OpenGL ES 3.2. It is
below the API 33 floor for RuntimeShader, so it draws the flat fallback and always has. The
sample was installed and launched on it during this pass: no crash, no shader error in logcat,
the activity resumes normally.
Measured across a panel's left edge on the captured frame, the fallback does exactly what the docs promise. Reading outward from the body: the interior sits flat at luminance 81, a lit rim band reads 124 over about three pixels, and beyond it the blue backdrop reads 91. So there is a rim, it is lit, and it is roughly 43 counts above the body.
The problem is not that it is broken, it is that "an opaque body with a three-pixel rim" is a fair description of a plain surface, which is what the documentation honestly promises and what the material actually delivers. Nothing here is a bug to fix. The gap is that a third of the install base gets a material that cannot possibly read as glass, and the route to changing that is item 3 above rather than anything in the fallback itself.
The Pixel 7 cannot test the platform backdrop path, and now we know exactly why. The roadmap says it is "not testable on this device, which is on Android 16", which is out of date but lands on the right answer for a better reason.
haze ships the path already, flag-gated and off by default
(haze/.../HazeBackdropRenderer.android.kt). It is RenderNode.setBackdropRenderEffect(effect),
gated on SDK_INT >= 36 and then SDK_INT_FULL >= 3_700_002, that is Android 17.2, or one
named 37.2 beta build. Their public API makes the fallback mandatory and the backend is a sticky
state machine with try-and-catch at every step, which is the right shape for an API this new.
One hard-won detail from their source, worth keeping: the render node must record a transparent
drawColor to keep the backdrop filter composited, because recording a clear operation instead
suppresses the backdrop entirely.
Checked here: the Pixel 7 reports sdk_full 37.0, and the installed platform is
android-37.0, whose android.graphics.RenderNode exposes only setRenderEffect with no
backdrop method anywhere in android.graphics. So the device is one minor version short and the
SDK does not yet carry the API. The work is blocked on a 17.2 build, not on hardware, and haze's
gate is the reference for what to test against when one arrives.
A bug in ours, found by comparison¶
haze inflates its capture by blurRadius + refractionScale * strength * (1 + 0.5 * chroma) +
max(edgeSoftness, foregroundOutset) (GlassRenderParams.kt:59-80). Ours is
refractionDepth * 1.4 + max(blurRadius, backdropBlur) (LiquidGlass.kt:258-259).
There is no dispersion term in our padding. Dispersion displaces each channel further than
the base bend, so a style combining a high dispersion with a high refractionDepth can push a
channel's sample past the recorded region. It will not crash, because sampleBounds clamps, but
it will clip the fringe asymmetrically at the rim. No shipped preset is anywhere near the
threshold, which is why nothing has been seen. It should still be fixed, and it wants a rendered
test at an exaggerated dispersion so it stays fixed.