Skip to content

ADR-0004: Use quality-gated adaptive input scaling for blur

Status

Accepted

Date

2026-07-26

Context

Blur cost is dominated by rendering and filtering the expanded retained capture layer. Reducing its input resolution reduces that work, but a single fixed scale is not appropriate for every blur: weak blur reveals downsampling more readily, and scaling a small layer saves too little work to justify resource churn.

HazeInputScale.Default previously resolved to explicit no scaling, preventing a visual effect from choosing an appropriate default. Blur and Glass also have different quality constraints: ordinary blur can hide conservative downsampling, while Glass combines refraction, fine optical detail, and retained stages that have their own render budget.

Decision

HazeSampling.Adaptive is semantically distinct from FullResolution and uses the adaptive policy for both built-in effects:

  • Blur uses one cross-platform policy based on the fully resolved blur radius in physical pixels and the expanded capture-layer pixel area, weighted by recent distinct input-update cadence.
  • Ordinary and non-progressive masked blur select only linear scales 1.0, 0.8, or 0.5 (pixel fractions 1.0, 0.64, or 0.25). Both radius and weighted area must cross the relevant workload boundary.
  • Progressive blur stays at linear scale 1.0 below the medium workload boundary and is capped at 0.8 (pixel fraction 0.64).
  • Updates no more than 100ms apart form a burst. Area is multiplied by up to three distinct updates; repeated draws of unchanged input do not accumulate work, and a quiet interval resets the multiplier.
  • Tier exits have a 12.5% hysteresis margin, preventing small radius or size changes from repeatedly reallocating retained layers.
  • Glass uses the same adaptive default mode with its effect-specific policy defined separately in ADR-0005.
  • Explicit FullResolution, Adaptive, and Fixed choices remain authoritative. Paths that do not perform a real blur, including scrim fallback, remain unscaled.

The physical-pixel boundaries preserve a substantial blur kernel after downsampling: 0.8 begins at a 32px radius and 300,000px layer, while 0.5 begins at a 60px radius and 500,000px layer. This leaves at least a 25.6px or 30px downsampled kernel respectively. The area gates avoid resizing for less than roughly 108,000 or 375,000 saved input pixels. These are workload boundaries, not device-model checks.

Validation

Representative screenshots passed on Desktop and Android API 28, 32, and 35 for both automatic tiers, progressive blur, gradient and hard-edged masks, and hysteresis boundaries. The automated perceptual-difference guard remained below its 0.01 mean-absolute-difference limit.

Three interleaved Pixel 6 Macrobenchmark pairs compared explicit unscaled input with the adaptive default. Each measurement used 16 iterations at 90Hz. Perfetto actual-frame duration and Macrobenchmark CPU P90 improved in every pair:

Pair CPU P90, unscaled → adaptive Actual-frame P90, unscaled → adaptive
1 9.817 → 9.746ms 11.741 → 11.366ms
2 9.774 → 9.641ms 11.889 → 11.388ms
3 10.071 → 9.975ms 12.247 → 11.654ms
Median 9.817 → 9.746ms (-0.73%) 11.889 → 11.388ms (-4.21%)

Frame-overrun headroom also improved in every pair. Representative progressive and masked workloads improved actual-frame P90 by 5.76% and 6.53% respectively, so the capped and masked paths showed no material regression.

The device refresh rate and workload state were checked before accepting a pair. Runs captured at 60Hz, with a mismatched refresh rate between halves, or with invalid app navigation state were discarded and repeated; they are not included above.

On the measured Scaffold, the 24dp blur resolved to 63px. Its expanded 637,200px top layer selected 0.5, while the smaller 362,880px bottom layer selected 0.8, confirming that both workload gates participate as intended.

To verify that the aggressive tier earns its additional quality cost, three further interleaved Pixel 6 pairs compared the selected adaptive policy against explicit linear scale 0.8 (now expressed as Fixed(0.64)). Adaptive scaling improved app-layer actual-frame P90 in every pair:

Pair CPU P90, fixed 0.8 → adaptive Actual-frame P90, fixed 0.8 → adaptive
1 9.703 → 9.814ms 11.685 → 11.610ms (-0.64%)
2 9.686 → 9.742ms 11.705 → 11.411ms (-2.51%)
3 9.846 → 9.596ms 11.712 → 11.282ms (-3.67%)
Median 9.703 → 9.742ms (+0.41%) 11.705 → 11.411ms (-2.51%)

The CPU metric did not improve consistently because the extra saving is predominantly in the RenderThread/Vulkan work that motivated this policy. The actual-frame result did improve in every pair, so 0.8 is retained as the least aggressive default tier and 0.5 is reserved for the larger 60px/500,000px workload boundary.

Alternatives Considered

Keep no scaling as the default

This preserves exact historical output but leaves a repeatable performance improvement opt-in, including on workloads where downsampling is visually hidden by strong blur.

Default every blur to one fixed scale

This is simple but ignores the two inputs that determine benefit and perceptibility. It either downsamples weak or small blurs unnecessarily or leaves useful savings on large, strong blurs.

Use an Android- or device-specific policy

The original performance evidence came from Android, but the quality relationship between physical blur radius, capture pixels, and downsampling is not device-model-specific. A common policy also preserves cross-platform behavior. A platform-specific exception requires evidence of a backend constraint.

Allow scales below 0.5

More aggressive downsampling saves additional pixels but produces a larger perceptual change and is unnecessary for the measured improvement. Automatic scaling is therefore bounded at 0.5.

Consequences

  • Fixed values express total input-pixel fractions, while the renderer converts them to an aspect-preserving linear scale.
  • Unspecified blur output can be slightly softer on sufficiently large, strongly blurred surfaces.
  • The common policy is deterministic but stateful at tier boundaries because hysteresis depends on the previous automatic tier.
  • Crossing a tier intentionally resizes retained resources; small boundary noise does not.
  • Callers can restore full-resolution input with HazeSampling.FullResolution.
  • Threshold changes require paired performance evidence and representative screenshot/perceptual review across ordinary, progressive, and masked blur.

References