Skip to content

[vector_graphics] Moved color and colorFilter effects into the raster cache to reduce rendering overhead. #9398

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

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions packages/vector_graphics/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

* Updates minimum supported SDK version to Flutter 3.27/Dart 3.6.

## 1.1.19

* Moved color and colorFilter effects into the raster cache to reduce subsequent rendering overhead.

## 1.1.18

* Allow transition between placeholder and loaded image to have an animation.
Expand Down
46 changes: 31 additions & 15 deletions packages/vector_graphics/lib/src/render_vector_graphic.dart
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import 'listener.dart';
@immutable
class RasterKey {
/// Create a new [RasterKey].
const RasterKey(this.assetKey, this.width, this.height);
const RasterKey(this.assetKey, this.width, this.height, this.paint);

/// An object that is used to identify the raster data this key will store.
///
Expand All @@ -28,16 +28,22 @@ class RasterKey {
/// The width of this vector graphic raster, in physical pixels.
final int height;

/// The paint of this vector graphic raster.
final Paint paint;

@override
bool operator ==(Object other) {
return other is RasterKey &&
other.assetKey == assetKey &&
other.width == width &&
other.height == height;
other.height == height &&
other.paint.color == paint.color &&
other.paint.colorFilter == paint.colorFilter;
}

@override
int get hashCode => Object.hash(assetKey, width, height);
int get hashCode =>
Object.hash(assetKey, width, height, paint.color, paint.colorFilter);
}

/// The cache entry for a rasterized vector graphic.
Expand Down Expand Up @@ -116,6 +122,7 @@ class RenderVectorGraphic extends RenderBox {
/// An optional [ColorFilter] to apply to the rasterized vector graphic.
ColorFilter? get colorFilter => _colorFilter;
ColorFilter? _colorFilter;
final Paint _colorPaint = Paint();
set colorFilter(ColorFilter? value) {
if (colorFilter == value) {
return;
Expand Down Expand Up @@ -196,7 +203,7 @@ class RenderVectorGraphic extends RenderBox {
}

static RasterData _createRaster(
RasterKey key, double scaleFactor, PictureInfo info) {
RasterKey key, double scaleFactor, PictureInfo info, Paint colorPaint) {
final int scaledWidth = key.width;
final int scaledHeight = key.height;
// In order to scale a picture, it must be placed in a new picture
Expand All @@ -206,9 +213,16 @@ class RenderVectorGraphic extends RenderBox {
// capture in a raster.
final ui.PictureRecorder recorder = ui.PictureRecorder();
final ui.Canvas canvas = ui.Canvas(recorder);

final Rect drawSize =
ui.Rect.fromLTWH(0, 0, scaledWidth.toDouble(), scaledHeight.toDouble());
canvas.clipRect(drawSize);
final int saveCount = canvas.getSaveCount();
if (colorPaint.color.opacity != 1.0 || colorPaint.colorFilter != null) {
canvas.saveLayer(drawSize, colorPaint);
}
canvas.scale(scaleFactor);
canvas.drawPicture(info.picture);
canvas.restoreToCount(saveCount);
final ui.Picture rasterPicture = recorder.endRecording();

final ui.Image pending =
Expand All @@ -235,7 +249,8 @@ class RenderVectorGraphic extends RenderBox {
(pictureInfo.size.width * devicePixelRatio / scale).round();
final int scaledHeight =
(pictureInfo.size.height * devicePixelRatio / scale).round();
final RasterKey key = RasterKey(assetKey, scaledWidth, scaledHeight);
final RasterKey key =
RasterKey(assetKey, scaledWidth, scaledHeight, _colorPaint);

// First check if the raster is available synchronously. This also handles
// a no-op change that would resolve to an identical picture.
Expand All @@ -249,7 +264,7 @@ class RenderVectorGraphic extends RenderBox {
return;
}
final RasterData data =
_createRaster(key, devicePixelRatio / scale, pictureInfo);
_createRaster(key, devicePixelRatio / scale, pictureInfo, _colorPaint);
data.count += 1;

assert(!_liveRasterCache.containsKey(key));
Expand Down Expand Up @@ -296,18 +311,17 @@ class RenderVectorGraphic extends RenderBox {
return;
}

// _colorPaint is used to create raster cache.
if (colorFilter != null) {
_colorPaint.colorFilter = colorFilter;
}
_colorPaint.color = Color.fromRGBO(0, 0, 0, _opacityValue);

_maybeUpdateRaster();
final ui.Image image = _rasterData!.image;
final int width = _rasterData!.key.width;
final int height = _rasterData!.key.height;

// Use `FilterQuality.low` to scale the image, which corresponds to
// bilinear interpolation.
final Paint colorPaint = Paint()..filterQuality = ui.FilterQuality.low;
if (colorFilter != null) {
colorPaint.colorFilter = colorFilter;
}
colorPaint.color = Color.fromRGBO(0, 0, 0, _opacityValue);
final Rect src = ui.Rect.fromLTWH(
0,
0,
Expand All @@ -321,11 +335,13 @@ class RenderVectorGraphic extends RenderBox {
pictureInfo.size.height,
);

// Use `FilterQuality.low` to scale the image, which corresponds to
// bilinear interpolation.
context.canvas.drawImageRect(
image,
src,
dst,
colorPaint,
Paint()..filterQuality = ui.FilterQuality.low,
);
}
}
Expand Down
2 changes: 1 addition & 1 deletion packages/vector_graphics/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ name: vector_graphics
description: A vector graphics rendering package for Flutter using a binary encoding.
repository: https://github.com/flutter/packages/tree/main/packages/vector_graphics
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+vector_graphics%22
version: 1.1.18
version: 1.1.19

environment:
sdk: ^3.6.0
Expand Down
106 changes: 101 additions & 5 deletions packages/vector_graphics/test/render_vector_graphics_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ void main() {
expect(identical(context.canvas.images[0], context.canvas.images[1]), true);
});

test('Changing color filter does not re-rasterize', () async {
test('Changing color filter does re-rasterize', () async {
final RenderVectorGraphic renderVectorGraphic = RenderVectorGraphic(
pictureInfo,
'test',
Expand All @@ -175,11 +175,11 @@ void main() {
const ui.ColorFilter.mode(Colors.red, ui.BlendMode.colorBurn);
renderVectorGraphic.paint(context, Offset.zero);

expect(firstImage.debugDisposed, false);
expect(firstImage.debugDisposed, true);

renderVectorGraphic.paint(context, Offset.zero);

expect(context.canvas.lastImage, equals(firstImage));
expect(context.canvas.lastImage, isNot(firstImage));
});

test('Changing device pixel ratio does re-rasterize and dispose old raster',
Expand Down Expand Up @@ -350,7 +350,8 @@ void main() {
final FakePaintingContext context = FakePaintingContext();
renderVectorGraphic.paint(context, Offset.zero);

expect(context.canvas.lastPaint?.color, const Color.fromRGBO(0, 0, 0, 0.5));
// opaque is used to generate raster cache.
expect(context.canvas.lastPaint?.color, const Color.fromRGBO(0, 0, 0, 1.0));
});

test('Disposing render object disposes picture', () async {
Expand Down Expand Up @@ -403,7 +404,9 @@ void main() {
final ui.PictureRecorder recorder = ui.PictureRecorder();
ui.Canvas(recorder);
final ui.Image image = await recorder.endRecording().toImage(1, 1);
final RasterData data = RasterData(image, 1, const RasterKey('test', 1, 1));
final Paint paint = Paint();
final RasterData data =
RasterData(image, 1, RasterKey('test', 1, 1, paint));

data.dispose();

Expand All @@ -426,6 +429,99 @@ void main() {
expect(context.canvas.totalSaves, 1);
expect(context.canvas.totalSaveLayers, 1);
});

testWidgets('Changing offset does not re-rasterize in raster strategy',
(WidgetTester tester) async {
final RenderVectorGraphic renderVectorGraphic =
RenderVectorGraphic(pictureInfo, 'testOffset', null, 1.0, null, 1.0);
renderVectorGraphic.layout(BoxConstraints.tight(const Size(50, 50)));
final FakePaintingContext context = FakePaintingContext();

renderVectorGraphic.paint(context, Offset.zero);
expect(context.canvas.lastImage, isNotNull);

final ui.Image? oldImage = context.canvas.lastImage;

renderVectorGraphic.paint(context, const Offset(20, 30));
expect(context.canvas.lastImage, isNotNull);
expect(context.canvas.lastImage, equals(oldImage));

renderVectorGraphic.dispose();
});

testWidgets('RenderVectorGraphic re-rasterizes when opacity changes',
(WidgetTester tester) async {
final FixedOpacityAnimation opacity = FixedOpacityAnimation(0.2);
final RenderVectorGraphic renderVectorGraphic = RenderVectorGraphic(
pictureInfo,
'testOpacity',
null,
1.0,
opacity,
1.0,
);

renderVectorGraphic.layout(BoxConstraints.tight(const Size(50, 50)));
final FakePaintingContext context = FakePaintingContext();
renderVectorGraphic.paint(context, Offset.zero);

final ui.Image? oldImage = context.canvas.lastImage;

opacity.value = 0.5;
opacity.notifyListeners();

// Changing opacity requires painting.
expect(renderVectorGraphic.debugNeedsPaint, true);

// Changing opacity need create new raster cache.
renderVectorGraphic.paint(context, Offset.zero);
expect(context.canvas.lastImage, isNotNull);

expect(context.canvas.lastImage, isNot(oldImage));

renderVectorGraphic.dispose();
});

testWidgets(
'Identical widgets reuse raster cache when available in raster startegy',
(WidgetTester tester) async {
final RenderVectorGraphic renderVectorGraphic1 = RenderVectorGraphic(
pictureInfo,
'testOffset',
null,
1.0,
null,
1.0,
);
final RenderVectorGraphic renderVectorGraphic2 = RenderVectorGraphic(
pictureInfo,
'testOffset',
null,
1.0,
null,
1.0,
);
renderVectorGraphic1.layout(BoxConstraints.tight(const Size(50, 50)));
renderVectorGraphic2.layout(BoxConstraints.tight(const Size(50, 50)));

final FakePaintingContext context = FakePaintingContext();

renderVectorGraphic1.paint(context, Offset.zero);

final ui.Image? image1 = context.canvas.lastImage;

renderVectorGraphic2.paint(context, Offset.zero);

final ui.Image? image2 = context.canvas.lastImage;

expect(image1, isNotNull);
expect(image2, isNotNull);

expect(image1, equals(image2));

renderVectorGraphic1.dispose();
renderVectorGraphic2.dispose();
});
}

class FakeCanvas extends Fake implements Canvas {
Expand Down