@@ -28,17 +28,17 @@ const CIRCLE_CENTER_OFFSET_X: f64 = 1.5;
2828const IMAGE_SOURCE_SIZE : f64 = 5.0 ;
2929const PATH_TOLERANCE : f64 = 0.1 ;
3030
31- const ELEMENTS : [ ProbeElement ; 8 ] = [
32- ProbeElement :: SolidRect ,
33- ProbeElement :: AlphaBlending ,
34- ProbeElement :: Gradient ,
35- ProbeElement :: ImageNearest ,
31+ const ELEMENTS : [ ProbeFeature ; 8 ] = [
32+ ProbeFeature :: SolidRect ,
33+ ProbeFeature :: AlphaBlending ,
34+ ProbeFeature :: Gradient ,
35+ ProbeFeature :: ImageNearest ,
3636 // Temporarily disabled.
37- // ProbeElement ::Filter,
38- ProbeElement :: ImageBilinear ,
39- ProbeElement :: OpacityLayer ,
40- ProbeElement :: Blending ,
41- ProbeElement :: Transformed ,
37+ // ProbeFeature ::Filter,
38+ ProbeFeature :: ImageBilinear ,
39+ ProbeFeature :: OpacityLayer ,
40+ ProbeFeature :: Blending ,
41+ ProbeFeature :: Transformed ,
4242] ;
4343/// Per-channel absolute tolerance used when comparing probe pixels.
4444const CHANNEL_TOLERANCE : u8 = 3 ;
@@ -63,6 +63,98 @@ pub struct ProbeResult {
6363 pub actual : ProbeImage ,
6464}
6565
66+ /// A feature exercised by the renderer probe.
67+ ///
68+ /// Each discriminant is the stable bit index used by [`ProbeStatistics::difference_mask`].
69+ /// Existing discriminants must not be changed when features are reordered, disabled, or added.
70+ #[ repr( u8 ) ]
71+ #[ derive( Debug , Clone , Copy , PartialEq , Eq , Hash ) ]
72+ pub enum ProbeFeature {
73+ /// Drawing a solid rectangle.
74+ SolidRect = 0 ,
75+ /// Alpha blending overlapping shapes.
76+ AlphaBlending = 1 ,
77+ /// Drawing a linear gradient.
78+ Gradient = 2 ,
79+ /// Drawing an image with nearest-neighbor sampling.
80+ ImageNearest = 3 ,
81+ /// Applying a filter effect.
82+ Filter = 4 ,
83+ /// Drawing an image with bilinear sampling.
84+ ImageBilinear = 5 ,
85+ /// Drawing within a layer with reduced opacity.
86+ OpacityLayer = 6 ,
87+ /// Drawing within a layer with a blend mode.
88+ Blending = 7 ,
89+ /// Drawing with a non-identity transform.
90+ Transformed = 8 ,
91+ }
92+
93+ /// Summary of the differences between the expected and actual probe images.
94+ #[ derive( Debug , Clone , Copy , Default , PartialEq , Eq ) ]
95+ pub struct ProbeStatistics {
96+ /// Number of active features exercised by the probe.
97+ pub element_count : u8 ,
98+ /// Width and height of the actual probe image.
99+ pub actual_size : ( u16 , u16 ) ,
100+ /// Number of pixels whose channels differ by more than the probe tolerance.
101+ pub different_pixel_count : u32 ,
102+ /// Largest absolute difference between corresponding red, green, blue, and alpha channels.
103+ pub max_channel_discrepancy : [ u8 ; 4 ] ,
104+ /// Bitmask identifying probe features containing a pixel outside the probe tolerance.
105+ ///
106+ /// Bit `n` corresponds to the [`ProbeFeature`] whose discriminant is `n`.
107+ pub difference_mask : u32 ,
108+ }
109+
110+ impl ProbeStatistics {
111+ /// Returns whether `feature` contained a pixel outside the probe tolerance.
112+ pub fn differs ( & self , feature : ProbeFeature ) -> bool {
113+ self . difference_mask & ( 1_u32 << feature as u8 ) != 0
114+ }
115+ }
116+
117+ impl ProbeResult {
118+ /// Return the statistics of the probe.
119+ pub fn statistics ( & self ) -> ProbeStatistics {
120+ let layout = GridLayout :: from_elements ( & ELEMENTS ) ;
121+ let mut statistics = ProbeStatistics {
122+ element_count : ELEMENTS . len ( ) as u8 ,
123+ actual_size : ( self . actual . width , self . actual . height ) ,
124+ ..Default :: default ( )
125+ } ;
126+
127+ for ( pixel_index, ( expected, actual) ) in self
128+ . expected
129+ . data
130+ . chunks_exact ( 4 )
131+ . zip ( self . actual . data . chunks_exact ( 4 ) )
132+ . enumerate ( )
133+ {
134+ if expected[ 3 ] != 0 || actual[ 3 ] != 0 {
135+ for ( max_discrepancy, ( expected, actual) ) in statistics
136+ . max_channel_discrepancy
137+ . iter_mut ( )
138+ . zip ( expected. iter ( ) . zip ( actual) )
139+ {
140+ * max_discrepancy = ( * max_discrepancy) . max ( expected. abs_diff ( * actual) ) ;
141+ }
142+ }
143+
144+ if !pixels_within_tolerance ( expected, actual, CHANNEL_TOLERANCE ) {
145+ statistics. different_pixel_count += 1 ;
146+
147+ let cell_index = layout. cell_index_for_pixel ( pixel_index) ;
148+ if let Some ( feature) = ELEMENTS . get ( cell_index) {
149+ statistics. difference_mask |= 1_u32 << * feature as u8 ;
150+ }
151+ }
152+ }
153+
154+ statistics
155+ }
156+ }
157+
66158/// A probe image stored as RGBA8 bytes.
67159#[ derive( Debug , Clone ) ]
68160pub struct ProbeImage {
@@ -132,19 +224,6 @@ pub trait ProbeRenderer {
132224 fn reset_paint_transform ( & mut self ) ;
133225}
134226
135- #[ derive( Clone , Copy , Debug ) ]
136- enum ProbeElement {
137- SolidRect ,
138- Transformed ,
139- AlphaBlending ,
140- Gradient ,
141- ImageNearest ,
142- // Filter,
143- ImageBilinear ,
144- OpacityLayer ,
145- Blending ,
146- }
147-
148227#[ derive( Clone , Copy , Debug ) ]
149228struct GridLayout {
150229 columns : usize ,
@@ -154,13 +233,13 @@ struct GridLayout {
154233}
155234
156235impl GridLayout {
157- fn from_elements ( elements : & [ ProbeElement ] ) -> Self {
236+ fn from_elements ( elements : & [ ProbeFeature ] ) -> Self {
158237 let columns = ELEMENTS_PER_ROW . min ( elements. len ( ) ) ;
159238 let rows = elements. len ( ) . div_ceil ( columns) ;
160239 let ( cell_width, cell_height) = elements
161240 . iter ( )
162241 . copied ( )
163- . map ( ProbeElement :: bounds)
242+ . map ( ProbeFeature :: bounds)
164243 . fold ( ( 0.0_f64 , 0.0_f64 ) , |( max_w, max_h) , ( w, h) | {
165244 ( max_w. max ( w) , max_h. max ( h) )
166245 } ) ;
@@ -174,10 +253,10 @@ impl GridLayout {
174253 }
175254
176255 fn canvas_size ( self ) -> ( u16 , u16 ) {
177- let width = self . columns as f64 * self . cell_width
178- + self . columns . saturating_sub ( 1 ) as f64 * ELEMENT_MARGIN ;
179- let height = self . rows as f64 * self . cell_height
180- + self . rows . saturating_sub ( 1 ) as f64 * ELEMENT_MARGIN ;
256+ let ( cell_stride_x , cell_stride_y ) = self . cell_stride ( ) ;
257+ // Margin only exists between cells, so subtract one.
258+ let width = self . columns as f64 * cell_stride_x - ELEMENT_MARGIN ;
259+ let height = self . rows as f64 * cell_stride_y - ELEMENT_MARGIN ;
181260 ( width. ceil ( ) as u16 , height. ceil ( ) as u16 )
182261 }
183262
@@ -186,23 +265,41 @@ impl GridLayout {
186265 Rect :: new ( 0.0 , 0.0 , f64:: from ( width) , f64:: from ( height) )
187266 }
188267
268+ fn cell_stride ( self ) -> ( f64 , f64 ) {
269+ (
270+ self . cell_width + ELEMENT_MARGIN ,
271+ self . cell_height + ELEMENT_MARGIN ,
272+ )
273+ }
274+
189275 fn cell_rect ( self , index : usize ) -> Rect {
190276 let column = index % self . columns ;
191277 let row = index / self . columns ;
192- let x0 = column as f64 * ( self . cell_width + ELEMENT_MARGIN ) ;
193- let y0 = row as f64 * ( self . cell_height + ELEMENT_MARGIN ) ;
278+ let ( cell_stride_x, cell_stride_y) = self . cell_stride ( ) ;
279+ let x0 = column as f64 * cell_stride_x;
280+ let y0 = row as f64 * cell_stride_y;
194281 Rect :: new ( x0, y0, x0 + self . cell_width , y0 + self . cell_height )
195282 }
283+
284+ fn cell_index_for_pixel ( self , pixel_index : usize ) -> usize {
285+ let ( cell_stride_x, cell_stride_y) = self . cell_stride ( ) ;
286+ let image_width = usize:: from ( self . canvas_size ( ) . 0 ) ;
287+ let x = pixel_index % image_width;
288+ let y = pixel_index / image_width;
289+ let column = x / cell_stride_x as usize ;
290+ let row = y / cell_stride_y as usize ;
291+ row * self . columns + column
292+ }
196293}
197294
198- impl ProbeElement {
295+ impl ProbeFeature {
199296 fn bounds ( self ) -> ( f64 , f64 ) {
200297 let ( width, height) = match self {
201298 Self :: SolidRect
202299 | Self :: Gradient
203300 | Self :: ImageNearest
204301 | Self :: ImageBilinear
205- // | Self::Filter
302+ | Self :: Filter
206303 | Self :: OpacityLayer => ( RECT_SIZE , RECT_SIZE ) ,
207304 Self :: Transformed => (
208305 RECT_SIZE * core:: f64:: consts:: SQRT_2 ,
@@ -287,19 +384,19 @@ fn pixels_within_tolerance(expected: &[u8], actual: &[u8], channel_tolerance: u8
287384fn draw_probe_element (
288385 ctx : & mut impl ProbeRenderer ,
289386 cell : Rect ,
290- element : ProbeElement ,
387+ element : ProbeFeature ,
291388 image_nearest : & PaintType ,
292389 image_bilinear : & PaintType ,
293390) {
294391 match element {
295- ProbeElement :: SolidRect => {
392+ ProbeFeature :: SolidRect => {
296393 ctx. set_paint ( css:: BLUE . into ( ) ) ;
297394 ctx. fill_rect ( & centered_rect ( cell, RECT_SIZE , RECT_SIZE ) ) ;
298395 }
299- ProbeElement :: Transformed => {
396+ ProbeFeature :: Transformed => {
300397 draw_transformed_rect ( ctx, centered_rect ( cell, RECT_SIZE , RECT_SIZE ) ) ;
301398 }
302- ProbeElement :: AlphaBlending => {
399+ ProbeFeature :: AlphaBlending => {
303400 let center = cell. center ( ) ;
304401 ctx. set_paint ( css:: YELLOW . with_alpha ( 0.5 ) . into ( ) ) ;
305402 ctx. fill_path (
@@ -312,18 +409,18 @@ fn draw_probe_element(
312409 . to_path ( PATH_TOLERANCE ) ,
313410 ) ;
314411 }
315- ProbeElement :: Gradient => {
412+ ProbeFeature :: Gradient => {
316413 let rect = centered_rect ( cell, RECT_SIZE , RECT_SIZE ) ;
317414 ctx. set_paint ( linear_gradient ( & rect) . into ( ) ) ;
318415 ctx. fill_rect ( & rect) ;
319416 }
320- ProbeElement :: ImageNearest => draw_centered_padded_image ( ctx, cell, image_nearest) ,
321- // ProbeElement ::Filter => draw_blurred_rect(ctx, centered_rect(cell, RECT_SIZE, RECT_SIZE)),
322- ProbeElement :: ImageBilinear => draw_centered_padded_image ( ctx, cell, image_bilinear) ,
323- ProbeElement :: OpacityLayer => {
417+ ProbeFeature :: ImageNearest => draw_centered_padded_image ( ctx, cell, image_nearest) ,
418+ ProbeFeature :: Filter => draw_blurred_rect ( ctx, centered_rect ( cell, RECT_SIZE , RECT_SIZE ) ) ,
419+ ProbeFeature :: ImageBilinear => draw_centered_padded_image ( ctx, cell, image_bilinear) ,
420+ ProbeFeature :: OpacityLayer => {
324421 draw_opacity_layer_rect ( ctx, centered_rect ( cell, RECT_SIZE , RECT_SIZE ) ) ;
325422 }
326- ProbeElement :: Blending => draw_layered_difference_circles ( ctx, cell) ,
423+ ProbeFeature :: Blending => draw_layered_difference_circles ( ctx, cell) ,
327424 }
328425}
329426
@@ -421,3 +518,71 @@ fn linear_gradient(rect: &Rect) -> Gradient {
421518 ..Default :: default ( )
422519 }
423520}
521+
522+ #[ cfg( test) ]
523+ mod tests {
524+ use super :: * ;
525+ use alloc:: vec;
526+
527+ #[ test]
528+ fn probe_result_reports_pixel_and_cell_differences ( ) {
529+ let ( width, height) = canvas_size ( ) ;
530+ let pixel_count = usize:: from ( width) * usize:: from ( height) ;
531+ let expected = ProbeImage {
532+ width,
533+ height,
534+ data : vec ! [ 255 ; pixel_count * 4 ] ,
535+ } ;
536+ let mut actual = expected. clone ( ) ;
537+ let layout = GridLayout :: from_elements ( & ELEMENTS ) ;
538+
539+ let set_channel = |actual : & mut ProbeImage , cell_index : usize , channel : usize , value| {
540+ let center = layout. cell_rect ( cell_index) . center ( ) ;
541+ let x = center. x . floor ( ) as usize ;
542+ let y = center. y . floor ( ) as usize ;
543+ actual. data [ ( y * usize:: from ( width) + x) * 4 + channel] = value;
544+ } ;
545+
546+ // This stays within the probe tolerance.
547+ set_channel ( & mut actual, 0 , 0 , 254 ) ;
548+
549+ set_channel ( & mut actual, 1 , 0 , 249 ) ;
550+ set_channel ( & mut actual, 5 , 1 , 0 ) ;
551+ set_channel ( & mut actual, 5 , 3 , 100 ) ;
552+
553+ let result = ProbeResult { expected, actual } ;
554+ let statistics = result. statistics ( ) ;
555+ assert_eq ! (
556+ statistics,
557+ ProbeStatistics {
558+ element_count: ELEMENTS . len( ) as u8 ,
559+ actual_size: ( width, height) ,
560+ different_pixel_count: 2 ,
561+ max_channel_discrepancy: [ 6 , 255 , 0 , 155 ] ,
562+ difference_mask: ( 1 << 1 ) | ( 1 << 6 ) ,
563+ }
564+ ) ;
565+ assert ! ( statistics. differs( ProbeFeature :: AlphaBlending ) ) ;
566+ assert ! ( statistics. differs( ProbeFeature :: OpacityLayer ) ) ;
567+ assert ! ( !statistics. differs( ProbeFeature :: Filter ) ) ;
568+ assert ! ( !statistics. differs( ProbeFeature :: ImageBilinear ) ) ;
569+ }
570+
571+ #[ test]
572+ fn probe_statistics_reports_actual_size ( ) {
573+ let result = ProbeResult {
574+ expected : ProbeImage {
575+ width : 1 ,
576+ height : 1 ,
577+ data : vec ! [ 0 ; 4 ] ,
578+ } ,
579+ actual : ProbeImage {
580+ width : 2 ,
581+ height : 1 ,
582+ data : vec ! [ 0 ; 8 ] ,
583+ } ,
584+ } ;
585+
586+ assert_eq ! ( result. statistics( ) . actual_size, ( 2 , 1 ) ) ;
587+ }
588+ }
0 commit comments