Skip to content

Commit 33a30cb

Browse files
authored
Impeller: Allows R32G32B32A32_SFLOAT images (#177959)
fixes flutter/flutter#141289 design doc: https://docs.google.com/document/d/1zpkMutZkqo2GVdMhiKzFURpgN8JDZTjtIVwwqqHKM90/edit?tab=t.0 ## Pre-launch Checklist - [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [x] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [x] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [x] I signed the [CLA]. - [x] I listed at least one issue that this PR fixes in the description above. - [x] I updated/added relevant documentation (doc comments with `///`). - [x] I added new tests to check the change I am making, or this PR is [test-exempt]. - [x] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [x] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. **Note**: The Flutter team is currently trialing the use of [Gemini Code Assist for GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code). Comments from the `gemini-code-assist` bot should not be taken as authoritative feedback from the Flutter team. If you find its comments useful you can update your code accordingly, but if you are unsure or disagree with the feedback, please feel free to wait for a Flutter team member's review for guidance on which automated comments should be addressed. <!-- Links --> [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md
1 parent c458ab7 commit 33a30cb

26 files changed

Lines changed: 879 additions & 294 deletions

.ci.yaml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5499,6 +5499,16 @@ targets:
54995499
["devicelab", "ios", "mac"]
55005500
task_name: wide_gamut_ios
55015501

5502+
- name: Mac_ios high_bitrate_images
5503+
recipe: devicelab/devicelab_drone
5504+
presubmit: true
5505+
bringup: true
5506+
timeout: 60
5507+
properties:
5508+
tags: >
5509+
["devicelab", "ios", "mac"]
5510+
task_name: high_bitrate_images_ios
5511+
55025512
- name: Mac_x64_ios hot_mode_dev_cycle_ios__benchmark
55035513
recipe: devicelab/devicelab_drone
55045514
presubmit: false

TESTOWNERS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@
9494
/dev/devicelab/bin/tasks/textfield_perf__e2e_summary.dart @jtmcdole @flutter/engine
9595
/dev/devicelab/bin/tasks/very_long_picture_scrolling_perf__e2e_summary.dart @flar @flutter/engine
9696
/dev/devicelab/bin/tasks/web_size__compile_test.dart @yjbanov @flutter/web
97+
/dev/devicelab/bin/tasks/high_bitrate_images_ios.dart @gaaclarke @flutter/engine
9798
/dev/devicelab/bin/tasks/wide_gamut_ios.dart @gaaclarke @flutter/engine
9899
/dev/devicelab/bin/tasks/animated_advanced_blend_perf__timeline_summary.dart @gaaclarke @flutter/engine
99100
/dev/devicelab/bin/tasks/animated_advanced_blend_perf_ios__timeline_summary.dart @gaaclarke @flutter/engine
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
// Copyright 2014 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
import 'package:flutter_devicelab/framework/devices.dart';
6+
import 'package:flutter_devicelab/framework/framework.dart';
7+
import 'package:flutter_devicelab/tasks/integration_tests.dart';
8+
9+
Future<void> main() async {
10+
deviceOperatingSystem = DeviceOperatingSystem.ios;
11+
await task(createHighBitrateImagesTest());
12+
}

dev/devicelab/lib/tasks/integration_tests.dart

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,14 @@ TaskFunction createWideGamutTest() {
247247
).call;
248248
}
249249

