Skip to content

sparse_strips: Add ColorMatrix filter - #1765

Open
AdrianEddy wants to merge 1 commit into
linebender:mainfrom
AdrianEddy:filter-colormatrix
Open

AdrianEddy wants to merge 1 commit into
linebender:mainfrom
AdrianEddy:filter-colormatrix

Conversation

@AdrianEddy

@AdrianEddy AdrianEddy commented Jul 24, 2026 •

Copy link
Copy Markdown
Contributor

Adds a ColorMatrix filter: a 4x5 matrix in the style of SVG feColorMatrix, where each output channel (R, G, B, A) is a linear combination of the straight-alpha input channels plus a constant offset, clamped to [0, 1].

This reimplements #1623 by @waywardmonkeys on top of current main (he is credited as co-author) and addresses the review feedback from there. On the color space question raised there: like the other filters, the matrix is applied to the straight-alpha, sRGB-encoded values, and the math stays local with no new dependency; it could move to a shared crate later.

In vello_cpu it is a per-pixel pass. Matrices that only mix the color channels and keep alpha unchanged (grayscale, sepia) are applied directly to the premultiplied pixels, skipping the unpremultiply/premultiply round trip. SIMD and running it outside the spatial filter path are left as a TODO (Flood has the same limitation).

In vello_gpu it is one more filter pass. A color matrix needs six texels of parameters, and until now every filter got the same fixed slot, so this switches the filter data to a variable stride like encoded paints: filters are packed back to back and each takes only what it needs (offset and flood one texel, blur two, drop shadow three, color matrix six).

Tests: snapshot tests, each run on vello_cpu (u8 and f32, every SIMD level) and on vello_gpu, for sepia (the premultiplied path on the CPU), ALPHA_TO_BLACK (the straight-alpha path), and a matrix with a distinct non-zero value in every coefficient and a positive alpha offset, so a layout mix-up shows and fully transparent pixels in the layer are covered too. Unit tests cover the CPU paths, including that the premultiplied shortcut matches the straight-alpha path, and the GPU packing.

This PR was generated by Claude.

@AdrianEddy
AdrianEddy force-pushed the filter-colormatrix branch 2 times, most recently from c85156e to 9af510d Compare July 27, 2026 12:32
@AdrianEddy
AdrianEddy force-pushed the filter-colormatrix branch 2 times, most recently from 89cc579 to b77f5f3 Compare August 14, 2026 06:22
Add a 4x5 color matrix filter (`feColorMatrix` style): each output
channel is a linear combination of the straight-alpha input channels plus
a constant offset.

vello_cpu applies matrices that only mix the color channels and preserve
alpha directly to the premultiplied channels, skipping the
unpremultiply/premultiply round trip.

vello_gpu now packs filter parameters back to back with a per-filter
texel stride, like encoded paints, instead of a fixed three-texel slot,
so the six-texel color matrix doesn't widen every other filter: offset
and flood take one texel, gaussian blur two, drop shadow three.

Builds on the original color matrix work in linebender#1623.

Co-authored-by: Bruce Mitchener <bruce.mitchener@gmail.com>
@nicoburns

Copy link
Copy Markdown
Contributor

@LaurenzV @laurenz-canva @grebmeg What would it take to land this?

This + "linear chains of filters" would be sufficient for feature complete CSS filter() support (for the built-in filters)

@nicoburns

Copy link
Copy Markdown
Contributor

AI-review finds one issue:

[P2] Preserve transparent draws when ColorMatrix can create opacity. The GPU renderer rejects fully transparent solid paints before recording their geometry or applying filters. With the new ColorMatrix support, those pixels can become visible through a positive alpha offset. scene.rs:455-460

Reproduction:

use vello_common::color::{AlphaColor, PremulRgba8};
use vello_common::filter_effects::{Filter, FilterPrimitive};
use vello_common::kurbo::Rect;
use vello_cpu::{Level, RenderMode};
use vello_tests::renderer::{CpuRenderer, GpuRenderer, Renderer};
 
fn render<R: Renderer>(alpha: u8, inline: bool) -> PremulRgba8 {
    let mut ctx = R::new(64, 64, 0, Level::baseline(), RenderMode::OptimizeQuality);
    let filter = Filter::from_primitive(FilterPrimitive::ColorMatrix {
        matrix: [
            0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
            0.0, 0.0, 1.0,
        ],
    });
    if inline {
        ctx.set_filter_effect(filter);
    } else {
        ctx.push_filter_layer(filter);
    }
    ctx.set_paint(AlphaColor::from_rgba8(0, 0, 255, alpha));
    ctx.fill_rect(&Rect::new(16.0, 16.0, 48.0, 48.0));
    if !inline {
        ctx.pop_layer();
    }
    ctx.flush();
    ctx.render();
    ctx.snapshot().data()[32 * 64 + 32]
}
 
#[test]
fn review_color_matrix_transparent_input() {
    let expected = PremulRgba8 {
        r: 255,
        g: 0,
        b: 0,
        a: 255,
    };
    let mut all_match = true;
    for inline in [false, true] {
        for alpha in [0, 1, 128, 255] {
            let cpu = render::<CpuRenderer>(alpha, inline);
            let gpu = render::<GpuRenderer>(alpha, inline);
            println!("inline={inline} alpha={alpha}: CPU={cpu:?} GPU={gpu:?}");
            all_match &= cpu == expected && gpu == expected;
        }
    }
    assert!(
        all_match,
        "constant red matrix must produce red for every input alpha"
    );
}

@AdrianEddy

Copy link
Copy Markdown
Contributor Author

AI-review finds one issue:

[P2] Preserve transparent draws when ColorMatrix can create opacity. The GPU renderer rejects fully transparent solid paints before recording their geometry or applying filters. With the new ColorMatrix support, those pixels can become visible through a positive alpha offset. scene.rs:455-460

Thanks, the issue is real, but it's pre-existing on main and not specific to ColorMatrix. Fixed separately in #1966, which keeps transparent draws while a filter is active.

@grebmeg
grebmeg self-requested a review September 30, 2026 05:55
@grebmeg

grebmeg commented Sep 30, 2026 •

Copy link
Copy Markdown
Collaborator

@LaurenzV @laurenz-canva @grebmeg What would it take to land this?

This + "linear chains of filters" would be sufficient for feature complete CSS filter() support (for the built-in filters)

I'm definitely in favour of landing ColorMatrix filter, and we also have plans to support filter chains. However, we'd like to avoid making our uber shader even more "uber", so we'd prefer to first land a mechanism for enabling/disabling functionality like this as needed.

At the earliest, I could look into that next week. Once that's landed, we can continue with this and other filters.

This branch has not been deployed

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants