Skip to content

Commit cd38479

Browse files
authored
vello_hybrid: Add better statistics for probing (#1779)
This PR adds a convenience method to get some more details about why the probe exactly failed. This PR was done with assistance of GPT Sol 5.6
1 parent b806f2e commit cd38479

3 files changed

Lines changed: 210 additions & 45 deletions

File tree

sparse_strips/vello_common/src/probe.rs

Lines changed: 208 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -28,17 +28,17 @@ const CIRCLE_CENTER_OFFSET_X: f64 = 1.5;
2828
const IMAGE_SOURCE_SIZE: f64 = 5.0;
2929
const 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.
4444
const 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)]
68160
pub 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)]
149228
struct GridLayout {
150229
columns: usize,
@@ -154,13 +233,13 @@ struct GridLayout {
154233
}
155234

156235
impl 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
287384
fn 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+
}

sparse_strips/vello_hybrid/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ pub use render::{AtlasTextureInfo, WebGlAtlasWriter, WebGlRenderer, WebGlTexture
9494
pub use render::{AtlasWriter, RenderTargetConfig, Renderer, TextureBindings};
9595
pub use render::{Config, GpuStrip, RenderSize};
9696
#[cfg(all(feature = "webgl", feature = "probe"))]
97-
pub use render::{Probe, ProbeResult};
97+
pub use render::{Probe, ProbeFeature, ProbeResult, ProbeStatistics};
9898
#[cfg(all(feature = "webgl", feature = "probe"))]
9999
pub use render::{WebGlPendingProbe, WebGlProbeError, WebGlProbeStatus};
100100
pub use resources::Resources;

sparse_strips/vello_hybrid/src/render/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ pub use common::{Config, GpuStrip, RenderSize};
2121
#[cfg(all(feature = "webgl", feature = "probe"))]
2222
pub use probe::{WebGlPendingProbe, WebGlProbeError, WebGlProbeStatus};
2323
#[cfg(all(feature = "webgl", feature = "probe"))]
24-
pub use vello_common::probe::{Probe, ProbeResult};
24+
pub use vello_common::probe::{Probe, ProbeFeature, ProbeResult, ProbeStatistics};
2525
#[cfg(feature = "webgl")]
2626
pub use webgl::{AtlasTextureInfo, WebGlAtlasWriter, WebGlRenderer, WebGlTextureWithDimensions};
2727
#[cfg(feature = "wgpu")]

0 commit comments

Comments
 (0)