Skip to content

Conversation

@gaaclarke
Copy link
Member

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-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.

@github-actions github-actions bot added the engine flutter/engine related. See also e: labels. label Dec 15, 2025
Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a 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;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This dart:math import appears to be unused in the file. It's good practice to remove unused imports to keep the code clean.

Comment on lines +16 to +132
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();
});
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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();
  });
}

Comment on lines +175 to +236
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;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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;
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

engine flutter/engine related. See also e: labels.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant