Skip to content

BackdropFilter shader UV sampling is incorrect on beta when using ClipRect #178798

Closed

Description

@timcreatedit

This issue might be related to #170820

Steps to reproduce

  1. Run the code sample on the current beta channel, and on 3.38.0 (stable)
  2. They produce different outputs

Expected results

The UV's of BackdropFilter used to stay consistent, even if it was clipped. Output on 3.38.0:

Image

Notice how the top left of the shader's rect is not entirely black, and the purple border isn't visible, because UV (0,0) is the top left of the screen, not of the clip rect.

Actual results

Output on 3.39.0-0.1.pre:

Image

Notice how the UV now seem to be in the coordinate space of the ClipRectLayer

Code sample

Code sample

assets/shaders/uv.frag:

#version 460 core
precision mediump float;

#include <flutter/runtime_effect.glsl>

uniform vec2 uSize;
uniform sampler2D uTexture;

out vec4 fragColor;

void main() {
    vec2 fragCoord = FlutterFragCoord().xy;

    vec2 screenUV = vec2(fragCoord.x / uSize.x, fragCoord.y / uSize.y);        
    vec4 texColor = texture(uTexture, screenUV);
        
    #ifdef IMPELLER_TARGET_OPENGLES
        screenUV.y = 1.0 - screenUV.y;
    #endif
    
    // Check if we're within 20px of any edge
    float borderWidth = 20.0;
    bool inBorder = fragCoord.x < borderWidth || 
                    fragCoord.x > (uSize.x - borderWidth) ||
                    fragCoord.y < borderWidth || 
                    fragCoord.y > (uSize.y - borderWidth);
    
    if (inBorder) {
        fragColor = vec4(0.5, 0.0, 0.5, 1.0); // Purple border
    } else {
        fragColor = vec4(screenUV, 0.0, 1.0);
        fragColor = mix(fragColor, texColor, .5);
    }
}

pubspec.yaml

name: flutter_sandbox
description: "A new Flutter project."
publish_to: 'none'
version: 0.1.0

environment:
  sdk: ^3.9.2

dependencies:
  flutter:
    sdk: flutter
  flutter_shaders: ^0.1.3

dev_dependencies:
  flutter_test:
    sdk: flutter
  flutter_lints: ^5.0.0

flutter:
  uses-material-design: true

  shaders:
    - assets/shaders/uv.frag

lib/main.dart:

import 'dart:ui';

import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_shaders/flutter_shaders.dart';

void main() {
  runApp(const MainApp());
}

class MainApp extends StatelessWidget {
  const MainApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        body: Stack(
          children: [
            Positioned.fill(child: GridPaper()),
            Positioned.fill(
              child: ShaderBuilder(
                assetKey: 'assets/shaders/uv.frag',
                (context, shader, child) => MyShaderWidget(shader: shader),
              ),
            ),
          ],
        ),
      ),
    );
  }
}

class MyShaderWidget extends SingleChildRenderObjectWidget {
  const MyShaderWidget({super.key, required this.shader, super.child});

  final FragmentShader shader;

  @override
  RenderObject createRenderObject(BuildContext context) {
    return RenderMyShader(shader);
  }
}

class RenderMyShader extends RenderProxyBox {
  RenderMyShader(this.shader);

  final FragmentShader shader;

  final LayerHandle<ClipRectLayer> _clipRectLayer =
      LayerHandle<ClipRectLayer>();

  final LayerHandle<BackdropFilterLayer> _backdropFilterLayer =
      LayerHandle<BackdropFilterLayer>();

  static const inset = EdgeInsets.all(66);

  @override
  void paint(PaintingContext context, Offset offset) {
    final rect = offset & size;
    _clipRectLayer.layer = context.pushClipRect(
      true,
      offset,
      inset.deflateRect(rect),
      (context, offset) {
        final layer = _backdropFilterLayer.layer ??= BackdropFilterLayer();

        layer.filter = ImageFilter.shader(shader);
        context.pushLayer(layer, (context, offset) {}, offset);
      },
      oldLayer: _clipRectLayer.layer,
    );

    super.paint(context, offset);
  }

  @override
  void dispose() {
    _clipRectLayer.layer = null;
    _backdropFilterLayer.layer = null;
    super.dispose();
  }
}

Logs

No response

Flutter Doctor output

Doctor output
[✓] Flutter (Channel beta, 3.39.0-0.1.pre, on macOS 15.7.2 24G325 darwin-arm64, locale
    en-US)
[✓] Android toolchain - develop for Android devices (Android SDK version 36.1.0-rc1)
[✓] Xcode - develop for iOS and macOS (Xcode 26.0)
[✓] Chrome - develop for the web
[✓] Connected device (4 available)
[✓] Network resources

