Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ tree_arena = { version = "0.2.0", path = "tree_arena" }

anymore = "1.0.0"
vello = { version = "0.6.0", default-features = false, features = ["wgpu"] }
vello_encoding = { version = "0.6.0", default-features = false }
kurbo = "0.12.0"
parley = { version = "0.7.0", features = ["accesskit"] }
# TODO: Use no_std correctly in Xilem Web.
Expand Down
39 changes: 37 additions & 2 deletions masonry/src/tests/paint.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@ use assert_matches::assert_matches;
use masonry_core::core::{NewWidget, WidgetTag};
use masonry_core::palette::css::{BLUE, GREEN, RED};
use masonry_core::util::{fill, stroke};
use masonry_testing::{ModularWidget, Record, TestHarness, TestWidgetExt, assert_render_snapshot};
use vello::kurbo::{Affine, Circle, Dashes, Point, Size, Stroke, Vec2};
use masonry_testing::{
ModularWidget, Record, TestHarness, TestWidgetExt, assert_debug_panics, assert_render_snapshot,
};
use vello::kurbo::{Affine, Circle, Dashes, Point, Rect, Size, Stroke, Vec2};
use vello::peniko::Color;

use crate::properties::Background;
Expand Down Expand Up @@ -155,3 +157,36 @@ fn paint_clipping() {
// The dashed circle shouldn't.
assert_render_snapshot!(harness, "paint_clipping");
}

#[test]
fn scene_validation_nan() {
let widget = NewWidget::new(
ModularWidget::new(())
.layout_fn(|_, _, _, _| Size::new(10., 10.))
.paint_fn(move |_, _, _, scene| {
let nan_rect = Rect::new(0., 1., f64::NAN, 3.);
fill(scene, &nan_rect, Color::WHITE);
}),
);

assert_debug_panics!(
TestHarness::create(test_property_set(), widget),
"HasNanValues"
);
}

#[test]
fn scene_validation_push_layer() {
let widget = NewWidget::new(
ModularWidget::new(())
.layout_fn(|_, _, _, _| Size::new(10., 10.))
.paint_fn(move |_, _, _, scene| {
scene.push_clip_layer(Affine::IDENTITY, &Rect::ZERO);
}),
);

assert_debug_panics!(
TestHarness::create(test_property_set(), widget),
"UnbalancedPushLayer"
);
}
1 change: 1 addition & 0 deletions masonry_core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ tracing-tracy = { version = "0.11.4", optional = true }
tree_arena.workspace = true
ui-events.workspace = true
vello.workspace = true
vello_encoding.workspace = true

[target.'cfg(target_arch = "wasm32")'.dependencies]
console_error_panic_hook.workspace = true
Expand Down
7 changes: 6 additions & 1 deletion masonry_core/src/passes/paint.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ use vello::peniko::{Color, Fill};
use crate::app::{RenderRoot, RenderRootState};
use crate::core::{DefaultProperties, PaintCtx, PropertiesRef, WidgetArenaNode, WidgetId};
use crate::passes::{enter_span_if, recurse_on_children};
use crate::util::{get_debug_color, stroke};
use crate::util::{get_debug_color, stroke, validate_scene};

// --- MARK: PAINT WIDGET
fn paint_widget(
Expand Down Expand Up @@ -65,6 +65,11 @@ fn paint_widget(
if ctx.widget_state.request_post_paint {
widget.post_paint(&mut ctx, &props, postfix_scene);
}

if cfg!(debug_assertions) {
validate_scene(&scene).unwrap();
validate_scene(&postfix_scene).unwrap();
}
}

state.request_paint = false;
Expand Down
90 changes: 90 additions & 0 deletions masonry_core/src/util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@
//! Miscellaneous utility functions.

use std::any::Any;
use std::fmt::Display;

use vello::Scene;
use vello::kurbo::{Affine, Join, Shape, Stroke};
use vello::peniko::{BrushRef, Color, Fill};
use vello_encoding::DrawTag;

/// Panic in debug and `tracing::error` in release mode.
///
Expand Down Expand Up @@ -81,6 +83,94 @@ pub fn fill_color(scene: &mut Scene, path: &impl Shape, color: Color) {

// ---

/// Error type returned by [`validate_scene()`].
#[derive(Debug, Clone, Copy)]
#[non_exhaustive]
pub enum ValidationError {
/// Scene was constructed with NaN values in its path data.
HasNanValues,
/// Scene had a `push_layer` command that was never popped.
UnbalancedPushLayer,
/// Scene had a `pop_layer` command with no layer pushed.
/// This is currently "unreachable" because Vello silently swallows these cases.
#[doc(hidden)]
UnbalancedPopLayer,
}

impl Display for ValidationError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ValidationError::HasNanValues => {
write!(f, "Scene was constructed with NaN values in its path data")
}
ValidationError::UnbalancedPushLayer => {
write!(f, "Scene had a `push_layer` command that was never popped")
}
ValidationError::UnbalancedPopLayer => {
write!(f, "Scene had a `pop_layer` command with no layer pushed")
}
}
}
}

/// Take a scene and return an error if the scene is invalid.
///
/// A scene is invalid if:
///
/// - It was constructed with NaN values in its path data.
/// - It had a `push_layer` command that was never popped.
///
/// ## Missing checks
///
/// This function may have some false negative in some cases, because Vello can
/// sometimes silently remove NaN values from the paths given to it.
///
/// We'd like to catch `pop_layer` commands with no layer pushed, but Vello
/// currently also swallows them silently.
pub fn validate_scene(scene: &Scene) -> Result<(), ValidationError> {
// This assumes that `vello_encoding::Encoding::path_data` only ever stores
// the float values of its paths.
// While in theory it can store other things, in practice it never does when created
// using a Vello Scene, and this will not change until vello is replaced with the sparse
// strips API, at which point this function will likely be discarded.
for path_data_elem in &scene.encoding().path_data {
if f32::from_bits(*path_data_elem).is_nan() {
return Err(ValidationError::HasNanValues);
}
}

for transform in &scene.encoding().transforms {
for value in &transform.matrix {
if value.is_nan() {
return Err(ValidationError::HasNanValues);
}
}
}

let mut layer_count = 0;
for tag in &scene.encoding().draw_tags {
match *tag {
DrawTag::BEGIN_CLIP => {
layer_count += 1;
}
DrawTag::END_CLIP => {
if layer_count == 0 {
return Err(ValidationError::UnbalancedPopLayer);
}
layer_count -= 1;
}
_ => {}
}
}
if layer_count > 0 {
return Err(ValidationError::UnbalancedPushLayer);
}

Ok(())
}

// ---

/// Convert a 2d rectangle from Parley to one used for drawing in Vello and other maths.
pub fn bounding_box_to_rect(bb: parley::BoundingBox) -> vello::kurbo::Rect {
vello::kurbo::Rect {
Expand Down
Loading