`PostProcessSequentialGroup` skips earlier post processes when the subtree is rendered more than once

Cerrado
#4,193 0 comentarios 0 reacciones 1 asignado Ver en GitHub

Los mantenedores suelen responder en 1 día

@ufrshubham ya está trabajando en esto.

Desde el 1/11/2026.

  • #4194 de @ufrshubham — abierto

Evaluación

Este issue todavía no se ha evaluado.

Descripción

bug
What happened?

When a post process in a PostProcessSequentialGroup renders its subtree more than once, only the first call runs the post processes before it in the group. Every later call renders the raw tree and silently skips them.

Rendering the subtree more than once is common for multi-pass effects, and the PostProcess docs explicitly allow it. A drop shadow, for example, renders the subtree once as a dark, blurred shadow and then again for the scene itself. In PostProcessSequentialGroup([Grayscale(), DropShadow()]), the grayscale is only applied to the shadow pass, so the scene itself stays in full color and the grayscale effect appears to do nothing. Nothing is thrown.

https://github.com/user-attachments/assets/7315eab9-d002-490a-a954-368d15e5dc82

What do you expect?

Every call to renderSubtree or rasterizeSubtree should run the full chain of post processes before it in the group. In the example below, the whole scene should be gray, with a shadow, the same as nesting the two post processes by hand.

How can we reproduce this?
  1. Create a PostProcessSequentialGroup with a post process that renders its subtree twice, such as a drop shadow, after another post process, such as a grayscale filter.
  2. Set it as camera.postProcess, with some colored components in the world.
  3. The shadows are drawn, but the components stay in color.
What steps should take to fix this?

PostProcessSequentialGroup.render (post_process.dart#L245-L260) builds a single list and removes post processes from it as it goes:

final stack = postProcesses.reversed.toList();

void runNext(Canvas c) {
  if (stack.isEmpty) {
    renderTree(c);
    return;
  }
  final postProcess = stack.removeAt(0);
  postProcess.render(c, size, runNext, updateContext);
}

The first subtree render empties the list, so the next one finds it empty and goes straight to renderTree.

Do have an example of where the bug occurs?
import 'dart:ui';

import 'package:flame/components.dart';
import 'package:flame/game.dart';
import 'package:flame/post_process.dart';
import 'package:flutter/material.dart' show Colors;
import 'package:flutter/widgets.dart';

void main() => runApp(GameWidget(game: ReproGame()));

class ReproGame extends FlameGame {
  @override
  Future<void> onLoad() async {
    world.addAll([
      CircleComponent(
        radius: 40,
        position: Vector2(-160, -40),
        paint: Paint()..color = Colors.red,
      ),
      RectangleComponent(
        size: Vector2.all(80),
        position: Vector2(-40, -40),
        paint: Paint()..color = Colors.green,
      ),
      CircleComponent(
        radius: 40,
        position: Vector2(80, -40),
        paint: Paint()..color = Colors.blue,
      ),
    ]);
    camera.postProcess = PostProcessSequentialGroup(
      // Grayscale is applied first, then DropShadow, so the whole scene
      // should be gray, with a shadow.
      postProcesses: [Grayscale(), DropShadow()],
    );
  }
}

/// Renders its subtree in grayscale.
class Grayscale extends PostProcess {
  final _paint = Paint()
    ..colorFilter = const ColorFilter.matrix([
      0.2126, 0.7152, 0.0722, 0, 0, //
      0.2126, 0.7152, 0.0722, 0, 0, //
      0.2126, 0.7152, 0.0722, 0, 0, //
      0, 0, 0, 1, 0, //
    ]);

  @override
  void postProcess(Vector2 size, Canvas canvas) {
    canvas.saveLayer(null, _paint);
    renderSubtree(canvas);
    canvas.restore();
  }
}

/// Renders its subtree as a dark, blurred shadow, then again on top.
class DropShadow extends PostProcess {
  final _shadowPaint = Paint()
    ..colorFilter = const ColorFilter.mode(Color(0xAA000000), BlendMode.srcIn)
    ..imageFilter = ImageFilter.blur(sigmaX: 3, sigmaY: 3);

  @override
  void postProcess(Vector2 size, Canvas canvas) {
    // First pass: the shadow.
    canvas
      ..save()
      ..translate(8, 10)
      ..saveLayer(null, _shadowPaint);
    renderSubtree(canvas);
    canvas
      ..restore()
      ..restore();

    // Second pass: the scene itself.
    renderSubtree(canvas);
  }
}

Expected: gray shapes with shadows. Actual: red, green and blue shapes with shadows. Replacing the group with Grayscale() alone turns the shapes gray, so the filter itself works.

Relevant log output
Nothing is logged or thrown.
Execute in a terminal and put output into the code block below
[√] Flutter (Channel stable, 3.47.8, on Microsoft Windows [Version 10.0.22631.6199], locale en-US)
    • Flutter version 3.47.8 on channel stable
    • Framework revision 737ba8f885 (8 days ago), 2026-10-22 21:37:31 -0700
    • Engine revision cf636b1dfb
    • Dart version 3.13.7
    • DevTools version 2.60.0

[√] Windows Version (11 Pro 64-bit, 23H2, 2009)
[√] Android toolchain - develop for Android devices (Android SDK version 36.1.0)
[√] Chrome - develop for the web
[√] Visual Studio - develop Windows apps (Visual Studio Community 2026 18.11.2)
[√] Connected device (3 available)
[√] Network resources

• No issues found!
Affected platforms

All

Are you interested in working on a PR for this?
  • I want to work on this
Lenguaje dominante
Dart
Estrellas
10.8k
Forks
1.1k
Merge medio
1 d 21 h
PR fusionados (30 d)
62

Preparar el entorno

Primeros pasos

  1. Lee el issue completo y luego la guía de contribución del proyecto.
  2. Comenta en el issue que vas a ocuparte — evita que dos personas hagan lo mismo.
  3. Haz un fork del repositorio y trabaja en una rama.
  4. Abre un pull request que haga referencia al número del issue.

Más de flame-engine/flame

Todos los issues de flame-engine/flame

Issues similares

Más issues de Dart

Recibe los nuevos issues en tu correo

Un resumen breve de issues de GitHub para principiantes.