From 5229402c1071be456bff9d86f662e49a5347f3cc Mon Sep 17 00:00:00 2001 From: Lukas Klingsbo Date: Wed, 5 Aug 2026 18:30:33 +0200 Subject: [PATCH 1/3] perf: Cache decorator and camera render closures instead of allocating per frame --- .../lib/src/camera/camera_component.dart | 36 +++++++++++-------- .../src/components/mixins/has_decorator.dart | 6 +++- .../src/components/position_component.dart | 6 +++- .../lib/src/components/router/route.dart | 6 +++- .../post_process/post_process_component.dart | 29 ++++++++------- .../flame/lib/src/rendering/decorator.dart | 22 ++++++++---- 6 files changed, 68 insertions(+), 37 deletions(-) diff --git a/packages/flame/lib/src/camera/camera_component.dart b/packages/flame/lib/src/camera/camera_component.dart index 3d1761dce70..c534b7ea137 100644 --- a/packages/flame/lib/src/camera/camera_component.dart +++ b/packages/flame/lib/src/camera/camera_component.dart @@ -203,16 +203,6 @@ class CameraComponent extends Component { canvas.save(); try { currentCameras.add(this); - void renderWorld(Canvas canvas) { - canvas.transform2D(viewfinder.transform); - world!.renderFromCamera(canvas); - - // Render the viewfinder elements, which will be in front of - // the world, - // but with the same transforms applied to them. - viewfinder.renderTree(canvas); - } - final postProcessors = children.query(); if (postProcessors.isNotEmpty) { assert( @@ -223,13 +213,11 @@ class CameraComponent extends Component { postProcessor.render( canvas, viewport.virtualSize, - renderWorld, - (context) { - renderContext.currentPostProcess = context; - }, + _renderWorld, + _updatePostProcessContext, ); } else { - renderWorld(canvas); + _renderWorld(canvas); } } finally { currentCameras.removeLast(); @@ -242,6 +230,24 @@ class CameraComponent extends Component { canvas.restore(); } + /// Renders the world and the viewfinder elements through the camera + /// transform. An instance method rather than a local function, so that the + /// render pass does not allocate a closure per camera per frame. + void _renderWorld(Canvas canvas) { + canvas.transform2D(viewfinder.transform); + world!.renderFromCamera(canvas); + // Render the viewfinder elements, which will be in front of the world, + // but with the same transforms applied to them. + viewfinder.renderTree(canvas); + } + + // Not a setter: this is passed as a `ValueSetter` tear-off to + // `PostProcess.render`. + // ignore: use_setters_to_change_properties + void _updatePostProcessContext(PostProcess? context) { + renderContext.currentPostProcess = context; + } + /// Converts from the global (canvas) coordinate space to /// local (camera = viewport + viewfinder). /// diff --git a/packages/flame/lib/src/components/mixins/has_decorator.dart b/packages/flame/lib/src/components/mixins/has_decorator.dart index b3da7be9104..9617ad1a773 100644 --- a/packages/flame/lib/src/components/mixins/has_decorator.dart +++ b/packages/flame/lib/src/components/mixins/has_decorator.dart @@ -16,12 +16,16 @@ import 'package:flame/src/rendering/decorator.dart'; mixin HasDecorator on Component { Decorator? decorator; + /// Cached `super.renderTree` tear-off, so that the render pass does not + /// allocate a fresh closure for [Decorator.applyChain] on every frame. + void Function(Canvas)? _superRenderTree; + @override void renderTree(Canvas canvas) { if (decorator == null) { super.renderTree(canvas); } else { - decorator!.applyChain(super.renderTree, canvas); + decorator!.applyChain(_superRenderTree ??= super.renderTree, canvas); } } } diff --git a/packages/flame/lib/src/components/position_component.dart b/packages/flame/lib/src/components/position_component.dart index 4eb3c2fa08c..80f74c1ca41 100644 --- a/packages/flame/lib/src/components/position_component.dart +++ b/packages/flame/lib/src/components/position_component.dart @@ -518,9 +518,13 @@ class PositionComponent extends Component } } + /// Cached `super.renderTree` tear-off, so that the render pass does not + /// allocate a fresh closure for [Decorator.applyChain] on every frame. + void Function(Canvas)? _superRenderTree; + @override void renderTree(Canvas canvas) { - decorator.applyChain(super.renderTree, canvas); + decorator.applyChain(_superRenderTree ??= super.renderTree, canvas); } @internal diff --git a/packages/flame/lib/src/components/router/route.dart b/packages/flame/lib/src/components/router/route.dart index dbbd91d21d3..3d2c271697e 100644 --- a/packages/flame/lib/src/components/router/route.dart +++ b/packages/flame/lib/src/components/router/route.dart @@ -162,10 +162,14 @@ class Route extends PositionComponent } } + /// Cached `super.renderTree` tear-off, so that the render pass does not + /// allocate a fresh closure for [Decorator.applyChain] on every frame. + void Function(Canvas)? _superRenderTree; + @override void renderTree(Canvas canvas) { if (isRendered) { - _renderEffect.applyChain(super.renderTree, canvas); + _renderEffect.applyChain(_superRenderTree ??= super.renderTree, canvas); } } diff --git a/packages/flame/lib/src/post_process/post_process_component.dart b/packages/flame/lib/src/post_process/post_process_component.dart index cfa597b261a..eb8712fba3a 100644 --- a/packages/flame/lib/src/post_process/post_process_component.dart +++ b/packages/flame/lib/src/post_process/post_process_component.dart @@ -105,22 +105,25 @@ class PostProcessComponent extends PositionComponent { return superSize; } + /// Cached render chain, so that the render pass does not allocate fresh + /// closures for `Decorator.applyChain` on every frame. + void Function(Canvas)? _renderChain; + @override @mustCallSuper void renderTree(Canvas canvas) { - decorator.applyChain( - (canvas) { - postProcess.render( - canvas, - size, - super.renderTreeWithoutDecorator, - (context) { - _renderContext.postProcess = postProcess; - }, - ); - }, - canvas, - ); + decorator.applyChain(_renderChain ??= _buildRenderChain(), canvas); + } + + void Function(Canvas) _buildRenderChain() { + final renderTree = super.renderTreeWithoutDecorator; + void updateContext(PostProcess? context) { + _renderContext.postProcess = postProcess; + } + + return (canvas) { + postProcess.render(canvas, size, renderTree, updateContext); + }; } } diff --git a/packages/flame/lib/src/rendering/decorator.dart b/packages/flame/lib/src/rendering/decorator.dart index b9457223ef1..a5f3190b6ab 100644 --- a/packages/flame/lib/src/rendering/decorator.dart +++ b/packages/flame/lib/src/rendering/decorator.dart @@ -31,16 +31,26 @@ class Decorator { /// The next decorator in the chain, or null if there is none. Decorator? _next; + /// Cached closure that forwards the draw call to the rest of the chain, + /// so that no closure needs to be allocated per frame. It is keyed by the + /// identity of the [_chainedDrawSource] it wraps: callers that pass the + /// same (cached) draw callback every frame reuse the same chain closure. + late void Function(Canvas) _chainedDraw; + void Function(Canvas)? _chainedDrawSource; + /// Applies this and all subsequent decorators if any. /// /// This method is the main method through which the decorator is applied. void applyChain(void Function(Canvas) draw, Canvas canvas) { - apply( - _next == null - ? draw - : (nextCanvas) => _next!.applyChain(draw, nextCanvas), - canvas, - ); + if (_next == null) { + apply(draw, canvas); + } else { + if (!identical(_chainedDrawSource, draw)) { + _chainedDrawSource = draw; + _chainedDraw = (nextCanvas) => _next!.applyChain(draw, nextCanvas); + } + apply(_chainedDraw, canvas); + } } /// Applies visual effect while [draw]ing on the [canvas]. From e5388bdb9a7f0b42e854142c2945eca307ee9a64 Mon Sep 17 00:00:00 2001 From: Lukas Klingsbo Date: Sun, 16 Aug 2026 15:35:56 +0200 Subject: [PATCH 2/3] refactor: Revert the chained-draw caching inside Decorator and add a per-frame allocation benchmark The Decorator-internal chained-draw cache only helped chained decorators, which are rare, and complicated the class, so Decorator is back to its version on main. The caller-side tear-off caching and the empty-queue lifecycle early-out remain, now backed by a benchmark that isolates the fixed per-frame overhead they remove. --- packages/flame/benchmark/README.md | 4 + packages/flame/benchmark/main.dart | 2 + .../per_frame_allocation_benchmark.dart | 145 ++++++++++++++++++ .../flame/lib/src/rendering/decorator.dart | 22 +-- 4 files changed, 157 insertions(+), 16 deletions(-) create mode 100644 packages/flame/benchmark/per_frame_allocation_benchmark.dart diff --git a/packages/flame/benchmark/README.md b/packages/flame/benchmark/README.md index 16c4521fa2b..ad0ddb47511 100644 --- a/packages/flame/benchmark/README.md +++ b/packages/flame/benchmark/README.md @@ -50,6 +50,10 @@ the benchmark results are printed above it. logic and inputs on a two-level tree. - `render_components_benchmark.dart`: render pass over a randomized tree onto a mock canvas. +- `per_frame_allocation_benchmark.dart`: fixed per-frame overhead paid even by + an idle game: the render chain over a wide tree of bare `PositionComponent`s + onto a no-op canvas (per-component closure allocations), and update ticks + with an empty lifecycle queue (per-tick bookkeeping allocations). - `components_at_point_benchmark.dart`: pointer hit testing (`componentsAtPoint`) with and without the hit-test cache. - `collision_detection_benchmark.dart`: the collision detection system with diff --git a/packages/flame/benchmark/main.dart b/packages/flame/benchmark/main.dart index 723946a20f9..abaae6a92b7 100644 --- a/packages/flame/benchmark/main.dart +++ b/packages/flame/benchmark/main.dart @@ -2,6 +2,7 @@ import 'children_traversal_benchmark.dart' as children_traversal; import 'collision_detection_benchmark.dart' as collision_detection; import 'component_churn_benchmark.dart' as component_churn; import 'components_at_point_benchmark.dart' as components_at_point; +import 'per_frame_allocation_benchmark.dart' as per_frame_allocation; import 'priority_change_benchmark.dart' as priority_change; import 'render_components_benchmark.dart' as render_components; import 'type_query_benchmark.dart' as type_query; @@ -14,6 +15,7 @@ Future main() async { await type_query.main(); await update_components.main(); await render_components.main(); + await per_frame_allocation.main(); await components_at_point.main(); await collision_detection.main(); } diff --git a/packages/flame/benchmark/per_frame_allocation_benchmark.dart b/packages/flame/benchmark/per_frame_allocation_benchmark.dart new file mode 100644 index 00000000000..89f30b9a83f --- /dev/null +++ b/packages/flame/benchmark/per_frame_allocation_benchmark.dart @@ -0,0 +1,145 @@ +import 'dart:typed_data'; +import 'dart:ui'; + +import 'package:benchmark_harness/benchmark_harness.dart'; +import 'package:flame/components.dart'; +import 'package:flame/game.dart'; +import 'package:flame/rendering.dart'; + +import 'common.dart'; + +const _dt = 1.0 / 60; + +/// These benchmarks isolate the per-frame allocation overhead of the render +/// and update passes: work that the engine performs on every single frame +/// even when the game itself does nothing. +/// +/// - The render suite draws a wide tree of decorated components onto a canvas +/// that discards every call. A `PositionComponent` routes its render through +/// `Decorator.applyChain`, so any closure or tear-off allocated on the way +/// to it is paid once per component per frame, and with the no-op canvas +/// those allocations are a large share of the measured time. The tree mixes +/// bare [PositionComponent]s with [HasDecorator] components carrying a +/// [PaintDecorator], so that the `Decorator.apply` call site stays +/// polymorphic like in a real game, rather than letting the optimizer +/// devirtualize the single-decorator case and sink the allocations. +/// - The update suite ticks a game with an empty component tree and an empty +/// lifecycle queue, which isolates the fixed per-tick bookkeeping cost of +/// `processLifecycleEvents` and friends from any traversal work. +class RenderDecoratedComponentsBenchmark extends AsyncBenchmarkBase { + static const _amountComponents = 10000; + static const _ticks = 20; + + late final FlameGame _game; + late final Canvas _canvas; + + RenderDecoratedComponentsBenchmark() + : super('Render wide tree of decorated components (10k x 1)'); + + static Future main() async { + await RenderDecoratedComponentsBenchmark().report(); + } + + @override + Future setup() async { + _canvas = _NoopCanvas(); + _game = FlameGame(); + await mountGame(_game); + _game.world.addAll( + List.generate( + _amountComponents, + (i) => i.isEven ? PositionComponent() : _TintedComponent(), + ), + ); + await _game.ready(); + } + + @override + Future run() async { + for (var i = 0; i < _ticks; i++) { + _game.render(_canvas); + } + } +} + +/// A game with nothing in it: each tick only pays the fixed per-tick cost of +/// the camera/world scaffolding and the lifecycle-queue check. +class EmptyLifecycleQueueTickBenchmark extends AsyncBenchmarkBase { + static const _ticks = 10000; + + late final FlameGame _game; + + EmptyLifecycleQueueTickBenchmark() + : super('Update empty game (10k ticks, empty lifecycle queue)'); + + static Future main() async { + await EmptyLifecycleQueueTickBenchmark().report(); + } + + @override + Future setup() async { + _game = FlameGame(); + await mountGame(_game); + } + + @override + Future run() async { + for (var i = 0; i < _ticks; i++) { + _game.update(_dt); + } + } +} + +class _TintedComponent extends Component with HasDecorator { + _TintedComponent() { + decorator = PaintDecorator.tint(const Color(0x44000000)); + } +} + +/// A canvas that discards every call. `MockCanvas` from `canvas_test` records +/// a command list and rescans it on every transform, which both allocates per +/// call and slows down as the list grows; a benchmark that measures +/// engine-side allocations needs the canvas itself to be allocation-free. +/// +/// The hot-path methods of the `PositionComponent` render chain (save, +/// transform, restore) are implemented explicitly so that they do not go +/// through [noSuchMethod], which would allocate an [Invocation] per call. +class _NoopCanvas implements Canvas { + int _saveCount = 1; + + @override + void save() => _saveCount++; + + @override + void restore() => _saveCount--; + + @override + int getSaveCount() => _saveCount; + + @override + void saveLayer(Rect? bounds, Paint paint) => _saveCount++; + + @override + void transform(Float64List matrix4) {} + + @override + void translate(double dx, double dy) {} + + @override + void scale(double sx, [double? sy]) {} + + @override + void clipRect( + Rect rect, { + ClipOp clipOp = ClipOp.intersect, + bool doAntiAlias = true, + }) {} + + @override + dynamic noSuchMethod(Invocation invocation) => null; +} + +Future main() async { + await RenderDecoratedComponentsBenchmark.main(); + await EmptyLifecycleQueueTickBenchmark.main(); +} diff --git a/packages/flame/lib/src/rendering/decorator.dart b/packages/flame/lib/src/rendering/decorator.dart index a5f3190b6ab..b9457223ef1 100644 --- a/packages/flame/lib/src/rendering/decorator.dart +++ b/packages/flame/lib/src/rendering/decorator.dart @@ -31,26 +31,16 @@ class Decorator { /// The next decorator in the chain, or null if there is none. Decorator? _next; - /// Cached closure that forwards the draw call to the rest of the chain, - /// so that no closure needs to be allocated per frame. It is keyed by the - /// identity of the [_chainedDrawSource] it wraps: callers that pass the - /// same (cached) draw callback every frame reuse the same chain closure. - late void Function(Canvas) _chainedDraw; - void Function(Canvas)? _chainedDrawSource; - /// Applies this and all subsequent decorators if any. /// /// This method is the main method through which the decorator is applied. void applyChain(void Function(Canvas) draw, Canvas canvas) { - if (_next == null) { - apply(draw, canvas); - } else { - if (!identical(_chainedDrawSource, draw)) { - _chainedDrawSource = draw; - _chainedDraw = (nextCanvas) => _next!.applyChain(draw, nextCanvas); - } - apply(_chainedDraw, canvas); - } + apply( + _next == null + ? draw + : (nextCanvas) => _next!.applyChain(draw, nextCanvas), + canvas, + ); } /// Applies visual effect while [draw]ing on the [canvas]. From 955aed830e6d4891b54e6853135e4d26334c26f0 Mon Sep 17 00:00:00 2001 From: Lukas Klingsbo Date: Sun, 16 Aug 2026 16:36:56 +0200 Subject: [PATCH 3/3] refactor: Keep only the camera render closure caching --- packages/flame/benchmark/README.md | 4 - packages/flame/benchmark/main.dart | 2 - .../per_frame_allocation_benchmark.dart | 145 ------------------ .../src/components/mixins/has_decorator.dart | 6 +- .../src/components/position_component.dart | 6 +- .../lib/src/components/router/route.dart | 6 +- .../post_process/post_process_component.dart | 29 ++-- 7 files changed, 16 insertions(+), 182 deletions(-) delete mode 100644 packages/flame/benchmark/per_frame_allocation_benchmark.dart diff --git a/packages/flame/benchmark/README.md b/packages/flame/benchmark/README.md index ad0ddb47511..16c4521fa2b 100644 --- a/packages/flame/benchmark/README.md +++ b/packages/flame/benchmark/README.md @@ -50,10 +50,6 @@ the benchmark results are printed above it. logic and inputs on a two-level tree. - `render_components_benchmark.dart`: render pass over a randomized tree onto a mock canvas. -- `per_frame_allocation_benchmark.dart`: fixed per-frame overhead paid even by - an idle game: the render chain over a wide tree of bare `PositionComponent`s - onto a no-op canvas (per-component closure allocations), and update ticks - with an empty lifecycle queue (per-tick bookkeeping allocations). - `components_at_point_benchmark.dart`: pointer hit testing (`componentsAtPoint`) with and without the hit-test cache. - `collision_detection_benchmark.dart`: the collision detection system with diff --git a/packages/flame/benchmark/main.dart b/packages/flame/benchmark/main.dart index abaae6a92b7..723946a20f9 100644 --- a/packages/flame/benchmark/main.dart +++ b/packages/flame/benchmark/main.dart @@ -2,7 +2,6 @@ import 'children_traversal_benchmark.dart' as children_traversal; import 'collision_detection_benchmark.dart' as collision_detection; import 'component_churn_benchmark.dart' as component_churn; import 'components_at_point_benchmark.dart' as components_at_point; -import 'per_frame_allocation_benchmark.dart' as per_frame_allocation; import 'priority_change_benchmark.dart' as priority_change; import 'render_components_benchmark.dart' as render_components; import 'type_query_benchmark.dart' as type_query; @@ -15,7 +14,6 @@ Future main() async { await type_query.main(); await update_components.main(); await render_components.main(); - await per_frame_allocation.main(); await components_at_point.main(); await collision_detection.main(); } diff --git a/packages/flame/benchmark/per_frame_allocation_benchmark.dart b/packages/flame/benchmark/per_frame_allocation_benchmark.dart deleted file mode 100644 index 89f30b9a83f..00000000000 --- a/packages/flame/benchmark/per_frame_allocation_benchmark.dart +++ /dev/null @@ -1,145 +0,0 @@ -import 'dart:typed_data'; -import 'dart:ui'; - -import 'package:benchmark_harness/benchmark_harness.dart'; -import 'package:flame/components.dart'; -import 'package:flame/game.dart'; -import 'package:flame/rendering.dart'; - -import 'common.dart'; - -const _dt = 1.0 / 60; - -/// These benchmarks isolate the per-frame allocation overhead of the render -/// and update passes: work that the engine performs on every single frame -/// even when the game itself does nothing. -/// -/// - The render suite draws a wide tree of decorated components onto a canvas -/// that discards every call. A `PositionComponent` routes its render through -/// `Decorator.applyChain`, so any closure or tear-off allocated on the way -/// to it is paid once per component per frame, and with the no-op canvas -/// those allocations are a large share of the measured time. The tree mixes -/// bare [PositionComponent]s with [HasDecorator] components carrying a -/// [PaintDecorator], so that the `Decorator.apply` call site stays -/// polymorphic like in a real game, rather than letting the optimizer -/// devirtualize the single-decorator case and sink the allocations. -/// - The update suite ticks a game with an empty component tree and an empty -/// lifecycle queue, which isolates the fixed per-tick bookkeeping cost of -/// `processLifecycleEvents` and friends from any traversal work. -class RenderDecoratedComponentsBenchmark extends AsyncBenchmarkBase { - static const _amountComponents = 10000; - static const _ticks = 20; - - late final FlameGame _game; - late final Canvas _canvas; - - RenderDecoratedComponentsBenchmark() - : super('Render wide tree of decorated components (10k x 1)'); - - static Future main() async { - await RenderDecoratedComponentsBenchmark().report(); - } - - @override - Future setup() async { - _canvas = _NoopCanvas(); - _game = FlameGame(); - await mountGame(_game); - _game.world.addAll( - List.generate( - _amountComponents, - (i) => i.isEven ? PositionComponent() : _TintedComponent(), - ), - ); - await _game.ready(); - } - - @override - Future run() async { - for (var i = 0; i < _ticks; i++) { - _game.render(_canvas); - } - } -} - -/// A game with nothing in it: each tick only pays the fixed per-tick cost of -/// the camera/world scaffolding and the lifecycle-queue check. -class EmptyLifecycleQueueTickBenchmark extends AsyncBenchmarkBase { - static const _ticks = 10000; - - late final FlameGame _game; - - EmptyLifecycleQueueTickBenchmark() - : super('Update empty game (10k ticks, empty lifecycle queue)'); - - static Future main() async { - await EmptyLifecycleQueueTickBenchmark().report(); - } - - @override - Future setup() async { - _game = FlameGame(); - await mountGame(_game); - } - - @override - Future run() async { - for (var i = 0; i < _ticks; i++) { - _game.update(_dt); - } - } -} - -class _TintedComponent extends Component with HasDecorator { - _TintedComponent() { - decorator = PaintDecorator.tint(const Color(0x44000000)); - } -} - -/// A canvas that discards every call. `MockCanvas` from `canvas_test` records -/// a command list and rescans it on every transform, which both allocates per -/// call and slows down as the list grows; a benchmark that measures -/// engine-side allocations needs the canvas itself to be allocation-free. -/// -/// The hot-path methods of the `PositionComponent` render chain (save, -/// transform, restore) are implemented explicitly so that they do not go -/// through [noSuchMethod], which would allocate an [Invocation] per call. -class _NoopCanvas implements Canvas { - int _saveCount = 1; - - @override - void save() => _saveCount++; - - @override - void restore() => _saveCount--; - - @override - int getSaveCount() => _saveCount; - - @override - void saveLayer(Rect? bounds, Paint paint) => _saveCount++; - - @override - void transform(Float64List matrix4) {} - - @override - void translate(double dx, double dy) {} - - @override - void scale(double sx, [double? sy]) {} - - @override - void clipRect( - Rect rect, { - ClipOp clipOp = ClipOp.intersect, - bool doAntiAlias = true, - }) {} - - @override - dynamic noSuchMethod(Invocation invocation) => null; -} - -Future main() async { - await RenderDecoratedComponentsBenchmark.main(); - await EmptyLifecycleQueueTickBenchmark.main(); -} diff --git a/packages/flame/lib/src/components/mixins/has_decorator.dart b/packages/flame/lib/src/components/mixins/has_decorator.dart index 9617ad1a773..b3da7be9104 100644 --- a/packages/flame/lib/src/components/mixins/has_decorator.dart +++ b/packages/flame/lib/src/components/mixins/has_decorator.dart @@ -16,16 +16,12 @@ import 'package:flame/src/rendering/decorator.dart'; mixin HasDecorator on Component { Decorator? decorator; - /// Cached `super.renderTree` tear-off, so that the render pass does not - /// allocate a fresh closure for [Decorator.applyChain] on every frame. - void Function(Canvas)? _superRenderTree; - @override void renderTree(Canvas canvas) { if (decorator == null) { super.renderTree(canvas); } else { - decorator!.applyChain(_superRenderTree ??= super.renderTree, canvas); + decorator!.applyChain(super.renderTree, canvas); } } } diff --git a/packages/flame/lib/src/components/position_component.dart b/packages/flame/lib/src/components/position_component.dart index 80f74c1ca41..4eb3c2fa08c 100644 --- a/packages/flame/lib/src/components/position_component.dart +++ b/packages/flame/lib/src/components/position_component.dart @@ -518,13 +518,9 @@ class PositionComponent extends Component } } - /// Cached `super.renderTree` tear-off, so that the render pass does not - /// allocate a fresh closure for [Decorator.applyChain] on every frame. - void Function(Canvas)? _superRenderTree; - @override void renderTree(Canvas canvas) { - decorator.applyChain(_superRenderTree ??= super.renderTree, canvas); + decorator.applyChain(super.renderTree, canvas); } @internal diff --git a/packages/flame/lib/src/components/router/route.dart b/packages/flame/lib/src/components/router/route.dart index 3d2c271697e..dbbd91d21d3 100644 --- a/packages/flame/lib/src/components/router/route.dart +++ b/packages/flame/lib/src/components/router/route.dart @@ -162,14 +162,10 @@ class Route extends PositionComponent } } - /// Cached `super.renderTree` tear-off, so that the render pass does not - /// allocate a fresh closure for [Decorator.applyChain] on every frame. - void Function(Canvas)? _superRenderTree; - @override void renderTree(Canvas canvas) { if (isRendered) { - _renderEffect.applyChain(_superRenderTree ??= super.renderTree, canvas); + _renderEffect.applyChain(super.renderTree, canvas); } } diff --git a/packages/flame/lib/src/post_process/post_process_component.dart b/packages/flame/lib/src/post_process/post_process_component.dart index eb8712fba3a..cfa597b261a 100644 --- a/packages/flame/lib/src/post_process/post_process_component.dart +++ b/packages/flame/lib/src/post_process/post_process_component.dart @@ -105,25 +105,22 @@ class PostProcessComponent extends PositionComponent { return superSize; } - /// Cached render chain, so that the render pass does not allocate fresh - /// closures for `Decorator.applyChain` on every frame. - void Function(Canvas)? _renderChain; - @override @mustCallSuper void renderTree(Canvas canvas) { - decorator.applyChain(_renderChain ??= _buildRenderChain(), canvas); - } - - void Function(Canvas) _buildRenderChain() { - final renderTree = super.renderTreeWithoutDecorator; - void updateContext(PostProcess? context) { - _renderContext.postProcess = postProcess; - } - - return (canvas) { - postProcess.render(canvas, size, renderTree, updateContext); - }; + decorator.applyChain( + (canvas) { + postProcess.render( + canvas, + size, + super.renderTreeWithoutDecorator, + (context) { + _renderContext.postProcess = postProcess; + }, + ); + }, + canvas, + ); } }