-
Notifications
You must be signed in to change notification settings - Fork 615
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Path rendering on software renderer #6032
base: master
Are you sure you want to change the base?
Changes from all commits
6f1f810
4516619
8450dc4
65a80e3
4d87d83
25de8ce
af28639
fa5e5b4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -18,3 +18,5 @@ docs/reference/src/language/builtins/structs.md | |
.env | ||
.envrc | ||
__pycache__ | ||
|
||
.idea |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -24,7 +24,7 @@ libm = ["num-traits/libm", "euclid/libm"] | |
# Allow the viewer to query at runtime information about item types | ||
rtti = [] | ||
# Use the standard library | ||
std = ["euclid/std", "once_cell/std", "scoped-tls-hkt", "lyon_path", "lyon_algorithms", "lyon_geom", "lyon_extra", "dep:web-time", "image-decoders", "svg", "raw-window-handle-06?/std", "chrono/std", "chrono/wasmbind", "chrono/clock"] | ||
std = ["euclid/std", "once_cell/std", "scoped-tls-hkt", "lyon_path", "lyon_algorithms", "lyon_geom", "lyon_extra", "dep:web-time", "image-decoders", "svg", "raw-window-handle-06?/std", "chrono/std", "chrono/wasmbind", "chrono/clock", "tiny-skia/std", "path"] | ||
# Unsafe feature meaning that there is only one core running and all thread_local are static. | ||
# You can only enable this feature if you are sure that any API of this crate is only called | ||
# from a single core, and not in a interrupt or signal handler. | ||
|
@@ -38,6 +38,8 @@ software-renderer = ["bytemuck"] | |
image-decoders = ["dep:image", "dep:clru"] | ||
svg = ["dep:resvg", "shared-fontdb"] | ||
|
||
path = ["dep:zeno"] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. perhaps should be renamed "software-renderer-path" since that doesn't impact other renderer. |
||
|
||
box-shadow-cache = [] | ||
|
||
shared-fontdb = ["i-slint-common/shared-fontdb"] | ||
|
@@ -85,6 +87,7 @@ clru = { workspace = true, optional = true } | |
resvg = { workspace = true, optional = true } | ||
fontdb = { workspace = true, optional = true } | ||
serde = { workspace = true, optional = true } | ||
zeno = { workspace = true, optional = true } | ||
|
||
raw-window-handle-06 = { workspace = true, optional = true } | ||
bitflags = { version = "2.4.2"} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -18,11 +18,8 @@ use crate::graphics::{ | |
BorderRadius, PixelFormat, Rgba8Pixel, SharedImageBuffer, SharedPixelBuffer, | ||
}; | ||
use crate::item_rendering::{CachedRenderingData, DirtyRegion, RenderBorderRectangle, RenderImage}; | ||
use crate::items::{ItemRc, TextOverflow, TextWrap}; | ||
use crate::lengths::{ | ||
LogicalBorderRadius, LogicalLength, LogicalPoint, LogicalRect, LogicalSize, LogicalVector, | ||
PhysicalPx, PointLengths, RectLengths, ScaleFactor, SizeLengths, | ||
}; | ||
use crate::items::{ItemRc, TextOverflow, TextWrap, FillRule}; | ||
use crate::lengths::{LogicalBorderRadius, LogicalLength, LogicalPoint, LogicalRect, LogicalSize, LogicalVector, PhysicalPx, PointLengths, RectLengths, ScaleFactor, SizeLengths}; | ||
use crate::renderer::{Renderer, RendererSealed}; | ||
use crate::textlayout::{AbstractFont, FontMetrics, TextParagraphLayout}; | ||
use crate::window::{WindowAdapter, WindowInner}; | ||
|
@@ -33,11 +30,11 @@ use alloc::{vec, vec::Vec}; | |
use core::cell::{Cell, RefCell}; | ||
use core::pin::Pin; | ||
use euclid::Length; | ||
use lyon_path::Event; | ||
use fixed::Fixed; | ||
#[allow(unused)] | ||
use num_traits::Float; | ||
use num_traits::NumCast; | ||
|
||
pub use draw_functions::{PremultipliedRgbaColor, Rgb565Pixel, TargetPixel}; | ||
|
||
type PhysicalLength = euclid::Length<i16, PhysicalPx>; | ||
|
@@ -974,6 +971,15 @@ fn render_window_frame_by_line( | |
extra_left_clip, | ||
); | ||
} | ||
SceneCommand::ZenoPath { zenopath_index } => { | ||
let cmd = &scene.vectors.zeno_paths[zenopath_index as usize]; | ||
draw_functions::draw_zeno_path_line( | ||
&PhysicalRect { origin: span.pos, size: span.size }, | ||
scene.current_line, | ||
cmd, | ||
range_buffer, | ||
) | ||
} | ||
} | ||
} | ||
}, | ||
|
@@ -993,6 +999,7 @@ struct SceneVectors { | |
rounded_rectangles: Vec<RoundedRectangle>, | ||
shared_buffers: Vec<SharedBufferCommand>, | ||
gradients: Vec<GradientCommand>, | ||
zeno_paths: Vec<ZenoPathCommand>, | ||
} | ||
|
||
struct Scene { | ||
|
@@ -1248,10 +1255,14 @@ enum SceneCommand { | |
RoundedRectangle { | ||
rectangle_index: u16, | ||
}, | ||
/// rectangle_index is an index in the [`SceneVectors::rounded_gradients`] array | ||
/// gradient_index is an index in the [`SceneVectors::rounded_gradients`] array | ||
Gradient { | ||
gradient_index: u16, | ||
}, | ||
/// zenopath_index is an index in the [`SceneVectors::zeno_paths`] array | ||
ZenoPath { | ||
zenopath_index: u16, | ||
} | ||
} | ||
|
||
struct SceneTexture<'a> { | ||
|
@@ -1398,6 +1409,16 @@ struct GradientCommand { | |
bottom_clip: PhysicalLength, | ||
} | ||
|
||
#[derive(Debug)] | ||
struct ZenoPathCommand { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. (in fact, this is a simplified version of SharedBufferCommand, for an alphamap) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yes. saving as an alpha map reduces the memory size, but makes it heavier on the line drawing function, as gradients need to be calculated there. This shouldn't be a problem as gradient fill on rectangles already does that There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I meant that the ZenoPathCommand is basically the same as two In fact, the draw_texture_line can be re-used, i think (if you create a |
||
stroke_mask: Option<Vec<u8>>, | ||
tronical marked this conversation as resolved.
Show resolved
Hide resolved
|
||
stroke_brush: Brush, | ||
|
||
fill_mask: Option<Vec<u8>>, | ||
fill_brush: Brush, | ||
} | ||
|
||
|
||
fn prepare_scene( | ||
window: &WindowInner, | ||
size: PhysicalSize, | ||
|
@@ -1478,6 +1499,7 @@ trait ProcessScene { | |
fn process_rounded_rectangle(&mut self, geometry: PhysicalRect, data: RoundedRectangle); | ||
fn process_shared_image_buffer(&mut self, geometry: PhysicalRect, buffer: SharedBufferCommand); | ||
fn process_gradient(&mut self, geometry: PhysicalRect, gradient: GradientCommand); | ||
fn process_path(&mut self, geometry: PhysicalRect, path: ZenoPathCommand); | ||
} | ||
|
||
struct RenderToBuffer<'a, TargetPixel> { | ||
|
@@ -1579,6 +1601,17 @@ impl<'a, T: TargetPixel> ProcessScene for RenderToBuffer<'a, T> { | |
); | ||
}); | ||
} | ||
|
||
fn process_path(&mut self, geometry: PhysicalRect, path: ZenoPathCommand) { | ||
self.foreach_ranges(&geometry, |line, buffer, _extra_left_clip, _extra_right_clip| { | ||
draw_functions::draw_zeno_path_line( | ||
&geometry, | ||
PhysicalLength::new(line), | ||
&path, | ||
buffer, | ||
) | ||
}); | ||
} | ||
} | ||
|
||
#[derive(Default)] | ||
|
@@ -1652,6 +1685,20 @@ impl ProcessScene for PrepareScene { | |
}); | ||
} | ||
} | ||
|
||
fn process_path(&mut self, geometry: PhysicalRect, path: ZenoPathCommand) { | ||
let size = geometry.size; | ||
if !size.is_empty() { | ||
let zenopath_index = self.vectors.zeno_paths.len() as u16; | ||
self.vectors.zeno_paths.push(path); | ||
self.items.push(SceneItem { | ||
pos: geometry.origin, | ||
size, | ||
z: self.items.len() as u16, | ||
command: SceneCommand::ZenoPath { zenopath_index }, | ||
}) | ||
} | ||
} | ||
} | ||
|
||
struct SceneBuilder<'a, T> { | ||
|
@@ -2495,9 +2542,100 @@ impl<'a, T: ProcessScene> crate::item_rendering::ItemRenderer for SceneBuilder<' | |
} | ||
} | ||
|
||
#[cfg(feature = "std")] | ||
fn draw_path(&mut self, _path: Pin<&crate::items::Path>, _: &ItemRc, _size: LogicalSize) { | ||
// TODO | ||
#[cfg(feature = "path")] | ||
fn draw_path(&mut self, path: Pin<&crate::items::Path>, item: &ItemRc, size: LogicalSize) { | ||
use zeno::PathBuilder; | ||
let geom = LogicalRect::from(size); | ||
|
||
let clipped = match geom.intersection(&self.current_state.clip) { | ||
Some(geom) => geom, | ||
None => return, | ||
}; | ||
|
||
let geometry = (clipped.translate(self.current_state.offset.to_vector()).cast() | ||
* self.scale_factor) | ||
.round() | ||
.cast() | ||
.transformed(self.rotation); | ||
|
||
let path_props = path.as_ref(); | ||
let mut zeno_pb: Vec<zeno::Command> = Vec::new(); | ||
let (logical_offset, path_events2) = path.fitted_path_events(item).unwrap(); | ||
|
||
for event in path_events2.iter() { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I wonder if we can implement zeno::PathData for our own path. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. agreed! one thing that is blocking to render paths on no_std is the dependency of lyon_path on other parts. |
||
match event { | ||
Event::Begin { at } => { | ||
zeno_pb.move_to([at.x, at.y]); | ||
} | ||
Event::Line { from: _, to } => { | ||
zeno_pb.line_to([to.x, to.y]); | ||
} | ||
Event::Quadratic { from: _, ctrl, to } => { | ||
zeno_pb.quad_to( | ||
[ctrl.x, ctrl.y], | ||
[to.x, to.y] | ||
); | ||
} | ||
Event::Cubic { from: _, ctrl1, ctrl2, to } => { | ||
zeno_pb.curve_to( | ||
[ctrl1.x, ctrl1.y], | ||
[ctrl2.x, ctrl2.y], | ||
[to.x, to.y], | ||
); | ||
} | ||
Event::End { last: _, first: _, close } => { | ||
if close { | ||
zeno_pb.close(); | ||
} | ||
} | ||
} | ||
} | ||
|
||
let transform = Some(zeno::Transform::translation(logical_offset.x, logical_offset.y)); | ||
|
||
let fill_mask = if !path_props.fill().is_transparent() { | ||
let mut mask = Vec::new(); | ||
mask.resize((geometry.size.width * geometry.size.height) as usize, 0u8); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That seems big. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yes, it doesn't seem to be able to render it line by line. at least the command saves two u8 buffers, which is less if it was a rgba buffer. On some occasions, there may be big blank areas, like when full sizing on flex layouts. Zeno provides a way to make the buffer on the needed size, but it would need to limit this somehow (prevent huge buffers with overflow lines) and have a reference point to translate the buffer to the correct location |
||
|
||
let fill_rule = match path_props.fill_rule() { | ||
FillRule::Evenodd => zeno::Fill::EvenOdd, | ||
FillRule::Nonzero => zeno::Fill::NonZero, | ||
}; | ||
|
||
zeno::Mask::new(&zeno_pb) | ||
.transform(transform) | ||
.style(fill_rule) | ||
.size(geometry.size.width, geometry.size.height) | ||
.render_into(&mut mask, None); | ||
|
||
Some(mask) | ||
} else { None }; | ||
|
||
let stroke_mask = if !path_props.stroke().is_transparent() { | ||
let mut mask = Vec::new(); | ||
mask.resize((geometry.size.width * geometry.size.height) as usize, 0u8); | ||
|
||
let stroke_width = path_props.stroke_width().0; | ||
|
||
zeno::Mask::new(&zeno_pb) | ||
.transform(transform) | ||
.style( | ||
zeno::Stroke::new(stroke_width) | ||
.cap(zeno::Cap::Butt) | ||
.join(zeno::Join::Miter) | ||
) | ||
.size(geometry.size.width, geometry.size.height) | ||
.render_into(&mut mask, None); | ||
|
||
Some(mask) | ||
} else { None }; | ||
|
||
self.processor.process_path(geometry.cast(), ZenoPathCommand { | ||
stroke_mask, | ||
stroke_brush: path_props.stroke(), | ||
fill_mask, | ||
fill_brush: path_props.fill(), | ||
Comment on lines
+2635
to
+2637
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Note: the "global" alpha needs to be applied to that color. (That's why other functions do |
||
}); | ||
} | ||
|
||
fn draw_box_shadow( | ||
|
@@ -2659,6 +2797,71 @@ impl<'a, T: ProcessScene> crate::item_rendering::ItemRenderer for SceneBuilder<' | |
} | ||
} | ||
|
||
// impl From<Color> for tiny_skia::Color { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. some leftover |
||
// fn from(value: Color) -> Self { | ||
// Self::from_rgba8( | ||
// value.red(), | ||
// value.green(), | ||
// value.blue(), | ||
// value.alpha() | ||
// ) | ||
// } | ||
// } | ||
// | ||
// fn brush_to_paint(brush: Brush, path: tiny_skia::Path) -> Option<tiny_skia::Paint<'static>> { | ||
// if brush.is_transparent() { | ||
// return None; | ||
// } | ||
// | ||
// let mut paint = tiny_skia::Paint::default(); | ||
// paint.anti_alias = true; | ||
// | ||
// match brush { | ||
// Brush::SolidColor(color) => { | ||
// paint.set_color(tiny_skia::Color::from(color)); | ||
// } | ||
// Brush::LinearGradient(gradient) => { | ||
// let stops = gradient.stops().map(|stop| { | ||
// tiny_skia::GradientStop::new(stop.position, tiny_skia::Color::from(stop.color)) | ||
// }).collect::<Vec<_>>(); | ||
// | ||
// let path_bounds = path.bounds(); | ||
// let (start, end) = crate::graphics::line_for_angle( | ||
// gradient.angle(), | ||
// [path_bounds.width(), path_bounds.height()].into(), | ||
// ); | ||
// | ||
// let gradient = tiny_skia::LinearGradient::new( | ||
// tiny_skia::Point::from_xy(start.x, start.y), | ||
// tiny_skia::Point::from_xy(end.x, end.y), | ||
// stops, | ||
// tiny_skia::SpreadMode::Pad, | ||
// tiny_skia::Transform::default(), | ||
// ).expect("could not create linear gradient shader"); | ||
// paint.shader = gradient | ||
// } | ||
// Brush::RadialGradient(gradient) => { | ||
// let stops = gradient.stops().map(|stop| { | ||
// tiny_skia::GradientStop::new(stop.position, tiny_skia::Color::from(stop.color)) | ||
// }).collect::<Vec<_>>(); | ||
// | ||
// let path_bounds = path.bounds(); | ||
// | ||
// let gradient = tiny_skia::RadialGradient::new( | ||
// tiny_skia::Point::from_xy(0.0, 0.0), | ||
// tiny_skia::Point::from_xy(path_bounds.width(), path_bounds.height()), // TODO: fix points | ||
// 0., // TODO: fix angle | ||
// stops, | ||
// tiny_skia::SpreadMode::Pad, | ||
// tiny_skia::Transform::default(), | ||
// ).expect("could not create radial gradient shader"); | ||
// paint.shader = gradient | ||
// } | ||
// } | ||
// | ||
// Some(paint) | ||
// } | ||
|
||
/// This is a minimal adapter for a Window that doesn't have any other feature than rendering | ||
/// using the software renderer. | ||
pub struct MinimalSoftwareWindow { | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is tiny-skia still required?