250+
TaskFunction createHighBitrateImagesTest() {
251+
return IntegrationTest(
252+
'${flutterDirectory.path}/dev/integration_tests/high_bitrate_images',
253+
'integration_test/app_test.dart',
254+
createPlatforms: <String>['ios'],
255+
).call;
256+
}
257+
250258
class DriverTest {
251259
DriverTest(
252260
this.testDirectory,
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# high_bitrate_images
2+
3+
An integration test used for testing high bitrate image support in the engine.
4+
5+
## Local run
6+
7+
```sh
8+
flutter create --platforms="ios" --no-overwrite .
9+
flutter test integration_test/app_test.dart
10+
```
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
// Copyright 2014 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
import 'dart:async' show Completer;
6+
import 'dart:typed_data';
7+
import 'dart:ui' as ui;
8+
9+
import 'package:flutter/services.dart';
10+
import 'package:flutter_test/flutter_test.dart';
11+
import 'package:high_bitrate_images/main.dart' as app;
12+
import 'package:integration_test/integration_test.dart';
13+
14+
// See: https://developer.apple.com/documentation/metal/mtlpixelformat/mtlpixelformatbgr10_xr.
15+
double _decodeBGR10(int x) {
16+
const double max = 1.25098;
17+
const double min = -0.752941;
18+
const double intercept = min;
19+
const double slope = (max - min) / 1024.0;
20+
return (x * slope) + intercept;
21+
}
22+
23+
Uint8List _convertBGRA10XRToBGRA8888(Uint8List bgra10xr) {
24+
final ByteData inputByteData = ByteData.sublistView(bgra10xr);
25+
final Uint8List bgra8888 = Uint8List(
26+
bgra10xr.lengthInBytes ~/ 2,
27+
); // 8 bytes per pixel -> 4 bytes per pixel
28+
final ByteData outputByteData = ByteData.view(bgra8888.buffer);
29+
30+
for (int i = 0, j = 0; i < bgra10xr.lengthInBytes; i += 8, j += 4) {
31+
final int pixel = inputByteData.getUint64(i, Endian.host);
32+
33+
final double blue10 = _decodeBGR10((pixel >> 6) & 0x3ff);
34+
final double green10 = _decodeBGR10((pixel >> 22) & 0x3ff);
35+
final double red10 = _decodeBGR10((pixel >> 38) & 0x3ff);
36+
37+
final int blue8 = (blue10.clamp(0.0, 1.0) * 255).round();
38+
final int green8 = (green10.clamp(0.0, 1.0) * 255).round();
39+
final int red8 = (red10.clamp(0.0, 1.0) * 255).round();
40+
const int alpha8 = 255; // Assuming opaque for BGRA8888
41+
42+
final int bgra8888Pixel = (alpha8 << 24) | (red8 << 16) | (green8 << 8) | blue8;
43+
outputByteData.setUint32(j, bgra8888Pixel, Endian.host);
44+
}
45+
return bgra8888;
46+
}
47+
48+
Future<ui.Image> _getScreenshot() async {
49+
const MethodChannel channel = MethodChannel('flutter/screenshot');
50+
final List<Object?> result = await channel.invokeMethod('test') as List<Object?>;
51+
52+
expect(result, isNotNull);
53+
expect(result.length, 4);
54+
final [int width, int height, String format, Uint8List bytes] = result as List<dynamic>;
55+
56+
expect(format, equals('MTLPixelFormatBGRA10_XR'));
57+
58+
final Completer<ui.Image> completer = Completer<ui.Image>();
59+
final Uint8List pixels = _convertBGRA10XRToBGRA8888(bytes);
60+
ui.decodeImageFromPixels(
61+
pixels,
62+
width,
63+
height,
64+
ui.PixelFormat.bgra8888,
65+
(ui.Image image) => completer.complete(image),
66+
);
67+
68+
return completer.future;
69+
}
70+
71+
void main() {
72+
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
73+
74+
group('end-to-end test', () {
75+
testWidgets('renders sdfs with rgba32f', (WidgetTester tester) async {
76+
app.main();
77+
await tester.pumpAndSettle(const Duration(seconds: 2));
78+
await _getScreenshot();
79+
// TODO(gaaclarke): Turn this into a golden test. This turned out to be
80+
// quite involved so it's deferred.
81+
// expect(
82+
// screenshot,
83+
// matchesGoldenFile('high_bitrate_images.rbga32f'),
84+
// );
85+
});
86+
});
87+
}
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
// Copyright 2014 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
import 'dart:async';
6+
import 'dart:math' show sqrt;
7+
import 'dart:typed_data';
8+
import 'dart:ui' as ui;
9+
import 'package:flutter/material.dart';
10+
import 'package:flutter/services.dart';
11+
12+
void main() {
13+
runApp(const MyApp());
14+
}
15+
16+
class MyApp extends StatelessWidget {
17+
const MyApp({super.key});
18+
19+
@override
20+
Widget build(BuildContext context) {
21+
return MaterialApp(title: 'SDF Demo', theme: ThemeData.dark(), home: const MyHomePage());
22+
}
23+
}
24+
25+
class MyHomePage extends StatefulWidget {
26+
const MyHomePage({super.key});
27+
28+
@override
29+
State<MyHomePage> createState() => _MyHomePageState();
30+
}
31+
32+
class _MyHomePageState extends State<MyHomePage> {
33+
@override
34+
Widget build(BuildContext context) {
35+
return const Scaffold(body: SdfCanvas());
36+
}
37+
}
38+
39+
class SdfCanvas extends StatefulWidget {
40+
const SdfCanvas({super.key});
41+
42+
@override
43+
State<SdfCanvas> createState() => _SdfCanvasState();
44+
}
45+
46+
class _SdfCanvasState extends State<SdfCanvas> {
47+
ui.FragmentShader? _shader;
48+
ui.Image? _sdfImage;
49+
50+
@override
51+
void initState() {
52+
super.initState();
53+
_loadShader().then((ui.FragmentShader shader) {
54+
setState(() {
55+
_shader = shader;
56+
});
57+
});
58+
_loadSdfImage().then((ui.Image image) {
59+
setState(() {
60+
_sdfImage = image;
61+
});
62+
});
63+
}
64+
65+
Future<ui.FragmentShader> _loadShader() async {
66+
final ui.FragmentProgram program = await ui.FragmentProgram.fromAsset('shaders/sdf.frag');
67+
return program.fragmentShader();
68+
}
69+
70+
Future<ui.Image> _loadSdfImage() async {
71+
const int width = 1024;
72+
const int height = 1024;
73+
const double radius = width / 4.0;
74+
final List<double> floats = List<double>.filled(width * height * 4, 0.0);
75+
for (int i = 0; i < height; ++i) {
76+
for (int j = 0; j < width; ++j) {
77+
double x = j.toDouble();
78+
double y = i.toDouble();
79+
x -= width / 2.0;
80+
y -= height / 2.0;
81+
final double length = sqrt(x * x + y * y) - radius;
82+
final int idx = i * width * 4 + j * 4;
83+
floats[idx + 0] = length - radius;
84+
floats[idx + 1] = 0.0;
85+
floats[idx + 2] = 0.0;
86+
floats[idx + 3] = 1.0;
87+
}
88+
}
89+
final Float32List floatList = Float32List.fromList(floats);
90+
final Uint8List intList = Uint8List.view(floatList.buffer);
91+
final Completer<ui.Image> completer = Completer<ui.Image>();
92+
ui.decodeImageFromPixels(
93+
intList,
94+
width,
95+
height,
96+
ui.PixelFormat.rgbaFloat32,
97+
targetFormat: ui.TargetPixelFormat.rgbaFloat32,
98+
(ui.Image image) {
99+
completer.complete(image);
100+
},
101+
);
102+
return completer.future;
103+
}
104+
105+
@override
106+
Widget build(BuildContext context) {
107+
if (_shader == null) {
108+
return const Center(child: CircularProgressIndicator());
109+
}
110+
return SizedBox.expand(
111+
child: (_shader != null && _sdfImage != null)
112+
? CustomPaint(painter: SdfPainter(_shader!, _sdfImage!))
113+
: Container(),
114+
);
115+
}
116+
}
117+
118+
class SdfPainter extends CustomPainter {
119+
SdfPainter(this.shader, this.image);
120+
121+
final ui.FragmentShader shader;
122+
final ui.Image image;
123+
124+
@override
125+
void paint(Canvas canvas, Size size) {
126+
shader.setFloat(0, size.width);
127+
shader.setFloat(1, size.height);
128+
shader.setImageSampler(0, image);
129+
final Paint paint = Paint()..shader = shader;
130+
canvas.drawRect(Offset.zero & size, paint);
131+
}
132+
133+
@override
134+
bool shouldRepaint(covariant CustomPainter oldDelegate) {
135+
return true;
136+
}
137+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
name: high_bitrate_images
2+
description: "A new Flutter project."
3+
publish_to: 'none' # Remove this line if you wish to publish to pub.dev
4+
version: 1.0.0+1
5+
6+
environment:
7+
sdk: ^3.11.0-88.0.dev
8+
9+
resolution: workspace
10+
11+
dependencies:
12+
flutter:
13+
sdk: flutter
14+
15+
dev_dependencies:
16+
flutter_test:
17+
sdk: flutter
18+
integration_test:
19+
sdk: flutter
20+
21+
flutter:
22+
uses-material-design: true
23+
shaders:
24+
- shaders/sdf.frag
25+
26+
# PUBSPEC CHECKSUM: vrimb3
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
#version 320 es
2+
3+
// Copyright 2014 The Flutter Authors. All rights reserved.
4+
// Use of this source code is governed by a BSD-style license that can be
5+
// found in the LICENSE file.
6+
7+
#include <flutter/runtime_effect.glsl>
8+
9+
out vec4 fragColor;
10+
11+
uniform vec2 uSize;
12+
uniform sampler2D uTex;
13+
14+
void main() {
15+
vec2 p = FlutterFragCoord().xy / uSize;
16+
float d = texture(uTex, p).r;
17+
vec3 col = d > 0.0 ? vec3(0.0) : vec3(1.0);
18+
fragColor = vec4(col, 1.0);
19+
}

engine/src/flutter/lib/ui/BUILD.gn

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,7 @@ source_set("ui") {
186186
"//flutter/skia",
187187
"//flutter/third_party/rapidjson",
188188
"//flutter/third_party/tonic",
189+
"//third_party/abseil-cpp/absl/status:statusor",
189190
"//third_party/zlib",
190191
]
191192

0 commit comments

Comments
 (0)