@@ -1909,6 +1909,20 @@ struct BuiltFilterResult {
19091909 /// exact byte-domain SrcOver. Native sRGB shadows reach Skia's lowp 8888
19101910 /// restore, whose default division rounds differently on NEON and x86.
19111911 requires_exact_restore : bool ,
1912+ /// A matrix node exposes a second measured final-layer boundary in the
1913+ /// pinned backend. Source-derived matrix output needs an explicit floating
1914+ /// SrcOver; generated-only matrix output needs the backend default.
1915+ matrix_restore : Option < MatrixRestore > ,
1916+ /// Chromium snapshots source-derived matrix input through one additional
1917+ /// source-image boundary unless the matrix creates alpha from transparent
1918+ /// input. Preserve that measured boundary through later graph nodes.
1919+ matrix_source_preflatten : bool ,
1920+ }
1921+
1922+ #[ derive( Clone , Copy , Debug , PartialEq , Eq ) ]
1923+ enum MatrixRestore {
1924+ Default ,
1925+ Floating ,
19121926}
19131927
19141928struct BuiltFilter {
@@ -1917,6 +1931,7 @@ struct BuiltFilter {
19171931 /// between their internal nodes. Source-derived coverage stays on Skia's
19181932 /// floating restore path.
19191933 restore_blender : Option < Blender > ,
1934+ matrix_source_preflatten : bool ,
19201935}
19211936
19221937fn source_alpha_filter ( ) -> Option < ImageFilter > {
@@ -1959,6 +1974,10 @@ fn convert_filter_space(
19591974 // A color-space conversion resumes the floating filter path; its
19601975 // eventual restore must not be forced through the N32 shadow rule.
19611976 requires_exact_restore : false ,
1977+ // Gamma conversion materializes a new floating result and ends the
1978+ // matrix-specific sRGB restore boundary.
1979+ matrix_restore : None ,
1980+ matrix_source_preflatten : result. matrix_source_preflatten ,
19621981 } )
19631982}
19641983
@@ -2093,6 +2112,8 @@ fn build_filter(filter: &ResolvedFilter) -> Result<BuiltFilter, String> {
20932112 color_space : ResolvedFilterColorSpace :: Srgb ,
20942113 source_dependent : true ,
20952114 requires_exact_restore : false ,
2115+ matrix_restore : None ,
2116+ matrix_source_preflatten : false ,
20962117 } ;
20972118 let source_alpha =
20982119 BuiltFilterResult {
@@ -2102,6 +2123,8 @@ fn build_filter(filter: &ResolvedFilter) -> Result<BuiltFilter, String> {
21022123 color_space : ResolvedFilterColorSpace :: Srgb ,
21032124 source_dependent : true ,
21042125 requires_exact_restore : false ,
2126+ matrix_restore : None ,
2127+ matrix_source_preflatten : false ,
21052128 } ;
21062129 let mut results: Vec < BuiltFilterResult > = Vec :: with_capacity ( filter. nodes . len ( ) ) ;
21072130 for node in filter. nodes . iter ( ) {
@@ -2120,6 +2143,36 @@ fn build_filter(filter: &ResolvedFilter) -> Result<BuiltFilter, String> {
21202143 let crop = Rect :: from_xywh ( node. region . x , node. region . y , node. region . w , node. region . h ) ;
21212144 let source_dependent = inputs. iter ( ) . any ( |input| input. source_dependent ) ;
21222145 let requires_exact_restore = inputs. iter ( ) . any ( |input| input. requires_exact_restore ) ;
2146+ let inherited_matrix_restore = if inputs
2147+ . iter ( )
2148+ . any ( |input| input. matrix_restore == Some ( MatrixRestore :: Floating ) )
2149+ {
2150+ Some ( MatrixRestore :: Floating )
2151+ } else if inputs
2152+ . iter ( )
2153+ . any ( |input| input. matrix_restore == Some ( MatrixRestore :: Default ) )
2154+ {
2155+ Some ( MatrixRestore :: Default )
2156+ } else {
2157+ None
2158+ } ;
2159+ let matrix_restore = match node. primitive {
2160+ ResolvedFilterPrimitive :: ColorMatrix { matrix } => {
2161+ Some ( if matrix[ 19 ] > 0.0 || !source_dependent {
2162+ MatrixRestore :: Default
2163+ } else {
2164+ MatrixRestore :: Floating
2165+ } )
2166+ }
2167+ ResolvedFilterPrimitive :: SolidColor { .. } => None ,
2168+ _ => inherited_matrix_restore,
2169+ } ;
2170+ let matrix_source_preflatten = inputs. iter ( ) . any ( |input| input. matrix_source_preflatten )
2171+ || matches ! (
2172+ & node. primitive,
2173+ ResolvedFilterPrimitive :: ColorMatrix { matrix }
2174+ if source_dependent && matrix[ 19 ] <= 0.0
2175+ ) ;
21232176 let ( image_filter, output_space, source_dependent, requires_exact_restore) = match node
21242177 . primitive
21252178 {
@@ -2321,6 +2374,24 @@ fn build_filter(filter: &ResolvedFilter) -> Result<BuiltFilter, String> {
23212374 node. color_space == ResolvedFilterColorSpace :: Srgb ,
23222375 )
23232376 }
2377+ ResolvedFilterPrimitive :: ColorMatrix { matrix } => {
2378+ let input = inputs. pop ( ) . expect ( "color matrix has one checked input" ) ;
2379+ let color_filter = skia_safe:: color_filters:: matrix_row_major ( & matrix, None ) ;
2380+ let filter =
2381+ skia_safe:: image_filters:: color_filter ( color_filter, input. image_filter , crop)
2382+ . ok_or_else ( || {
2383+ "the backend could not construct a color-matrix operation" . to_string ( )
2384+ } ) ?;
2385+ // Matrix arithmetic supersedes any final-restore policy on
2386+ // its input. Chromium's source-derived and generated-only
2387+ // matrix results take two distinct measured sRGB restores.
2388+ (
2389+ Some ( filter) ,
2390+ node. color_space ,
2391+ input. source_dependent ,
2392+ false ,
2393+ )
2394+ }
23242395 ResolvedFilterPrimitive :: Merge => {
23252396 let mut inputs = inputs. into_iter ( ) ;
23262397 let image_filter = if let Some ( first) = inputs. next ( ) {
@@ -2368,6 +2439,8 @@ fn build_filter(filter: &ResolvedFilter) -> Result<BuiltFilter, String> {
23682439 color_space : output_space,
23692440 source_dependent,
23702441 requires_exact_restore,
2442+ matrix_restore,
2443+ matrix_source_preflatten,
23712444 } ) ;
23722445 }
23732446 let output = results
@@ -2393,17 +2466,120 @@ fn build_filter(filter: &ResolvedFilter) -> Result<BuiltFilter, String> {
23932466 } ) ?,
23942467 ) ;
23952468 }
2396- let restore_blender = if output. source_dependent && !output. requires_exact_restore {
2469+ let restore_blender = if output. requires_exact_restore {
2470+ Some ( exact_unorm8_blender ( ResolvedFilterComposite :: Over ) ?)
2471+ } else if output. matrix_restore == Some ( MatrixRestore :: Floating ) {
2472+ Some ( floating_porter_duff_blender ( ResolvedFilterComposite :: Over ) ?)
2473+ } else if output. matrix_restore == Some ( MatrixRestore :: Default ) || output. source_dependent {
23972474 None
23982475 } else {
23992476 Some ( exact_unorm8_blender ( ResolvedFilterComposite :: Over ) ?)
24002477 } ;
24012478 Ok ( BuiltFilter {
24022479 image_filter : output. image_filter ,
24032480 restore_blender,
2481+ matrix_source_preflatten : output. matrix_source_preflatten ,
24042482 } )
24052483}
24062484
2485+ #[ cfg( test) ]
2486+ mod color_matrix_filter_policy_tests {
2487+ use std:: sync:: Arc ;
2488+
2489+ use n0_model:: math:: RectF ;
2490+ use n0_model:: model:: Color32F ;
2491+
2492+ use crate :: drawlist:: ResolvedFilterNode ;
2493+
2494+ use super :: {
2495+ build_filter, ResolvedFilter , ResolvedFilterColorSpace , ResolvedFilterInput ,
2496+ ResolvedFilterPrimitive ,
2497+ } ;
2498+
2499+ const REGION : RectF = RectF {
2500+ x : 0.0 ,
2501+ y : 0.0 ,
2502+ w : 16.0 ,
2503+ h : 16.0 ,
2504+ } ;
2505+
2506+ fn identity ( alpha_offset : f32 ) -> [ f32 ; 20 ] {
2507+ let mut matrix = [ 0.0 ; 20 ] ;
2508+ matrix[ 0 ] = 1.0 ;
2509+ matrix[ 6 ] = 1.0 ;
2510+ matrix[ 12 ] = 1.0 ;
2511+ matrix[ 18 ] = 1.0 ;
2512+ matrix[ 19 ] = alpha_offset;
2513+ matrix
2514+ }
2515+
2516+ fn source_matrix ( alpha_offset : f32 , color_space : ResolvedFilterColorSpace ) -> ResolvedFilter {
2517+ ResolvedFilter {
2518+ region : REGION ,
2519+ nodes : Arc :: from ( [ ResolvedFilterNode {
2520+ inputs : Arc :: from ( [ ResolvedFilterInput :: Source ] ) ,
2521+ region : REGION ,
2522+ color_space,
2523+ primitive : ResolvedFilterPrimitive :: ColorMatrix {
2524+ matrix : identity ( alpha_offset) ,
2525+ } ,
2526+ } ] ) ,
2527+ }
2528+ }
2529+
2530+ #[ test]
2531+ fn source_generated_and_alpha_creating_matrices_keep_distinct_layer_policies ( ) {
2532+ let source = build_filter ( & source_matrix ( 0.0 , ResolvedFilterColorSpace :: Srgb ) )
2533+ . expect ( "source matrix builds" ) ;
2534+ assert ! ( source. matrix_source_preflatten) ;
2535+ assert ! (
2536+ source. restore_blender. is_some( ) ,
2537+ "source-derived sRGB output uses the measured floating restore"
2538+ ) ;
2539+
2540+ let alpha_creating = build_filter ( & source_matrix ( 0.25 , ResolvedFilterColorSpace :: Srgb ) )
2541+ . expect ( "alpha-creating matrix builds" ) ;
2542+ assert ! ( !alpha_creating. matrix_source_preflatten) ;
2543+ assert ! ( alpha_creating. restore_blender. is_none( ) ) ;
2544+
2545+ let generated = ResolvedFilter {
2546+ region : REGION ,
2547+ nodes : Arc :: from ( [
2548+ ResolvedFilterNode {
2549+ inputs : Arc :: from ( [ ] ) ,
2550+ region : REGION ,
2551+ color_space : ResolvedFilterColorSpace :: Srgb ,
2552+ primitive : ResolvedFilterPrimitive :: SolidColor {
2553+ color : Color32F :: new ( 0.2 , 0.4 , 0.8 , 0.5 ) . expect ( "unit color" ) ,
2554+ } ,
2555+ } ,
2556+ ResolvedFilterNode {
2557+ inputs : Arc :: from ( [ ResolvedFilterInput :: Node ( 0 ) ] ) ,
2558+ region : REGION ,
2559+ color_space : ResolvedFilterColorSpace :: Srgb ,
2560+ primitive : ResolvedFilterPrimitive :: ColorMatrix {
2561+ matrix : identity ( 0.0 ) ,
2562+ } ,
2563+ } ,
2564+ ] ) ,
2565+ } ;
2566+ let generated = build_filter ( & generated) . expect ( "generated matrix builds" ) ;
2567+ assert ! ( !generated. matrix_source_preflatten) ;
2568+ assert ! (
2569+ generated. restore_blender. is_none( ) ,
2570+ "generated matrix output keeps the backend-default measured restore"
2571+ ) ;
2572+
2573+ let converted = build_filter ( & source_matrix ( 0.0 , ResolvedFilterColorSpace :: LinearRgb ) )
2574+ . expect ( "linear matrix builds" ) ;
2575+ assert ! ( converted. matrix_source_preflatten) ;
2576+ assert ! (
2577+ converted. restore_blender. is_none( ) ,
2578+ "the output gamma conversion ends the matrix-specific sRGB restore"
2579+ ) ;
2580+ }
2581+ }
2582+
24072583/// Product-build preflight for a resolved image-filter graph. Replay repeats
24082584/// the same pure builders and may therefore treat success as proven.
24092585pub ( crate ) fn preflight_filter ( filter : & ResolvedFilter ) -> Result < ( ) , String > {
@@ -2694,7 +2870,7 @@ pub fn execute_unchecked<K>(canvas: &Canvas, list: &DrawList<K>, view: &Affine,
26942870 Clip ,
26952871 MaskContent ,
26962872 MaskSource ,
2697- Filter ,
2873+ Filter { matrix_source_preflatten : bool } ,
26982874 }
26992875
27002876 let initial_save_count = canvas. save_count ( ) ;
@@ -2833,12 +3009,23 @@ pub fn execute_unchecked<K>(canvas: &Canvas, list: &DrawList<K>, view: &Affine,
28333009 . bounds ( & region)
28343010 . paint ( & restore_paint) ;
28353011 canvas. save_layer ( & layer) ;
2836- scopes. push ( Scope :: Filter ) ;
3012+ if built_filter. matrix_source_preflatten {
3013+ canvas. save_layer ( & SaveLayerRec :: default ( ) ) ;
3014+ }
3015+ scopes. push ( Scope :: Filter {
3016+ matrix_source_preflatten : built_filter. matrix_source_preflatten ,
3017+ } ) ;
28373018 }
28383019 ItemKind :: EndFilter => {
28393020 let scope = scopes. pop ( ) ;
2840- debug_assert_eq ! ( scope, Some ( Scope :: Filter ) ) ;
2841- if scope. is_some ( ) {
3021+ debug_assert ! ( matches!( scope, Some ( Scope :: Filter { .. } ) ) ) ;
3022+ if let Some ( Scope :: Filter {
3023+ matrix_source_preflatten,
3024+ } ) = scope
3025+ {
3026+ if matrix_source_preflatten {
3027+ canvas. restore ( ) ;
3028+ }
28423029 // Restore the filtered layer, then the local-space hard
28433030 // region and transform saved immediately outside it.
28443031 canvas. restore ( ) ;
0 commit comments