Skia's dithered gradient shaders cost ~10ms per full-screen pass on Mali-class TV GPUs: the two spotlight scrims alone were ~20ms of a 27ms raster frame, while flat blended quads at the same coverage are ~free. Bake axis-aligned LinearGradients once into cached 1x1024 premultiplied strip textures drawn as stretched quads (shader fallback for unsupported shapes and the first frame). Converted the spotlight, TV detail backdrop, app-bar scrims, rail bleed, person-card overlay, and the video-controls scrim, which now also keeps one widget type across hasFrame flips so the controls subtree survives in-place source switches. Scripted-browse on a Mali-G31 box: draw p50 27.5ms -> 7.1ms, swap block 20.3ms -> 0.9ms, janky draws 69% -> 2%; screenshots pixel-identical.
97 lines
3.4 KiB
Dart
97 lines
3.4 KiB
Dart
import 'dart:typed_data';
|
|
import 'dart:ui' as ui;
|
|
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter/rendering.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:plezy/widgets/rasterized_gradient.dart';
|
|
|
|
/// The strip texture must composite identically to Skia's gradient shader.
|
|
/// Translucent non-black ramps are the regression case: rgba8888 strips are
|
|
/// consumed premultiplied, so straight-alpha baking renders them far too
|
|
/// bright (a white fade-to-transparent scrim becomes an opaque sheet).
|
|
void main() {
|
|
Future<ui.Image> capture(WidgetTester tester, Key key) async {
|
|
final boundary = tester.renderObject<RenderRepaintBoundary>(find.byKey(key));
|
|
late ui.Image image;
|
|
await tester.runAsync(() async => image = await boundary.toImage());
|
|
return image;
|
|
}
|
|
|
|
Future<List<int>> centerColumn(WidgetTester tester, ui.Image image) async {
|
|
late ByteData bytes;
|
|
await tester.runAsync(() async => bytes = (await image.toByteData())!);
|
|
final x = image.width ~/ 2;
|
|
final column = <int>[];
|
|
for (var y = 0; y < image.height; y += 8) {
|
|
final offset = (y * image.width + x) * 4;
|
|
column.addAll([bytes.getUint8(offset), bytes.getUint8(offset + 1), bytes.getUint8(offset + 2)]);
|
|
}
|
|
return column;
|
|
}
|
|
|
|
testWidgets('strip path matches the gradient shader over a colored background', (tester) async {
|
|
final gradient = LinearGradient(
|
|
begin: Alignment.topCenter,
|
|
end: Alignment.bottomCenter,
|
|
colors: [
|
|
Colors.white.withValues(alpha: 0.8),
|
|
Colors.white.withValues(alpha: 0.4),
|
|
Colors.white.withValues(alpha: 0.0),
|
|
],
|
|
stops: const [0.0, 0.4, 1.0],
|
|
);
|
|
|
|
// Bake ahead of the build so RasterizedGradient paints the strip from
|
|
// its first frame instead of the (trivially identical) shader fallback.
|
|
late ui.Image? strip;
|
|
await tester.runAsync(() async => strip = await RasterizedGradient.prebake(gradient));
|
|
expect(strip, isNotNull, reason: 'axis-aligned gradient must bake to a strip');
|
|
|
|
const rasterizedKey = Key('rasterized');
|
|
const shaderKey = Key('shader');
|
|
Widget sample(Key key, Widget gradientBox) => RepaintBoundary(
|
|
key: key,
|
|
child: SizedBox(
|
|
width: 64,
|
|
height: 256,
|
|
child: Stack(
|
|
fit: StackFit.expand,
|
|
children: [
|
|
const ColoredBox(color: Color(0xFF406080)),
|
|
gradientBox,
|
|
],
|
|
),
|
|
),
|
|
);
|
|
|
|
await tester.pumpWidget(
|
|
Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: Center(
|
|
child: Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
sample(rasterizedKey, RasterizedGradient(gradient: gradient)),
|
|
sample(shaderKey, DecoratedBox(decoration: BoxDecoration(gradient: gradient))),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
final rasterized = await centerColumn(tester, await capture(tester, rasterizedKey));
|
|
final shader = await centerColumn(tester, await capture(tester, shaderKey));
|
|
|
|
for (var i = 0; i < shader.length; i++) {
|
|
// Skia dithers its gradient shader; the strip doesn't. Allow a couple
|
|
// of levels of noise — the premul bug is off by 50+ on this ramp.
|
|
expect(
|
|
(rasterized[i] - shader[i]).abs(),
|
|
lessThanOrEqualTo(3),
|
|
reason: 'channel ${i % 3} at sample ${i ~/ 3}: strip=${rasterized[i]} shader=${shader[i]}',
|
|
);
|
|
}
|
|
});
|
|
}
|