• No issues found!

Activity

  1. added
    e: impellerImpeller rendering backend issues and features requests
    on Nov 19, 2025
  2. gaaclarke commented on Nov 20, 2025

    @gaaclarke
    Member

    To clarify, the OP is expecting that image because they expect the purple rect to be part of the clipped region.

    Like this unclipped:

    Image

    I'm not sure if this is desired behavior or not. The behavior was not documented anywhere I can find either in comments or in tests. That means it's not technically a breaking change. We have to decide if this is the behavior we want or not though.

  3. gaaclarke commented on Nov 20, 2025

    @gaaclarke
    Member

    Here is a more simple version:

    Details
    import 'dart:ui';
    
    import 'package:flutter/material.dart';
    import 'package:flutter_shaders/flutter_shaders.dart';
    
    void main() {
      runApp(const MainApp());
    }
    
    class MainApp extends StatelessWidget {
      const MainApp({super.key});
    
      @override
      Widget build(BuildContext context) {
        return MaterialApp(
          home: Scaffold(
            body: Stack(
              children: [
                Positioned.fill(child: GridPaper()),
                Positioned.fill(
                  child: Padding(
                    padding: const EdgeInsets.all(66.0),
                    child: ClipRect(
                      child: ShaderBuilder(
                        assetKey: 'assets/shaders/uv.frag',
                        (context, shader, child) {
                          return BackdropFilter(
                            filter: ImageFilter.shader(shader),
                            child: child,
                          );
                        },
                        child: const ColoredBox(color: Colors.transparent),
                      ),
                    ),
                  ),
                ),
              ],
            ),
          ),
        );
      }
    }

    If you switch the code from a backdrop filter to a gradient you can see that gradients are exhibiting the same behavior as the fragment shaders. The top righthand corner in the clipped region is is (0, 255, 0). That suggests to me that the current behavior is the expected one.

    Details
    import 'dart:ui';
    
    import 'package:flutter/material.dart';
    
    void main() {
      runApp(const MainApp());
    }
    
    class MainApp extends StatelessWidget {
      const MainApp({super.key});
    
      @override
      Widget build(BuildContext context) {
        return MaterialApp(
          home: Scaffold(
            body: Stack(
              children: [
                Positioned.fill(child: GridPaper()),
                Positioned.fill(
                  child: Padding(
                    padding: const EdgeInsets.all(66.0),
                    child: ClipRect(
                      child: DecoratedBox(
                        decoration: BoxDecoration(
                          gradient: LinearGradient(
                            colors: [
                              Colors.black,
                              const Color.fromARGB(255, 0, 255, 8),
                            ],
                          ),
                        ),
                      ),
                    ),
                  ),
                ),
              ],
            ),
          ),
        );
      }
    }
    
    Image

    Also, if you read the documentation for the ClipRect it says "ClipRect prevents its child from painting outside its bounds". That suggests to me that the current behavior is the correct behavior because FragCoords should not be drawing or considering fragments outside of the bounds of the ClipRect.

    The change is not technically a breaking change because it broke no tests. I can look into doing a courtesy notification.

  4. gaaclarke commented on Nov 20, 2025

    @gaaclarke
    Member

    @timcreatedit also fyi there are customer submitted tests in flutter. When your project becomes more mature it's worth considering contributing there: https://github.com/flutter/tests

  5. gaaclarke commented on Nov 20, 2025

    @gaaclarke
    Member

    Reopening this since I'd like to codify it in our own test so that it doesn't change again in the future.

  6. gaaclarke commented on Nov 21, 2025

    @gaaclarke
    Member

    @jason-simmons made a good point that Canvas.drawPaint with a fragment shader behaves like the old behavior. Also, the example above with the gradient is potentially flawed since the padding is also affecting the widget size and not just the cliprect size. We need to circle back to this. We should be careful too of performance, it may be that we can get better performance by editing the uniforms instead of tweaking the vertices.

  7. gaaclarke commented on Nov 21, 2025

    @gaaclarke
    Member

    I converted the reproduction to an impeller unittest, it's a bit easier to see for engine types: #178940

  8. added
    P2Important issues not at the top of the work list
    on Nov 24, 2025
  9. self-assigned this
    on Nov 25, 2025
  10. github-actions commented on Dec 11, 2025

    @github-actions

    This thread has been automatically locked since there has not been any recent activity after it was closed. If you are still experiencing a similar issue, please open a new bug, including the output of flutter doctor -v and a minimal reproduction of the issue.

  11. locked as resolved and limited conversation to collaborators on Dec 11, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

P2Important issues not at the top of the work liste: impellerImpeller rendering backend issues and features requeststeam-engineOwned by Engine teamtriaged-engineTriaged by Engine team

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions