From b2cbda7a5502650efd7018c3ed727f1672d74162 Mon Sep 17 00:00:00 2001 From: Katherine Whitlock Date: Thu, 30 Oct 2025 10:56:35 -0400 Subject: [PATCH 1/5] ChangeLog: Add path drawing to software renderer --- internal/core/Cargo.toml | 6 +- internal/core/software_renderer.rs | 129 +++++++++++++++++- internal/core/software_renderer/path.rs | 165 ++++++++++++++++++++++++ 3 files changed, 296 insertions(+), 4 deletions(-) create mode 100644 internal/core/software_renderer/path.rs diff --git a/internal/core/Cargo.toml b/internal/core/Cargo.toml index f52210daa0a..9d8a99a5a87 100644 --- a/internal/core/Cargo.toml +++ b/internal/core/Cargo.toml @@ -20,7 +20,7 @@ path = "lib.rs" [features] ffi = ["dep:static_assertions"] # Expose C ABI -libm = ["num-traits/libm", "euclid/libm"] +libm = ["num-traits/libm", "euclid/libm", "zeno?/libm"] # Allow the viewer to query at runtime information about item types rtti = [] # Use the standard library @@ -37,6 +37,7 @@ std = [ "image-decoders", "svg", "raw-window-handle-06?/std", + "zeno?/std", "chrono/std", "chrono/wasmbind", "chrono/clock", @@ -50,7 +51,7 @@ unsafe-single-threaded = [] unicode = ["unicode-script", "unicode-linebreak"] software-renderer-systemfonts = ["shared-fontique", "skrifa", "fontdue", "software-renderer", "shared-parley"] -software-renderer = ["bytemuck"] +software-renderer = ["bytemuck", "dep:zeno"] image-decoders = ["dep:image", "dep:clru"] image-default-formats = ["image?/default-formats"] @@ -107,6 +108,7 @@ unicode-linebreak = { version = "0.1.5", optional = true } unicode-script = { version = "0.5.7", optional = true } integer-sqrt = { version = "0.1.5" } bytemuck = { workspace = true, optional = true, features = ["derive"] } +zeno = { version = "0.3.3", optional = true, default-features = false, features = ["eval"] } sys-locale = { version = "0.3.2", optional = true } parley = { version = "0.6.0", optional = true } pulldown-cmark = { version = "0.13.0", optional = true } diff --git a/internal/core/software_renderer.rs b/internal/core/software_renderer.rs index 994c484344c..f10642f3836 100644 --- a/internal/core/software_renderer.rs +++ b/internal/core/software_renderer.rs @@ -11,6 +11,7 @@ mod draw_functions; mod fixed; mod fonts; mod minimal_software_window; +mod path; mod scene; use self::fonts::GlyphRenderer; @@ -1301,6 +1302,21 @@ trait ProcessScene { fn process_linear_gradient(&mut self, geometry: PhysicalRect, gradient: LinearGradientCommand); fn process_radial_gradient(&mut self, geometry: PhysicalRect, gradient: RadialGradientCommand); fn process_conic_gradient(&mut self, geometry: PhysicalRect, gradient: ConicGradientCommand); + fn process_filled_path( + &mut self, + path_geometry: PhysicalRect, + clip_geometry: PhysicalRect, + commands: alloc::vec::Vec, + color: PremultipliedRgbaColor, + ); + fn process_stroked_path( + &mut self, + path_geometry: PhysicalRect, + clip_geometry: PhysicalRect, + commands: alloc::vec::Vec, + color: PremultipliedRgbaColor, + stroke_width: f32, + ); } fn process_rectangle_impl( @@ -1681,6 +1697,27 @@ impl ProcessScene for RenderToBuffer< ); }); } + + fn process_filled_path( + &mut self, + path_geometry: PhysicalRect, + clip_geometry: PhysicalRect, + commands: alloc::vec::Vec, + color: PremultipliedRgbaColor, + ) { + path::render_filled_path(&commands, &path_geometry, &clip_geometry, color, self.buffer); + } + + fn process_stroked_path( + &mut self, + path_geometry: PhysicalRect, + clip_geometry: PhysicalRect, + commands: alloc::vec::Vec, + color: PremultipliedRgbaColor, + stroke_width: f32, + ) { + path::render_stroked_path(&commands, &path_geometry, &clip_geometry, color, stroke_width, self.buffer); + } } #[derive(Default)] @@ -1814,6 +1851,29 @@ impl ProcessScene for PrepareScene { }); } } + + fn process_filled_path( + &mut self, + _path_geometry: PhysicalRect, + _clip_geometry: PhysicalRect, + _commands: alloc::vec::Vec, + _color: PremultipliedRgbaColor, + ) { + // Path rendering is not supported in line-by-line mode (PrepareScene/render_by_line) + // Only works with buffer-based rendering (RenderToBuffer) + } + + fn process_stroked_path( + &mut self, + _path_geometry: PhysicalRect, + _clip_geometry: PhysicalRect, + _commands: alloc::vec::Vec, + _color: PremultipliedRgbaColor, + _stroke_width: f32, + ) { + // Path rendering is not supported in line-by-line mode (PrepareScene/render_by_line) + // Only works with buffer-based rendering (RenderToBuffer) + } } struct SceneBuilder<'a, T> { @@ -2679,8 +2739,73 @@ impl crate::item_rendering::ItemRenderer for SceneBuilder<'_, T } #[cfg(feature = "std")] - fn draw_path(&mut self, _path: Pin<&crate::items::Path>, _: &ItemRc, _size: LogicalSize) { - // TODO + fn draw_path(&mut self, path: Pin<&crate::items::Path>, self_rc: &ItemRc, size: LogicalSize) { + let geom = LogicalRect::from(size); + if !self.should_draw(&geom) { + return; + } + + // Get the fitted path events from the Path item + let Some((offset, path_iterator)) = path.fitted_path_events(self_rc) else { + return; + }; + + // Convert to zeno commands + let zeno_commands = path::convert_path_data_to_zeno(path_iterator); + + // Calculate the physical geometry + let physical_geom = (geom.translate(self.current_state.offset.to_vector()).cast() + * self.scale_factor) + .round() + .cast() + .transformed(self.rotation); + + let physical_clip = + (self.current_state.clip.translate(self.current_state.offset.to_vector()).cast() + * self.scale_factor) + .round() + .cast::() + .transformed(self.rotation); + + // Apply the offset from fitted path + let physical_offset = (offset.cast::() * self.scale_factor).cast::(); + let adjusted_geom = physical_geom.translate(physical_offset); + + // Clip the geometry - early return if nothing to draw + let Some(clipped_geom) = adjusted_geom.intersection(&physical_clip) else { + return; + }; + + // Draw fill if specified + let fill_brush = path.fill(); + if !fill_brush.is_transparent() { + let fill_color = self.alpha_color(fill_brush.color()); + if fill_color.alpha() > 0 { + self.processor.process_filled_path( + adjusted_geom, + clipped_geom, + zeno_commands.clone(), + fill_color.into(), + ); + } + } + + // Draw stroke if specified + let stroke_brush = path.stroke(); + let stroke_width = path.stroke_width(); + if !stroke_brush.is_transparent() && stroke_width.get() > 0.0 { + let stroke_color = self.alpha_color(stroke_brush.color()); + if stroke_color.alpha() > 0 { + let physical_stroke_width = (stroke_width.cast() * self.scale_factor).get(); + self.processor.process_stroked_path( + adjusted_geom, + clipped_geom, + zeno_commands, + stroke_color.into(), + physical_stroke_width, + ); + } + } } fn draw_box_shadow( diff --git a/internal/core/software_renderer/path.rs b/internal/core/software_renderer/path.rs new file mode 100644 index 00000000000..c5277ee5da2 --- /dev/null +++ b/internal/core/software_renderer/path.rs @@ -0,0 +1,165 @@ +// Copyright © SixtyFPS GmbH +// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0 + +//! Path rendering support for the software renderer using zeno + +use super::draw_functions::{PremultipliedRgbaColor, TargetPixel}; +use super::PhysicalRect; +#[cfg(any(feature = "std"))] +use crate::graphics::PathDataIterator; +use alloc::vec::Vec; +use zeno::{Fill, Mask, Stroke, Point}; +pub use zeno::Command; + +/// Convert Slint's PathDataIterator to zeno's Command format +#[cfg(any(feature = "std"))] +pub fn convert_path_data_to_zeno(path_data: PathDataIterator) -> Vec { + use lyon_path::Event; + let mut commands = Vec::new(); + + for event in path_data.iter() { + match event { + Event::Begin { at } => { + commands.push(Command::MoveTo(Point::new(at.x, at.y))); + } + Event::Line { to, .. } => { + commands.push(Command::LineTo(Point::new(to.x, to.y))); + } + Event::Quadratic { ctrl, to, .. } => { + commands.push(Command::QuadTo( + Point::new(ctrl.x, ctrl.y), + Point::new(to.x, to.y), + )); + } + Event::Cubic { ctrl1, ctrl2, to, .. } => { + commands.push(Command::CurveTo( + Point::new(ctrl1.x, ctrl1.y), + Point::new(ctrl2.x, ctrl2.y), + Point::new(to.x, to.y), + )); + } + Event::End { close, .. } => { + if close { + commands.push(Command::Close); + } + } + } + } + + commands +} + +/// Common rendering logic for both filled and stroked paths +fn render_path_with_style( + commands: &[Command], + path_geometry: &PhysicalRect, + clip_geometry: &PhysicalRect, + color: PremultipliedRgbaColor, + style: zeno::Style, + buffer: &mut impl crate::software_renderer::target_pixel_buffer::TargetPixelBuffer, +) { + // The mask needs to be rendered at the full path size + let path_width = path_geometry.size.width as usize; + let path_height = path_geometry.size.height as usize; + + if path_width == 0 || path_height == 0 { + return; + } + + // Create a buffer for the mask output + let mut mask_buffer = Vec::with_capacity(path_width * path_height); + mask_buffer.resize(path_width * path_height, 0u8); + + // Render the full path into the mask + Mask::new(commands) + .size(path_width as u32, path_height as u32) + .style(style) + .render_into(&mut mask_buffer, None); + + // Calculate the intersection region - only apply within clipped area + // clip_geometry is relative to screen, path_geometry is also relative to screen + let clip_x_start = clip_geometry.origin.x.max(0) as usize; + let clip_y_start = clip_geometry.origin.y.max(0) as usize; + let clip_x_end = (clip_geometry.max_x().max(0) as usize).min(buffer.line_slice(0).len()); + let clip_y_end = (clip_geometry.max_y().max(0) as usize).min(buffer.num_lines()); + + let path_x_start = path_geometry.origin.x as isize; + let path_y_start = path_geometry.origin.y as isize; + + // Apply the mask only within the clipped region + for screen_y in clip_y_start..clip_y_end { + let line = buffer.line_slice(screen_y); + + // Calculate the y coordinate in the mask buffer + let mask_y = screen_y as isize - path_y_start; + if mask_y < 0 || mask_y >= path_height as isize { + continue; + } + + for screen_x in clip_x_start..clip_x_end { + // Calculate the x coordinate in the mask buffer + let mask_x = screen_x as isize - path_x_start; + if mask_x < 0 || mask_x >= path_width as isize { + continue; + } + + let mask_idx = (mask_y as usize) * path_width + (mask_x as usize); + let coverage = mask_buffer[mask_idx]; + + if coverage > 0 { + // Scale all color components by coverage to maintain premultiplication + let coverage_factor = coverage as u16; + let alpha_color = PremultipliedRgbaColor { + red: ((color.red as u16 * coverage_factor) / 255) as u8, + green: ((color.green as u16 * coverage_factor) / 255) as u8, + blue: ((color.blue as u16 * coverage_factor) / 255) as u8, + alpha: ((color.alpha as u16 * coverage_factor) / 255) as u8, + }; + T::blend(&mut line[screen_x], alpha_color); + } + } + } +} + +/// Render a filled path +/// +/// * `commands` - The path commands to render +/// * `path_geometry` - The full bounding box of the path in screen coordinates +/// * `clip_geometry` - The clipped region where the path should be rendered (intersection of path and clip) +/// * `color` - The color to render the path +/// * `buffer` - The target pixel buffer +pub fn render_filled_path( + commands: &[Command], + path_geometry: &PhysicalRect, + clip_geometry: &PhysicalRect, + color: PremultipliedRgbaColor, + buffer: &mut impl crate::software_renderer::target_pixel_buffer::TargetPixelBuffer, +) { + render_path_with_style(commands, path_geometry, clip_geometry, color, zeno::Style::Fill(Fill::NonZero), buffer); +} + +/// Render a stroked path +/// +/// * `commands` - The path commands to render +/// * `path_geometry` - The full bounding box of the path in screen coordinates +/// * `clip_geometry` - The clipped region where the path should be rendered (intersection of path and clip) +/// * `color` - The color to render the path +/// * `stroke_width` - The width of the stroke +/// * `buffer` - The target pixel buffer +pub fn render_stroked_path( + commands: &[Command], + path_geometry: &PhysicalRect, + clip_geometry: &PhysicalRect, + color: PremultipliedRgbaColor, + stroke_width: f32, + buffer: &mut impl crate::software_renderer::target_pixel_buffer::TargetPixelBuffer, +) { + render_path_with_style( + commands, + path_geometry, + clip_geometry, + color, + zeno::Style::Stroke(Stroke::new(stroke_width)), + buffer, + ); +} From d0c066ceafe013002fb43b62504386ce281c5782 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Thu, 30 Oct 2025 15:58:41 +0000 Subject: [PATCH 2/5] [autofix.ci] apply automated fixes --- internal/core/Cargo.toml | 2 +- internal/core/software_renderer.rs | 9 ++++++++- internal/core/software_renderer/path.rs | 22 +++++++++++++--------- 3 files changed, 22 insertions(+), 11 deletions(-) diff --git a/internal/core/Cargo.toml b/internal/core/Cargo.toml index 9d8a99a5a87..a12aa547a6b 100644 --- a/internal/core/Cargo.toml +++ b/internal/core/Cargo.toml @@ -19,7 +19,7 @@ categories = ["gui", "development-tools", "no-std"] path = "lib.rs" [features] -ffi = ["dep:static_assertions"] # Expose C ABI +ffi = ["dep:static_assertions"] # Expose C ABI libm = ["num-traits/libm", "euclid/libm", "zeno?/libm"] # Allow the viewer to query at runtime information about item types rtti = [] diff --git a/internal/core/software_renderer.rs b/internal/core/software_renderer.rs index f10642f3836..b599311435f 100644 --- a/internal/core/software_renderer.rs +++ b/internal/core/software_renderer.rs @@ -1716,7 +1716,14 @@ impl ProcessScene for RenderToBuffer< color: PremultipliedRgbaColor, stroke_width: f32, ) { - path::render_stroked_path(&commands, &path_geometry, &clip_geometry, color, stroke_width, self.buffer); + path::render_stroked_path( + &commands, + &path_geometry, + &clip_geometry, + color, + stroke_width, + self.buffer, + ); } } diff --git a/internal/core/software_renderer/path.rs b/internal/core/software_renderer/path.rs index c5277ee5da2..8dec73feadb 100644 --- a/internal/core/software_renderer/path.rs +++ b/internal/core/software_renderer/path.rs @@ -8,8 +8,8 @@ use super::PhysicalRect; #[cfg(any(feature = "std"))] use crate::graphics::PathDataIterator; use alloc::vec::Vec; -use zeno::{Fill, Mask, Stroke, Point}; pub use zeno::Command; +use zeno::{Fill, Mask, Point, Stroke}; /// Convert Slint's PathDataIterator to zeno's Command format #[cfg(any(feature = "std"))] @@ -26,10 +26,7 @@ pub fn convert_path_data_to_zeno(path_data: PathDataIterator) -> Vec { commands.push(Command::LineTo(Point::new(to.x, to.y))); } Event::Quadratic { ctrl, to, .. } => { - commands.push(Command::QuadTo( - Point::new(ctrl.x, ctrl.y), - Point::new(to.x, to.y), - )); + commands.push(Command::QuadTo(Point::new(ctrl.x, ctrl.y), Point::new(to.x, to.y))); } Event::Cubic { ctrl1, ctrl2, to, .. } => { commands.push(Command::CurveTo( @@ -89,7 +86,7 @@ fn render_path_with_style( // Apply the mask only within the clipped region for screen_y in clip_y_start..clip_y_end { let line = buffer.line_slice(screen_y); - + // Calculate the y coordinate in the mask buffer let mask_y = screen_y as isize - path_y_start; if mask_y < 0 || mask_y >= path_height as isize { @@ -122,7 +119,7 @@ fn render_path_with_style( } /// Render a filled path -/// +/// /// * `commands` - The path commands to render /// * `path_geometry` - The full bounding box of the path in screen coordinates /// * `clip_geometry` - The clipped region where the path should be rendered (intersection of path and clip) @@ -135,11 +132,18 @@ pub fn render_filled_path( color: PremultipliedRgbaColor, buffer: &mut impl crate::software_renderer::target_pixel_buffer::TargetPixelBuffer, ) { - render_path_with_style(commands, path_geometry, clip_geometry, color, zeno::Style::Fill(Fill::NonZero), buffer); + render_path_with_style( + commands, + path_geometry, + clip_geometry, + color, + zeno::Style::Fill(Fill::NonZero), + buffer, + ); } /// Render a stroked path -/// +/// /// * `commands` - The path commands to render /// * `path_geometry` - The full bounding box of the path in screen coordinates /// * `clip_geometry` - The clipped region where the path should be rendered (intersection of path and clip) From 0d92e4d207b7c08a99698460f3fa748e142feb81 Mon Sep 17 00:00:00 2001 From: Katherine Whitlock Date: Thu, 30 Oct 2025 13:33:59 -0400 Subject: [PATCH 3/5] gate path module behind `software-renderer-path` feature --- internal/core/Cargo.toml | 3 ++- internal/core/software_renderer.rs | 9 ++++++++- internal/core/software_renderer/path.rs | 8 +++++--- 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/internal/core/Cargo.toml b/internal/core/Cargo.toml index a12aa547a6b..b46b19c3f28 100644 --- a/internal/core/Cargo.toml +++ b/internal/core/Cargo.toml @@ -51,7 +51,8 @@ unsafe-single-threaded = [] unicode = ["unicode-script", "unicode-linebreak"] software-renderer-systemfonts = ["shared-fontique", "skrifa", "fontdue", "software-renderer", "shared-parley"] -software-renderer = ["bytemuck", "dep:zeno"] +software-renderer-path = ["dep:zeno"] +software-renderer = ["bytemuck"] image-decoders = ["dep:image", "dep:clru"] image-default-formats = ["image?/default-formats"] diff --git a/internal/core/software_renderer.rs b/internal/core/software_renderer.rs index b599311435f..d30000c17b1 100644 --- a/internal/core/software_renderer.rs +++ b/internal/core/software_renderer.rs @@ -11,6 +11,7 @@ mod draw_functions; mod fixed; mod fonts; mod minimal_software_window; +#[cfg(feature = "software-renderer-path")] mod path; mod scene; @@ -1302,6 +1303,7 @@ trait ProcessScene { fn process_linear_gradient(&mut self, geometry: PhysicalRect, gradient: LinearGradientCommand); fn process_radial_gradient(&mut self, geometry: PhysicalRect, gradient: RadialGradientCommand); fn process_conic_gradient(&mut self, geometry: PhysicalRect, gradient: ConicGradientCommand); + #[cfg(feature = "software-renderer-path")] fn process_filled_path( &mut self, path_geometry: PhysicalRect, @@ -1309,6 +1311,7 @@ trait ProcessScene { commands: alloc::vec::Vec, color: PremultipliedRgbaColor, ); + #[cfg(feature = "software-renderer-path")] fn process_stroked_path( &mut self, path_geometry: PhysicalRect, @@ -1698,6 +1701,7 @@ impl ProcessScene for RenderToBuffer< }); } + #[cfg(feature = "software-renderer-path")] fn process_filled_path( &mut self, path_geometry: PhysicalRect, @@ -1708,6 +1712,7 @@ impl ProcessScene for RenderToBuffer< path::render_filled_path(&commands, &path_geometry, &clip_geometry, color, self.buffer); } + #[cfg(feature = "software-renderer-path")] fn process_stroked_path( &mut self, path_geometry: PhysicalRect, @@ -1859,6 +1864,7 @@ impl ProcessScene for PrepareScene { } } + #[cfg(feature = "software-renderer-path")] fn process_filled_path( &mut self, _path_geometry: PhysicalRect, @@ -1870,6 +1876,7 @@ impl ProcessScene for PrepareScene { // Only works with buffer-based rendering (RenderToBuffer) } + #[cfg(feature = "software-renderer-path")] fn process_stroked_path( &mut self, _path_geometry: PhysicalRect, @@ -2745,7 +2752,7 @@ impl crate::item_rendering::ItemRenderer for SceneBuilder<'_, T } } - #[cfg(feature = "std")] + #[cfg(all(feature = "std", feature = "software-renderer-path"))] fn draw_path(&mut self, path: Pin<&crate::items::Path>, self_rc: &ItemRc, size: LogicalSize) { let geom = LogicalRect::from(size); if !self.should_draw(&geom) { diff --git a/internal/core/software_renderer/path.rs b/internal/core/software_renderer/path.rs index 8dec73feadb..aa651a29e3e 100644 --- a/internal/core/software_renderer/path.rs +++ b/internal/core/software_renderer/path.rs @@ -5,16 +5,18 @@ use super::draw_functions::{PremultipliedRgbaColor, TargetPixel}; use super::PhysicalRect; -#[cfg(any(feature = "std"))] +#[cfg(feature = "std")] use crate::graphics::PathDataIterator; use alloc::vec::Vec; +use zeno::{Fill, Mask, Stroke}; + pub use zeno::Command; -use zeno::{Fill, Mask, Point, Stroke}; /// Convert Slint's PathDataIterator to zeno's Command format -#[cfg(any(feature = "std"))] +#[cfg(feature = "std")] pub fn convert_path_data_to_zeno(path_data: PathDataIterator) -> Vec { use lyon_path::Event; + use zeno::Point; let mut commands = Vec::new(); for event in path_data.iter() { From ca423fea3dc73528a57dabdab05bc0ba7fa093d4 Mon Sep 17 00:00:00 2001 From: Katherine Whitlock Date: Thu, 30 Oct 2025 14:11:03 -0400 Subject: [PATCH 4/5] Add stub path drawing function for std but no path feature --- internal/core/software_renderer.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/internal/core/software_renderer.rs b/internal/core/software_renderer.rs index d30000c17b1..4f60a0a0b2f 100644 --- a/internal/core/software_renderer.rs +++ b/internal/core/software_renderer.rs @@ -2752,6 +2752,11 @@ impl crate::item_rendering::ItemRenderer for SceneBuilder<'_, T } } + #[cfg(all(feature = "std", not(feature = "software-renderer-path")))] + fn draw_path(&mut self, _path: Pin<&crate::items::Path>, _self_rc: &ItemRc, _size: LogicalSize) { + // Path rendering is disabled without the software-renderer-path feature + } + #[cfg(all(feature = "std", feature = "software-renderer-path"))] fn draw_path(&mut self, path: Pin<&crate::items::Path>, self_rc: &ItemRc, size: LogicalSize) { let geom = LogicalRect::from(size); From a0c2e74637f44c3d1fdd2504c42d756e876860a8 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Thu, 30 Oct 2025 18:12:48 +0000 Subject: [PATCH 5/5] [autofix.ci] apply automated fixes --- internal/core/software_renderer.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/internal/core/software_renderer.rs b/internal/core/software_renderer.rs index 4f60a0a0b2f..634678a94ff 100644 --- a/internal/core/software_renderer.rs +++ b/internal/core/software_renderer.rs @@ -2753,7 +2753,12 @@ impl crate::item_rendering::ItemRenderer for SceneBuilder<'_, T } #[cfg(all(feature = "std", not(feature = "software-renderer-path")))] - fn draw_path(&mut self, _path: Pin<&crate::items::Path>, _self_rc: &ItemRc, _size: LogicalSize) { + fn draw_path( + &mut self, + _path: Pin<&crate::items::Path>, + _self_rc: &ItemRc, + _size: LogicalSize, + ) { // Path rendering is disabled without the software-renderer-path feature }