-
Notifications
You must be signed in to change notification settings - Fork 29.7k
Moved high bitrate texture tests to engine #179906
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Code Review
This pull request successfully moves the high bitrate texture tests from a devicelab integration test to a more direct and scalable engine unit test. The changes involve removing the old integration test suite and adding a new test file within the engine, along with the necessary shader files and build configurations. My feedback focuses on improving the maintainability of the new test code by refactoring duplicated logic and removing an unused import.
| import 'dart:async'; | ||
| import 'dart:collection'; | ||
| import 'dart:convert' as convert; | ||
| import 'dart:math' as math; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| void main() async { | ||
| final ImageComparer comparer = await ImageComparer.create(); | ||
|
|
||
| test('decodeImageFromPixels with RGBA Float32', () async { | ||
| const int dimension = 1024; | ||
| final Image image = await _createRGBA32FloatImage(dimension, dimension); | ||
| final Image shaderImage = await _drawIntoImage(image); | ||
|
|
||
| final ByteData data = (await shaderImage.toByteData())!; | ||
|
|
||
| // Check top left is Red | ||
| int offset = 0; | ||
| expect(data.getUint8(offset), 255, reason: 'Top left Red'); | ||
| expect(data.getUint8(offset + 1), 0, reason: 'Top left Green'); | ||
| expect(data.getUint8(offset + 2), 0, reason: 'Top left Blue'); | ||
| expect(data.getUint8(offset + 3), 255, reason: 'Top left Alpha'); | ||
|
|
||
| // Check center is Black | ||
| offset = ((dimension ~/ 2) * dimension + (dimension ~/ 2)) * 4; | ||
| expect(data.getUint8(offset), 0, reason: 'Center Red'); | ||
| expect(data.getUint8(offset + 1), 0, reason: 'Center Green'); | ||
| expect(data.getUint8(offset + 2), 0, reason: 'Center Blue'); | ||
| expect(data.getUint8(offset + 3), 255, reason: 'Center Alpha'); | ||
|
|
||
| await comparer.addGoldenImage(shaderImage, 'decode_image_from_pixels_rgba_float32.png'); | ||
| image.dispose(); | ||
| }); | ||
|
|
||
| test('decodeImageFromPixels with R Float32', () async { | ||
| if (!impellerEnabled) { | ||
| print('Skipped for Skia'); | ||
| return; | ||
| } | ||
| const int dimension = 1024; | ||
| final Image image = await _createR32FloatImage(dimension, dimension); | ||
| final Image shaderImage = await _drawIntoImage(image); | ||
|
|
||
| final ByteData data = (await shaderImage.toByteData())!; | ||
|
|
||
| // Check top left is Red | ||
| int offset = 0; | ||
| expect(data.getUint8(offset), 255, reason: 'Top left Red'); | ||
| expect(data.getUint8(offset + 1), 0, reason: 'Top left Green'); | ||
| expect(data.getUint8(offset + 2), 0, reason: 'Top left Blue'); | ||
| expect(data.getUint8(offset + 3), 255, reason: 'Top left Alpha'); | ||
|
|
||
| // Check center is Black | ||
| offset = ((dimension ~/ 2) * dimension + (dimension ~/ 2)) * 4; | ||
| expect(data.getUint8(offset), 0, reason: 'Center Red'); | ||
| expect(data.getUint8(offset + 1), 0, reason: 'Center Green'); | ||
| expect(data.getUint8(offset + 2), 0, reason: 'Center Blue'); | ||
| expect(data.getUint8(offset + 3), 255, reason: 'Center Alpha'); | ||
|
|
||
| image.dispose(); | ||
| }); | ||
|
|
||
| test('Picture.toImageSync with rgbaFloat32', () async { | ||
| const int dimension = 1024; | ||
| final Image image = await _drawWithCircleShader( | ||
| dimension, | ||
| dimension, | ||
| TargetPixelFormat.rgbaFloat32, | ||
| ); | ||
| final Image shaderImage = await _drawWithShader(image); | ||
|
|
||
| final ByteData data = (await shaderImage.toByteData())!; | ||
|
|
||
| // Check top left is Black (outside circle, d > 0 -> vec3(0.0)) | ||
| int offset = 0; | ||
| expect(data.getUint8(offset), 0, reason: 'Top left Red'); | ||
| expect(data.getUint8(offset + 1), 0, reason: 'Top left Green'); | ||
| expect(data.getUint8(offset + 2), 0, reason: 'Top left Blue'); | ||
| expect(data.getUint8(offset + 3), 255, reason: 'Top left Alpha'); | ||
|
|
||
| // Check center is White (inside circle, d <= 0 -> vec3(1.0)) | ||
| offset = ((dimension ~/ 2) * dimension + (dimension ~/ 2)) * 4; | ||
| expect(data.getUint8(offset), 255, reason: 'Center Red'); | ||
| expect(data.getUint8(offset + 1), 255, reason: 'Center Green'); | ||
| expect(data.getUint8(offset + 2), 255, reason: 'Center Blue'); | ||
| expect(data.getUint8(offset + 3), 255, reason: 'Center Alpha'); | ||
|
|
||
| await comparer.addGoldenImage(shaderImage, 'picture_to_image_rgba_float32.png'); | ||
| image.dispose(); | ||
| }); | ||
|
|
||
| test('Picture.toImageSync with rFloat32', () async { | ||
| if (!impellerEnabled) { | ||
| print('Skipped for Skia'); | ||
| return; | ||
| } | ||
| const int dimension = 1024; | ||
| final Image image = await _drawWithCircleShader( | ||
| dimension, | ||
| dimension, | ||
| TargetPixelFormat.rFloat32, | ||
| ); | ||
| final Image shaderImage = await _drawWithShader(image); | ||
|
|
||
| final ByteData data = (await shaderImage.toByteData())!; | ||
|
|
||
| // Check top left is Black | ||
| int offset = 0; | ||
| expect(data.getUint8(offset), 0, reason: 'Top left Red'); | ||
| expect(data.getUint8(offset + 1), 0, reason: 'Top left Green'); | ||
| expect(data.getUint8(offset + 2), 0, reason: 'Top left Blue'); | ||
| expect(data.getUint8(offset + 3), 255, reason: 'Top left Alpha'); | ||
|
|
||
| // Check center is White | ||
| offset = ((dimension ~/ 2) * dimension + (dimension ~/ 2)) * 4; | ||
| expect(data.getUint8(offset), 255, reason: 'Center Red'); | ||
| expect(data.getUint8(offset + 1), 255, reason: 'Center Green'); | ||
| expect(data.getUint8(offset + 2), 255, reason: 'Center Blue'); | ||
| expect(data.getUint8(offset + 3), 255, reason: 'Center Alpha'); | ||
|
|
||
| image.dispose(); | ||
| }); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There's some code duplication within the test bodies. The pixel checking logic is repeated in the decodeImageFromPixels tests, and also in the Picture.toImageSync tests. You can extract these into helper functions to make the tests cleaner and more maintainable. This suggestion also presumes the refactoring of _createRGBA32FloatImage and _createR32FloatImage into a single _createSdfImage function, which I've suggested in another comment.
void main() async {
final ImageComparer comparer = await ImageComparer.create();
void checkDirectSdfRender(ByteData data, int dimension) {
// Check top left is Red
int offset = 0;
expect(data.getUint8(offset), 255, reason: 'Top left Red');
expect(data.getUint8(offset + 1), 0, reason: 'Top left Green');
expect(data.getUint8(offset + 2), 0, reason: 'Top left Blue');
expect(data.getUint8(offset + 3), 255, reason: 'Top left Alpha');
// Check center is Black
offset = ((dimension ~/ 2) * dimension + (dimension ~/ 2)) * 4;
expect(data.getUint8(offset), 0, reason: 'Center Red');
expect(data.getUint8(offset + 1), 0, reason: 'Center Green');
expect(data.getUint8(offset + 2), 0, reason: 'Center Blue');
expect(data.getUint8(offset + 3), 255, reason: 'Center Alpha');
}
void checkVisualizedSdfRender(ByteData data, int dimension) {
// Check top left is Black (outside circle, d > 0 -> vec3(0.0))
int offset = 0;
expect(data.getUint8(offset), 0, reason: 'Top left Red');
expect(data.getUint8(offset + 1), 0, reason: 'Top left Green');
expect(data.getUint8(offset + 2), 0, reason: 'Top left Blue');
expect(data.getUint8(offset + 3), 255, reason: 'Top left Alpha');
// Check center is White (inside circle, d <= 0 -> vec3(1.0))
offset = ((dimension ~/ 2) * dimension + (dimension ~/ 2)) * 4;
expect(data.getUint8(offset), 255, reason: 'Center Red');
expect(data.getUint8(offset + 1), 255, reason: 'Center Green');
expect(data.getUint8(offset + 2), 255, reason: 'Center Blue');
expect(data.getUint8(offset + 3), 255, reason: 'Center Alpha');
}
test('decodeImageFromPixels with RGBA Float32', () async {
const int dimension = 1024;
final Image image = await _createSdfImage(dimension, dimension, PixelFormat.rgbaFloat32);
final Image shaderImage = await _drawIntoImage(image);
final ByteData data = (await shaderImage.toByteData())!;
checkDirectSdfRender(data, dimension);
await comparer.addGoldenImage(shaderImage, 'decode_image_from_pixels_rgba_float32.png');
image.dispose();
});
test('decodeImageFromPixels with R Float32', () async {
if (!impellerEnabled) {
print('Skipped for Skia');
return;
}
const int dimension = 1024;
final Image image = await _createSdfImage(dimension, dimension, PixelFormat.rFloat32);
final Image shaderImage = await _drawIntoImage(image);
final ByteData data = (await shaderImage.toByteData())!;
checkDirectSdfRender(data, dimension);
image.dispose();
});
test('Picture.toImageSync with rgbaFloat32', () async {
const int dimension = 1024;
final Image image = await _drawWithCircleShader(
dimension,
dimension,
TargetPixelFormat.rgbaFloat32,
);
final Image shaderImage = await _drawWithShader(image);
final ByteData data = (await shaderImage.toByteData())!;
checkVisualizedSdfRender(data, dimension);
await comparer.addGoldenImage(shaderImage, 'picture_to_image_rgba_float32.png');
image.dispose();
});
test('Picture.toImageSync with rFloat32', () async {
if (!impellerEnabled) {
print('Skipped for Skia');
return;
}
const int dimension = 1024;
final Image image = await _drawWithCircleShader(
dimension,
dimension,
TargetPixelFormat.rFloat32,
);
final Image shaderImage = await _drawWithShader(image);
final ByteData data = (await shaderImage.toByteData())!;
checkVisualizedSdfRender(data, dimension);
image.dispose();
});
}| Future<Image> _createRGBA32FloatImage(int width, int height) async { | ||
| final double radius = width / 4.0; | ||
| final floats = List<double>.filled(width * height * 4, 0.0); | ||
| for (var i = 0; i < height; ++i) { | ||
| for (var j = 0; j < width; ++j) { | ||
| double x = j.toDouble(); | ||
| double y = i.toDouble(); | ||
| x -= width / 2.0; | ||
| y -= height / 2.0; | ||
| final double length = math.sqrt(x * x + y * y); | ||
| final int idx = i * width * 4 + j * 4; | ||
| floats[idx + 0] = length - radius; | ||
| floats[idx + 1] = 0.0; | ||
| floats[idx + 2] = 0.0; | ||
| floats[idx + 3] = 1.0; | ||
| } | ||
| } | ||
| final floatList = Float32List.fromList(floats); | ||
| final intList = Uint8List.view(floatList.buffer); | ||
| final completer = Completer<Image>(); | ||
| decodeImageFromPixels( | ||
| intList, | ||
| width, | ||
| height, | ||
| PixelFormat.rgbaFloat32, | ||
| targetFormat: TargetPixelFormat.rgbaFloat32, | ||
| (Image image) { | ||
| completer.complete(image); | ||
| }, | ||
| ); | ||
| return completer.future; | ||
| } | ||
|
|
||
| Future<Image> _createR32FloatImage(int width, int height) async { | ||
| final double radius = width / 4.0; | ||
| final floats = List<double>.filled(width * height, 0.0); | ||
| for (var i = 0; i < height; ++i) { | ||
| for (var j = 0; j < width; ++j) { | ||
| double x = j.toDouble(); | ||
| double y = i.toDouble(); | ||
| x -= width / 2.0; | ||
| y -= height / 2.0; | ||
| final double length = math.sqrt(x * x + y * y); | ||
| final int idx = i * width + j; | ||
| floats[idx] = length - radius; | ||
| } | ||
| } | ||
| final floatList = Float32List.fromList(floats); | ||
| final intList = Uint8List.view(floatList.buffer); | ||
| final completer = Completer<Image>(); | ||
| decodeImageFromPixels( | ||
| intList, | ||
| width, | ||
| height, | ||
| PixelFormat.rFloat32, | ||
| targetFormat: TargetPixelFormat.rFloat32, | ||
| (Image image) { | ||
| completer.complete(image); | ||
| }, | ||
| ); | ||
| return completer.future; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The functions _createRGBA32FloatImage and _createR32FloatImage are very similar. To improve maintainability and reduce code duplication, you can merge them into a single function that takes PixelFormat as a parameter.
Future<Image> _createSdfImage(int width, int height, PixelFormat format) {
final double radius = width / 4.0;
final bool isRgba = format == PixelFormat.rgbaFloat32;
final int componentCount = isRgba ? 4 : 1;
final List<double> floats = List<double>.filled(width * height * componentCount, 0.0);
for (int i = 0; i < height; ++i) {
for (int j = 0; j < width; ++j) {
double x = j.toDouble();
double y = i.toDouble();
x -= width / 2.0;
y -= height / 2.0;
final double length = math.sqrt(x * x + y * y);
final int idx = (i * width + j) * componentCount;
floats[idx] = length - radius;
if (isRgba) {
floats[idx + 1] = 0.0;
floats[idx + 2] = 0.0;
floats[idx + 3] = 1.0;
}
}
}
final Float32List floatList = Float32List.fromList(floats);
final Uint8List intList = Uint8List.view(floatList.buffer);
final Completer<Image> completer = Completer<Image>();
decodeImageFromPixels(
intList,
width,
height,
format,
targetFormat: isRgba ? TargetPixelFormat.rgbaFloat32 : TargetPixelFormat.rFloat32,
(Image image) {
completer.complete(image);
},
);
return completer.future;
}
This adds a few goldens where skia supports the file format. This is a bit more scalable and direct than managing integration tests.
Pre-launch Checklist
///).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. Comments from the
gemini-code-assistbot 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.