Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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 {
Self::HasNanValues => {
write!(f, "Scene was constructed with NaN values in its path data")
}
Self::UnbalancedPushLayer => {
write!(f, "Scene had a `push_layer` command that was never popped")
}
Self::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