Skip to content
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

Draft
wants to merge 8 commits into
base: master
Choose a base branch
from
Draft
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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,5 @@ docs/reference/src/language/builtins/structs.md
.env
.envrc
__pycache__

.idea
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,7 @@ spin_on = { version = "0.1" }
strum = { version = "0.26.1", default-features = false, features = ["derive"] }
toml_edit = { version = "0.22.7" }
ttf-parser = { version = "0.21" }
zeno = { version = "0.3.1" }

raw-window-handle-06 = { package = "raw-window-handle", version = "0.6", features = ["alloc"] }

Expand Down
5 changes: 4 additions & 1 deletion internal/core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Copy link
Member

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?

# 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.
Expand All @@ -38,6 +38,8 @@ software-renderer = ["bytemuck"]
image-decoders = ["dep:image", "dep:clru"]
svg = ["dep:resvg", "shared-fontdb"]

path = ["dep:zeno"]
Copy link
Member

Choose a reason for hiding this comment

The 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"]
Expand Down Expand Up @@ -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"}
Expand Down
223 changes: 213 additions & 10 deletions internal/core/software_renderer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand All @@ -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>;
Expand Down Expand Up @@ -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,
)
}
}
}
},
Expand All @@ -993,6 +999,7 @@ struct SceneVectors {
rounded_rectangles: Vec<RoundedRectangle>,
shared_buffers: Vec<SharedBufferCommand>,
gradients: Vec<GradientCommand>,
zeno_paths: Vec<ZenoPathCommand>,
}

struct Scene {
Expand Down Expand Up @@ -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> {
Expand Down Expand Up @@ -1398,6 +1409,16 @@ struct GradientCommand {
bottom_clip: PhysicalLength,
}

#[derive(Debug)]
struct ZenoPathCommand {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(in fact, this is a simplified version of SharedBufferCommand, for an alphamap)

Copy link
Author

Choose a reason for hiding this comment

The 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

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I meant that the ZenoPathCommand is basically the same as two SharedBufferCommand { buffer: SharedBufferData::AlphaMap { ... }, ... }

In fact, the draw_texture_line can be re-used, i think (if you create a SceneTexture)

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,
Expand Down Expand Up @@ -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> {
Expand Down Expand Up @@ -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)]
Expand Down Expand Up @@ -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> {
Expand Down Expand Up @@ -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() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if we can implement zeno::PathData for our own path.

Copy link
Author

Choose a reason for hiding this comment

The 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);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That seems big.
Internally, zeno renders row by row as well, I wonder if there is a way to do the actual path rendering in draw_zeno_path_line instead. (assuming that the storage needed in the rasterizer is less)
But anyway, there doesn't seem to be API in zeno to do that.

Copy link
Author

Choose a reason for hiding this comment

The 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
Copy link
Member

Choose a reason for hiding this comment

The 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 self.alpha_color

});
}

fn draw_box_shadow(
Expand Down Expand Up @@ -2659,6 +2797,71 @@ impl<'a, T: ProcessScene> crate::item_rendering::ItemRenderer for SceneBuilder<'
}
}

// impl From<Color> for tiny_skia::Color {
Copy link
Member

Choose a reason for hiding this comment

The 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 {
Expand Down
Loading
Loading