use crate::types::{Color, Fixed, Point, Rect, Transform, Transform3D, Viewport};
use crate::render::canvas::{Canvas, Paint};
use crate::render::command::{CompositeMode, DrawCommand};
use crate::render::path::{Path, PathCmd};
use crate::render::renderer::{
DrawRequest, FallbackRegion, ProjectiveDrawError, RenderError, RenderFeature, RenderRoute,
Renderer,
};
use crate::render::texture::Texture;
#[cfg(feature = "perf")]
pub mod perf;
#[cfg(feature = "perf")]
pub use perf::{PerfCtx, quad_perf};
mod blit_dispatch;
mod blit_fast;
pub mod blur;
mod label;
mod label_sdf;
pub mod mix;
mod path;
mod quad;
mod quad_aa;
mod rect_fill;
mod rect_stroke;
mod transformed;
use quad::{blit_quad, fill_rect_quad, stroke_rect_quad};
use transformed::{blit_transformed, fill_rect_transformed, offset_point, offset_rect};
pub use crate::render::texture::AlphaMode;
pub struct SwRenderer<'a> {
pub target: Texture<'a>,
pub viewport: Viewport,
pub(super) scratch: ScratchStorage<'a>,
#[cfg(feature = "perf")]
pub perf: Option<PerfCtx>,
}
pub(crate) struct SwScratch {
pub(super) flatten_buf: alloc::vec::Vec<crate::render::raster::LineSeg>,
pub(super) primitive_path: crate::render::path::Path,
pub(super) stroke_outline: crate::render::path::Path,
pub(super) subpath_scratch: alloc::vec::Vec<crate::render::raster::SubPath>,
pub(super) semantic_joins: alloc::vec::Vec<usize>,
pub(super) dash_segments: alloc::vec::Vec<crate::render::raster::LineSeg>,
pub(super) dash_scratch: alloc::vec::Vec<crate::render::raster::SubPath>,
pub(super) dashed_semantic_joins: alloc::vec::Vec<usize>,
pub(super) scanline_acc: alloc::vec::Vec<Fixed>,
pub(super) scanline_crossings: alloc::vec::Vec<(Fixed, i8)>,
pub(super) stroke_normals: alloc::vec::Vec<crate::types::Point>,
pub(super) stroke_rail: alloc::vec::Vec<crate::types::Point>,
pub(super) stroke_left_rail: alloc::vec::Vec<crate::types::Point>,
pub(super) stroke_arc: alloc::vec::Vec<crate::types::Point>,
pub(super) clip_stack: alloc::vec::Vec<ClipMask>,
pub(super) clip_recycled: alloc::vec::Vec<ClipMask>,
pub(super) clip_mask_buf: alloc::vec::Vec<u8>,
}
#[allow(
clippy::large_enum_variant,
reason = "direct renderer scratch stays on the stack"
)]
pub(super) enum ScratchStorage<'a> {
Owned(SwScratch),
Borrowed(&'a mut SwScratch),
}
impl core::ops::Deref for ScratchStorage<'_> {
type Target = SwScratch;
fn deref(&self) -> &Self::Target {
match self {
Self::Owned(scratch) => scratch,
Self::Borrowed(scratch) => scratch,
}
}
}
impl core::ops::DerefMut for ScratchStorage<'_> {
fn deref_mut(&mut self) -> &mut Self::Target {
match self {
Self::Owned(scratch) => scratch,
Self::Borrowed(scratch) => scratch,
}
}
}
pub(super) struct ClipMask {
pub alpha: alloc::vec::Vec<u8>,
}
impl SwScratch {
pub(crate) fn new() -> Self {
Self {
flatten_buf: alloc::vec::Vec::new(),
primitive_path: crate::render::path::Path::new(),
stroke_outline: crate::render::path::Path::new(),
subpath_scratch: alloc::vec::Vec::new(),
semantic_joins: alloc::vec::Vec::new(),
dash_segments: alloc::vec::Vec::new(),
dash_scratch: alloc::vec::Vec::new(),
dashed_semantic_joins: alloc::vec::Vec::new(),
scanline_acc: alloc::vec::Vec::new(),
scanline_crossings: alloc::vec::Vec::new(),
stroke_normals: alloc::vec::Vec::new(),
stroke_rail: alloc::vec::Vec::new(),
stroke_left_rail: alloc::vec::Vec::new(),
stroke_arc: alloc::vec::Vec::new(),
clip_stack: alloc::vec::Vec::new(),
clip_recycled: alloc::vec::Vec::new(),
clip_mask_buf: alloc::vec::Vec::new(),
}
}
fn reset_frame(&mut self) {
while let Some(mut mask) = self.clip_stack.pop() {
mask.alpha.clear();
self.clip_recycled.push(mask);
}
}
#[cfg(test)]
pub(crate) fn retained_buffer_state(&self) -> (usize, usize, usize, usize) {
assert!(self.flatten_buf.capacity() > 0);
assert!(self.scanline_acc.capacity() > 0);
assert!(self.stroke_rail.capacity() > 0);
let mask = &self.clip_recycled[0].alpha;
assert!(mask.capacity() >= 64 * 64);
(
self.flatten_buf.as_ptr() as usize,
self.scanline_acc.as_ptr() as usize,
self.stroke_rail.as_ptr() as usize,
mask.as_ptr() as usize,
)
}
}
impl<'a> SwRenderer<'a> {
pub fn new(target: Texture<'a>) -> Self {
Self::with_storage(target, ScratchStorage::Owned(SwScratch::new()))
}
pub(crate) fn with_scratch(target: Texture<'a>, scratch: &'a mut SwScratch) -> Self {
scratch.reset_frame();
Self::with_storage(target, ScratchStorage::Borrowed(scratch))
}
fn with_storage(target: Texture<'a>, scratch: ScratchStorage<'a>) -> Self {
let viewport = Viewport::new(target.width, target.height, Fixed::ONE);
Self {
target,
viewport,
scratch,
#[cfg(feature = "perf")]
perf: None,
}
}
pub fn with_alpha_mode(mut self, mode: AlphaMode) -> Self {
self.target.alpha_mode = mode;
self
}
}
impl<'a> SwRenderer<'a> {
#[inline(never)]
fn draw_transformed(&mut self, cmd: &DrawCommand, clip: &Rect, tf: &Transform) {
let vp = self.viewport.as_transform();
let phys_tf = vp.compose(tf);
let phys_clip = self.viewport.rect_to_physical(*clip);
match cmd {
DrawCommand::PushClip {
path, fill_rule, ..
} => {
self.push_clip_inner(path, &phys_tf, *fill_rule);
}
DrawCommand::PopClip => {
self.pop_clip();
}
DrawCommand::ApplyBlur { alpha, region } => {
let phys_region = self.viewport.rect_to_physical(*region);
crate::render::backends::sw::blur::iir_blur_inplace(
&mut self.target,
*alpha,
phys_region,
);
}
DrawCommand::Fill {
area, color, opa, ..
} => {
fill_rect_transformed(&mut self.target, *area, phys_clip, &phys_tf, color, *opa);
}
DrawCommand::Blit {
pos,
size,
texture,
radius,
composite,
..
} => {
if *radius != Fixed::ZERO {
unimplemented!(
"sw backend: Blit.radius mask under non-axis-aligned transform not yet implemented",
);
}
if !matches!(composite, CompositeMode::SourceOver) {
unimplemented!(
"sw backend: composite {composite:?} under non-axis-aligned transform not yet implemented",
);
}
let src_rect = Rect::new(0, 0, texture.width, texture.height);
let dst = Rect {
x: pos.x,
y: pos.y,
w: size.x,
h: size.y,
};
blit_transformed(
&mut self.target,
texture,
&src_rect,
dst,
phys_clip,
&phys_tf,
);
}
DrawCommand::FillPath {
path,
paint,
opa,
fill_rule,
..
} => {
self.fill_path_transformed(path, phys_clip, &phys_tf, paint, *opa, *fill_rule);
}
DrawCommand::StrokePath {
path,
paint,
width,
opa,
line_cap,
line_join,
miter_limit,
dash,
..
} => {
self.stroke_path_transformed(
path,
phys_clip,
&phys_tf,
*width,
paint,
*opa,
*line_cap,
*line_join,
*miter_limit,
dash,
);
}
DrawCommand::Border {
area,
color,
width,
radius,
opa,
..
} => {
let path = crate::render::path::Path::rounded_rect(
area.x + *width / 2,
area.y + *width / 2,
area.w - *width,
area.h - *width,
(*radius - *width / 2).max(Fixed::ZERO),
);
let paint = Paint::Color((*color).into());
self.stroke_path_transformed(
&path,
phys_clip,
&phys_tf,
*width,
&paint,
*opa,
crate::render::raster::LineCap::Butt,
crate::render::raster::LineJoin::Miter,
Fixed::from_int(4),
&[],
);
}
DrawCommand::Line {
p1,
p2,
color,
width,
opa,
..
} => {
let mut path = crate::render::path::Path::new();
path.move_to(*p1).line_to(*p2);
let paint = Paint::Color((*color).into());
self.stroke_path_transformed(
&path,
phys_clip,
&phys_tf,
*width,
&paint,
*opa,
crate::render::raster::LineCap::Butt,
crate::render::raster::LineJoin::Miter,
Fixed::from_int(4),
&[],
);
}
DrawCommand::Arc {
center,
radius,
start_angle,
end_angle,
color,
width,
opa,
..
} => {
let path =
crate::render::path::Path::arc(*center, *radius, *start_angle, *end_angle);
let paint = Paint::Color((*color).into());
self.stroke_path_transformed(
&path,
phys_clip,
&phys_tf,
*width,
&paint,
*opa,
crate::render::raster::LineCap::Butt,
crate::render::raster::LineJoin::Miter,
Fixed::from_int(4),
&[],
);
}
DrawCommand::GlyphRun {
pos,
glyphs,
font,
color,
opa,
..
} => self.draw_glyph_run_transformed_inner(label::TransformedRun {
pos,
glyphs,
font,
transform: &phys_tf,
clip: phys_clip,
color,
opacity: *opa,
}),
DrawCommand::PosedGlyphRun {
pos,
glyphs,
font,
color,
opa,
..
} => self.draw_posed_glyph_run_inner(label::PosedRun {
pos,
glyphs: glyphs.glyphs(),
frames: glyphs.frames(),
font,
transform: &phys_tf,
clip: phys_clip,
color,
opacity: *opa,
}),
}
}
}
impl<'a> Canvas for SwRenderer<'a> {
fn fill_path(
&mut self,
path: &Path,
clip: &Rect,
paint: &Paint,
opa: u8,
fill_rule: crate::render::raster::FillRule,
) {
self.fill_path_inner(path, clip, paint, opa, fill_rule);
}
fn stroke_path(
&mut self,
path: &Path,
clip: &Rect,
width: Fixed,
paint: &Paint,
opa: u8,
cap: crate::render::raster::LineCap,
join: crate::render::raster::LineJoin,
miter_limit: Fixed,
dash: &[Fixed],
) {
self.stroke_path_inner(path, clip, width, paint, opa, cap, join, miter_limit, dash);
}
fn fill_rect(&mut self, area: &Rect, clip: &Rect, color: &Color, radius: Fixed, opa: u8) {
self.fill_rect_inner(area, clip, color, radius, opa);
}
fn push_clip(
&mut self,
path: &Path,
transform: &Transform,
fill_rule: crate::render::raster::FillRule,
) {
let phys_tf = self.viewport.as_transform().compose(transform);
self.push_clip_inner(path, &phys_tf, fill_rule);
}
fn pop_clip(&mut self) {
if let Some(mut mask) = self.scratch.clip_stack.pop() {
mask.alpha.clear();
self.scratch.clip_recycled.push(mask);
}
}
fn stroke_rect(
&mut self,
area: &Rect,
clip: &Rect,
width: Fixed,
color: &Color,
radius: Fixed,
opa: u8,
) {
self.stroke_rect_inner(area, clip, width, color, radius, opa);
}
fn blit(
&mut self,
src: &Texture,
src_rect: &Rect,
dst: Point,
dst_size: Point,
clip: &Rect,
opa: u8,
radius: Fixed,
composite: CompositeMode,
) {
self.blit_inner(src, src_rect, dst, dst_size, clip, opa, radius, composite);
}
fn clear(&mut self, area: &Rect, color: &Color) {
let phys_area = self.viewport.rect_to_physical(*area);
let screen = Rect::new(0, 0, self.target.width, self.target.height);
let Some(draw_area) = phys_area.intersect(&screen) else {
return;
};
let (px_x0, px_y0, px_x1, px_y1) = draw_area.pixel_bounds();
for py in px_y0..px_y1 {
for px in px_x0..px_x1 {
self.target.set_pixel(px, py, color);
}
}
}
fn draw_glyph_run(
&mut self,
pos: &Point,
glyphs: &[textflow::shaping::PositionedGlyph],
font: &crate::render::font::Font,
clip: &Rect,
color: &Color,
opa: u8,
) {
self.draw_glyph_run_inner(pos, glyphs, font, clip, color, opa);
}
fn draw_posed_glyph_run(
&mut self,
pos: &Point,
glyphs: crate::render::PosedGlyphs<'_>,
font: &crate::render::font::Font,
clip: &Rect,
color: &Color,
opa: u8,
) {
self.draw_posed_glyph_run_inner(label::PosedRun {
pos,
glyphs: glyphs.glyphs(),
frames: glyphs.frames(),
font,
transform: &Transform::IDENTITY,
clip: *clip,
color,
opacity: opa,
});
}
fn flush(&mut self) {}
}
impl SwRenderer<'_> {
#[inline(never)]
fn dispatch_fill_quad(
&mut self,
q: &[Point; 4],
area: &Rect,
color: &Color,
radius: Fixed,
opa: u8,
clip: &Rect,
) {
#[cfg(feature = "perf")]
let t0 = quad_perf::now();
let phys_clip = self.viewport.rect_to_physical(*clip);
let phys_q = [
self.viewport.point_to_physical(q[0]),
self.viewport.point_to_physical(q[1]),
self.viewport.point_to_physical(q[2]),
self.viewport.point_to_physical(q[3]),
];
let s = self.viewport.scale();
let clip_mask = self.scratch.clip_stack.last().map(|m| m.alpha.as_slice());
fill_rect_quad(
&mut self.target,
&phys_q,
phys_clip,
color,
radius * s,
area.w * s,
area.h * s,
opa,
clip_mask,
);
#[cfg(feature = "perf")]
quad_perf::add_fill(quad_perf::now().wrapping_sub(t0));
}
#[inline(never)]
#[allow(clippy::too_many_arguments)]
fn dispatch_blit_quad(
&mut self,
q: &[Point; 4],
texture: &Texture,
size: Point,
clip: &Rect,
radius: Fixed,
opa: u8,
composite: CompositeMode,
) {
#[cfg(feature = "perf")]
let t0 = quad_perf::now();
let phys_clip = self.viewport.rect_to_physical(*clip);
let phys_q = [
self.viewport.point_to_physical(q[0]),
self.viewport.point_to_physical(q[1]),
self.viewport.point_to_physical(q[2]),
self.viewport.point_to_physical(q[3]),
];
let scale = self.viewport.scale();
let clip_mask = self.scratch.clip_stack.last().map(|m| m.alpha.as_slice());
blit_quad(
&mut self.target,
texture,
&phys_q,
phys_clip,
Point {
x: size.x * scale,
y: size.y * scale,
},
radius * scale,
opa,
composite,
clip_mask,
);
#[cfg(feature = "perf")]
quad_perf::add_blit(quad_perf::now().wrapping_sub(t0));
}
#[inline(never)]
fn dispatch_border_quad(
&mut self,
q: &[Point; 4],
color: &Color,
width: Fixed,
radius: Fixed,
opa: u8,
clip: &Rect,
) {
let phys_clip = self.viewport.rect_to_physical(*clip);
let phys_q = [
self.viewport.point_to_physical(q[0]),
self.viewport.point_to_physical(q[1]),
self.viewport.point_to_physical(q[2]),
self.viewport.point_to_physical(q[3]),
];
let s = self.viewport.scale();
let clip_mask = self.scratch.clip_stack.last().map(|m| m.alpha.as_slice());
stroke_rect_quad(
&mut self.target,
&phys_q,
phys_clip,
color,
width * s,
radius * s,
opa,
clip_mask,
);
}
#[inline(never)]
#[allow(clippy::too_many_arguments)]
fn dispatch_fill(
&mut self,
area: &Rect,
color: &Color,
radius: Fixed,
opa: u8,
tx: Fixed,
ty: Fixed,
clip: &Rect,
) {
#[cfg(feature = "perf")]
let t0 = self.perf.as_ref().map(|p| (p.clock)());
let area = offset_rect(area, tx, ty);
self.fill_rect(&area, clip, color, radius, opa);
#[cfg(feature = "perf")]
if let (Some(t0), Some(p)) = (t0, self.perf.as_mut()) {
p.fill += (p.clock)() - t0;
p.count_fill += 1;
}
}
#[inline(never)]
#[allow(clippy::too_many_arguments)]
fn dispatch_border(
&mut self,
area: &Rect,
color: &Color,
width: Fixed,
radius: Fixed,
opa: u8,
tx: Fixed,
ty: Fixed,
clip: &Rect,
) {
#[cfg(feature = "perf")]
let t0 = self.perf.as_ref().map(|p| (p.clock)());
let area = offset_rect(area, tx, ty);
self.stroke_rect(&area, clip, width, color, radius, opa);
#[cfg(feature = "perf")]
if let (Some(t0), Some(p)) = (t0, self.perf.as_mut()) {
p.stroke += (p.clock)() - t0;
p.count_stroke += 1;
}
}
#[inline(never)]
#[allow(clippy::too_many_arguments)]
fn dispatch_blit(
&mut self,
pos: &Point,
size: Point,
texture: &Texture,
tx: Fixed,
ty: Fixed,
clip: &Rect,
opa: u8,
radius: Fixed,
composite: CompositeMode,
) {
#[cfg(feature = "perf")]
let t0 = self.perf.as_ref().map(|p| (p.clock)());
let src_rect = Rect::new(0, 0, texture.width, texture.height);
let pos = offset_point(pos, tx, ty);
self.blit(texture, &src_rect, pos, size, clip, opa, radius, composite);
#[cfg(feature = "perf")]
if let (Some(t0), Some(p)) = (t0, self.perf.as_mut()) {
p.blit += (p.clock)() - t0;
p.count_blit += 1;
}
}
#[inline(never)]
#[allow(clippy::too_many_arguments)]
fn dispatch_glyph_run(
&mut self,
pos: &Point,
glyphs: &[textflow::shaping::PositionedGlyph],
font: &crate::render::font::Font,
color: &Color,
opa: u8,
tx: Fixed,
ty: Fixed,
clip: &Rect,
) {
#[cfg(feature = "perf")]
let t0 = self.perf.as_ref().map(|p| (p.clock)());
let pos = offset_point(pos, tx, ty);
self.draw_glyph_run(&pos, glyphs, font, clip, color, opa);
#[cfg(feature = "perf")]
if let (Some(t0), Some(p)) = (t0, self.perf.as_mut()) {
p.label += (p.clock)() - t0;
p.count_label += 1;
}
}
#[inline(never)]
#[allow(clippy::too_many_arguments)]
fn dispatch_line(
&mut self,
p1: &Point,
p2: &Point,
color: &Color,
width: Fixed,
opa: u8,
tx: Fixed,
ty: Fixed,
clip: &Rect,
) {
#[cfg(feature = "perf")]
let t0 = self.perf.as_ref().map(|p| (p.clock)());
let p1 = offset_point(p1, tx, ty);
let p2 = offset_point(p2, tx, ty);
self.draw_line(p1, p2, clip, width, color, opa);
#[cfg(feature = "perf")]
if let (Some(t0), Some(p)) = (t0, self.perf.as_mut()) {
p.stroke += (p.clock)() - t0;
p.count_stroke += 1;
}
}
#[inline(never)]
#[allow(clippy::too_many_arguments)]
fn dispatch_arc(
&mut self,
center: &Point,
radius: Fixed,
start_angle: Fixed,
end_angle: Fixed,
color: &Color,
width: Fixed,
opa: u8,
tx: Fixed,
ty: Fixed,
clip: &Rect,
) {
#[cfg(feature = "perf")]
let t0 = self.perf.as_ref().map(|p| (p.clock)());
let center = offset_point(center, tx, ty);
self.draw_arc(
center,
radius,
start_angle,
end_angle,
clip,
width,
color,
opa,
);
#[cfg(feature = "perf")]
if let (Some(t0), Some(p)) = (t0, self.perf.as_mut()) {
p.stroke += (p.clock)() - t0;
p.count_stroke += 1;
}
}
}
impl SwRenderer<'_> {
fn route(&self, request: &DrawRequest<'_, '_>) -> Result<RenderRoute, RenderError> {
use crate::types::TransformClass;
request.validate_projection()?;
request.validate_texture()?;
if !request.projective.is_identity() {
self.preflight_projective(request.command, &request.clip, &request.projective)
.map_err(RenderError::from)?;
return Ok(RenderRoute::Native);
}
let affine = !matches!(
request.command.transform().classify(),
TransformClass::Identity | TransformClass::Translate
);
match request.command {
DrawCommand::FillPath {
path,
paint: paint @ (Paint::LinearGradient(_) | Paint::RadialGradient(_)),
transform,
..
} => {
let Some(bbox) = path.bbox() else {
return Err(RenderError::InvalidGeometry);
};
let draw = self.viewport.as_transform().compose(transform);
if crate::render::paint::GradientPaint::new(paint, draw, bbox).is_none() {
return Err(RenderError::InvalidGeometry);
}
}
DrawCommand::StrokePath {
path,
paint: paint @ (Paint::LinearGradient(_) | Paint::RadialGradient(_)),
transform,
width,
..
} => {
let Some(bbox) = path.bbox() else {
return Err(RenderError::InvalidGeometry);
};
let half = *width / 2;
let bbox = Rect::new(
bbox.x - half,
bbox.y - half,
bbox.w + *width,
bbox.h + *width,
);
let draw = self.viewport.as_transform().compose(transform);
if crate::render::paint::GradientPaint::new(paint, draw, bbox).is_none() {
return Err(RenderError::InvalidGeometry);
}
}
DrawCommand::Fill {
quad: None, radius, ..
} if affine && *radius != Fixed::ZERO => {
return Err(RenderError::Unsupported(RenderFeature::RoundedFill));
}
DrawCommand::Blit {
quad,
radius,
composite,
opa,
..
} if affine && quad.is_none() => {
if *radius != Fixed::ZERO {
return Err(RenderError::Unsupported(RenderFeature::RoundedBlit));
}
if *composite != CompositeMode::SourceOver {
return Err(RenderError::Unsupported(RenderFeature::Composite(
*composite,
)));
}
if *opa != 255 {
return Err(RenderError::Unsupported(RenderFeature::BlitOpacity));
}
}
_ => {}
}
Ok(RenderRoute::Native)
}
fn submit(&mut self, request: &DrawRequest<'_, '_>) -> Result<(), RenderError> {
use crate::types::TransformClass;
request.validate_projection()?;
request.validate_texture()?;
if !request.projective.is_identity() {
return self
.draw_projective(request.command, &request.clip, &request.projective)
.map_err(RenderError::from);
}
if matches!(
request.command.transform().classify(),
TransformClass::Identity | TransformClass::Translate
) && !matches!(request.command, DrawCommand::Blit { quad: Some(_), .. })
&& !matches!(
request.command,
DrawCommand::FillPath {
paint: Paint::LinearGradient(_) | Paint::RadialGradient(_),
..
} | DrawCommand::StrokePath {
paint: Paint::LinearGradient(_) | Paint::RadialGradient(_),
..
}
)
{
self.draw(request.command, &request.clip);
return Ok(());
}
self.route(request)?;
self.draw(request.command, &request.clip);
Ok(())
}
fn output_scale(&self) -> Fixed {
self.viewport.scale()
}
fn plan_scope(&self, bounds: &Rect) -> Result<FallbackRegion, RenderError> {
FallbackRegion::from_logical_bounds(*bounds, self.viewport, Some(usize::MAX))
}
pub(crate) fn draw(&mut self, cmd: &DrawCommand, clip: &Rect) {
use crate::types::TransformClass;
if let DrawCommand::Fill {
area,
quad: Some(q),
color,
opa,
radius,
..
} = cmd
{
crate::trace_span!("sw.fill_quad");
self.dispatch_fill_quad(q, area, color, *radius, *opa, clip);
return;
}
if let DrawCommand::Blit {
quad: Some(q),
texture,
size,
radius,
opa,
composite,
..
} = cmd
{
crate::trace_span!("sw.blit_quad");
self.dispatch_blit_quad(q, texture, *size, clip, *radius, *opa, *composite);
return;
}
if let DrawCommand::Border {
quad: Some(q),
color,
width,
radius,
opa,
..
} = cmd
{
crate::trace_span!("sw.border_quad");
self.dispatch_border_quad(q, color, *width, *radius, *opa, clip);
return;
}
let tf = cmd.transform();
let class = tf.classify();
if !matches!(class, TransformClass::Identity | TransformClass::Translate) {
crate::trace_span!("sw.transformed");
self.draw_transformed(cmd, clip, &tf);
return;
}
let (tx, ty) = match class {
TransformClass::Identity => (Fixed::ZERO, Fixed::ZERO),
TransformClass::Translate => (tf.tx, tf.ty),
_ => unreachable!(),
};
match cmd {
DrawCommand::PushClip {
path,
transform,
fill_rule,
} => {
crate::trace_span!("sw.push_clip");
let phys_tf = self.viewport.as_transform().compose(transform);
self.push_clip_inner(path, &phys_tf, *fill_rule);
}
DrawCommand::PopClip => {
crate::trace_span!("sw.pop_clip");
self.pop_clip();
}
DrawCommand::ApplyBlur { alpha, region } => {
crate::trace_span!("sw.apply_blur");
let phys_region = self.viewport.rect_to_physical(*region);
crate::render::backends::sw::blur::iir_blur_inplace(
&mut self.target,
*alpha,
phys_region,
);
}
DrawCommand::Fill {
area,
color,
radius,
opa,
..
} => {
crate::trace_span!("sw.fill");
self.dispatch_fill(area, color, *radius, *opa, tx, ty, clip);
}
DrawCommand::Border {
area,
color,
width,
radius,
opa,
..
} => {
crate::trace_span!("sw.border");
self.dispatch_border(area, color, *width, *radius, *opa, tx, ty, clip);
}
DrawCommand::Blit {
pos,
size,
texture,
opa,
radius,
composite,
..
} => {
crate::trace_span!("sw.blit");
self.dispatch_blit(pos, *size, texture, tx, ty, clip, *opa, *radius, *composite);
}
DrawCommand::GlyphRun {
pos,
glyphs,
font,
color,
opa,
..
} => {
crate::trace_span!("sw.glyph_run");
self.dispatch_glyph_run(pos, glyphs, font, color, *opa, tx, ty, clip);
}
DrawCommand::PosedGlyphRun {
pos,
glyphs,
font,
color,
opa,
transform,
} => {
crate::trace_span!("sw.posed_glyph_run");
let phys_tf = self.viewport.as_transform().compose(transform);
self.draw_posed_glyph_run_inner(label::PosedRun {
pos,
glyphs: glyphs.glyphs(),
frames: glyphs.frames(),
font,
transform: &phys_tf,
clip: self.viewport.rect_to_physical(*clip),
color,
opacity: *opa,
});
}
DrawCommand::Line {
p1,
p2,
color,
width,
opa,
..
} => {
crate::trace_span!("sw.line");
self.dispatch_line(p1, p2, color, *width, *opa, tx, ty, clip);
}
DrawCommand::Arc {
center,
radius,
start_angle,
end_angle,
color,
width,
opa,
..
} => {
crate::trace_span!("sw.arc");
self.dispatch_arc(
center,
*radius,
*start_angle,
*end_angle,
color,
*width,
*opa,
tx,
ty,
clip,
);
}
DrawCommand::FillPath {
path,
paint,
opa,
fill_rule,
..
} => {
crate::trace_span!("sw.fill_path");
if tx == Fixed::ZERO && ty == Fixed::ZERO {
self.fill_path_inner(path, clip, paint, *opa, *fill_rule);
} else {
let phys_tf = self
.viewport
.as_transform()
.compose(&Transform::translate(tx, ty));
let phys_clip = self.viewport.rect_to_physical(*clip);
self.fill_path_transformed(path, phys_clip, &phys_tf, paint, *opa, *fill_rule);
}
}
DrawCommand::StrokePath {
path,
paint,
width,
opa,
line_cap,
line_join,
miter_limit,
dash,
..
} => {
crate::trace_span!("sw.stroke_path");
if tx == Fixed::ZERO && ty == Fixed::ZERO {
self.stroke_path_inner(
path,
clip,
*width,
paint,
*opa,
*line_cap,
*line_join,
*miter_limit,
dash,
);
} else {
let phys_tf = self
.viewport
.as_transform()
.compose(&Transform::translate(tx, ty));
let phys_clip = self.viewport.rect_to_physical(*clip);
self.stroke_path_transformed(
path,
phys_clip,
&phys_tf,
*width,
paint,
*opa,
*line_cap,
*line_join,
*miter_limit,
dash,
);
}
}
}
}
pub(crate) fn draw_projective(
&mut self,
cmd: &DrawCommand,
clip: &Rect,
projective: &Transform3D,
) -> Result<(), ProjectiveDrawError> {
self.preflight_projective(cmd, clip, projective)?;
if projective.is_identity() {
self.draw(cmd, clip);
return Ok(());
}
let logical = projective.compose(&Transform3D::from_affine(cmd.transform()));
match cmd {
DrawCommand::Fill {
area,
color,
radius,
opa,
..
} => {
let quad = logical
.apply_rect(*area)
.ok_or(ProjectiveDrawError::InvalidProjection)?;
self.dispatch_fill_quad(&quad, area, color, *radius, *opa, clip);
}
DrawCommand::Border {
area,
color,
width,
radius,
opa,
..
} => {
let quad = logical
.apply_rect(*area)
.ok_or(ProjectiveDrawError::InvalidProjection)?;
self.dispatch_border_quad(&quad, color, *width, *radius, *opa, clip);
}
DrawCommand::Blit {
pos,
size,
texture,
opa,
radius,
composite,
..
} => {
let quad = logical
.apply_rect(Rect {
x: pos.x,
y: pos.y,
w: size.x,
h: size.y,
})
.ok_or(ProjectiveDrawError::InvalidProjection)?;
self.dispatch_blit_quad(&quad, texture, *size, clip, *radius, *opa, *composite);
}
DrawCommand::GlyphRun {
pos,
glyphs,
font,
color,
opa,
..
} => {
let physical =
Transform3D::from_affine(self.viewport.as_transform()).compose(&logical);
self.draw_glyph_run_projective_inner(label::ProjectiveRun {
pos,
glyphs,
font,
transform: &physical,
clip: self.viewport.rect_to_physical(*clip),
color,
opacity: *opa,
});
}
DrawCommand::PosedGlyphRun {
pos,
transform,
glyphs,
font,
color,
opa,
} => {
let physical_projective =
Transform3D::from_affine(self.viewport.as_transform()).compose(projective);
self.draw_posed_glyph_run_projective_inner(label::ProjectivePosedRun {
pos,
glyphs: glyphs.glyphs(),
frames: glyphs.frames(),
font,
command_transform: transform,
projective_transform: &physical_projective,
clip: self.viewport.rect_to_physical(*clip),
color,
opacity: *opa,
});
}
DrawCommand::FillPath {
path,
paint,
opa,
fill_rule,
..
} => {
let physical =
Transform3D::from_affine(self.viewport.as_transform()).compose(&logical);
self.fill_path_projective(
path,
&physical,
self.viewport.rect_to_physical(*clip),
paint,
*opa,
*fill_rule,
)?;
}
DrawCommand::StrokePath {
path,
transform,
paint,
width,
opa,
line_cap,
line_join,
miter_limit,
dash,
} => {
let physical_projective =
Transform3D::from_affine(self.viewport.as_transform()).compose(projective);
self.stroke_commands_projective(
path.commands(),
transform,
&physical_projective,
self.viewport.rect_to_physical(*clip),
*width,
paint,
*opa,
*line_cap,
*line_join,
*miter_limit,
dash,
)?;
}
DrawCommand::Line {
p1,
p2,
transform,
color,
width,
opa,
} => {
let commands = [PathCmd::MoveTo(*p1), PathCmd::LineTo(*p2)];
let paint = Paint::Color((*color).into());
let physical_projective =
Transform3D::from_affine(self.viewport.as_transform()).compose(projective);
self.stroke_commands_projective(
&commands,
transform,
&physical_projective,
self.viewport.rect_to_physical(*clip),
*width,
&paint,
*opa,
crate::render::raster::LineCap::Butt,
crate::render::raster::LineJoin::Miter,
Fixed::from_int(4),
&[],
)?;
}
DrawCommand::Arc {
center,
transform,
radius,
start_angle,
end_angle,
color,
width,
opa,
} => {
let mut path = core::mem::take(&mut self.scratch.primitive_path);
path.set_arc(*center, *radius, *start_angle, *end_angle);
let paint = Paint::Color((*color).into());
let physical_projective =
Transform3D::from_affine(self.viewport.as_transform()).compose(projective);
let result = self.stroke_commands_projective(
path.commands(),
transform,
&physical_projective,
self.viewport.rect_to_physical(*clip),
*width,
&paint,
*opa,
crate::render::raster::LineCap::Butt,
crate::render::raster::LineJoin::Miter,
Fixed::from_int(4),
&[],
);
self.scratch.primitive_path = path;
result?;
}
DrawCommand::PushClip {
path, fill_rule, ..
} => {
let physical =
Transform3D::from_affine(self.viewport.as_transform()).compose(&logical);
self.push_clip_projective(path, &physical, *fill_rule)?;
}
DrawCommand::PopClip => self.pop_clip(),
DrawCommand::ApplyBlur { .. } => return Err(ProjectiveDrawError::Unsupported),
}
Ok(())
}
pub(crate) fn preflight_projective(
&self,
command: &DrawCommand,
_clip: &Rect,
projective: &Transform3D,
) -> Result<(), ProjectiveDrawError> {
if projective.is_identity() {
return Ok(());
}
let logical = projective.compose(&Transform3D::from_affine(command.transform()));
if logical.inverse().is_none() {
return Err(ProjectiveDrawError::InvalidProjection);
}
match command {
DrawCommand::FillPath {
path,
paint: paint @ (Paint::LinearGradient(_) | Paint::RadialGradient(_)),
..
} => {
let Some(bbox) = path.bbox() else {
return Err(ProjectiveDrawError::InvalidProjection);
};
if crate::render::paint::ProjectiveGradientPaint::new(paint, logical, bbox)
.is_none()
{
return Err(ProjectiveDrawError::InvalidProjection);
}
}
DrawCommand::StrokePath {
path,
paint: paint @ (Paint::LinearGradient(_) | Paint::RadialGradient(_)),
width,
..
} => {
let Some(bbox) = path.bbox() else {
return Err(ProjectiveDrawError::InvalidProjection);
};
let half = *width / 2;
let bbox = Rect::new(
bbox.x - half,
bbox.y - half,
bbox.w + *width,
bbox.h + *width,
);
if crate::render::paint::ProjectiveGradientPaint::new(paint, logical, bbox)
.is_none()
{
return Err(ProjectiveDrawError::InvalidProjection);
}
}
_ => {}
}
let supported = match command {
DrawCommand::Fill { .. }
| DrawCommand::Border { .. }
| DrawCommand::GlyphRun { .. }
| DrawCommand::PosedGlyphRun { .. }
| DrawCommand::PushClip { .. }
| DrawCommand::PopClip => true,
DrawCommand::Blit { .. } => true,
DrawCommand::FillPath { .. } => true,
DrawCommand::Line { .. } => true,
DrawCommand::Arc { .. } => true,
DrawCommand::StrokePath { .. } => true,
DrawCommand::ApplyBlur { .. } => false,
};
if !supported {
return Err(ProjectiveDrawError::Unsupported);
}
let valid_geometry = match command {
DrawCommand::Fill { area, .. } | DrawCommand::Border { area, .. } => {
logical.apply_rect(*area).is_some()
}
DrawCommand::Blit { pos, size, .. } => logical
.apply_rect(Rect::new(pos.x, pos.y, size.x, size.y))
.is_some(),
DrawCommand::PosedGlyphRun {
pos,
glyphs,
font,
transform,
..
} => glyphs
.ink_bounds(font, *pos, *transform, self.viewport.scale())
.is_none_or(|bounds| projective.apply_rect(bounds).is_some()),
DrawCommand::GlyphRun {
pos,
transform,
glyphs,
font,
..
} => {
let physical =
Transform3D::from_affine(self.viewport.as_transform()).compose(&logical);
let output_ppem = crate::render::font::output_ppem(
font.size.max(1),
physical.raster_scale_at(*pos),
);
font.glyph_run_ink_bounds(glyphs, *pos, *transform, output_ppem)
.is_none_or(|bounds| projective.apply_rect(bounds).is_some())
}
DrawCommand::FillPath { path, .. }
| DrawCommand::StrokePath { path, .. }
| DrawCommand::PushClip { path, .. } => path
.bbox()
.is_none_or(|bounds| logical.apply_rect(bounds).is_some()),
DrawCommand::Line { p1, p2, .. } => {
logical.apply_point(*p1).is_some() && logical.apply_point(*p2).is_some()
}
DrawCommand::Arc { center, radius, .. } => logical
.apply_rect(Rect::new(
center.x - *radius,
center.y - *radius,
*radius * 2,
*radius * 2,
))
.is_some(),
DrawCommand::PopClip => true,
DrawCommand::ApplyBlur { .. } => true,
};
if !valid_geometry {
return Err(ProjectiveDrawError::InvalidProjection);
}
Ok(())
}
fn flush(&mut self) {
Canvas::flush(self);
}
fn supports_offscreen(&self) -> bool {
true
}
fn offscreen_format(&self) -> Option<crate::render::texture::ColorFormat> {
Some(self.target.format)
}
fn sample_target_region(
&self,
src: &Rect,
) -> Result<Option<crate::render::texture::Texture<'static>>, RenderError> {
let (sx0, sy0, sx1, sy1) = self.viewport.rect_to_physical_pixel_bounds(*src);
if sx0 >= i32::from(self.target.width)
|| sy0 >= i32::from(self.target.height)
|| sx1 <= 0
|| sy1 <= 0
{
return Ok(None);
}
let w = u16::try_from((sx1 - sx0).max(1)).map_err(|_| RenderError::InvalidGeometry)?;
let h = u16::try_from((sy1 - sy0).max(1)).map_err(|_| RenderError::InvalidGeometry)?;
let mut tex = crate::render::texture::Texture::owned(w, h, self.target.format);
self.read_target_region(src, &mut tex)?;
Ok(Some(tex))
}
fn modify_target_region(
&mut self,
src: &Rect,
f: &mut dyn FnMut(&mut crate::render::texture::Texture) -> Result<(), RenderError>,
) -> Result<bool, RenderError> {
use crate::render::texture::{TexBuf, Texture};
if !self.target.valid_storage()
|| matches!(&self.target.buf, crate::render::texture::TexBuf::Ref(_))
{
return Err(RenderError::InvalidTexture);
}
let (sx0, sy0, sx1, sy1) = self.viewport.rect_to_physical_pixel_bounds(*src);
let target_w = self.target.width as i32;
let target_h = self.target.height as i32;
let cx0 = sx0.max(0);
let cy0 = sy0.max(0);
let cx1 = sx1.min(target_w);
let cy1 = sy1.min(target_h);
if cx1 <= cx0 || cy1 <= cy0 {
return Ok(false);
}
let bpp = self.target.format.bytes_per_pixel();
let target_stride = self.target.stride;
let off_start = cy0 as usize * target_stride + cx0 as usize * bpp;
let row_w = (cx1 - cx0) as u16;
let row_h = (cy1 - cy0) as u16;
let view_bytes = (row_h as usize - 1) * target_stride + row_w as usize * bpp;
let buf = &mut self.target.buf.as_mut_slice()[off_start..off_start + view_bytes];
let mut view = Texture {
buf: TexBuf::Mut(buf),
width: row_w,
height: row_h,
format: self.target.format,
stride: target_stride,
alpha_mode: self.target.alpha_mode,
cache_revision: self.target.cache_revision,
transient: self.target.transient,
};
f(&mut view)?;
Ok(true)
}
fn supports_scroll_blit(&self) -> bool {
true
}
fn scroll_target_region(
&mut self,
area: &Rect,
dx: Fixed,
dy: Fixed,
) -> Result<(), RenderError> {
let Some(area) = self.viewport.physical_rect(*area) else {
return Ok(());
};
let scale = self.viewport.scale();
let dx_phys = (dx * scale).trunc_to_int();
let dy_phys = (dy * scale).trunc_to_int();
crate::surface::mirror::texture_scroll_in_place(&mut self.target, area, dx_phys, dy_phys);
Ok(())
}
fn read_target_region(
&self,
src: &Rect,
dst: &mut crate::render::texture::Texture,
) -> Result<(), RenderError> {
if !dst.valid_storage() || matches!(&dst.buf, crate::render::texture::TexBuf::Ref(_)) {
return Err(RenderError::InvalidTexture);
}
let (sx0, sy0, sx1, sy1) = self.viewport.rect_to_physical_pixel_bounds(*src);
let target_w = self.target.width as i32;
let target_h = self.target.height as i32;
let copy_w = ((sx1 - sx0).min(dst.width as i32)).max(0);
let copy_h = ((sy1 - sy0).min(dst.height as i32)).max(0);
if copy_w == 0 || copy_h == 0 {
return Ok(());
}
if self.target.format == dst.format {
let bpp = dst.format.bytes_per_pixel();
let target_buf = self.target.buf.as_slice();
let dst_buf = dst.buf.as_mut_slice();
let target_stride = self.target.stride;
let dst_stride = dst.stride;
for dy in 0..copy_h as usize {
let phys_y = sy0 + dy as i32;
if phys_y < 0 || phys_y >= target_h {
continue;
}
let src_x_start = sx0.max(0);
let src_x_end = (sx0 + copy_w).min(target_w);
if src_x_end <= src_x_start {
continue;
}
let dst_x_start = (src_x_start - sx0) as usize;
let row_bytes = (src_x_end - src_x_start) as usize * bpp;
let src_row_off = phys_y as usize * target_stride + src_x_start as usize * bpp;
let dst_row_off = dy * dst_stride + dst_x_start * bpp;
dst_buf[dst_row_off..dst_row_off + row_bytes]
.copy_from_slice(&target_buf[src_row_off..src_row_off + row_bytes]);
}
return Ok(());
}
for dy in 0..copy_h {
for dx in 0..copy_w {
let phys_x = sx0 + dx;
let phys_y = sy0 + dy;
if phys_x < 0 || phys_y < 0 || phys_x >= target_w || phys_y >= target_h {
continue;
}
let px = self.target.get_pixel(phys_x, phys_y);
dst.set_pixel(dx, dy, &px);
}
}
Ok(())
}
}
impl Renderer for SwRenderer<'_> {
fn route(&self, request: &DrawRequest<'_, '_>) -> Result<RenderRoute, RenderError> {
SwRenderer::route(self, request)
}
fn submit(&mut self, request: &DrawRequest<'_, '_>) -> Result<(), RenderError> {
SwRenderer::submit(self, request)
}
fn flush(&mut self) {
SwRenderer::flush(self)
}
fn output_scale(&self) -> Fixed {
SwRenderer::output_scale(self)
}
fn plan_scope(&self, bounds: &Rect) -> Result<FallbackRegion, RenderError> {
SwRenderer::plan_scope(self, bounds)
}
fn supports_offscreen(&self) -> bool {
SwRenderer::supports_offscreen(self)
}
fn offscreen_format(&self) -> Option<crate::render::texture::ColorFormat> {
SwRenderer::offscreen_format(self)
}
fn sample_target_region(&self, src: &Rect) -> Result<Option<Texture<'static>>, RenderError> {
SwRenderer::sample_target_region(self, src)
}
fn modify_target_region(
&mut self,
src: &Rect,
f: &mut dyn FnMut(&mut Texture) -> Result<(), RenderError>,
) -> Result<bool, RenderError> {
SwRenderer::modify_target_region(self, src, f)
}
fn supports_scroll_blit(&self) -> bool {
SwRenderer::supports_scroll_blit(self)
}
fn scroll_target_region(
&mut self,
area: &Rect,
dx: Fixed,
dy: Fixed,
) -> Result<(), RenderError> {
SwRenderer::scroll_target_region(self, area, dx, dy)
}
fn read_target_region(&self, src: &Rect, dst: &mut Texture) -> Result<(), RenderError> {
SwRenderer::read_target_region(self, src, dst)
}
}
#[cfg(test)]
mod tests {
use super::blit_fast::{blit_1to1_fast, blit_2to2_fast, blit_dda, blit_generic_slow};
use super::*;
use crate::render::RenderResource;
use crate::render::texture::ColorFormat;
use alloc::vec;
#[test]
fn checked_plain_blit_rejects_short_texture_before_fast_path() {
let mut renderer = SwRenderer::new(Texture::owned(4, 4, ColorFormat::RGBA8888));
let short = Texture::from_ref(&[0u8; 1], 2, 2, ColorFormat::RGBA8888);
let command = DrawCommand::Blit {
pos: Point::ZERO,
size: Point::new(2, 2),
transform: Transform::IDENTITY,
quad: None,
texture: &short,
opa: 255,
radius: Fixed::ZERO,
composite: CompositeMode::SourceOver,
};
let request = DrawRequest::new(&command, Rect::new(0, 0, 4, 4));
assert_eq!(renderer.route(&request), Err(RenderError::InvalidTexture));
assert_eq!(renderer.submit(&request), Err(RenderError::InvalidTexture));
}
#[test]
fn route_rejects_software_draws_that_drop_requested_semantics() {
let mut renderer = SwRenderer::new(Texture::owned(16, 16, ColorFormat::RGBA8888));
let clip = Rect::new(0, 0, 16, 16);
let affine = Transform::rotate_deg(Fixed::from_int(20));
let fill = DrawCommand::Fill {
area: clip,
transform: affine,
quad: None,
color: Color::rgb(20, 30, 40),
radius: Fixed::from_int(3),
opa: 255,
};
assert_eq!(
renderer.route(&DrawRequest::new(&fill, clip)),
Err(RenderError::Unsupported(RenderFeature::RoundedFill))
);
let texture = Texture::owned(2, 2, ColorFormat::RGBA8888);
let blit = DrawCommand::Blit {
pos: Point::ZERO,
size: Point::new(2, 2),
transform: affine,
quad: None,
texture: &texture,
opa: 128,
radius: Fixed::ZERO,
composite: CompositeMode::SourceOver,
};
assert_eq!(
renderer.route(&DrawRequest::new(&blit, clip)),
Err(RenderError::Unsupported(RenderFeature::BlitOpacity))
);
let blur = DrawCommand::ApplyBlur {
alpha: Fixed::from_ratio(1, 2),
region: clip,
};
let projective =
Transform3D::rotate_y_perspective(Fixed::from_int(18), Fixed::from_int(400));
assert_eq!(
renderer.route(&DrawRequest::new(&blur, clip).with_projective(projective)),
Err(RenderError::Unsupported(RenderFeature::ProjectiveGeometry))
);
assert_eq!(
renderer.submit(&DrawRequest::new(&blur, clip).with_projective(projective)),
Err(RenderError::Unsupported(RenderFeature::ProjectiveGeometry))
);
}
#[test]
fn submit_draws_plain_commands_and_checks_exceptional_semantics() {
let mut renderer = SwRenderer::new(Texture::owned(16, 16, ColorFormat::RGBA8888));
let clip = Rect::new(0, 0, 16, 16);
let fill = DrawCommand::Fill {
area: Rect::new(2, 3, 4, 5),
transform: Transform::IDENTITY,
quad: None,
color: Color::rgb(20, 30, 40),
radius: Fixed::ZERO,
opa: 255,
};
renderer.submit(&DrawRequest::new(&fill, clip)).unwrap();
assert_eq!(renderer.target.get_pixel(3, 4), Color::rgb(20, 30, 40));
let unsupported = DrawCommand::Fill {
area: clip,
transform: Transform::rotate_deg(Fixed::from_int(20)),
quad: None,
color: Color::rgb(200, 10, 10),
radius: Fixed::from_int(3),
opa: 255,
};
assert_eq!(
renderer.submit(&DrawRequest::new(&unsupported, clip)),
Err(RenderError::Unsupported(RenderFeature::RoundedFill))
);
assert_eq!(renderer.target.get_pixel(3, 4), Color::rgb(20, 30, 40));
}
#[test]
fn blit_at_negative_x_does_not_wrap_into_wrong_rows() {
let mut tgt_buf = vec![0u8; 64 * 64 * 4];
let tgt_tex = Texture::new(&mut tgt_buf, 64, 64, ColorFormat::RGBA8888);
let mut backend = SwRenderer::new(tgt_tex);
let mut src_buf = vec![0u8; 32 * 32 * 4];
for px in src_buf.chunks_exact_mut(4) {
px[0] = 200;
px[1] = 100;
px[2] = 50;
px[3] = 255;
}
let src_tex = Texture::new(&mut src_buf, 32, 32, ColorFormat::RGBA8888);
let src_rect = Rect::new(0, 0, 32, 32);
let dst = Point::new(Fixed::from_int(-24), Fixed::from_int(8));
let dst_size = Point::new(Fixed::from_int(32), Fixed::from_int(32));
let clip = Rect {
x: Fixed::from_int(-24),
y: Fixed::from_int(0),
w: Fixed::from_int(80),
h: Fixed::from_int(64),
};
backend.blit(
&src_tex,
&src_rect,
dst,
dst_size,
&clip,
255,
Fixed::ZERO,
CompositeMode::SourceOver,
);
for y in 8..40 {
for x in 0..8 {
let p = backend.target.get_pixel(x, y);
assert_eq!(
(p.r, p.g, p.b),
(200, 100, 50),
"visible pixel ({},{}) should be source colour",
x,
y
);
}
}
for y in 0..64 {
for x in 0..64 {
let in_visible = (8..40).contains(&y) && x < 8;
if in_visible {
continue;
}
let p = backend.target.get_pixel(x, y);
assert_eq!(
(p.r, p.g, p.b, p.a),
(0, 0, 0, 0),
"out-of-visible pixel ({},{}) was written",
x,
y
);
}
}
}
#[test]
fn blit_at_negative_x_rgb565_swapped_does_not_wrap() {
let mut tgt_buf = vec![0u8; 64 * 64 * 2];
let tgt_tex = Texture::new(&mut tgt_buf, 64, 64, ColorFormat::RGB565Swapped);
let mut backend = SwRenderer::new(tgt_tex);
let mut src_buf = vec![0u8; 32 * 32 * 2];
for px in src_buf.chunks_exact_mut(2) {
px[0] = 0xF8;
px[1] = 0x00;
}
let src_tex = Texture::new(&mut src_buf, 32, 32, ColorFormat::RGB565Swapped);
let src_rect = Rect::new(0, 0, 32, 32);
let dst = Point::new(Fixed::from_int(-24), Fixed::from_int(8));
let dst_size = Point::new(Fixed::from_int(32), Fixed::from_int(32));
let clip = Rect {
x: Fixed::from_int(-24),
y: Fixed::from_int(0),
w: Fixed::from_int(80),
h: Fixed::from_int(64),
};
backend.blit(
&src_tex,
&src_rect,
dst,
dst_size,
&clip,
255,
Fixed::ZERO,
CompositeMode::SourceOver,
);
for y in 8..40 {
for x in 0..8 {
let p = backend.target.get_pixel(x, y);
assert_eq!(p.r, 255, "visible pixel ({},{}) should be red", x, y);
}
}
for y in 0..64 {
for x in 0..64 {
let in_visible = (8..40).contains(&y) && x < 8;
if in_visible {
continue;
}
let off = (y as usize * 64 + x as usize) * 2;
assert_eq!(
(
backend.target.buf.as_slice()[off],
backend.target.buf.as_slice()[off + 1]
),
(0, 0),
"out-of-visible pixel ({},{}) was written",
x,
y
);
}
}
}
#[test]
fn fill_rect_basic() {
let mut buf = vec![0u8; 16 * 16 * 4];
let tex = Texture::new(&mut buf, 16, 16, ColorFormat::RGBA8888);
let mut backend = SwRenderer::new(tex);
let rect = Rect::new(2, 2, 4, 4);
let clip = Rect::new(0, 0, 16, 16);
backend.fill_rect(&rect, &clip, &Color::rgb(255, 0, 0), Fixed::ZERO, 255);
let c = backend.target.get_pixel(3, 3);
assert_eq!(c.r, 255);
assert_eq!(c.g, 0);
assert_eq!(c.b, 0);
let c = backend.target.get_pixel(0, 0);
assert_eq!(c.r, 0);
}
#[test]
fn clear_fills_area() {
let mut buf = vec![0u8; 8 * 8 * 4];
let tex = Texture::new(&mut buf, 8, 8, ColorFormat::RGBA8888);
let mut backend = SwRenderer::new(tex);
backend.clear(&Rect::new(0, 0, 8, 8), &Color::rgb(50, 100, 150));
let c = backend.target.get_pixel(4, 4);
assert_eq!(c.r, 50);
assert_eq!(c.g, 100);
assert_eq!(c.b, 150);
}
#[test]
fn fill_path_rect_matches_fill_rect() {
let mut buf = vec![0u8; 16 * 16 * 4];
let tex = Texture::new(&mut buf, 16, 16, ColorFormat::RGBA8888);
let mut backend = SwRenderer::new(tex);
let path = crate::render::path::Path::rect(
Fixed::from_int(2),
Fixed::from_int(2),
Fixed::from_int(8),
Fixed::from_int(8),
);
let clip = Rect::new(0, 0, 16, 16);
let paint = Paint::Color(Color::rgb(0, 0, 255).into());
backend.fill_path(
&path,
&clip,
&paint,
255,
crate::render::raster::FillRule::EvenOdd,
);
let c = backend.target.get_pixel(5, 5);
assert_eq!(c.b, 255);
assert_eq!(c.r, 0);
let c = backend.target.get_pixel(0, 0);
assert_eq!(c.b, 0);
}
#[test]
fn fill_path_empty_is_noop() {
let mut buf = vec![0u8; 4 * 4 * 4];
let tex = Texture::new(&mut buf, 4, 4, ColorFormat::RGBA8888);
let mut backend = SwRenderer::new(tex);
let path = crate::render::path::Path::new();
let clip = Rect::new(0, 0, 4, 4);
let paint = Paint::Color(Color::rgb(255, 255, 255).into());
backend.fill_path(
&path,
&clip,
&paint,
255,
crate::render::raster::FillRule::EvenOdd,
);
for y in 0..4 {
for x in 0..4 {
assert_eq!(backend.target.get_pixel(x, y).r, 0);
}
}
}
#[test]
fn fill_path_zero_opa_is_noop() {
let mut buf = vec![0u8; 4 * 4 * 4];
let tex = Texture::new(&mut buf, 4, 4, ColorFormat::RGBA8888);
let mut backend = SwRenderer::new(tex);
let path = crate::render::path::Path::rect(
Fixed::ZERO,
Fixed::ZERO,
Fixed::from_int(4),
Fixed::from_int(4),
);
let clip = Rect::new(0, 0, 4, 4);
let paint = Paint::Color(Color::rgb(255, 0, 0).into());
backend.fill_path(
&path,
&clip,
&paint,
0,
crate::render::raster::FillRule::EvenOdd,
);
assert_eq!(backend.target.get_pixel(2, 2).r, 0);
}
#[test]
fn fill_path_triangle_interior_vs_exterior() {
let mut buf = vec![0u8; 16 * 16 * 4];
let tex = Texture::new(&mut buf, 16, 16, ColorFormat::RGBA8888);
let mut backend = SwRenderer::new(tex);
let mut path = crate::render::path::Path::new();
path.move_to(Point {
x: Fixed::ZERO,
y: Fixed::ZERO,
})
.line_to(Point {
x: Fixed::from_int(10),
y: Fixed::ZERO,
})
.line_to(Point {
x: Fixed::ZERO,
y: Fixed::from_int(10),
})
.close();
let clip = Rect::new(0, 0, 16, 16);
let paint = Paint::Color(Color::rgb(0, 200, 0).into());
backend.fill_path(
&path,
&clip,
&paint,
255,
crate::render::raster::FillRule::EvenOdd,
);
assert_eq!(backend.target.get_pixel(2, 2).g, 200);
assert_eq!(backend.target.get_pixel(8, 8).g, 0);
}
#[test]
fn draw_glyph_run_is_reachable_via_trait() {
let mut buf = vec![0u8; 32 * 16 * 4];
let tex = Texture::new(&mut buf, 32, 16, ColorFormat::RGBA8888);
let mut backend = SwRenderer::new(tex);
let pos = Point {
x: Fixed::from_int(1),
y: Fixed::from_int(1),
};
let clip = Rect::new(0, 0, 32, 16);
let font = crate::render::font::Font::bitmap_8x8();
let glyphs = [textflow::shaping::PositionedGlyph::new(
crate::render::font::GlyphId::new(65),
textflow::shaping::FlowPoint { x: 0, y: 7 << 8 },
)];
Canvas::draw_glyph_run(
&mut backend,
&pos,
&glyphs,
&font,
&clip,
&Color::rgb(255, 0, 0),
255,
);
let mut found = false;
for y in 0..16 {
for x in 0..32 {
if backend.target.get_pixel(x, y).r > 0 {
found = true;
break;
}
}
}
assert!(found, "expected at least one red pixel from glyph");
}
#[test]
fn renderer_routes_affine_glyph_runs() {
let mut buf = vec![0u8; 16 * 16 * 4];
let tex = Texture::new(&mut buf, 16, 16, ColorFormat::RGBA8888);
let mut renderer = SwRenderer::new(tex);
let font = crate::render::font::Font::bitmap_8x8();
let glyphs = [textflow::shaping::PositionedGlyph::new(
crate::render::font::GlyphId::new(u16::from(b'A')),
textflow::shaping::FlowPoint { x: 0, y: 7 << 8 },
)];
let transform = Transform::translate(Fixed::from_int(12), Fixed::from_int(1))
.compose(&Transform::rotate_deg(Fixed::from_int(90)));
renderer.draw(
&DrawCommand::GlyphRun {
pos: Point::ZERO,
transform,
glyphs: &glyphs,
font: &font,
color: Color::rgb(255, 0, 0),
opa: 255,
},
&Rect::new(0, 0, 16, 16),
);
assert!(buf.chunks_exact(4).any(|pixel| pixel[0] != 0));
}
#[test]
fn stroke_path_line_colors_interior_and_skips_far_pixels() {
let mut buf = vec![0u8; 16 * 16 * 4];
let tex = Texture::new(&mut buf, 16, 16, ColorFormat::RGBA8888);
let mut backend = SwRenderer::new(tex);
let mut path = crate::render::path::Path::new();
path.move_to(Point {
x: Fixed::from_int(2),
y: Fixed::from_int(8),
})
.line_to(Point {
x: Fixed::from_int(14),
y: Fixed::from_int(8),
});
let clip = Rect::new(0, 0, 16, 16);
let paint = Paint::Color(Color::rgb(255, 0, 0).into());
backend.stroke_path(
&path,
&clip,
Fixed::from_int(2),
&paint,
255,
crate::render::raster::LineCap::Butt,
crate::render::raster::LineJoin::Miter,
Fixed::from_int(4),
&[],
);
assert!(backend.target.get_pixel(8, 8).r > 0);
assert_eq!(backend.target.get_pixel(8, 0).r, 0);
assert_eq!(backend.target.get_pixel(8, 15).r, 0);
}
#[test]
fn renderer_dispatches_line_command() {
let mut buf = vec![0u8; 16 * 16 * 4];
let tex = Texture::new(&mut buf, 16, 16, ColorFormat::RGBA8888);
let mut backend = SwRenderer::new(tex);
let cmd = DrawCommand::Line {
p1: Point {
x: Fixed::from_int(2),
y: Fixed::from_int(8),
},
p2: Point {
x: Fixed::from_int(14),
y: Fixed::from_int(8),
},
transform: crate::types::Transform::IDENTITY,
color: Color::rgb(255, 0, 0),
width: Fixed::from_int(2),
opa: 255,
};
let clip = Rect::new(0, 0, 16, 16);
backend.submit(&DrawRequest::new(&cmd, clip)).unwrap();
assert!(backend.target.get_pixel(8, 8).r > 0);
}
#[test]
fn renderer_dispatches_arc_command() {
let mut buf = vec![0u8; 32 * 32 * 4];
let tex = Texture::new(&mut buf, 32, 32, ColorFormat::RGBA8888);
let mut backend = SwRenderer::new(tex);
let cmd = DrawCommand::Arc {
center: Point {
x: Fixed::from_int(16),
y: Fixed::from_int(16),
},
transform: crate::types::Transform::IDENTITY,
radius: Fixed::from_int(10),
start_angle: Fixed::from_int(0),
end_angle: Fixed::from_int(90),
color: Color::rgb(0, 255, 0),
width: Fixed::from_int(2),
opa: 255,
};
let clip = Rect::new(0, 0, 32, 32);
backend.submit(&DrawRequest::new(&cmd, clip)).unwrap();
let hit = backend.target.get_pixel(26, 16).g > 0 || backend.target.get_pixel(25, 16).g > 0;
assert!(hit);
}
#[test]
fn draw_line_default_impl_strokes_pixels() {
let mut buf = vec![0u8; 16 * 16 * 4];
let tex = Texture::new(&mut buf, 16, 16, ColorFormat::RGBA8888);
let mut backend = SwRenderer::new(tex);
let p1 = Point {
x: Fixed::from_int(2),
y: Fixed::from_int(8),
};
let p2 = Point {
x: Fixed::from_int(14),
y: Fixed::from_int(8),
};
let clip = Rect::new(0, 0, 16, 16);
backend.draw_line(
p1,
p2,
&clip,
Fixed::from_int(2),
&Color::rgb(255, 0, 0),
255,
);
assert!(backend.target.get_pixel(8, 8).r > 0);
}
#[test]
fn draw_arc_default_impl_strokes_pixels() {
let mut buf = vec![0u8; 32 * 32 * 4];
let tex = Texture::new(&mut buf, 32, 32, ColorFormat::RGBA8888);
let mut backend = SwRenderer::new(tex);
let center = Point {
x: Fixed::from_int(16),
y: Fixed::from_int(16),
};
let clip = Rect::new(0, 0, 32, 32);
backend.draw_arc(
center,
Fixed::from_int(10),
Fixed::from_int(0),
Fixed::from_int(90),
&clip,
Fixed::from_int(2),
&Color::rgb(0, 255, 0),
255,
);
assert!(backend.target.get_pixel(26, 16).g > 0 || backend.target.get_pixel(25, 16).g > 0);
}
#[test]
fn stroke_path_zero_width_is_noop() {
let mut buf = vec![0u8; 8 * 8 * 4];
let tex = Texture::new(&mut buf, 8, 8, ColorFormat::RGBA8888);
let mut backend = SwRenderer::new(tex);
let mut path = crate::render::path::Path::new();
path.move_to(Point {
x: Fixed::ZERO,
y: Fixed::ZERO,
})
.line_to(Point {
x: Fixed::from_int(8),
y: Fixed::ZERO,
});
let clip = Rect::new(0, 0, 8, 8);
let paint = Paint::Color(Color::rgb(255, 0, 0).into());
backend.stroke_path(
&path,
&clip,
Fixed::ZERO,
&paint,
255,
crate::render::raster::LineCap::Butt,
crate::render::raster::LineJoin::Miter,
Fixed::from_int(4),
&[],
);
for y in 0..8 {
for x in 0..8 {
assert_eq!(backend.target.get_pixel(x, y).r, 0);
}
}
}
#[test]
fn painter_fill_rect_with_backend() {
use crate::render::painter::Painter;
let mut buf = vec![0u8; 16 * 16 * 4];
let tex = Texture::new(&mut buf, 16, 16, ColorFormat::RGBA8888);
let mut backend = SwRenderer::new(tex);
{
let mut painter = Painter::new(&mut backend);
let rect = Rect::new(1, 1, 6, 6);
let clip = Rect::new(0, 0, 16, 16);
painter.fill_rect(&rect, &clip, &Color::rgb(0, 255, 0), Fixed::ZERO, 255);
}
let c = backend.target.get_pixel(3, 3);
assert_eq!(c.r, 0);
assert_eq!(c.g, 255);
assert_eq!(c.b, 0);
}
#[test]
fn painter_forwards_path_and_stroke_methods() {
use crate::render::painter::Painter;
use crate::render::path::Path;
let mut buf = vec![0u8; 32 * 32 * 4];
let tex = Texture::new(&mut buf, 32, 32, ColorFormat::RGBA8888);
let mut backend = SwRenderer::new(tex);
let clip = Rect::new(0, 0, 32, 32);
{
let mut painter = Painter::new(&mut backend);
let path = Path::rect(
Fixed::from_int(4),
Fixed::from_int(4),
Fixed::from_int(10),
Fixed::from_int(10),
);
let paint = Paint::Color(Color::rgb(255, 0, 0).into());
painter.fill_path(
&path,
&clip,
&paint,
255,
crate::render::raster::FillRule::EvenOdd,
);
painter.draw_line(
Point {
x: Fixed::from_int(20),
y: Fixed::from_int(20),
},
Point {
x: Fixed::from_int(28),
y: Fixed::from_int(28),
},
&clip,
Fixed::from_int(2),
&Color::rgb(0, 255, 0),
255,
);
painter.draw_arc(
Point {
x: Fixed::from_int(24),
y: Fixed::from_int(8),
},
Fixed::from_int(4),
Fixed::from_int(0),
Fixed::from_int(90),
&clip,
Fixed::from_int(2),
&Color::rgb(0, 0, 255),
255,
);
}
assert_eq!(backend.target.get_pixel(8, 8).r, 255);
assert!(backend.target.get_pixel(24, 24).g > 0);
assert!(
backend.target.get_pixel(28, 8).b > 0
|| backend.target.get_pixel(27, 8).b > 0
|| backend.target.get_pixel(28, 9).b > 0,
);
}
#[test]
fn painter_draw_glyph_run_forwards_to_backend() {
use crate::render::painter::Painter;
let mut buf = vec![0u8; 32 * 16 * 4];
let tex = Texture::new(&mut buf, 32, 16, ColorFormat::RGBA8888);
let mut backend = SwRenderer::new(tex);
let clip = Rect::new(0, 0, 32, 16);
{
let font = crate::render::font::Font::bitmap_8x8();
let mut painter = Painter::new(&mut backend);
let glyphs = [textflow::shaping::PositionedGlyph::new(
crate::render::font::GlyphId::new(66),
textflow::shaping::FlowPoint { x: 0, y: 7 << 8 },
)];
painter.draw_glyph_run(
&Point {
x: Fixed::from_int(1),
y: Fixed::from_int(1),
},
&glyphs,
&font,
&clip,
&Color::rgb(200, 100, 50),
255,
);
}
let mut found = false;
for y in 0..16 {
for x in 0..32 {
if backend.target.get_pixel(x, y).r > 0 {
found = true;
break;
}
}
}
assert!(found);
}
#[test]
fn blit_dda_matches_generic_slow() {
let mut src_buf = vec![0u8; 4 * 4 * 4];
for y in 0..4 {
for x in 0..4 {
let i = (y * 4 + x) * 4;
src_buf[i] = (y * 4 + x) as u8 * 16 + 1;
src_buf[i + 3] = 255;
}
}
let src = Texture::new(&mut src_buf, 4, 4, ColorFormat::RGBA8888);
let mut dst_a = vec![0u8; 8 * 6 * 4];
let mut dst_b = vec![0u8; 8 * 6 * 4];
{
let mut tex_a = Texture::new(&mut dst_a, 8, 6, ColorFormat::RGBA8888);
blit_generic_slow(&mut tex_a, &src, 0, 0, 4, 4, 0, 0, 7, 5, 0, 0, 8, 6, 255);
}
{
let mut tex_b = Texture::new(&mut dst_b, 8, 6, ColorFormat::RGBA8888);
blit_dda(
&mut tex_b, &src, 0, 0, 4, 4, 0, 0, 7, 5, 0, 0, 8, 6, 255, None,
);
}
assert_eq!(dst_a, dst_b, "dda sampling diverged from divide path");
}
#[test]
fn blit_1to1_matches_generic_for_argb_to_argb() {
let mut src_buf = vec![0u8; 4 * 4 * 4];
for i in 0..16 {
src_buf[i * 4] = 30 + i as u8;
src_buf[i * 4 + 1] = 60 + i as u8;
src_buf[i * 4 + 2] = 90 + i as u8;
src_buf[i * 4 + 3] = match i {
0 => 0,
3 | 7 => 128,
_ => 255,
};
}
let src = Texture::new(&mut src_buf, 4, 4, ColorFormat::RGBA8888);
let mut dst_a = vec![0u8; 6 * 6 * 4];
for (i, byte) in dst_a.iter_mut().enumerate() {
*byte = (i * 3) as u8;
}
let mut dst_b = dst_a.clone();
{
let mut tex = Texture::new(&mut dst_a, 6, 6, ColorFormat::RGBA8888);
blit_generic_slow(&mut tex, &src, 0, 0, 4, 4, 1, 1, 4, 4, 0, 0, 6, 6, 255);
}
{
let mut tex = Texture::new(&mut dst_b, 6, 6, ColorFormat::RGBA8888);
blit_1to1_fast(&mut tex, &src, 0, 0, 4, 4, 1, 1, 0, 0, 6, 6);
}
for (i, (&a, &b)) in dst_a.iter().zip(dst_b.iter()).enumerate() {
assert!(
(a as i32 - b as i32).abs() <= 1,
"byte {} diverged by more than 1: slow={} fast={}",
i,
a,
b
);
}
}
#[test]
fn blit_1to1_matches_generic_for_argb_to_565sw() {
let mut src_buf = vec![0u8; 4 * 4 * 4];
for i in 0..16 {
src_buf[i * 4] = 30 + i as u8 * 5;
src_buf[i * 4 + 1] = 40 + i as u8 * 3;
src_buf[i * 4 + 2] = 50 + i as u8 * 7;
src_buf[i * 4 + 3] = if i == 0 { 0 } else { 255 };
}
let src = Texture::new(&mut src_buf, 4, 4, ColorFormat::RGBA8888);
let mut dst_a = vec![0u8; 6 * 6 * 2];
for i in 0..dst_a.len() {
dst_a[i] = (i * 5) as u8;
}
let mut dst_b = dst_a.clone();
{
let mut tex = Texture::new(&mut dst_a, 6, 6, ColorFormat::RGB565Swapped);
blit_generic_slow(&mut tex, &src, 0, 0, 4, 4, 1, 1, 4, 4, 0, 0, 6, 6, 255);
}
{
let mut tex = Texture::new(&mut dst_b, 6, 6, ColorFormat::RGB565Swapped);
blit_1to1_fast(&mut tex, &src, 0, 0, 4, 4, 1, 1, 0, 0, 6, 6);
}
assert_eq!(dst_a, dst_b);
}
#[test]
fn blit_2to2_565sw_matches_dda() {
let mut src_buf = vec![0u8; 3 * 3 * 2];
for i in 0..9 {
src_buf[i * 2] = 0x12 + i as u8;
src_buf[i * 2 + 1] = 0x34 + i as u8;
}
let src = Texture::new(&mut src_buf, 3, 3, ColorFormat::RGB565Swapped);
let mut dst_a = vec![0u8; 10 * 10 * 2];
let mut dst_b = vec![0u8; 10 * 10 * 2];
{
let mut tex = Texture::new(&mut dst_a, 10, 10, ColorFormat::RGB565Swapped);
blit_dda(
&mut tex, &src, 0, 0, 3, 3, 1, 1, 6, 6, 0, 0, 10, 10, 255, None,
);
}
{
let mut tex = Texture::new(&mut dst_b, 10, 10, ColorFormat::RGB565Swapped);
blit_2to2_fast(&mut tex, &src, 0, 0, 3, 3, 1, 1, 0, 0, 10, 10);
}
assert_eq!(dst_a, dst_b);
}
#[test]
fn blit_2to2_odd_clip_falls_back_cleanly() {
let mut src_buf = vec![0u8; 2 * 2 * 2];
src_buf[0] = 0xAA;
src_buf[1] = 0xBB;
let src = Texture::new(&mut src_buf, 2, 2, ColorFormat::RGB565Swapped);
let mut dst = vec![0u8; 6 * 6 * 2];
let mut tex = Texture::new(&mut dst, 6, 6, ColorFormat::RGB565Swapped);
blit_2to2_fast(&mut tex, &src, 0, 0, 2, 2, 0, 0, 1, 0, 6, 6);
assert_eq!(dst[0], 0);
assert_eq!(dst[1], 0);
assert_ne!(dst[2], 0);
}
#[test]
fn fill_rect_transformed_90deg_rotation() {
let mut buf = vec![0u8; 16 * 16 * 4];
let mut dst = Texture::new(&mut buf, 16, 16, ColorFormat::RGBA8888);
let rect = Rect::new(6, 6, 4, 4);
let cx = rect.x + rect.w / Fixed::from_int(2);
let cy = rect.y + rect.h / Fixed::from_int(2);
let tf = Transform::translate(cx, cy)
.compose(&Transform::rotate_deg(Fixed::from_int(90)))
.compose(&Transform::translate(Fixed::ZERO - cx, Fixed::ZERO - cy));
let red = Color::rgb(255, 0, 0);
fill_rect_transformed(&mut dst, rect, Rect::new(0, 0, 16, 16), &tf, &red, 255);
let mut painted = 0;
for y in 0..16 {
for x in 0..16 {
if dst.get_pixel(x, y).r == 255 {
painted += 1;
}
}
}
assert!(
(12..=20).contains(&painted),
"expected ~16 painted pixels, got {}",
painted
);
}
#[test]
fn renderer_transformed_fill_uses_logical_area_under_hidpi() {
let mut buf = vec![0u8; 64 * 64 * 4];
let tex = Texture::new(&mut buf, 64, 64, ColorFormat::RGBA8888);
let mut renderer = SwRenderer::new(tex);
renderer.viewport = Viewport::new(32, 32, Fixed::from_int(2));
renderer.draw(
&DrawCommand::Fill {
area: Rect::new(4, 4, 8, 8),
transform: Transform::scale(Fixed::from_int(2), Fixed::from_int(2)),
quad: None,
color: Color::rgb(255, 0, 0),
radius: Fixed::ZERO,
opa: 255,
},
&Rect::new(0, 0, 32, 32),
);
assert_eq!(renderer.target.get_pixel(20, 20).r, 255);
assert_eq!(renderer.target.get_pixel(10, 10).r, 0);
assert_eq!(renderer.target.get_pixel(50, 50).r, 0);
}
#[test]
fn blit_1to1_with_clip_restricted() {
let mut src_buf = vec![0u8; 4 * 4 * 4];
for i in 0..16 {
src_buf[i * 4] = 100 + i as u8;
src_buf[i * 4 + 3] = 255;
}
let src = Texture::new(&mut src_buf, 4, 4, ColorFormat::RGBA8888);
let mut dst_a = vec![0u8; 8 * 8 * 4];
let mut dst_b = vec![0u8; 8 * 8 * 4];
{
let mut tex = Texture::new(&mut dst_a, 8, 8, ColorFormat::RGBA8888);
blit_generic_slow(&mut tex, &src, 0, 0, 4, 4, 1, 1, 4, 4, 3, 0, 8, 8, 255);
}
{
let mut tex = Texture::new(&mut dst_b, 8, 8, ColorFormat::RGBA8888);
blit_1to1_fast(&mut tex, &src, 0, 0, 4, 4, 1, 1, 3, 0, 8, 8);
}
assert_eq!(dst_a, dst_b);
}
#[test]
fn blit_dda_with_partial_clip() {
let mut src_buf = vec![0u8; 4 * 4 * 4];
for i in 0..16 {
src_buf[i * 4] = (i * 16 + 1) as u8;
src_buf[i * 4 + 3] = 255;
}
let src = Texture::new(&mut src_buf, 4, 4, ColorFormat::RGBA8888);
let mut dst_a = vec![0u8; 10 * 10 * 4];
let mut dst_b = vec![0u8; 10 * 10 * 4];
{
let mut tex_a = Texture::new(&mut dst_a, 10, 10, ColorFormat::RGBA8888);
blit_generic_slow(&mut tex_a, &src, 0, 0, 4, 4, 1, 1, 8, 8, 2, 0, 7, 10, 255);
}
{
let mut tex_b = Texture::new(&mut dst_b, 10, 10, ColorFormat::RGBA8888);
blit_dda(
&mut tex_b, &src, 0, 0, 4, 4, 1, 1, 8, 8, 2, 0, 7, 10, 255, None,
);
}
assert_eq!(dst_a, dst_b);
}
#[test]
fn sw_renderer_supports_offscreen() {
let mut buf = vec![0u8; 4 * 4 * 4];
let tex = Texture::new(&mut buf, 4, 4, ColorFormat::RGBA8888);
let backend = SwRenderer::new(tex);
assert!(Renderer::supports_offscreen(&backend));
}
#[test]
fn modify_target_region_writes_exact_rect() {
let mut buf = vec![0u8; 16 * 16 * 4];
let tex = Texture::new(&mut buf, 16, 16, ColorFormat::RGBA8888);
let mut backend = SwRenderer::new(tex);
let rect = Rect::new(
Fixed::from_int(4),
Fixed::from_int(4),
Fixed::from_int(6),
Fixed::from_int(5),
);
let ran = backend.modify_target_region(&rect, &mut |view| {
assert_eq!(view.width, 6);
assert_eq!(view.height, 5);
assert_eq!(view.stride, 16 * 4);
for y in 0..view.height as i32 {
for x in 0..view.width as i32 {
view.set_pixel(x, y, &Color::rgb(255, 128, 64));
}
}
Ok(())
});
assert_eq!(ran, Ok(true));
let target = &backend.target;
for py in 0..16i32 {
for px in 0..16i32 {
let p = target.get_pixel(px, py);
let in_rect = (4..10).contains(&px) && (4..9).contains(&py);
if in_rect {
assert_eq!((p.r, p.g, p.b), (255, 128, 64), "in-rect ({px},{py})");
} else {
assert_eq!((p.r, p.g, p.b), (0, 0, 0), "outside-rect ({px},{py})");
}
}
}
}
#[test]
fn modify_target_region_propagates_callback_failure() {
let mut backend = SwRenderer::new(Texture::owned(8, 8, ColorFormat::RGBA8888));
let result = backend.modify_target_region(&Rect::new(0, 0, 4, 4), &mut |_| {
Err(RenderError::ResourceLimit(RenderResource::Geometry))
});
assert_eq!(
result,
Err(RenderError::ResourceLimit(RenderResource::Geometry))
);
}
#[test]
fn scroll_target_region_shift_up_moves_rows_correctly() {
let mut buf = vec![0u8; 8 * 8 * 4];
for y in 0..8 {
for x in 0..8 {
let off = (y * 8 + x) * 4;
buf[off] = (y * 30) as u8;
buf[off + 3] = 255;
}
}
let tex = Texture::new(&mut buf, 8, 8, ColorFormat::RGBA8888);
let mut backend = SwRenderer::new(tex);
let area = Rect::new(
Fixed::ZERO,
Fixed::ZERO,
Fixed::from_int(8),
Fixed::from_int(8),
);
backend
.scroll_target_region(&area, Fixed::ZERO, Fixed::from_int(-2))
.unwrap();
for y in 0..6 {
let src_y_before = y + 2;
let p = backend.target.get_pixel(0, y);
assert_eq!(
p.r,
(src_y_before * 30) as u8,
"row {y} should hold pre-scroll row {src_y_before}",
);
}
}
#[test]
fn scroll_target_region_shift_down_moves_rows_correctly() {
let mut buf = vec![0u8; 8 * 8 * 4];
for y in 0..8 {
for x in 0..8 {
let off = (y * 8 + x) * 4;
buf[off] = (y * 30) as u8;
buf[off + 3] = 255;
}
}
let tex = Texture::new(&mut buf, 8, 8, ColorFormat::RGBA8888);
let mut backend = SwRenderer::new(tex);
let area = Rect::new(
Fixed::ZERO,
Fixed::ZERO,
Fixed::from_int(8),
Fixed::from_int(8),
);
backend
.scroll_target_region(&area, Fixed::ZERO, Fixed::from_int(2))
.unwrap();
for y in 2..8 {
let src_y_before = y - 2;
let p = backend.target.get_pixel(0, y);
assert_eq!(
p.r,
(src_y_before * 30) as u8,
"row {y} should hold pre-scroll row {src_y_before}",
);
}
}
#[test]
fn scroll_target_region_sub_pixel_dy_is_noop() {
let mut buf = vec![1u8; 4 * 4 * 4];
let tex = Texture::new(&mut buf, 4, 4, ColorFormat::RGBA8888);
let mut backend = SwRenderer::new(tex);
let area = Rect::new(
Fixed::ZERO,
Fixed::ZERO,
Fixed::from_int(4),
Fixed::from_int(4),
);
let half = Fixed::from_int(1) / Fixed::from_int(2);
backend
.scroll_target_region(&area, Fixed::ZERO, half)
.unwrap();
for px in backend.target.buf.as_slice() {
assert_eq!(*px, 1);
}
}
#[test]
fn scroll_target_region_negative_sub_pixel_dy_is_noop() {
let mut buf = vec![0u8; 4 * 4 * 4];
for y in 0..4 {
for x in 0..4 {
let off = (y * 4 + x) * 4;
buf[off] = (y * 50) as u8;
buf[off + 3] = 255;
}
}
let snapshot = buf.clone();
let tex = Texture::new(&mut buf, 4, 4, ColorFormat::RGBA8888);
let mut backend = SwRenderer::new(tex);
let area = Rect::new(
Fixed::ZERO,
Fixed::ZERO,
Fixed::from_int(4),
Fixed::from_int(4),
);
let neg_half = Fixed::ZERO - (Fixed::from_int(1) / Fixed::from_int(2));
backend
.scroll_target_region(&area, Fixed::ZERO, neg_half)
.unwrap();
assert_eq!(
backend.target.buf.as_slice(),
snapshot.as_slice(),
"negative sub-pixel dy must not modify the buffer"
);
}
#[test]
fn scroll_target_region_shift_left_moves_columns_correctly() {
let mut buf = vec![0u8; 8 * 8 * 4];
for y in 0..8 {
for x in 0..8 {
let off = (y * 8 + x) * 4;
buf[off] = (x * 30) as u8;
buf[off + 3] = 255;
}
}
let tex = Texture::new(&mut buf, 8, 8, ColorFormat::RGBA8888);
let mut backend = SwRenderer::new(tex);
let area = Rect::new(
Fixed::ZERO,
Fixed::ZERO,
Fixed::from_int(8),
Fixed::from_int(8),
);
backend
.scroll_target_region(&area, Fixed::from_int(-2), Fixed::ZERO)
.unwrap();
for x in 0..6 {
let src_x_before = x + 2;
let p = backend.target.get_pixel(x, 3);
assert_eq!(
p.r,
(src_x_before * 30) as u8,
"col {x} should hold pre-scroll col {src_x_before}",
);
}
}
#[test]
fn scroll_target_region_shift_right_moves_columns_correctly() {
let mut buf = vec![0u8; 8 * 8 * 4];
for y in 0..8 {
for x in 0..8 {
let off = (y * 8 + x) * 4;
buf[off] = (x * 30) as u8;
buf[off + 3] = 255;
}
}
let tex = Texture::new(&mut buf, 8, 8, ColorFormat::RGBA8888);
let mut backend = SwRenderer::new(tex);
let area = Rect::new(
Fixed::ZERO,
Fixed::ZERO,
Fixed::from_int(8),
Fixed::from_int(8),
);
backend
.scroll_target_region(&area, Fixed::from_int(2), Fixed::ZERO)
.unwrap();
for x in 2..8 {
let src_x_before = x - 2;
let p = backend.target.get_pixel(x, 3);
assert_eq!(
p.r,
(src_x_before * 30) as u8,
"col {x} should hold pre-scroll col {src_x_before}",
);
}
}
#[test]
fn opaque_mode_writes_full_alpha() {
let mut buf = std::vec![0u8; 4 * 4];
let mut tex = Texture::new(&mut buf, 2, 2, ColorFormat::RGBA8888);
tex.blend_pixel_int(0, 0, &Color::rgba(255, 0, 0, 100), 100);
tex.blend_pixel_int(1, 0, &Color::rgba(0, 255, 0, 200), 200);
assert_eq!(tex.get_pixel(0, 0).a, 255, "opaque mode: dst.a always 255");
assert_eq!(tex.get_pixel(1, 0).a, 255, "opaque mode: dst.a always 255");
}
#[test]
fn blend_alpha_accumulates_on_clear_transparent() {
let mut buf = std::vec![0u8; 4 * 4];
let mut tex = Texture::new(&mut buf, 2, 2, ColorFormat::RGBA8888);
tex.alpha_mode = AlphaMode::Blend;
tex.blend_pixel_int(0, 0, &Color::rgba(255, 0, 0, 128), 128);
let p = tex.get_pixel(0, 0);
assert_eq!(
p.a, 128,
"blend mode + transparent dst: dst.a should equal src.a, got {}",
p.a,
);
}
#[test]
fn blend_alpha_blends_when_dst_partial() {
let mut buf = std::vec![0u8; 4 * 4];
let mut tex = Texture::new(&mut buf, 2, 2, ColorFormat::RGBA8888);
tex.alpha_mode = AlphaMode::Blend;
tex.blend_pixel_int(0, 0, &Color::rgba(255, 0, 0, 100), 100);
tex.blend_pixel_int(0, 0, &Color::rgba(0, 0, 255, 100), 100);
let p = tex.get_pixel(0, 0);
let expected = 100 + (100 * 155) / 255;
assert!(
(p.a as i32 - expected as i32).abs() <= 2,
"blend mode source-over: expected ~{expected}, got {}",
p.a,
);
}
#[test]
fn blend_a_eq_255_writes_full_alpha_in_blend_mode() {
let mut buf = std::vec![0u8; 4 * 4];
let mut tex = Texture::new(&mut buf, 2, 2, ColorFormat::RGBA8888);
tex.alpha_mode = AlphaMode::Blend;
tex.blend_pixel_int(0, 0, &Color::rgba(255, 0, 0, 255), 255);
assert_eq!(tex.get_pixel(0, 0).a, 255);
}
#[test]
fn blit_opa_128_midgray_red_source() {
let mut tgt_buf = std::vec![0u8; 4 * 4 * 4];
let tgt_tex = Texture::new(&mut tgt_buf, 4, 4, ColorFormat::RGBA8888);
let mut backend = SwRenderer::new(tgt_tex);
let mut src_buf = std::vec![0u8; 4 * 4 * 4];
for px in src_buf.chunks_exact_mut(4) {
px[0] = 255;
px[1] = 0;
px[2] = 0;
px[3] = 255;
}
let src_tex = Texture::new(&mut src_buf, 4, 4, ColorFormat::RGBA8888);
let src_rect = Rect::new(0, 0, 4, 4);
let dst = Point::new(Fixed::ZERO, Fixed::ZERO);
let dst_size = Point::new(Fixed::from_int(4), Fixed::from_int(4));
let clip = Rect {
x: Fixed::ZERO,
y: Fixed::ZERO,
w: Fixed::from_int(4),
h: Fixed::from_int(4),
};
backend.blit(
&src_tex,
&src_rect,
dst,
dst_size,
&clip,
128,
Fixed::ZERO,
CompositeMode::SourceOver,
);
for y in 0..4 {
for x in 0..4 {
let p = backend.target.get_pixel(x, y);
assert!(
p.r >= 120 && p.r <= 135,
"({x},{y}) red channel {} not in [120,135]",
p.r,
);
assert_eq!((p.g, p.b), (0, 0), "({x},{y}) green/blue must stay zero",);
}
}
}
#[test]
fn blit_opa_zero_skips_writes() {
let mut tgt_buf = std::vec![0u8; 4 * 4 * 4];
let tgt_tex = Texture::new(&mut tgt_buf, 4, 4, ColorFormat::RGBA8888);
let mut backend = SwRenderer::new(tgt_tex);
let mut src_buf = std::vec![255u8; 4 * 4 * 4];
let src_tex = Texture::new(&mut src_buf, 4, 4, ColorFormat::RGBA8888);
let src_rect = Rect::new(0, 0, 4, 4);
let dst = Point::new(Fixed::ZERO, Fixed::ZERO);
let dst_size = Point::new(Fixed::from_int(4), Fixed::from_int(4));
let clip = Rect {
x: Fixed::ZERO,
y: Fixed::ZERO,
w: Fixed::from_int(4),
h: Fixed::from_int(4),
};
backend.blit(
&src_tex,
&src_rect,
dst,
dst_size,
&clip,
0,
Fixed::ZERO,
CompositeMode::SourceOver,
);
for y in 0..4 {
for x in 0..4 {
let p = backend.target.get_pixel(x, y);
assert_eq!((p.r, p.g, p.b, p.a), (0, 0, 0, 0));
}
}
}
#[test]
fn blit_opa_255_bit_exact_with_fast_path() {
let make_target = || {
let buf = std::vec![0u8; 4 * 4 * 4];
(buf, Vec::<u8>::new())
};
let (mut tgt_buf_a, _) = make_target();
let (mut tgt_buf_b, _) = make_target();
let mut src_buf = std::vec![0u8; 4 * 4 * 4];
for (i, px) in src_buf.chunks_exact_mut(4).enumerate() {
px[0] = (i * 16) as u8;
px[1] = (255 - i * 8) as u8;
px[2] = (i * 4) as u8;
px[3] = 255;
}
for (tgt, opa) in [(&mut tgt_buf_a, 255), (&mut tgt_buf_b, 255)] {
let tgt_tex = Texture::new(tgt, 4, 4, ColorFormat::RGBA8888);
let mut backend = SwRenderer::new(tgt_tex);
let src_tex = Texture::new(&mut src_buf, 4, 4, ColorFormat::RGBA8888);
let src_rect = Rect::new(0, 0, 4, 4);
let dst = Point::new(Fixed::ZERO, Fixed::ZERO);
let dst_size = Point::new(Fixed::from_int(4), Fixed::from_int(4));
let clip = Rect {
x: Fixed::ZERO,
y: Fixed::ZERO,
w: Fixed::from_int(4),
h: Fixed::from_int(4),
};
backend.blit(
&src_tex,
&src_rect,
dst,
dst_size,
&clip,
opa,
Fixed::ZERO,
CompositeMode::SourceOver,
);
}
assert_eq!(tgt_buf_a, tgt_buf_b, "opa==255 must be deterministic");
assert_eq!(tgt_buf_a, src_buf, "opa==255 must equal source");
}
#[test]
fn fill_path_scale_transform_renders_without_panic() {
use crate::render::command::DrawCommand;
use crate::types::Transform;
let mut buf = vec![0u8; 32 * 32 * 4];
let tex = Texture::new(&mut buf, 32, 32, ColorFormat::RGBA8888);
let mut backend = SwRenderer::new(tex);
let path = crate::render::path::Path::rect(
Fixed::ZERO,
Fixed::ZERO,
Fixed::from_int(4),
Fixed::from_int(4),
);
let clip = Rect::new(0, 0, 32, 32);
let paint = Paint::Color(Color::rgb(0, 255, 0).into());
let cmd = DrawCommand::FillPath {
path: &path,
transform: Transform::scale(Fixed::from_int(4), Fixed::from_int(4)),
paint: &paint,
opa: 255,
fill_rule: crate::render::raster::FillRule::EvenOdd,
};
backend.draw(&cmd, &clip);
let inside = backend.target.get_pixel(8, 8);
assert_eq!(inside.g, 255, "scaled rect interior should be green");
let outside = backend.target.get_pixel(20, 20);
assert_eq!(outside.g, 0, "outside scaled rect must stay zero");
}
fn run_composite_blit(
src_rgba: [u8; 4],
dst_rgba: [u8; 4],
composite: CompositeMode,
opa: u8,
) -> Color {
let mut src_buf = std::vec![src_rgba[0], src_rgba[1], src_rgba[2], src_rgba[3]];
let src_tex = Texture::new(&mut src_buf, 1, 1, ColorFormat::RGBA8888);
let mut dst_buf = std::vec![dst_rgba[0], dst_rgba[1], dst_rgba[2], dst_rgba[3]];
let mut backend = SwRenderer::new(Texture::new(&mut dst_buf, 1, 1, ColorFormat::RGBA8888));
backend.viewport = Viewport::new(1, 1, Fixed::ONE);
let cmd = DrawCommand::Blit {
pos: Point::ZERO,
size: Point {
x: Fixed::from_int(1),
y: Fixed::from_int(1),
},
transform: Transform::IDENTITY,
quad: None,
texture: &src_tex,
opa,
radius: Fixed::ZERO,
composite,
};
backend.draw(&cmd, &Rect::new(0, 0, 1, 1));
backend.target.get_pixel(0, 0)
}
#[test]
fn composite_add_saturates_at_full_intensity() {
let out = run_composite_blit([128, 0, 0, 255], [128, 0, 0, 255], CompositeMode::Add, 255);
assert_eq!(out.r, 255);
}
#[test]
fn composite_screen_blends_half_on_half() {
let out = run_composite_blit(
[128, 0, 0, 255],
[128, 0, 0, 255],
CompositeMode::Screen,
255,
);
assert!(
(190..=192).contains(&out.r),
"screen(128,128) = ~191; got {}",
out.r
);
}
#[test]
fn composite_multiply_halves_at_50_percent() {
let out = run_composite_blit(
[128, 0, 0, 255],
[128, 0, 0, 255],
CompositeMode::Multiply,
255,
);
assert!(
(63..=65).contains(&out.r),
"multiply(128,128) = ~64; got {}",
out.r
);
}
#[test]
fn composite_darken_picks_smaller_channel() {
let out = run_composite_blit(
[64, 0, 0, 255],
[192, 0, 0, 255],
CompositeMode::Darken,
255,
);
assert_eq!(out.r, 64);
}
#[test]
fn composite_lighten_picks_larger_channel() {
let out = run_composite_blit(
[64, 0, 0, 255],
[192, 0, 0, 255],
CompositeMode::Lighten,
255,
);
assert_eq!(out.r, 192);
}
#[test]
fn composite_difference_is_absolute_diff() {
let out = run_composite_blit(
[192, 0, 0, 255],
[64, 0, 0, 255],
CompositeMode::Difference,
255,
);
assert_eq!(out.r, 128);
}
#[test]
fn composite_with_zero_src_alpha_preserves_dst() {
for mode in [
CompositeMode::Add,
CompositeMode::Screen,
CompositeMode::Multiply,
CompositeMode::Darken,
CompositeMode::Lighten,
CompositeMode::Difference,
] {
let out = run_composite_blit([255, 255, 255, 0], [50, 80, 110, 255], mode, 255);
assert_eq!(
(out.r, out.g, out.b),
(50, 80, 110),
"mode {mode:?} clobbered dst"
);
}
}
#[test]
fn composite_half_alpha_src_blends_mode_against_dst() {
let out = run_composite_blit([128, 0, 0, 128], [128, 0, 0, 255], CompositeMode::Add, 255);
assert!(
(190..=192).contains(&out.r),
"Add half-alpha = ~191; got {}",
out.r
);
}
#[test]
fn blit_radius_mask_clips_corner_pixels() {
let mut src_buf = vec![255u8; 8 * 8 * 4];
let src_tex = Texture::new(&mut src_buf, 8, 8, ColorFormat::RGBA8888);
let mut dst_buf = vec![0u8; 8 * 8 * 4];
let mut backend = SwRenderer::new(Texture::new(&mut dst_buf, 8, 8, ColorFormat::RGBA8888));
backend.viewport = Viewport::new(8, 8, Fixed::ONE);
let cmd = DrawCommand::Blit {
pos: Point::ZERO,
size: Point {
x: Fixed::from_int(8),
y: Fixed::from_int(8),
},
transform: Transform::IDENTITY,
quad: None,
texture: &src_tex,
opa: 255,
radius: Fixed::from_int(3),
composite: CompositeMode::SourceOver,
};
backend.draw(&cmd, &Rect::new(0, 0, 8, 8));
let corner = backend.target.get_pixel(0, 0);
assert_eq!(corner.r, 0, "rounded corner must clip top-left pixel");
let center = backend.target.get_pixel(4, 4);
assert_eq!(center.r, 255, "rect interior unaffected by mask");
}
#[test]
fn blit_zero_radius_matches_unmasked_path() {
let mut src_buf = vec![200u8; 4 * 4 * 4];
let src_tex = Texture::new(&mut src_buf, 4, 4, ColorFormat::RGBA8888);
let mut dst_no_radius = vec![0u8; 4 * 4 * 4];
let mut dst_zero_radius = vec![0u8; 4 * 4 * 4];
for (buf, radius) in [
(&mut dst_no_radius, Fixed::ZERO),
(&mut dst_zero_radius, Fixed::ZERO),
] {
let mut backend = SwRenderer::new(Texture::new(buf, 4, 4, ColorFormat::RGBA8888));
backend.viewport = Viewport::new(4, 4, Fixed::ONE);
let cmd = DrawCommand::Blit {
pos: Point::ZERO,
size: Point {
x: Fixed::from_int(4),
y: Fixed::from_int(4),
},
transform: Transform::IDENTITY,
quad: None,
texture: &src_tex,
opa: 255,
radius,
composite: CompositeMode::SourceOver,
};
backend.draw(&cmd, &Rect::new(0, 0, 4, 4));
}
assert_eq!(dst_no_radius, dst_zero_radius);
}
#[test]
fn projected_quad_blit_preserves_radius_opacity_and_composite() {
let src = [255u8, 0, 0, 255].repeat(16);
let texture = Texture::from_ref(&src, 4, 4, ColorFormat::RGBA8888);
let mut pixels = [0u8, 0, 255, 255].repeat(12 * 12);
let mut renderer =
SwRenderer::new(Texture::new(&mut pixels, 12, 12, ColorFormat::RGBA8888));
let command = DrawCommand::Blit {
pos: Point::new(2, 2),
size: Point::new(8, 8),
transform: Transform::IDENTITY,
quad: Some([
Point::new(2, 2),
Point::new(10, 2),
Point::new(10, 10),
Point::new(2, 10),
]),
texture: &texture,
opa: 128,
radius: Fixed::from_int(2),
composite: CompositeMode::Difference,
};
let clip = Rect::new(0, 0, 12, 12);
assert_eq!(renderer.submit(&DrawRequest::new(&command, clip)), Ok(()));
let corner = renderer.target.get_pixel(2, 2);
let center = renderer.target.get_pixel(6, 6);
assert!(center.r > 0 && center.b > 0);
assert!(corner.r < center.r);
assert_eq!(renderer.target.get_pixel(0, 0), Color::rgb(0, 0, 255));
}
#[test]
fn projective_blit_accepts_effects_without_intermediate_storage() {
let src = [255u8, 0, 0, 255].repeat(16);
let texture = Texture::from_ref(&src, 4, 4, ColorFormat::RGBA8888);
let mut pixels = [0u8, 0, 255, 255].repeat(12 * 12);
let mut renderer =
SwRenderer::new(Texture::new(&mut pixels, 12, 12, ColorFormat::RGBA8888));
let command = DrawCommand::Blit {
pos: Point::new(2, 2),
size: Point::new(8, 8),
transform: Transform::IDENTITY,
quad: None,
texture: &texture,
opa: 128,
radius: Fixed::from_int(2),
composite: CompositeMode::Difference,
};
let clip = Rect::new(0, 0, 12, 12);
let projection =
Transform3D::rotate_y_perspective(Fixed::from_int(10), Fixed::from_int(400));
assert_eq!(
renderer.submit(&DrawRequest::new(&command, clip).with_projective(projection)),
Ok(())
);
let center = renderer.target.get_pixel(6, 6);
assert!(center.r > 0 && center.b > 0);
}
#[test]
fn projected_blit_corner_radius_tracks_local_geometry() {
let source = [255u8, 0, 0, 255].repeat(16);
let texture = Texture::from_ref(&source, 4, 4, ColorFormat::RGBA8888);
let quad = [
Point::new(2, 2),
Point::new(22, 2),
Point::new(16, 22),
Point::new(8, 22),
];
let mut first = [0u8; 24 * 24 * 4];
let mut second = [0u8; 24 * 24 * 4];
for (pixels, size, radius) in [
(&mut first, Point::new(20, 20), Fixed::from_int(4)),
(&mut second, Point::new(40, 40), Fixed::from_int(8)),
] {
let mut target = Texture::new(pixels, 24, 24, ColorFormat::RGBA8888);
target.alpha_mode = AlphaMode::Blend;
blit_quad(
&mut target,
&texture,
&quad,
Rect::new(0, 0, 24, 24),
size,
radius,
255,
CompositeMode::SourceOver,
None,
);
}
for y in 7..22 {
for x in 2..22 {
let offset = (y * 24 + x) * 4;
assert_eq!(first[offset] > 0, second[offset] > 0, "pixel ({x}, {y})");
}
}
assert!(first[(20 * 24 + 9) * 4] > 0);
}
#[test]
fn projected_quads_combine_the_active_path_clip() {
const WIDTH: usize = 16;
const HEIGHT: usize = 8;
let quad = [
Point::new(0, 0),
Point::new(WIDTH as i32, 0),
Point::new(WIDTH as i32, HEIGHT as i32),
Point::new(0, HEIGHT as i32),
];
let clip = Rect::new(0, 0, WIDTH as i32, HEIGHT as i32);
let mut mask = [0u8; WIDTH * HEIGHT];
for row in mask.chunks_exact_mut(WIDTH) {
row[..WIDTH / 2].fill(255);
}
let mut fill_pixels = [0u8; WIDTH * HEIGHT * 4];
let mut fill_target = Texture::new(
&mut fill_pixels,
WIDTH as u16,
HEIGHT as u16,
ColorFormat::RGBA8888,
);
fill_rect_quad(
&mut fill_target,
&quad,
clip,
&Color::rgb(220, 20, 30),
Fixed::ZERO,
Fixed::from_int(WIDTH as i32),
Fixed::from_int(HEIGHT as i32),
255,
Some(&mask),
);
assert_eq!(fill_target.get_pixel(4, 4).r, 220);
assert_eq!(fill_target.get_pixel(12, 4).a, 0);
let source_pixels = [255u8; WIDTH * HEIGHT * 4];
let source = Texture::from_ref(
&source_pixels,
WIDTH as u16,
HEIGHT as u16,
ColorFormat::RGBA8888,
);
let mut blit_pixels = [0u8; WIDTH * HEIGHT * 4];
let mut blit_target = Texture::new(
&mut blit_pixels,
WIDTH as u16,
HEIGHT as u16,
ColorFormat::RGBA8888,
);
blit_quad(
&mut blit_target,
&source,
&quad,
clip,
Point::new(WIDTH as i32, HEIGHT as i32),
Fixed::ZERO,
255,
CompositeMode::SourceOver,
Some(&mask),
);
assert_eq!(blit_target.get_pixel(4, 4).a, 255);
assert_eq!(blit_target.get_pixel(12, 4).a, 0);
let mut stroke_pixels = [0u8; WIDTH * HEIGHT * 4];
let mut stroke_target = Texture::new(
&mut stroke_pixels,
WIDTH as u16,
HEIGHT as u16,
ColorFormat::RGBA8888,
);
stroke_rect_quad(
&mut stroke_target,
&quad,
clip,
&Color::rgb(20, 220, 30),
Fixed::from_int(2),
Fixed::ZERO,
255,
Some(&mask),
);
assert!(stroke_target.get_pixel(4, 0).g > 0);
assert_eq!(stroke_target.get_pixel(12, 0).a, 0);
}
#[test]
fn projective_glyph_run_renders_without_an_intermediate_surface() {
let mut pixels = vec![0u8; 32 * 16 * 4];
let mut renderer =
SwRenderer::new(Texture::new(&mut pixels, 32, 16, ColorFormat::RGBA8888));
let font = crate::render::font::Font::bitmap_8x8();
let glyphs = [textflow::shaping::PositionedGlyph::new(
crate::render::font::GlyphId::new(65),
textflow::shaping::FlowPoint { x: 0, y: 7 << 8 },
)];
let command = DrawCommand::GlyphRun {
pos: Point::new(8, 4),
transform: Transform::IDENTITY,
glyphs: &glyphs,
font: &font,
color: Color::rgb(255, 255, 255),
opa: 255,
};
let projective =
Transform3D::rotate_y_perspective(Fixed::from_int(18), Fixed::from_int(400));
assert_eq!(
renderer.draw_projective(&command, &Rect::new(0, 0, 32, 16), &projective),
Ok(())
);
assert!(pixels.chunks_exact(4).any(|pixel| pixel[3] != 0));
}
#[test]
fn projective_glyph_run_rejects_ink_behind_near_plane() {
let mut pixels = vec![0u8; 16 * 16 * 4];
let before = pixels.clone();
let mut renderer =
SwRenderer::new(Texture::new(&mut pixels, 16, 16, ColorFormat::RGBA8888));
let font = crate::render::font::Font::bitmap_8x8();
let glyphs = [textflow::shaping::PositionedGlyph::new(
crate::render::font::GlyphId::new(65),
textflow::shaping::FlowPoint { x: 0, y: 7 << 8 },
)];
let command = DrawCommand::GlyphRun {
pos: Point::ZERO,
transform: Transform::IDENTITY,
glyphs: &glyphs,
font: &font,
color: Color::rgb(255, 255, 255),
opa: 255,
};
let projective = Transform3D {
m20: crate::types::Fixed64::ONE,
m22: crate::types::Fixed64::from_int(-4),
..Transform3D::IDENTITY
};
assert_eq!(
renderer.draw_projective(&command, &Rect::new(0, 0, 16, 16), &projective),
Err(ProjectiveDrawError::InvalidProjection)
);
assert_eq!(pixels, before);
}
#[test]
fn projective_fill_rejects_singular_geometry_before_writing() {
let mut pixels = vec![0u8; 8 * 8 * 4];
let before = pixels.clone();
let mut renderer = SwRenderer::new(Texture::new(&mut pixels, 8, 8, ColorFormat::RGBA8888));
let command = DrawCommand::Fill {
area: Rect::new(0, 0, 8, 8),
transform: Transform::IDENTITY,
quad: None,
color: Color::rgb(255, 255, 255),
radius: Fixed::ZERO,
opa: 255,
};
let projective = Transform3D {
m22: crate::types::Fixed64::ZERO,
..Transform3D::IDENTITY
};
assert_eq!(
renderer.draw_projective(&command, &Rect::new(0, 0, 8, 8), &projective),
Err(ProjectiveDrawError::InvalidProjection)
);
assert_eq!(pixels, before);
}
#[test]
fn projective_solid_path_flattens_into_the_software_target() {
let mut pixels = vec![0u8; 8 * 8 * 4];
let mut renderer = SwRenderer::new(Texture::new(&mut pixels, 8, 8, ColorFormat::RGBA8888));
let path = Path::rect(
Fixed::ZERO,
Fixed::ZERO,
Fixed::from_int(4),
Fixed::from_int(4),
);
let paint = Paint::Color(Color::rgb(255, 255, 255).into());
let command = DrawCommand::FillPath {
path: &path,
transform: Transform::IDENTITY,
paint: &paint,
opa: 255,
fill_rule: crate::render::raster::FillRule::NonZero,
};
assert_eq!(
renderer.draw_projective(
&command,
&Rect::new(0, 0, 8, 8),
&Transform3D::translate(Fixed::from_int(2), Fixed::ONE),
),
Ok(())
);
assert_eq!(renderer.target.get_pixel(3, 2), Color::rgb(255, 255, 255));
assert_eq!(renderer.target.get_pixel(1, 2), Color::rgba(0, 0, 0, 0));
}
#[test]
fn projective_gradient_path_samples_back_in_paint_space() {
use alloc::borrow::Cow;
use mirx::scene::{GradientStop, GradientUnits, LinearGradient, SpreadMode};
let mut pixels = vec![0u8; 16 * 8 * 4];
let mut renderer = SwRenderer::new(Texture::new(&mut pixels, 16, 8, ColorFormat::RGBA8888));
let path = Path::rect(
Fixed::ZERO,
Fixed::ZERO,
Fixed::from_int(8),
Fixed::from_int(4),
);
let paint = Paint::LinearGradient(LinearGradient {
start: mirx::types::Point::new(mirx::types::Fixed::ZERO, mirx::types::Fixed::ZERO),
end: mirx::types::Point::new(mirx::types::Fixed::ONE, mirx::types::Fixed::ZERO),
stops: Cow::Owned(vec![
GradientStop {
offset: mirx::types::Fixed::ZERO,
color: mirx::types::Color::rgb(0, 20, 240),
},
GradientStop {
offset: mirx::types::Fixed::ONE,
color: mirx::types::Color::rgb(240, 20, 0),
},
]),
spread: SpreadMode::Pad,
units: GradientUnits::ObjectBoundingBox,
transform: mirx::types::Transform::IDENTITY,
});
let command = DrawCommand::FillPath {
path: &path,
transform: Transform::IDENTITY,
paint: &paint,
opa: 255,
fill_rule: crate::render::raster::FillRule::NonZero,
};
let clip = Rect::new(0, 0, 16, 8);
let projection = Transform3D::translate(Fixed::from_int(3), Fixed::from_int(2));
assert_eq!(
renderer.route(&DrawRequest::new(&command, clip).with_projective(projection)),
Ok(RenderRoute::Native)
);
renderer
.submit(&DrawRequest::new(&command, clip).with_projective(projection))
.unwrap();
let left = renderer.target.get_pixel(4, 3);
let right = renderer.target.get_pixel(9, 3);
assert!(left.b > left.r);
assert!(right.r > right.b);
assert_eq!(renderer.target.get_pixel(1, 3), Color::rgba(0, 0, 0, 0));
}
#[test]
fn projective_solid_stroke_uses_the_retained_outline() {
let mut pixels = vec![0u8; 12 * 8 * 4];
let mut renderer = SwRenderer::new(Texture::new(&mut pixels, 12, 8, ColorFormat::RGBA8888));
let mut path = Path::new();
path.move_to(Point::new(1, 2)).line_to(Point::new(7, 2));
let paint = Paint::Color(Color::rgb(240, 120, 40).into());
let command = DrawCommand::StrokePath {
path: &path,
transform: Transform::IDENTITY,
paint: &paint,
width: Fixed::from_int(2),
opa: 255,
line_cap: crate::render::raster::LineCap::Butt,
line_join: crate::render::raster::LineJoin::Miter,
miter_limit: Fixed::from_int(4),
dash: &[],
};
let projection = Transform3D::translate(Fixed::from_int(2), Fixed::ONE);
assert_eq!(
renderer.draw_projective(&command, &Rect::new(0, 0, 12, 8), &projection),
Ok(())
);
assert_eq!(renderer.target.get_pixel(5, 3), Color::rgb(240, 120, 40));
assert_eq!(renderer.target.get_pixel(1, 3), Color::rgba(0, 0, 0, 0));
}
#[test]
fn projective_gradient_stroke_preserves_its_color_axis() {
use alloc::borrow::Cow;
use mirx::scene::{GradientStop, GradientUnits, LinearGradient, SpreadMode};
let mut pixels = vec![0u8; 16 * 8 * 4];
let mut renderer = SwRenderer::new(Texture::new(&mut pixels, 16, 8, ColorFormat::RGBA8888));
let mut path = Path::new();
path.move_to(Point::new(1, 3)).line_to(Point::new(9, 3));
let paint = Paint::LinearGradient(LinearGradient {
start: mirx::types::Point::new(mirx::types::Fixed::ZERO, mirx::types::Fixed::ZERO),
end: mirx::types::Point::new(mirx::types::Fixed::ONE, mirx::types::Fixed::ZERO),
stops: Cow::Owned(vec![
GradientStop {
offset: mirx::types::Fixed::ZERO,
color: mirx::types::Color::rgb(10, 240, 40),
},
GradientStop {
offset: mirx::types::Fixed::ONE,
color: mirx::types::Color::rgb(240, 20, 180),
},
]),
spread: SpreadMode::Pad,
units: GradientUnits::ObjectBoundingBox,
transform: mirx::types::Transform::IDENTITY,
});
let command = DrawCommand::StrokePath {
path: &path,
transform: Transform::IDENTITY,
paint: &paint,
width: Fixed::from_int(2),
opa: 255,
line_cap: crate::render::raster::LineCap::Butt,
line_join: crate::render::raster::LineJoin::Miter,
miter_limit: Fixed::from_int(4),
dash: &[],
};
let clip = Rect::new(0, 0, 16, 8);
let projection = Transform3D::translate(Fixed::from_int(3), Fixed::ONE);
renderer
.submit(&DrawRequest::new(&command, clip).with_projective(projection))
.unwrap();
let left = renderer.target.get_pixel(5, 4);
let right = renderer.target.get_pixel(10, 4);
assert!(left.g > left.r);
assert!(right.r > right.g);
}
#[test]
fn projective_line_uses_stroke_geometry_without_a_path_allocation() {
let mut pixels = vec![0u8; 12 * 8 * 4];
let mut renderer = SwRenderer::new(Texture::new(&mut pixels, 12, 8, ColorFormat::RGBA8888));
let command = DrawCommand::Line {
p1: Point::new(1, 2),
p2: Point::new(7, 2),
transform: Transform::IDENTITY,
color: Color::rgb(80, 180, 250),
width: Fixed::from_int(2),
opa: 255,
};
let clip = Rect::new(0, 0, 12, 8);
let projection = Transform3D::translate(Fixed::from_int(2), Fixed::ONE);
assert_eq!(
renderer.route(&DrawRequest::new(&command, clip).with_projective(projection)),
Ok(RenderRoute::Native)
);
assert_eq!(
renderer.submit(&DrawRequest::new(&command, clip).with_projective(projection)),
Ok(())
);
assert_eq!(renderer.target.get_pixel(5, 3), Color::rgb(80, 180, 250));
assert_eq!(renderer.target.get_pixel(1, 3), Color::rgba(0, 0, 0, 0));
}
#[test]
fn projective_arc_reuses_primitive_path_storage() {
let mut pixels = vec![0u8; 16 * 16 * 4];
let mut renderer =
SwRenderer::new(Texture::new(&mut pixels, 16, 16, ColorFormat::RGBA8888));
let command = DrawCommand::Arc {
center: Point::new(6, 6),
transform: Transform::IDENTITY,
radius: Fixed::from_int(4),
start_angle: Fixed::ZERO,
end_angle: Fixed::from_int(180),
color: Color::rgb(250, 190, 70),
width: Fixed::from_int(2),
opa: 255,
};
let clip = Rect::new(0, 0, 16, 16);
let projection = Transform3D::translate(Fixed::ONE, Fixed::from_int(2));
assert_eq!(
renderer.submit(&DrawRequest::new(&command, clip).with_projective(projection)),
Ok(())
);
let capacity = renderer.scratch.primitive_path.command_capacity();
assert!(capacity >= 3);
assert!(
renderer
.target
.buf
.as_slice()
.chunks_exact(4)
.any(|pixel| pixel[3] != 0)
);
renderer.target.buf.as_mut_slice().fill(0);
assert_eq!(
renderer.submit(&DrawRequest::new(&command, clip).with_projective(projection)),
Ok(())
);
assert_eq!(renderer.scratch.primitive_path.command_capacity(), capacity);
}
#[test]
fn projective_clip_masks_following_draws() {
let mut pixels = vec![0u8; 8 * 8 * 4];
let mut renderer = SwRenderer::new(Texture::new(&mut pixels, 8, 8, ColorFormat::RGBA8888));
let path = Path::rect(
Fixed::ZERO,
Fixed::ZERO,
Fixed::from_int(4),
Fixed::from_int(4),
);
let clip = Rect::new(0, 0, 8, 8);
let push = DrawCommand::PushClip {
path: &path,
transform: Transform::IDENTITY,
fill_rule: crate::render::raster::FillRule::NonZero,
};
let fill = DrawCommand::Fill {
area: clip,
transform: Transform::IDENTITY,
quad: None,
color: Color::rgb(120, 180, 240),
radius: Fixed::ZERO,
opa: 255,
};
let projection = Transform3D::translate(Fixed::from_int(2), Fixed::ONE);
renderer
.submit(&DrawRequest::new(&push, clip).with_projective(projection))
.unwrap();
renderer.submit(&DrawRequest::new(&fill, clip)).unwrap();
renderer
.submit(&DrawRequest::new(&DrawCommand::PopClip, clip).with_projective(projection))
.unwrap();
assert_eq!(renderer.target.get_pixel(3, 2), Color::rgb(120, 180, 240));
assert_eq!(renderer.target.get_pixel(1, 2), Color::rgba(0, 0, 0, 0));
assert_eq!(renderer.target.get_pixel(6, 2), Color::rgba(0, 0, 0, 0));
}
}