use kurbo::{PathEl, Shape as _};
use uzor_urx_core::math::{
Affine, BezPath, BlendMode, Brush, Color, Compose, Extend, Gradient, GradientKind, Mix, Point, Rect,
RoundedRect, RoundedRectRadii, Vec2,
};
use uzor_urx_core::metrics_keys::{KEY_RENDER_GLYPH_INSTANCES, KEY_RENDER_PRIMITIVES, KEY_RENDER_SKIPPED_NONFINITE};
use uzor_urx_core::scene::{DrawCommand, FillRule, FontId, Glyph, ImageId, LineCap, Scene, Stroke};
use uzor_urx_core::validate::{validate_command, ValidationIssue};
use std::sync::Arc;
use crate::atlas::NativeGlyphAtlas;
use crate::gradient_lut::GradientLutAtlas;
use crate::pipelines::blend_composite::BlendCompositeInstance;
use crate::pipelines::glyph::GlyphInstance;
use crate::pipelines::gradient::GradientInstance;
use crate::pipelines::image::ImageInstance;
use crate::pipelines::line::LineInstance;
use crate::pipelines::path::{
GeometryCapacityError, TriInstance, MAX_GEOMETRY_BUFFER_BYTES,
};
use crate::pipelines::quad::{pack_rgba8, QuadInstance};
use crate::renderer::Viewport;
use crate::tessellate::{TessCache, TessMesh};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum MaskOp {
Increment,
Decrement,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum BatchKind {
Quad,
Line,
Triangle,
Glyph,
StencilMask(MaskOp),
Gradient,
Image(ImageId),
}
#[derive(Debug, Clone, Copy)]
pub(crate) struct Batch {
pub(crate) kind: BatchKind,
pub(crate) start: u32,
pub(crate) count: u32,
pub(crate) stencil_ref: Option<u32>,
}
#[derive(Debug, Clone, Copy)]
pub(crate) enum FrameOp {
Draw(Batch),
PushLayer { depth: u32 },
PopLayer { depth: u32, stencil_ref: Option<u32> },
}
#[derive(Debug, Default)]
pub(crate) struct EncodedFrame {
pub(crate) quads: Vec<QuadInstance>,
pub(crate) lines: Vec<LineInstance>,
pub(crate) triangles: Vec<TriInstance>,
pub(crate) glyphs: Vec<GlyphInstance>,
pub(crate) stencil_masks: Vec<TriInstance>,
pub(crate) gradients: Vec<GradientInstance>,
pub(crate) images: Vec<ImageInstance>,
pub(crate) composites: Vec<BlendCompositeInstance>,
pub(crate) ops: Vec<FrameOp>,
pub(crate) has_rounded_clip: bool,
current_stencil_ref: Option<u32>,
pub(crate) geometry_capacity_error: Option<GeometryCapacityError>,
pub(crate) geometry_profile: GeometryProfile,
}
#[derive(Debug, Clone, Copy, Default)]
struct GeometryReserveProfile {
requested_bytes: usize,
growth_bytes: usize,
reserve_us: u128,
}
#[derive(Debug, Clone, Copy, Default)]
pub(crate) struct GeometryProfile {
pub(crate) reserve_calls: u64,
pub(crate) growths: u64,
pub(crate) growth_bytes: usize,
pub(crate) peak_requested_bytes: usize,
pub(crate) reserve_us: u128,
}
impl GeometryProfile {
fn record(&mut self, reserve: GeometryReserveProfile) {
self.reserve_calls += 1;
self.growths += u64::from(reserve.growth_bytes != 0);
self.growth_bytes = self.growth_bytes.saturating_add(reserve.growth_bytes);
self.peak_requested_bytes = self.peak_requested_bytes.max(reserve.requested_bytes);
self.reserve_us += reserve.reserve_us;
}
}
fn try_reserve_geometry_with_limit<T>(
instances: &mut Vec<T>,
additional: usize,
max_bytes: usize,
buffer: &'static str,
) -> Result<GeometryReserveProfile, GeometryCapacityError> {
let instance_size = std::mem::size_of::<T>();
let requested_instances = instances.len().checked_add(additional).ok_or(GeometryCapacityError {
stage: "cpu_geometry_reserve",
buffer,
requested_instances: usize::MAX,
requested_bytes: usize::MAX,
current_capacity_instances: instances.capacity(),
instance_size,
max_bytes,
})?;
let requested_bytes = requested_instances.checked_mul(instance_size).unwrap_or(usize::MAX);
let max_instances = max_bytes / instance_size;
if requested_bytes > max_bytes {
return Err(GeometryCapacityError {
stage: "cpu_geometry_reserve",
buffer,
requested_instances,
requested_bytes,
current_capacity_instances: instances.capacity(),
instance_size,
max_bytes,
});
}
let capacity_before = instances.capacity();
let mut reserve_us = 0;
if requested_instances > instances.capacity() {
let target_capacity = instances
.capacity()
.max(1024)
.saturating_mul(2)
.max(requested_instances)
.min(max_instances);
crate::profile::stage(
"cpu_geometry_grow_begin",
format_args!(
"buffer={} current_instances={} additional_instances={} requested_instances={} \
requested_bytes={} capacity_before_instances={} capacity_target_instances={}",
buffer,
instances.len(),
additional,
requested_instances,
requested_bytes,
instances.capacity(),
target_capacity,
),
);
let reserve_t0 = crate::profile::enabled().then(std::time::Instant::now);
if instances.try_reserve_exact(target_capacity - instances.len()).is_err() {
return Err(GeometryCapacityError {
stage: "cpu_geometry_reserve",
buffer,
requested_instances,
requested_bytes,
current_capacity_instances: instances.capacity(),
instance_size,
max_bytes,
});
}
reserve_us = reserve_t0.map_or(0, |started| started.elapsed().as_micros());
}
Ok(GeometryReserveProfile {
requested_bytes,
growth_bytes: instances
.capacity()
.saturating_sub(capacity_before)
.saturating_mul(instance_size),
reserve_us,
})
}
fn try_reserve_geometry<T>(
instances: &mut Vec<T>,
additional: usize,
buffer: &'static str,
) -> Result<GeometryReserveProfile, GeometryCapacityError> {
try_reserve_geometry_with_limit(
instances,
additional,
MAX_GEOMETRY_BUFFER_BYTES,
buffer,
)
}
impl EncodedFrame {
fn reset(&mut self) {
self.quads.clear();
self.lines.clear();
self.triangles.clear();
self.glyphs.clear();
self.stencil_masks.clear();
self.gradients.clear();
self.images.clear();
self.composites.clear();
self.ops.clear();
self.has_rounded_clip = false;
self.current_stencil_ref = None;
self.geometry_capacity_error = None;
self.geometry_profile = GeometryProfile::default();
}
pub(crate) fn geometry_used_bytes(&self) -> usize {
self.lines
.len()
.saturating_mul(std::mem::size_of::<LineInstance>())
.saturating_add(
self.triangles
.len()
.saturating_mul(std::mem::size_of::<TriInstance>()),
)
.saturating_add(
self.stencil_masks
.len()
.saturating_mul(std::mem::size_of::<TriInstance>()),
)
.saturating_add(
self.gradients
.len()
.saturating_mul(std::mem::size_of::<GradientInstance>()),
)
}
pub(crate) fn geometry_capacity_bytes(&self) -> usize {
self.lines
.capacity()
.saturating_mul(std::mem::size_of::<LineInstance>())
.saturating_add(
self.triangles
.capacity()
.saturating_mul(std::mem::size_of::<TriInstance>()),
)
.saturating_add(
self.stencil_masks
.capacity()
.saturating_mul(std::mem::size_of::<TriInstance>()),
)
.saturating_add(
self.gradients
.capacity()
.saturating_mul(std::mem::size_of::<GradientInstance>()),
)
}
fn push_quad(&mut self, instance: QuadInstance) {
let start = self.quads.len() as u32;
self.quads.push(instance);
let stencil_ref = self.current_stencil_ref;
self.bump_batch(BatchKind::Quad, start, stencil_ref);
}
fn push_line(&mut self, instance: LineInstance) {
let start = self.lines.len() as u32;
self.lines.push(instance);
let stencil_ref = self.current_stencil_ref;
self.bump_batch(BatchKind::Line, start, stencil_ref);
}
fn push_glyph(&mut self, instance: GlyphInstance) {
let start = self.glyphs.len() as u32;
self.glyphs.push(instance);
let stencil_ref = self.current_stencil_ref;
self.bump_batch(BatchKind::Glyph, start, stencil_ref);
}
fn push_image(&mut self, instance: ImageInstance, id: ImageId) {
let start = self.images.len() as u32;
self.images.push(instance);
let stencil_ref = self.current_stencil_ref;
self.bump_batch(BatchKind::Image(id), start, stencil_ref);
}
fn bump_batch(&mut self, kind: BatchKind, start: u32, stencil_ref: Option<u32>) {
self.bump_batch_by(kind, start, 1, stencil_ref);
}
fn bump_batch_by(&mut self, kind: BatchKind, start: u32, count: u32, stencil_ref: Option<u32>) {
if count == 0 {
return;
}
if let Some(FrameOp::Draw(last)) = self.ops.last_mut() {
if last.kind == kind && last.stencil_ref == stencil_ref {
last.count += count;
return;
}
}
self.ops.push(FrameOp::Draw(Batch { kind, start, count, stencil_ref }));
}
fn push_marker(&mut self, op: FrameOp) {
self.ops.push(op);
}
#[cfg(test)]
pub(crate) fn draw_batches(&self) -> Vec<Batch> {
self.ops
.iter()
.filter_map(|op| match op {
FrameOp::Draw(b) => Some(*b),
_ => None,
})
.collect()
}
}
enum ClipFrame {
Rect([f32; 4]),
Rounded {
rect_device: [f32; 4],
stencil_depth: u32,
mesh: Arc<TessMesh>,
transform: Affine,
},
}
struct ClipStack {
stack: Vec<ClipFrame>,
rounded_depth: u32,
}
impl ClipStack {
fn new(viewport: Viewport) -> Self {
Self {
stack: vec![ClipFrame::Rect([0.0, 0.0, viewport.width as f32, viewport.height as f32])],
rounded_depth: 0,
}
}
fn current(&self) -> [f32; 4] {
match self.stack.last().expect("ClipStack is seeded with a root entry that is never popped") {
ClipFrame::Rect(r) => *r,
ClipFrame::Rounded { rect_device, .. } => *rect_device,
}
}
fn rounded_depth(&self) -> u32 {
self.rounded_depth
}
fn push_rect_device(&mut self, r: [f32; 4]) {
let cur = self.current();
let x0 = cur[0].max(r[0]);
let y0 = cur[1].max(r[1]);
let x1 = (cur[0] + cur[2]).min(r[0] + r[2]);
let y1 = (cur[1] + cur[3]).min(r[1] + r[3]);
self.stack.push(ClipFrame::Rect([x0, y0, (x1 - x0).max(0.0), (y1 - y0).max(0.0)]));
}
fn push_rounded_rect_device(&mut self, r: [f32; 4], mesh: Arc<TessMesh>, transform: Affine) -> u32 {
let cur = self.current();
let x0 = cur[0].max(r[0]);
let y0 = cur[1].max(r[1]);
let x1 = (cur[0] + cur[2]).min(r[0] + r[2]);
let y1 = (cur[1] + cur[3]).min(r[1] + r[3]);
let rect_device = [x0, y0, (x1 - x0).max(0.0), (y1 - y0).max(0.0)];
self.rounded_depth += 1;
let depth = self.rounded_depth;
self.stack.push(ClipFrame::Rounded { rect_device, stencil_depth: depth, mesh, transform });
depth
}
fn pop(&mut self) -> Option<(Arc<TessMesh>, Affine, u32)> {
if self.stack.len() <= 1 {
return None;
}
match self.stack.pop().expect("length > 1 checked above") {
ClipFrame::Rounded { mesh, transform, stencil_depth, rect_device: _ } => {
self.rounded_depth -= 1;
Some((mesh, transform, stencil_depth))
}
ClipFrame::Rect(_) => None,
}
}
fn force_close_all_rounded(&mut self) -> Vec<(Arc<TessMesh>, Affine, u32)> {
let mut closed = Vec::new();
while let Some(entry) = self.pop() {
closed.push(entry);
}
closed
}
fn active_rounded_frames_for_replay(&self) -> Vec<(Arc<TessMesh>, Affine, u32, [f32; 4])> {
let mut out = Vec::new();
for i in 0..self.stack.len() {
if let ClipFrame::Rounded { mesh, transform, stencil_depth, .. } = &self.stack[i] {
let parent_bbox = match &self.stack[i - 1] {
ClipFrame::Rect(r) => *r,
ClipFrame::Rounded { rect_device, .. } => *rect_device,
};
out.push((mesh.clone(), *transform, *stencil_depth, parent_bbox));
}
}
out
}
}
#[inline]
fn active_clip(clip: &ClipStack, frame: &mut EncodedFrame) -> Option<[f32; 4]> {
let r = clip.current();
if r[2] <= 0.0 || r[3] <= 0.0 {
return None;
}
let depth = clip.rounded_depth();
frame.current_stencil_ref = if depth > 0 { Some(depth) } else { None };
Some(r)
}
struct LayerStack {
open: Vec<(BlendMode, f32)>,
suppressed: u32,
max_depth: usize,
}
enum LayerPopOutcome {
Popped { depth: u32, mode: BlendMode, alpha: f32 },
Suppressed,
Underflow,
}
impl LayerStack {
fn new(max_depth: usize) -> Self {
Self { open: Vec::new(), suppressed: 0, max_depth }
}
fn push(&mut self, mode: BlendMode, alpha: f32) -> Option<u32> {
if self.open.len() >= self.max_depth {
self.suppressed += 1;
return None;
}
self.open.push((mode, alpha));
Some(self.open.len() as u32)
}
fn pop(&mut self) -> LayerPopOutcome {
if self.suppressed > 0 {
self.suppressed -= 1;
return LayerPopOutcome::Suppressed;
}
let Some((mode, alpha)) = self.open.pop() else {
return LayerPopOutcome::Underflow;
};
let depth = self.open.len() as u32 + 1; LayerPopOutcome::Popped { depth, mode, alpha }
}
fn force_close_all(&mut self) -> Vec<(u32, BlendMode, f32)> {
let mut closed = Vec::new();
while let Some((mode, alpha)) = self.open.pop() {
let depth = self.open.len() as u32 + 1;
closed.push((depth, mode, alpha));
}
closed
}
}
fn degrade_blend_mode(mode: &BlendMode) {
if mode.mix != Mix::Normal {
degrade("native_blend_layer_mix_to_normal");
}
if mode.compose != Compose::SrcOver {
degrade("native_blend_layer_compose_to_srcover");
}
}
fn decompose_translate_scale(t: &Affine) -> (f64, f64, f64, f64) {
let c = t.as_coeffs();
(c[0], c[3], c[4], c[5])
}
fn transform_rect(rect: Rect, t: &Affine) -> (f64, f64, f64, f64, f64, f64) {
let (sx, sy, tx, ty) = decompose_translate_scale(t);
let x0 = rect.x0 * sx + tx;
let y0 = rect.y0 * sy + ty;
let x1 = rect.x1 * sx + tx;
let y1 = rect.y1 * sy + ty;
let (x0, x1) = if x0 <= x1 { (x0, x1) } else { (x1, x0) };
let (y0, y1) = if y0 <= y1 { (y0, y1) } else { (y1, y0) };
(x0, y0, x1 - x0, y1 - y0, sx.abs(), sy.abs())
}
#[inline]
fn project_local(p: [f32; 2], t: &Affine) -> [f32; 2] {
let mapped = *t * Point::new(p[0] as f64, p[1] as f64);
[mapped.x as f32, mapped.y as f32]
}
fn transform_point_full(t: &Affine, p: Point) -> Point {
let c = t.as_coeffs();
let (a, b, e, d, tx, ty) = (c[0], c[1], c[2], c[3], c[4], c[5]);
Point::new(a * p.x + e * p.y + tx, b * p.x + d * p.y + ty)
}
fn affine_scale_factors(t: &Affine) -> (f64, f64) {
let c = t.as_coeffs();
(c[0].hypot(c[1]), c[2].hypot(c[3]))
}
fn max_affine_scale(t: &Affine) -> f64 {
let c = t.as_coeffs();
let (a, b, cc, d) = (c[0], c[1], c[2], c[3]);
let sum_sq = a * a + b * b + cc * cc + d * d;
let det = a * d - b * cc;
let discriminant = (sum_sq * sum_sq - 4.0 * det * det).max(0.0);
((sum_sq + discriminant.sqrt()) * 0.5).sqrt()
}
fn local_tess_tolerance(transform: &Affine) -> f32 {
let scale = max_affine_scale(transform).max(f64::MIN_POSITIVE);
(crate::tessellate::TESS_TOLERANCE_PX / scale)
.min(f32::MAX as f64) as f32
}
fn tess_fill_scaled(
tess_cache: &mut TessCache,
path: &BezPath,
rule: FillRule,
transform: &Affine,
) -> Arc<TessMesh> {
let tolerance = local_tess_tolerance(transform);
let mesh = tess_cache.get_or_insert_fill_with_tolerance(path, rule, tolerance);
tess_cache.profile_mesh("fill", path, transform.as_coeffs(), tolerance, mesh.triangles.len());
mesh
}
fn affine_rotation_angle(t: &Affine) -> f64 {
let c = t.as_coeffs();
c[1].atan2(c[0])
}
fn decompose_similarity_raw(t: &Affine) -> (f64, f64, f64, f64, f64) {
let c = t.as_coeffs();
let (a, b, cc, d, e, f) = (c[0], c[1], c[2], c[3], c[4], c[5]);
(a.hypot(b), cc.hypot(d), b.atan2(a), e, f)
}
fn decompose_similarity(t: &Affine) -> Option<(f64, f64, f64, f64, f64)> {
let c = t.as_coeffs();
let (a, b, cc, d, _, _) = (c[0], c[1], c[2], c[3], c[4], c[5]);
const EPS: f64 = 1e-6;
let dot = a * cc + b * d;
if dot.abs() > EPS * (a.hypot(b) * cc.hypot(d)).max(1e-12) {
return None; }
let det = a * d - b * cc;
if det <= 0.0 {
return None; }
Some(decompose_similarity_raw(t))
}
#[inline]
fn degrade(kind: &'static str) {
metrics::counter!(KEY_RENDER_PRIMITIVES, "kind" => kind).increment(1);
}
enum BrushKind {
Solid,
Gradient,
Image,
}
#[inline]
fn first_stop_color(gradient: &Gradient) -> Color {
gradient
.stops
.first()
.map(|s| s.color.to_alpha_color::<peniko::color::Srgb>())
.unwrap_or(Color::from_rgba8(0, 0, 0, 0))
}
fn resolve_brush_color(brush: &Brush) -> (Color, BrushKind) {
match brush {
Brush::Solid(c) => (*c, BrushKind::Solid),
Brush::Gradient(g) => (first_stop_color(g), BrushKind::Gradient),
Brush::Image(_) => (Color::from_rgba8(0, 0, 0, 0), BrushKind::Image),
}
}
#[inline]
fn packed_color(c: Color) -> u32 {
let p = c.to_rgba8();
pack_rgba8([p.r, p.g, p.b, p.a])
}
fn uniform_radius(radii: &Option<[f32; 4]>) -> Option<f32> {
match radii {
None => Some(0.0),
Some(r) => {
if r.iter().any(|v| (*v - r[0]).abs() > 0.01) {
None
} else {
Some(r[0].max(0.0))
}
}
}
}
#[inline]
fn rect_center(rect: Rect) -> Point {
rect.center()
}
fn local_stroke_width(stroke_width: f32, transform: &Affine) -> f32 {
let (sx, sy) = affine_scale_factors(transform);
let avg_scale = ((sx.abs() + sy.abs()) * 0.5).max(1e-6);
(stroke_width as f64 / avg_scale) as f32
}
fn tess_stroke_scaled(tess_cache: &mut TessCache, path: &BezPath, stroke: &Stroke, transform: &Affine) -> Arc<TessMesh> {
let adjusted = Stroke { width: local_stroke_width(stroke.width, transform), ..stroke.clone() };
let tolerance = local_tess_tolerance(transform);
let mesh = tess_cache.get_or_insert_stroke_with_tolerance(path, &adjusted, tolerance);
tess_cache.profile_mesh("stroke", path, transform.as_coeffs(), tolerance, mesh.triangles.len());
mesh
}
fn resolve_dash(path: &BezPath, stroke: &Stroke) -> (BezPath, Stroke) {
match &stroke.dash {
Some(dash) => {
let dashed = uzor_urx_core::dash::dash_path(path, dash);
(dashed, Stroke { dash: None, ..stroke.clone() })
}
None => (path.clone(), stroke.clone()),
}
}
fn emit_stencil_mask_batch(
frame: &mut EncodedFrame,
mesh: &TessMesh,
transform: &Affine,
clip_rect: [f32; 4],
op: MaskOp,
gate_ref: u32,
) {
const DUMMY_COLOR: u32 = 0xFFFF_FFFF;
if frame.geometry_capacity_error.is_some() {
return;
}
let start = frame.stencil_masks.len() as u32;
match try_reserve_geometry(
&mut frame.stencil_masks,
mesh.triangles.len(),
"stencil_mask",
) {
Ok(reserve) => frame.geometry_profile.record(reserve),
Err(error) => {
frame.geometry_capacity_error = Some(error);
return;
}
}
for tri in &mesh.triangles {
frame.stencil_masks.push(TriInstance {
v0: project_local(tri[0], transform),
v1: project_local(tri[1], transform),
v2: project_local(tri[2], transform),
color0: DUMMY_COLOR,
color1: DUMMY_COLOR,
color2: DUMMY_COLOR,
_pad0: 0.0,
clip_rect,
});
}
frame.bump_batch_by(BatchKind::StencilMask(op), start, mesh.triangles.len() as u32, Some(gate_ref));
}
#[cfg(test)]
pub(crate) fn encode_scene(
scene: &Scene,
viewport: Viewport,
tess_cache: &mut TessCache,
atlas: Option<&mut NativeGlyphAtlas>,
lut: Option<&mut GradientLutAtlas>,
blend_layer_max_depth: usize,
text_gamma_enabled: bool,
) -> EncodedFrame {
encode_scene_reusing(
EncodedFrame::default(),
scene,
viewport,
tess_cache,
atlas,
lut,
blend_layer_max_depth,
text_gamma_enabled,
)
}
pub(crate) fn encode_scene_reusing(
mut frame: EncodedFrame,
scene: &Scene,
viewport: Viewport,
tess_cache: &mut TessCache,
mut atlas: Option<&mut NativeGlyphAtlas>,
mut lut: Option<&mut GradientLutAtlas>,
blend_layer_max_depth: usize,
text_gamma_enabled: bool,
) -> EncodedFrame {
frame.reset();
let mut clip = ClipStack::new(viewport);
let mut layer_stack = LayerStack::new(blend_layer_max_depth);
for cmd in &scene.commands {
if let Err(ValidationIssue::NonFinite) = validate_command(cmd) {
metrics::counter!(KEY_RENDER_SKIPPED_NONFINITE).increment(1);
continue;
}
match cmd {
DrawCommand::PushClipRect { rect, transform } => {
let (x0, y0, w, h, _, _) = transform_rect(*rect, transform);
clip.push_rect_device([x0 as f32, y0 as f32, w as f32, h as f32]);
}
DrawCommand::PushClipRoundedRect { rect, transform } => {
let (x0, y0, w, h, _, _) = transform_rect(rect.rect(), transform);
let parent_clip_rect = clip.current(); let tolerance = local_tess_tolerance(transform);
let path = (*rect).into_path(tolerance as f64);
let mesh = tess_fill_scaled(tess_cache, &path, FillRule::NonZero, transform);
let parent_depth = clip.rounded_depth();
let new_depth =
clip.push_rounded_rect_device([x0 as f32, y0 as f32, w as f32, h as f32], mesh.clone(), *transform);
debug_assert_eq!(new_depth, parent_depth + 1, "push must increment depth by exactly 1");
emit_stencil_mask_batch(&mut frame, &mesh, transform, parent_clip_rect, MaskOp::Increment, parent_depth);
frame.has_rounded_clip = true;
}
DrawCommand::PopClip => {
if let Some((mesh, transform, depth)) = clip.pop() {
let parent_clip_rect = clip.current();
emit_stencil_mask_batch(&mut frame, &mesh, &transform, parent_clip_rect, MaskOp::Decrement, depth);
}
}
DrawCommand::GlyphRun { glyphs, font, font_size, brush, transform, text: _ } => {
let Some(clip_rect) = active_clip(&clip, &mut frame) else { continue };
encode_glyph_run(
&mut frame,
atlas.as_deref_mut(),
*font,
*font_size,
glyphs,
brush,
transform,
clip_rect,
text_gamma_enabled,
);
}
DrawCommand::Image { src, src_rect, dest, transform } => {
let Some(clip_rect) = active_clip(&clip, &mut frame) else { continue };
encode_image(&mut frame, *src, *src_rect, *dest, transform, clip_rect);
}
DrawCommand::PushBlendLayer { mode, alpha, transform } => {
if *transform != Affine::IDENTITY {
degrade("native_blend_layer_transform_ignored");
}
match layer_stack.push(*mode, *alpha) {
Some(depth) => {
frame.push_marker(FrameOp::PushLayer { depth });
for (mesh, transform, stencil_depth, parent_bbox) in clip.active_rounded_frames_for_replay() {
emit_stencil_mask_batch(
&mut frame,
&mesh,
&transform,
parent_bbox,
MaskOp::Increment,
stencil_depth - 1,
);
}
}
None => degrade("native_blend_layer_depth_exceeded"),
}
}
DrawCommand::PopBlendLayer => match layer_stack.pop() {
LayerPopOutcome::Popped { depth, mode, alpha } => {
degrade_blend_mode(&mode);
let clip_rect = clip.current();
let stencil_ref = if clip.rounded_depth() > 0 { Some(clip.rounded_depth()) } else { None };
frame.push_marker(FrameOp::PopLayer { depth, stencil_ref });
frame.composites.push(BlendCompositeInstance { alpha, _pad: [0.0; 3], clip_rect });
}
LayerPopOutcome::Suppressed => {}
LayerPopOutcome::Underflow => degrade("native_blend_layer_pop_underflow"),
},
DrawCommand::FillRect { rect, radii, brush, transform } => {
let Some(clip_rect) = active_clip(&clip, &mut frame) else { continue };
encode_fill_rect(&mut frame, tess_cache, lut.as_deref_mut(), *rect, radii, brush, transform, clip_rect);
}
DrawCommand::StrokeRect { rect, radii, stroke, brush, transform } => {
let Some(clip_rect) = active_clip(&clip, &mut frame) else { continue };
encode_stroke_rect(&mut frame, tess_cache, lut.as_deref_mut(), *rect, radii, stroke, brush, transform, clip_rect);
}
DrawCommand::Line { from, to, stroke, brush, transform } => {
let Some(clip_rect) = active_clip(&clip, &mut frame) else { continue };
encode_line(&mut frame, tess_cache, *from, *to, stroke, brush, transform, clip_rect);
}
DrawCommand::LineBatch { segments, stroke, brush, transform } => {
let Some(clip_rect) = active_clip(&clip, &mut frame) else { continue };
encode_line_batch(
&mut frame,
tess_cache,
segments,
stroke,
brush,
transform,
clip_rect,
);
}
DrawCommand::FillPath { path, rule, brush, transform } => {
let Some(clip_rect) = active_clip(&clip, &mut frame) else { continue };
encode_fill_path(&mut frame, tess_cache, lut.as_deref_mut(), path, *rule, brush, transform, clip_rect);
}
DrawCommand::StrokePath { path, stroke, brush, transform } => {
let Some(clip_rect) = active_clip(&clip, &mut frame) else { continue };
encode_stroke_path(&mut frame, tess_cache, lut.as_deref_mut(), path, stroke, brush, transform, clip_rect);
}
}
}
for (depth, mode, alpha) in layer_stack.force_close_all() {
degrade_blend_mode(&mode);
let clip_rect = clip.current();
let stencil_ref = if clip.rounded_depth() > 0 { Some(clip.rounded_depth()) } else { None };
frame.push_marker(FrameOp::PopLayer { depth, stencil_ref });
frame.composites.push(BlendCompositeInstance { alpha, _pad: [0.0; 3], clip_rect });
degrade("native_blend_layer_force_closed_at_scene_end");
}
for (mesh, transform, depth) in clip.force_close_all_rounded() {
let parent_clip_rect = clip.current();
emit_stencil_mask_batch(&mut frame, &mesh, &transform, parent_clip_rect, MaskOp::Decrement, depth);
degrade("native_rounded_clip_force_closed_at_scene_end");
}
frame
}
fn encode_fill_rect(
frame: &mut EncodedFrame,
tess_cache: &mut TessCache,
lut: Option<&mut GradientLutAtlas>,
rect: Rect,
radii: &Option<[f32; 4]>,
brush: &Brush,
transform: &Affine,
clip_rect: [f32; 4],
) {
match brush {
Brush::Solid(c) => encode_fill_rect_solid(frame, tess_cache, rect, radii, *c, transform, clip_rect),
Brush::Gradient(g) => {
let path = rect_bez_path(rect, radii, local_tess_tolerance(transform) as f64);
let mesh = tess_fill_scaled(tess_cache, &path, FillRule::NonZero, transform);
emit_gradient_mesh(frame, lut, &mesh, g, transform, clip_rect);
}
Brush::Image(_) => {
degrade("native_fill_rect_image_dropped");
}
}
}
fn encode_fill_rect_solid(
frame: &mut EncodedFrame,
tess_cache: &mut TessCache,
rect: Rect,
radii: &Option<[f32; 4]>,
color: Color,
transform: &Affine,
clip_rect: [f32; 4],
) {
if let Some(uniform_r) = uniform_radius(radii) {
if let Some((sx, sy, angle, _, _)) = decompose_similarity(transform) {
let normalized = rect.abs();
let w = normalized.width() * sx;
let h = normalized.height() * sy;
if w <= 0.0 || h <= 0.0 {
return;
}
let scale = ((sx + sy) * 0.5) as f32;
let center = transform_point_full(transform, rect_center(normalized));
let pos = [(center.x - w * 0.5) as f32, (center.y - h * 0.5) as f32];
frame.push_quad(QuadInstance {
pos,
size: [w as f32, h as f32],
color: packed_color(color),
border_color: 0,
corner_radius: uniform_r * scale,
border_width: 0.0,
_pad0: [angle as f32, 0.0],
clip_rect,
});
return;
}
degrade("native_rect_shear_to_triangle_pipeline");
}
let path = rect_bez_path(rect, radii, local_tess_tolerance(transform) as f64);
let mesh = tess_fill_scaled(tess_cache, &path, FillRule::NonZero, transform);
emit_solid_mesh(frame, &mesh, transform, packed_color(color), clip_rect);
}
fn rect_bez_path(rect: Rect, radii: &Option<[f32; 4]>, tolerance: f64) -> BezPath {
if let Some(r) = radii {
if r.iter().any(|v| *v > 0.0) {
let rr = RoundedRect::from_rect(
rect,
RoundedRectRadii::new(
r[0].max(0.0) as f64,
r[1].max(0.0) as f64,
r[2].max(0.0) as f64,
r[3].max(0.0) as f64,
),
);
return rr.into_path(tolerance);
}
}
let mut path = BezPath::new();
path.move_to((rect.x0, rect.y0));
path.line_to((rect.x1, rect.y0));
path.line_to((rect.x1, rect.y1));
path.line_to((rect.x0, rect.y1));
path.close_path();
path
}
struct GradientDeviceParams {
p0: [f32; 2],
p1: f32,
p2: f32,
kind_extend: u32,
}
fn transform_gradient_params(kind: &GradientKind, extend: Extend, transform: &Affine) -> GradientDeviceParams {
let extend_bits: u32 = match extend {
Extend::Pad => 0,
Extend::Repeat => 1,
Extend::Reflect => 2,
};
match kind {
GradientKind::Radial(pos) => {
let center = transform_point_full(transform, pos.end_center);
let (sx, sy) = affine_scale_factors(transform);
let radius = (pos.end_radius as f64 * (sx + sy) * 0.5).max(1e-3);
GradientDeviceParams {
p0: [center.x as f32, center.y as f32],
p1: radius as f32,
p2: 0.0,
kind_extend: (extend_bits << 2), }
}
GradientKind::Sweep(pos) => {
let center = transform_point_full(transform, pos.center);
let rot = affine_rotation_angle(transform);
GradientDeviceParams {
p0: [center.x as f32, center.y as f32],
p1: (pos.start_angle as f64 + rot) as f32,
p2: (pos.end_angle as f64 + rot) as f32,
kind_extend: 1u32 | (extend_bits << 2),
}
}
GradientKind::Linear(pos) => {
let start = transform_point_full(transform, pos.start);
let end = transform_point_full(transform, pos.end);
GradientDeviceParams {
p0: [start.x as f32, start.y as f32],
p1: (end.x - start.x) as f32,
p2: (end.y - start.y) as f32,
kind_extend: 2u32 | (extend_bits << 2), }
}
}
}
fn emit_gradient_lut_triangles(
frame: &mut EncodedFrame,
lut: Option<&mut GradientLutAtlas>,
mesh: &TessMesh,
gradient: &Gradient,
transform: &Affine,
clip_rect: [f32; 4],
) {
let row = match lut {
Some(lut) => lut.get_or_insert(&gradient.stops, gradient.extend),
None => None,
};
let Some(row) = row else {
degrade("native_gradient_lut_full_this_frame");
emit_solid_mesh(frame, mesh, transform, packed_color(first_stop_color(gradient)), clip_rect);
return;
};
let params = transform_gradient_params(&gradient.kind, gradient.extend, transform);
if frame.geometry_capacity_error.is_some() {
return;
}
let start = frame.gradients.len() as u32;
match try_reserve_geometry(&mut frame.gradients, mesh.triangles.len(), "gradient") {
Ok(reserve) => frame.geometry_profile.record(reserve),
Err(error) => {
frame.geometry_capacity_error = Some(error);
return;
}
}
for tri in &mesh.triangles {
frame.gradients.push(GradientInstance {
v0: project_local(tri[0], transform),
v1: project_local(tri[1], transform),
v2: project_local(tri[2], transform),
p0: params.p0,
p1: params.p1,
p2: params.p2,
kind_extend: params.kind_extend,
lut_row: row,
clip_rect,
});
}
let stencil_ref = frame.current_stencil_ref;
frame.bump_batch_by(BatchKind::Gradient, start, mesh.triangles.len() as u32, stencil_ref);
}
fn emit_gradient_mesh(
frame: &mut EncodedFrame,
lut: Option<&mut GradientLutAtlas>,
mesh: &TessMesh,
gradient: &Gradient,
transform: &Affine,
clip_rect: [f32; 4],
) {
if let GradientKind::Radial(pos) = &gradient.kind {
let dx_c = (pos.end_center.x - pos.start_center.x).abs();
let dy_c = (pos.end_center.y - pos.start_center.y).abs();
if dx_c > 0.5 || dy_c > 0.5 || pos.start_radius.abs() > 0.5 {
degrade("gradient_radial_focal_degraded");
}
}
emit_gradient_lut_triangles(frame, lut, mesh, gradient, transform, clip_rect);
}
fn encode_stroke_rect(
frame: &mut EncodedFrame,
tess_cache: &mut TessCache,
lut: Option<&mut GradientLutAtlas>,
rect: Rect,
radii: &Option<[f32; 4]>,
stroke: &Stroke,
brush: &Brush,
transform: &Affine,
clip_rect: [f32; 4],
) {
if stroke.width <= 0.0 {
return;
}
if let Brush::Gradient(g) = brush {
let path = rect_bez_path(rect, radii, local_tess_tolerance(transform) as f64);
let (path, stroke) = resolve_dash(&path, stroke);
let mesh = tess_stroke_scaled(tess_cache, &path, &stroke, transform);
emit_gradient_mesh(frame, lut, &mesh, g, transform, clip_rect);
return;
}
let (color, kind) = resolve_brush_color(brush);
if matches!(kind, BrushKind::Image) {
degrade("native_strokerect_image_to_solid");
}
if stroke.dash.is_none() {
if let Some(uniform_r) = uniform_radius(radii) {
if let Some((sx, sy, angle, _, _)) = decompose_similarity(transform) {
let normalized = rect.abs();
let w = normalized.width() * sx;
let h = normalized.height() * sy;
if w <= 0.0 || h <= 0.0 {
return;
}
let scale = ((sx + sy) * 0.5) as f32;
let center = transform_point_full(transform, rect_center(normalized));
let pos = [(center.x - w * 0.5) as f32, (center.y - h * 0.5) as f32];
let border_width = stroke.width.max(0.0);
frame.push_quad(QuadInstance {
pos,
size: [w as f32, h as f32],
color: 0,
border_color: packed_color(color),
corner_radius: uniform_r * scale,
border_width,
_pad0: [angle as f32, 0.0],
clip_rect,
});
return;
}
degrade("native_rect_shear_to_triangle_pipeline");
}
} else {
degrade("native_strokerect_dash_to_triangle_pipeline");
}
let path = rect_bez_path(rect, radii, local_tess_tolerance(transform) as f64);
let (path, stroke) = resolve_dash(&path, stroke);
let mesh = tess_stroke_scaled(tess_cache, &path, &stroke, transform);
emit_solid_mesh(frame, &mesh, transform, packed_color(color), clip_rect);
}
fn encode_line(
frame: &mut EncodedFrame,
tess_cache: &mut TessCache,
from: Vec2,
to: Vec2,
stroke: &Stroke,
brush: &Brush,
transform: &Affine,
clip_rect: [f32; 4],
) {
let (color, kind) = resolve_brush_color(brush);
match kind {
BrushKind::Gradient => degrade("native_line_gradient_to_solid"),
BrushKind::Image => degrade("native_line_image_to_solid"),
BrushKind::Solid => {}
}
let width = stroke.width.max(0.0); if width <= 0.0 {
return;
}
if stroke.dash.is_some() {
degrade("native_line_dash_to_triangle_pipeline");
let mut path = BezPath::new();
path.move_to(Point::new(from.x, from.y));
path.line_to(Point::new(to.x, to.y));
let (path, stroke) = resolve_dash(&path, stroke);
let mesh = tess_stroke_scaled(tess_cache, &path, &stroke, transform);
emit_solid_mesh(frame, &mesh, transform, packed_color(color), clip_rect);
return;
}
let from_p = transform_point_full(transform, Point::new(from.x, from.y));
let to_p = transform_point_full(transform, Point::new(to.x, to.y));
let start = [from_p.x as f32, from_p.y as f32];
let end = [to_p.x as f32, to_p.y as f32];
let cap_flags = match stroke.cap {
LineCap::Round => 0.0,
LineCap::Butt => 3.0,
LineCap::Square => {
degrade("native_line_square_cap_to_round");
0.0
}
};
frame.push_line(LineInstance {
start,
end,
color: packed_color(color),
width,
cap_flags,
clip_rect,
});
}
fn encode_line_batch(
frame: &mut EncodedFrame,
tess_cache: &mut TessCache,
segments: &[uzor_urx_core::scene::LineBatchSegment],
stroke: &Stroke,
brush: &Brush,
transform: &Affine,
clip_rect: [f32; 4],
) {
if segments.is_empty() {
return;
}
if stroke.dash.is_some() || !matches!(brush, Brush::Solid(_)) {
for segment in segments {
encode_line(
frame,
tess_cache,
segment.from,
segment.to,
stroke,
brush,
transform,
clip_rect,
);
}
return;
}
let Brush::Solid(color) = brush else {
unreachable!("non-solid brushes returned through encode_line above");
};
let width = stroke.width.max(0.0);
if width <= 0.0 {
return;
}
let cap_flags = match stroke.cap {
LineCap::Round => 0.0,
LineCap::Butt => 3.0,
LineCap::Square => {
degrade("native_line_square_cap_to_round");
0.0
}
};
let start = frame.lines.len() as u32;
match try_reserve_geometry(&mut frame.lines, segments.len(), "line") {
Ok(reserve) => frame.geometry_profile.record(reserve),
Err(error) => {
frame.geometry_capacity_error = Some(error);
return;
}
}
let color = packed_color(*color);
if *transform == Affine::IDENTITY {
for segment in segments {
frame.lines.push(LineInstance {
start: [segment.from.x as f32, segment.from.y as f32],
end: [segment.to.x as f32, segment.to.y as f32],
color,
width,
cap_flags,
clip_rect,
});
}
} else {
for segment in segments {
let from = transform_point_full(
transform,
Point::new(segment.from.x, segment.from.y),
);
let to = transform_point_full(
transform,
Point::new(segment.to.x, segment.to.y),
);
frame.lines.push(LineInstance {
start: [from.x as f32, from.y as f32],
end: [to.x as f32, to.y as f32],
color,
width,
cap_flags,
clip_rect,
});
}
}
let stencil_ref = frame.current_stencil_ref;
frame.bump_batch_by(BatchKind::Line, start, segments.len() as u32, stencil_ref);
}
fn emit_solid_mesh(
frame: &mut EncodedFrame,
mesh: &crate::tessellate::TessMesh,
transform: &Affine,
packed: u32,
clip_rect: [f32; 4],
) {
if frame.geometry_capacity_error.is_some() {
return;
}
let start = frame.triangles.len() as u32;
match try_reserve_geometry(&mut frame.triangles, mesh.triangles.len(), "path") {
Ok(reserve) => frame.geometry_profile.record(reserve),
Err(error) => {
frame.geometry_capacity_error = Some(error);
return;
}
}
for tri in &mesh.triangles {
frame.triangles.push(TriInstance {
v0: project_local(tri[0], transform),
v1: project_local(tri[1], transform),
v2: project_local(tri[2], transform),
color0: packed,
color1: packed,
color2: packed,
_pad0: 0.0,
clip_rect,
});
}
let stencil_ref = frame.current_stencil_ref;
frame.bump_batch_by(BatchKind::Triangle, start, mesh.triangles.len() as u32, stencil_ref);
}
fn encode_fill_path(
frame: &mut EncodedFrame,
tess_cache: &mut TessCache,
lut: Option<&mut GradientLutAtlas>,
path: &BezPath,
rule: FillRule,
brush: &Brush,
transform: &Affine,
clip_rect: [f32; 4],
) {
if let Brush::Gradient(g) = brush {
let mesh = tess_fill_scaled(tess_cache, path, rule, transform);
emit_gradient_mesh(frame, lut, &mesh, g, transform, clip_rect);
return;
}
let (color, kind) = resolve_brush_color(brush);
if matches!(kind, BrushKind::Image) {
degrade("native_fillpath_image_to_solid");
}
let mesh = tess_fill_scaled(tess_cache, path, rule, transform);
emit_solid_mesh(frame, &mesh, transform, packed_color(color), clip_rect);
}
fn encode_stroke_path(
frame: &mut EncodedFrame,
tess_cache: &mut TessCache,
lut: Option<&mut GradientLutAtlas>,
path: &BezPath,
stroke: &Stroke,
brush: &Brush,
transform: &Affine,
clip_rect: [f32; 4],
) {
if stroke.width <= 0.0 {
return;
}
if try_encode_independent_solid_line_path(frame, path, stroke, brush, transform, clip_rect) {
return;
}
let (path, stroke) = resolve_dash(path, stroke);
if let Brush::Gradient(g) = brush {
let mesh = tess_stroke_scaled(tess_cache, &path, &stroke, transform);
emit_gradient_mesh(frame, lut, &mesh, g, transform, clip_rect);
return;
}
let (color, kind) = resolve_brush_color(brush);
if matches!(kind, BrushKind::Image) {
degrade("native_strokepath_image_to_solid");
}
let mesh = tess_stroke_scaled(tess_cache, &path, &stroke, transform);
emit_solid_mesh(frame, &mesh, transform, packed_color(color), clip_rect);
}
fn try_encode_independent_solid_line_path(
frame: &mut EncodedFrame,
path: &BezPath,
stroke: &Stroke,
brush: &Brush,
transform: &Affine,
clip_rect: [f32; 4],
) -> bool {
if stroke.dash.is_some() {
return false;
}
let Brush::Solid(color) = brush else {
return false;
};
let cap_flags = match stroke.cap {
LineCap::Round => 0.0,
LineCap::Butt => 3.0,
LineCap::Square => return false,
};
let Some((scale_x, scale_y, _, _, _)) = decompose_similarity(transform) else {
return false;
};
if (scale_x - scale_y).abs() > 1e-6 * scale_x.max(scale_y).max(1.0) {
return false;
}
let elements = path.elements();
if elements.len() % 2 != 0 {
return false;
}
if !elements.chunks_exact(2).all(|pair| {
matches!(pair, [PathEl::MoveTo(_), PathEl::LineTo(_)])
}) {
return false;
}
let segment_count = elements.len() / 2;
if segment_count == 0 {
return true;
}
let start = frame.lines.len() as u32;
match try_reserve_geometry(&mut frame.lines, segment_count, "line") {
Ok(reserve) => frame.geometry_profile.record(reserve),
Err(error) => {
frame.geometry_capacity_error = Some(error);
return true;
}
}
let packed = packed_color(*color);
for pair in elements.chunks_exact(2) {
let [PathEl::MoveTo(from), PathEl::LineTo(to)] = pair else {
unreachable!("independent line path was validated above");
};
let from = transform_point_full(transform, *from);
let to = transform_point_full(transform, *to);
frame.lines.push(LineInstance {
start: [from.x as f32, from.y as f32],
end: [to.x as f32, to.y as f32],
color: packed,
width: stroke.width,
cap_flags,
clip_rect,
});
}
let stencil_ref = frame.current_stencil_ref;
frame.bump_batch_by(BatchKind::Line, start, segment_count as u32, stencil_ref);
true
}
fn encode_glyph_run(
frame: &mut EncodedFrame,
atlas: Option<&mut NativeGlyphAtlas>,
font: FontId,
font_size: f32,
glyphs: &[Glyph],
brush: &Brush,
transform: &Affine,
clip_rect: [f32; 4],
text_gamma_enabled: bool,
) {
let (color, kind) = resolve_brush_color(brush);
match kind {
BrushKind::Gradient => degrade("native_glyphrun_gradient_to_solid"),
BrushKind::Image => degrade("native_glyphrun_image_to_solid"),
BrushKind::Solid => {}
}
let packed = packed_color(color);
let gamma_bin: f32 = if text_gamma_enabled {
let rgba = color.to_rgba8();
uzor_urx_core::text_gamma::luma_bin([rgba.r, rgba.g, rgba.b, rgba.a]) as f32
} else {
0.0
};
let coeffs = transform.as_coeffs();
let (tx, ty) = (coeffs[4] as f32, coeffs[5] as f32);
let mut atlas = atlas;
for g in glyphs {
let px = tx + g.x;
let py = ty + g.y;
let subpx = uzor_urx_glyph::subpixel_bin_for_x(px);
let key = uzor_urx_glyph::GlyphKey::new(font, g.glyph_id, font_size, subpx);
let bitmap = match uzor_urx_glyph::rasterise_glyph(font, g.glyph_id as u16, font_size, subpx) {
Ok(bm) => bm,
Err(_) => {
degrade("native_glyph_rasterise_failed");
continue;
}
};
if bitmap.width == 0 || bitmap.height == 0 {
continue; }
let Some(uv_rect) = atlas.as_deref_mut().and_then(|a| a.get_or_insert(key, &bitmap)) else {
degrade("native_glyph_atlas_full_this_frame");
continue;
};
let dst_x0 = px.floor() + bitmap.left as f32;
let dst_y0 = py.round() - bitmap.top as f32;
frame.push_glyph(GlyphInstance {
pos: [dst_x0, dst_y0],
size: [bitmap.width as f32, bitmap.height as f32],
uv_pos: [uv_rect[0], uv_rect[1]],
uv_size: [uv_rect[2], uv_rect[3]],
color: packed,
_pad0: gamma_bin,
clip_rect,
});
}
metrics::counter!(KEY_RENDER_GLYPH_INSTANCES).increment(glyphs.len() as u64);
}
fn encode_image(
frame: &mut EncodedFrame,
src: ImageId,
src_rect: Option<Rect>,
dest: Rect,
transform: &Affine,
clip_rect: [f32; 4],
) {
let Some(data) = uzor_urx_image::lookup_image(src) else {
degrade("image_id_unknown");
return;
};
let img_w = data.width.max(1) as f64;
let img_h = data.height.max(1) as f64;
let src_box = src_rect.unwrap_or(Rect::new(0.0, 0.0, img_w, img_h));
if src_box.width() <= 0.0 || src_box.height() <= 0.0 {
return;
}
let uv_pos = [(src_box.x0 / img_w) as f32, (src_box.y0 / img_h) as f32];
let uv_size = [(src_box.width() / img_w) as f32, (src_box.height() / img_h) as f32];
let (sx, sy, angle) = match decompose_similarity(transform) {
Some((sx, sy, angle, _, _)) => (sx, sy, angle),
None => {
degrade("native_image_shear_to_rotation_approx");
let (sx, sy, angle, _, _) = decompose_similarity_raw(transform);
(sx, sy, angle)
}
};
let w = dest.width() * sx;
let h = dest.height() * sy;
if w <= 0.0 || h <= 0.0 {
return;
}
let dest_center = Point::new(dest.x0 + dest.width() * 0.5, dest.y0 + dest.height() * 0.5);
let center = transform_point_full(transform, dest_center);
let pos = [(center.x - w * 0.5) as f32, (center.y - h * 0.5) as f32];
frame.push_image(
ImageInstance {
pos,
size: [w as f32, h as f32],
uv_pos,
uv_size,
rotation: angle as f32,
tint: 0xFFFF_FFFF,
clip_rect,
},
src,
);
}
#[cfg(test)]
mod tests {
use super::*;
use peniko::{ColorStops, LinearGradientPosition};
use uzor_urx_core::scene::Stroke as SceneStroke;
fn viewport() -> Viewport {
Viewport { width: 100, height: 100 }
}
fn cache() -> TessCache {
TessCache::new()
}
fn max_depth() -> usize {
8
}
#[test]
fn fill_rect_solid_emits_one_quad() {
let mut scene = Scene::new();
scene.fill_rect_solid(Rect::new(10.0, 10.0, 50.0, 50.0), Color::from_rgba8(255, 0, 0, 255));
let frame = encode_scene(&scene, viewport(), &mut cache(), None, None, max_depth(), false);
assert_eq!(frame.quads.len(), 1);
assert_eq!(frame.quads[0].pos, [10.0, 10.0]);
assert_eq!(frame.quads[0].size, [40.0, 40.0]);
assert_eq!(frame.quads[0].border_width, 0.0);
}
#[test]
fn fill_rect_with_uniform_radii_sets_corner_radius_without_degrade() {
let mut scene = Scene::new();
scene.push(DrawCommand::FillRect {
rect: Rect::new(0.0, 0.0, 40.0, 40.0),
radii: Some([8.0, 8.0, 8.0, 8.0]),
brush: Brush::Solid(Color::from_rgba8(0, 255, 0, 255)),
transform: Affine::IDENTITY,
});
let frame = encode_scene(&scene, viewport(), &mut cache(), None, None, max_depth(), false);
assert_eq!(frame.quads.len(), 1);
assert!((frame.quads[0].corner_radius - 8.0).abs() < 0.01);
}
#[test]
fn stroke_rect_border_width_stays_device_constant_at_2x_scale() {
let stroke_rect = |transform: Affine| DrawCommand::StrokeRect {
rect: Rect::new(0.0, 0.0, 40.0, 40.0),
radii: None,
stroke: SceneStroke { width: 3.0, ..SceneStroke::default() },
brush: Brush::Solid(Color::from_rgba8(0, 0, 255, 255)),
transform,
};
let mut scene_1x = Scene::new();
scene_1x.push(stroke_rect(Affine::IDENTITY));
let frame_1x = encode_scene(&scene_1x, viewport(), &mut cache(), None, None, max_depth(), false);
let mut scene_2x = Scene::new();
scene_2x.push(stroke_rect(Affine::scale(2.0)));
let frame_2x = encode_scene(&scene_2x, viewport(), &mut cache(), None, None, max_depth(), false);
assert_eq!(frame_1x.quads.len(), 1);
assert_eq!(frame_2x.quads.len(), 1);
assert!((frame_1x.quads[0].border_width - 3.0).abs() < 0.01);
assert!(
(frame_1x.quads[0].border_width - frame_2x.quads[0].border_width).abs() < 0.01,
"border_width must be DEVICE-CONSTANT regardless of transform scale: 1x={} 2x={}",
frame_1x.quads[0].border_width,
frame_2x.quads[0].border_width
);
assert_eq!(frame_2x.quads[0].size, [80.0, 80.0]);
}
#[test]
fn rotated_uniform_radius_rect_routes_through_quad_sdf_not_triangle() {
let mut scene = Scene::new();
scene.push(DrawCommand::FillRect {
rect: Rect::new(10.0, 10.0, 30.0, 30.0),
radii: Some([4.0, 4.0, 4.0, 4.0]),
brush: Brush::Solid(Color::from_rgba8(255, 0, 0, 255)),
transform: Affine::rotate(std::f64::consts::FRAC_PI_4),
});
let frame = encode_scene(&scene, viewport(), &mut cache(), None, None, max_depth(), false);
assert_eq!(frame.quads.len(), 1, "a similarity transform with UNIFORM radii must stay on Quad SDF");
assert!(frame.triangles.is_empty(), "must NOT route through the Triangle pipeline");
assert!(
(frame.quads[0]._pad0[0] - std::f64::consts::FRAC_PI_4 as f32).abs() < 0.001,
"rotation angle must be baked into _pad0[0]: got {}",
frame.quads[0]._pad0[0]
);
}
#[test]
fn stroke_rect_emits_transparent_fill_with_border() {
let mut scene = Scene::new();
scene.push(DrawCommand::StrokeRect {
rect: Rect::new(0.0, 0.0, 40.0, 40.0),
radii: None,
stroke: SceneStroke { width: 3.0, ..SceneStroke::default() },
brush: Brush::Solid(Color::from_rgba8(0, 0, 255, 255)),
transform: Affine::IDENTITY,
});
let frame = encode_scene(&scene, viewport(), &mut cache(), None, None, max_depth(), false);
assert_eq!(frame.quads.len(), 1);
assert_eq!(frame.quads[0].color, 0);
assert!((frame.quads[0].border_width - 3.0).abs() < 0.01);
assert_ne!(frame.quads[0].border_color, 0);
}
#[test]
fn zero_width_stroke_emits_nothing() {
let mut scene = Scene::new();
scene.push(DrawCommand::StrokeRect {
rect: Rect::new(0.0, 0.0, 40.0, 40.0),
radii: None,
stroke: SceneStroke { width: 0.0, ..SceneStroke::default() },
brush: Brush::Solid(Color::from_rgba8(0, 0, 255, 255)),
transform: Affine::IDENTITY,
});
let frame = encode_scene(&scene, viewport(), &mut cache(), None, None, max_depth(), false);
assert!(frame.quads.is_empty());
}
#[test]
fn degenerate_rect_is_skipped_not_panicked() {
let mut scene = Scene::new();
scene.push(DrawCommand::FillRect {
rect: Rect::new(10.0, 10.0, 10.0, 10.0),
radii: None,
brush: Brush::Solid(Color::from_rgba8(255, 0, 0, 255)),
transform: Affine::IDENTITY,
});
let frame = encode_scene(&scene, viewport(), &mut cache(), None, None, max_depth(), false);
assert!(frame.quads.is_empty());
}
#[test]
fn line_solid_emits_one_line_not_a_quad() {
let mut scene = Scene::new();
scene.line_solid(
Vec2 { x: 0.0, y: 0.0 },
Vec2 { x: 10.0, y: 10.0 },
2.0,
Color::from_rgba8(255, 255, 255, 255),
);
let frame = encode_scene(&scene, viewport(), &mut cache(), None, None, max_depth(), false);
assert!(frame.quads.is_empty());
assert_eq!(frame.lines.len(), 1);
assert_eq!(frame.lines[0].start, [0.0, 0.0]);
assert_eq!(frame.lines[0].end, [10.0, 10.0]);
assert!((frame.lines[0].width - 2.0).abs() < 0.01);
}
#[test]
fn line_width_stays_device_constant_under_scale() {
let mut scene = Scene::new();
scene.push(DrawCommand::Line {
from: Vec2 { x: 0.0, y: 0.0 },
to: Vec2 { x: 10.0, y: 0.0 },
stroke: SceneStroke { width: 4.0, ..SceneStroke::default() },
brush: Brush::Solid(Color::from_rgba8(255, 255, 255, 255)),
transform: Affine::scale(2.0),
});
let frame = encode_scene(&scene, viewport(), &mut cache(), None, None, max_depth(), false);
assert_eq!(frame.lines.len(), 1);
assert_eq!(frame.lines[0].end, [20.0, 0.0], "endpoints still transform through the full affine");
assert!(
(frame.lines[0].width - 4.0).abs() < 0.01,
"stroke width must stay DEVICE-CONSTANT under scale (design §0.2), not scale to 8.0"
);
}
#[test]
fn explicit_line_batch_bulk_encodes_without_path_tessellation() {
let mut scene = Scene::new();
scene.push(DrawCommand::LineBatch {
segments: vec![
uzor_urx_core::scene::LineBatchSegment {
from: Vec2 { x: 1.0, y: 2.0 },
to: Vec2 { x: 3.0, y: 4.0 },
},
uzor_urx_core::scene::LineBatchSegment {
from: Vec2 { x: 5.0, y: 6.0 },
to: Vec2 { x: 7.0, y: 8.0 },
},
],
stroke: SceneStroke {
width: 2.0,
cap: LineCap::Butt,
..SceneStroke::default()
},
brush: Brush::Solid(Color::from_rgba8(255, 255, 255, 255)),
transform: Affine::scale(2.0),
});
let frame = encode_scene(
&scene,
viewport(),
&mut cache(),
None,
None,
max_depth(),
false,
);
assert!(frame.triangles.is_empty());
assert_eq!(frame.lines.len(), 2);
assert_eq!(frame.lines[0].start, [2.0, 4.0]);
assert_eq!(frame.lines[1].end, [14.0, 16.0]);
assert_eq!(frame.draw_batches().len(), 1);
assert_eq!(frame.draw_batches()[0].count, 2);
}
#[test]
fn identity_line_batch_preserves_source_coordinates_exactly() {
let mut scene = Scene::new();
scene.push(DrawCommand::LineBatch {
segments: vec![
uzor_urx_core::scene::LineBatchSegment {
from: Vec2 { x: -12.5, y: 3.25 },
to: Vec2 { x: 40.75, y: -8.5 },
},
uzor_urx_core::scene::LineBatchSegment {
from: Vec2 { x: 0.0, y: 1.0 },
to: Vec2 { x: 2.0, y: 3.0 },
},
],
stroke: SceneStroke {
width: 2.0,
cap: LineCap::Butt,
..SceneStroke::default()
},
brush: Brush::Solid(Color::from_rgba8(255, 255, 255, 255)),
transform: Affine::IDENTITY,
});
let frame = encode_scene(
&scene,
viewport(),
&mut cache(),
None,
None,
max_depth(),
false,
);
assert_eq!(frame.lines.len(), 2);
assert_eq!(frame.lines[0].start, [-12.5, 3.25]);
assert_eq!(frame.lines[0].end, [40.75, -8.5]);
assert_eq!(frame.lines[1].start, [0.0, 1.0]);
assert_eq!(frame.lines[1].end, [2.0, 3.0]);
assert_eq!(frame.draw_batches().len(), 1);
}
#[test]
fn independent_solid_stroke_subpaths_bulk_encode_as_one_native_line_batch() {
let mut path = BezPath::new();
path.move_to(Point::new(1.0, 2.0));
path.line_to(Point::new(3.0, 4.0));
path.move_to(Point::new(5.0, 6.0));
path.line_to(Point::new(7.0, 8.0));
let mut scene = Scene::new();
scene.push(DrawCommand::StrokePath {
path,
stroke: SceneStroke { width: 2.0, cap: LineCap::Butt, ..SceneStroke::default() },
brush: Brush::Solid(Color::from_rgba8(255, 255, 255, 255)),
transform: Affine::scale(2.0),
});
let frame = encode_scene(&scene, viewport(), &mut cache(), None, None, max_depth(), false);
assert!(frame.triangles.is_empty(), "independent line pairs must bypass path tessellation");
assert_eq!(frame.lines.len(), 2);
assert_eq!(frame.lines[0].start, [2.0, 4.0]);
assert_eq!(frame.lines[1].end, [14.0, 16.0]);
assert_eq!(
frame.geometry_used_bytes(),
2 * std::mem::size_of::<LineInstance>(),
"profiling must include native line geometry",
);
let batches = frame.draw_batches();
assert_eq!(batches.len(), 1);
assert_eq!(batches[0].kind, BatchKind::Line);
assert_eq!(batches[0].count, 2);
}
#[test]
fn connected_solid_stroke_path_keeps_join_aware_tessellation() {
let mut path = BezPath::new();
path.move_to(Point::new(1.0, 2.0));
path.line_to(Point::new(3.0, 4.0));
path.line_to(Point::new(5.0, 6.0));
let mut scene = Scene::new();
scene.push(DrawCommand::StrokePath {
path,
stroke: SceneStroke { width: 2.0, ..SceneStroke::default() },
brush: Brush::Solid(Color::from_rgba8(255, 255, 255, 255)),
transform: Affine::IDENTITY,
});
let frame = encode_scene(&scene, viewport(), &mut cache(), None, None, max_depth(), false);
assert!(frame.lines.is_empty(), "connected paths need join-aware tessellation");
assert!(!frame.triangles.is_empty());
}
#[test]
fn independent_stroke_subpaths_with_nonuniform_transform_keep_tessellation() {
let mut path = BezPath::new();
path.move_to(Point::new(1.0, 2.0));
path.line_to(Point::new(3.0, 4.0));
let mut scene = Scene::new();
scene.push(DrawCommand::StrokePath {
path,
stroke: SceneStroke { width: 2.0, ..SceneStroke::default() },
brush: Brush::Solid(Color::from_rgba8(255, 255, 255, 255)),
transform: Affine::new([2.0, 0.0, 0.0, 1.0, 0.0, 0.0]),
});
let frame = encode_scene(&scene, viewport(), &mut cache(), None, None, max_depth(), false);
assert!(frame.lines.is_empty(), "non-uniform transforms change stroke shape and must not use LineInstance");
assert!(!frame.triangles.is_empty());
}
#[test]
fn reusable_encoded_frame_retains_geometry_capacity_between_frames() {
let mut path = BezPath::new();
for i in 0..4096 {
path.move_to(Point::new(i as f64, 0.0));
path.line_to(Point::new(i as f64, 10.0));
}
let mut scene = Scene::new();
scene.push(DrawCommand::StrokePath {
path,
stroke: SceneStroke { width: 1.0, ..SceneStroke::default() },
brush: Brush::Solid(Color::from_rgba8(255, 255, 255, 255)),
transform: Affine::IDENTITY,
});
let first = encode_scene(&scene, viewport(), &mut cache(), None, None, max_depth(), false);
assert!(first.geometry_profile.growths > 0);
let retained_capacity = first.geometry_capacity_bytes();
let second = encode_scene_reusing(
first,
&scene,
viewport(),
&mut cache(),
None,
None,
max_depth(),
false,
);
assert_eq!(second.lines.len(), 4096);
assert_eq!(second.geometry_profile.growths, 0);
assert_eq!(second.geometry_capacity_bytes(), retained_capacity);
}
#[test]
fn zero_width_line_emits_nothing() {
let mut scene = Scene::new();
scene.line_solid(
Vec2 { x: 0.0, y: 0.0 },
Vec2 { x: 10.0, y: 10.0 },
0.0,
Color::from_rgba8(255, 255, 255, 255),
);
let frame = encode_scene(&scene, viewport(), &mut cache(), None, None, max_depth(), false);
assert!(frame.lines.is_empty());
}
fn max_x_span_covering(triangles: &[TriInstance], probe_x: f32) -> bool {
triangles.iter().any(|t| {
let xs = [t.v0[0], t.v1[0], t.v2[0]];
let (min_x, max_x) = xs.iter().fold((f32::INFINITY, f32::NEG_INFINITY), |(lo, hi), &x| (lo.min(x), hi.max(x)));
probe_x >= min_x && probe_x <= max_x
})
}
#[test]
fn dashed_line_routes_through_triangle_pipeline_not_the_fast_line_batch() {
let mut scene = Scene::new();
scene.push(DrawCommand::Line {
from: Vec2 { x: 0.0, y: 0.0 },
to: Vec2 { x: 100.0, y: 0.0 },
stroke: SceneStroke {
width: 4.0,
dash: Some(uzor_urx_core::scene::Dash { pattern: vec![10.0, 10.0], phase: 0.0 }),
..SceneStroke::default()
},
brush: Brush::Solid(Color::from_rgba8(255, 255, 255, 255)),
transform: Affine::IDENTITY,
});
let frame = encode_scene(&scene, viewport(), &mut cache(), None, None, max_depth(), false);
assert!(frame.lines.is_empty(), "a dashed Line must NOT use the fast (dash-blind) LineInstance path");
assert!(!frame.triangles.is_empty(), "must route through the Triangle pipeline instead");
assert!(max_x_span_covering(&frame.triangles, 5.0), "x=5 (an 'on' dash run) must be covered");
assert!(!max_x_span_covering(&frame.triangles, 15.0), "x=15 (an 'off' gap) must NOT be covered");
assert!(max_x_span_covering(&frame.triangles, 25.0), "x=25 (the next 'on' run) must be covered again");
}
#[test]
fn dashed_stroke_rect_routes_through_triangle_pipeline_not_the_fast_quad_path() {
let mut scene = Scene::new();
scene.push(DrawCommand::StrokeRect {
rect: Rect::new(0.0, 0.0, 40.0, 40.0),
radii: None,
stroke: SceneStroke {
width: 3.0,
dash: Some(uzor_urx_core::scene::Dash { pattern: vec![8.0, 8.0], phase: 0.0 }),
..SceneStroke::default()
},
brush: Brush::Solid(Color::from_rgba8(0, 255, 0, 255)),
transform: Affine::IDENTITY,
});
let frame = encode_scene(&scene, viewport(), &mut cache(), None, None, max_depth(), false);
assert!(frame.quads.is_empty(), "a dashed StrokeRect must NOT use the fast (dash-blind) Quad SDF border path");
assert!(!frame.triangles.is_empty(), "must route through the Triangle pipeline instead");
}
#[test]
fn dashed_stroke_path_produces_disjoint_mesh_regions() {
let mut path = BezPath::new();
path.move_to(Point::new(0.0, 0.0));
path.line_to(Point::new(100.0, 0.0));
let mut scene = Scene::new();
scene.push(DrawCommand::StrokePath {
path,
stroke: SceneStroke {
width: 4.0,
dash: Some(uzor_urx_core::scene::Dash { pattern: vec![10.0, 10.0], phase: 0.0 }),
..SceneStroke::default()
},
brush: Brush::Solid(Color::from_rgba8(255, 255, 255, 255)),
transform: Affine::IDENTITY,
});
let frame = encode_scene(&scene, viewport(), &mut cache(), None, None, max_depth(), false);
assert!(!frame.triangles.is_empty());
assert!(max_x_span_covering(&frame.triangles, 5.0), "an 'on' dash run must be covered");
assert!(!max_x_span_covering(&frame.triangles, 15.0), "an 'off' gap must NOT be covered");
assert!(max_x_span_covering(&frame.triangles, 25.0), "the next 'on' run must be covered again");
}
#[test]
fn dashed_line_scales_the_dash_period_with_the_ctm() {
let dashed_line = |transform: Affine| DrawCommand::Line {
from: Vec2 { x: 0.0, y: 0.0 },
to: Vec2 { x: 100.0, y: 0.0 },
stroke: SceneStroke {
width: 4.0,
dash: Some(uzor_urx_core::scene::Dash { pattern: vec![10.0, 10.0], phase: 0.0 }),
..SceneStroke::default()
},
brush: Brush::Solid(Color::from_rgba8(255, 255, 255, 255)),
transform,
};
let mut scene_1x = Scene::new();
scene_1x.push(dashed_line(Affine::IDENTITY));
let frame_1x = encode_scene(&scene_1x, viewport(), &mut cache(), None, None, max_depth(), false);
assert!(max_x_span_covering(&frame_1x.triangles, 8.0), "x=8 must be covered at 1x (still inside the first 'on' run)");
let mut scene_2x = Scene::new();
scene_2x.push(dashed_line(Affine::scale(2.0)));
let frame_2x = encode_scene(&scene_2x, viewport(), &mut cache(), None, None, max_depth(), false);
assert!(max_x_span_covering(&frame_2x.triangles, 16.0), "device x=16 (2x of local x=8) must be covered at 2x scale");
}
#[test]
fn cap_round_maps_to_flag_zero() {
let mut scene = Scene::new();
scene.push(DrawCommand::Line {
from: Vec2 { x: 0.0, y: 0.0 },
to: Vec2 { x: 10.0, y: 0.0 },
stroke: SceneStroke { width: 2.0, cap: LineCap::Round, ..SceneStroke::default() },
brush: Brush::Solid(Color::from_rgba8(255, 255, 255, 255)),
transform: Affine::IDENTITY,
});
let frame = encode_scene(&scene, viewport(), &mut cache(), None, None, max_depth(), false);
assert_eq!(frame.lines[0].cap_flags, 0.0);
}
#[test]
fn cap_butt_maps_to_flag_three() {
let mut scene = Scene::new();
scene.push(DrawCommand::Line {
from: Vec2 { x: 0.0, y: 0.0 },
to: Vec2 { x: 10.0, y: 0.0 },
stroke: SceneStroke { width: 2.0, cap: LineCap::Butt, ..SceneStroke::default() },
brush: Brush::Solid(Color::from_rgba8(255, 255, 255, 255)),
transform: Affine::IDENTITY,
});
let frame = encode_scene(&scene, viewport(), &mut cache(), None, None, max_depth(), false);
assert_eq!(frame.lines[0].cap_flags, 3.0);
}
#[test]
fn cap_square_degrades_to_round_flag_zero() {
let mut scene = Scene::new();
scene.push(DrawCommand::Line {
from: Vec2 { x: 0.0, y: 0.0 },
to: Vec2 { x: 10.0, y: 0.0 },
stroke: SceneStroke { width: 2.0, cap: LineCap::Square, ..SceneStroke::default() },
brush: Brush::Solid(Color::from_rgba8(255, 255, 255, 255)),
transform: Affine::IDENTITY,
});
let frame = encode_scene(&scene, viewport(), &mut cache(), None, None, max_depth(), false);
assert_eq!(frame.lines[0].cap_flags, 0.0);
}
#[test]
fn batches_coalesce_by_kind_and_preserve_painters_order() {
let mut scene = Scene::new();
scene.fill_rect_solid(Rect::new(0.0, 0.0, 10.0, 10.0), Color::from_rgba8(255, 0, 0, 255));
scene.fill_rect_solid(Rect::new(20.0, 0.0, 30.0, 10.0), Color::from_rgba8(0, 255, 0, 255));
scene.line_solid(
Vec2 { x: 0.0, y: 20.0 },
Vec2 { x: 30.0, y: 20.0 },
2.0,
Color::from_rgba8(0, 0, 255, 255),
);
scene.fill_rect_solid(Rect::new(0.0, 30.0, 10.0, 40.0), Color::from_rgba8(255, 255, 0, 255));
let frame = encode_scene(&scene, viewport(), &mut cache(), None, None, max_depth(), false);
assert_eq!(frame.quads.len(), 3);
assert_eq!(frame.lines.len(), 1);
assert_eq!(frame.draw_batches().len(), 3, "batches: {:?}", frame.draw_batches());
assert_eq!(frame.draw_batches()[0].kind, BatchKind::Quad);
assert_eq!(frame.draw_batches()[0].start, 0);
assert_eq!(frame.draw_batches()[0].count, 2);
assert_eq!(frame.draw_batches()[1].kind, BatchKind::Line);
assert_eq!(frame.draw_batches()[1].start, 0);
assert_eq!(frame.draw_batches()[1].count, 1);
assert_eq!(frame.draw_batches()[2].kind, BatchKind::Quad);
assert_eq!(frame.draw_batches()[2].start, 2, "third batch must resume at quad index 2, not restart at 0");
assert_eq!(frame.draw_batches()[2].count, 1);
}
#[test]
fn alternating_kinds_never_coalesce_across_a_gap() {
let mut scene = Scene::new();
scene.fill_rect_solid(Rect::new(0.0, 0.0, 10.0, 10.0), Color::from_rgba8(255, 0, 0, 255));
scene.line_solid(Vec2 { x: 0.0, y: 0.0 }, Vec2 { x: 10.0, y: 0.0 }, 2.0, Color::from_rgba8(0, 0, 255, 255));
scene.fill_rect_solid(Rect::new(0.0, 20.0, 10.0, 30.0), Color::from_rgba8(0, 255, 0, 255));
scene.line_solid(Vec2 { x: 0.0, y: 40.0 }, Vec2 { x: 10.0, y: 40.0 }, 2.0, Color::from_rgba8(255, 255, 0, 255));
let frame = encode_scene(&scene, viewport(), &mut cache(), None, None, max_depth(), false);
assert_eq!(frame.draw_batches().len(), 4);
let kinds: Vec<BatchKind> = frame.draw_batches().iter().map(|b| b.kind).collect();
assert_eq!(kinds, vec![BatchKind::Quad, BatchKind::Line, BatchKind::Quad, BatchKind::Line]);
}
#[test]
fn fill_path_triangle_emits_triangles_and_batches() {
let mut path = BezPath::new();
path.move_to((0.0, 0.0));
path.line_to((20.0, 0.0));
path.line_to((10.0, 20.0));
path.close_path();
let mut scene = Scene::new();
scene.push(DrawCommand::FillPath {
path,
rule: FillRule::NonZero,
brush: Brush::Solid(Color::from_rgba8(10, 20, 30, 255)),
transform: Affine::IDENTITY,
});
let frame = encode_scene(&scene, viewport(), &mut cache(), None, None, max_depth(), false);
assert!(!frame.triangles.is_empty());
assert_eq!(frame.draw_batches().len(), 1);
assert_eq!(frame.draw_batches()[0].kind, BatchKind::Triangle);
for tri in &frame.triangles {
assert_eq!(tri.color0, tri.color1);
assert_eq!(tri.color1, tri.color2);
}
}
#[test]
fn transform_scaled_tolerance_stays_within_a_quarter_screen_pixel() {
for transform in [
Affine::scale(0.000_1),
Affine::scale(10_000.0),
Affine::new([0.01, 0.003, 0.02, 0.004, 0.0, 0.0]),
] {
let tolerance = local_tess_tolerance(&transform) as f64;
let screen_error_bound = tolerance * max_affine_scale(&transform);
assert!(
screen_error_bound <= crate::tessellate::TESS_TOLERANCE_PX * 1.000_001,
"local tolerance must conservatively bound screen error: {screen_error_bound}"
);
}
}
#[test]
fn transform_scaled_tolerance_avoids_world_space_curve_over_tessellation() {
let mut path = BezPath::new();
path.move_to(Point::new(0.0, 0.0));
path.curve_to(
Point::new(0.0, 1_000_000.0),
Point::new(1_000_000.0, 1_000_000.0),
Point::new(1_000_000.0, 0.0),
);
path.line_to(Point::new(0.0, 0.0));
path.close_path();
let mut cache = TessCache::new();
cache.begin_frame();
let fixed_local = cache.get_or_insert_fill(&path, FillRule::NonZero);
let fit_transform = Affine::scale(0.000_1);
let screen_scaled = tess_fill_scaled(&mut cache, &path, FillRule::NonZero, &fit_transform);
if std::env::var_os("UZOR_PROFILE_FRAMES").is_some() {
eprintln!(
"[uzor-urx-profile-tess-test] transform={:?} local_tolerance={} fixed_triangles={} scaled_triangles={}",
fit_transform.as_coeffs(),
local_tess_tolerance(&fit_transform),
fixed_local.triangles.len(),
screen_scaled.triangles.len(),
);
}
assert!(
screen_scaled.triangles.len() * 8 < fixed_local.triangles.len(),
"fit-to-screen transform should avoid local-space oversampling: fixed={} scaled={}",
fixed_local.triangles.len(),
screen_scaled.triangles.len(),
);
assert!(
local_tess_tolerance(&fit_transform) as f64 * max_affine_scale(&fit_transform)
<= crate::tessellate::TESS_TOLERANCE_PX * 1.000_001
);
}
#[test]
fn geometry_capacity_guard_rejects_before_path_buffer_allocation() {
let mut instances = Vec::<TriInstance>::new();
let over_limit = MAX_GEOMETRY_BUFFER_BYTES / std::mem::size_of::<TriInstance>() + 1;
let error = try_reserve_geometry(&mut instances, over_limit, "path")
.expect_err("path geometry above the safety limit must be rejected");
assert!(instances.is_empty());
assert_eq!(error.stage, "cpu_geometry_reserve");
assert_eq!(error.buffer, "path");
assert_eq!(error.requested_instances, over_limit);
assert_eq!(
error.requested_bytes,
over_limit * std::mem::size_of::<TriInstance>(),
);
assert_eq!(error.max_bytes, MAX_GEOMETRY_BUFFER_BYTES);
}
#[test]
fn aggregate_many_mesh_growth_stops_at_geometry_limit() {
let mut instances = Vec::<[u8; 56]>::new();
let max_bytes = 8 * std::mem::size_of::<[u8; 56]>();
for _ in 0..4 {
try_reserve_geometry_with_limit(&mut instances, 2, max_bytes, "test")
.expect("aggregate geometry through the limit must fit");
instances.extend([[0; 56]; 2]);
}
let error = try_reserve_geometry_with_limit(&mut instances, 2, max_bytes, "test")
.expect_err("aggregate geometry over the limit must be rejected");
assert_eq!(instances.len(), 8);
assert_eq!(error.requested_instances, 10);
assert_eq!(error.instance_size, 56);
assert_eq!(error.max_bytes, max_bytes);
}
#[test]
fn stroke_path_zero_width_emits_nothing() {
let mut path = BezPath::new();
path.move_to((0.0, 0.0));
path.line_to((20.0, 0.0));
let mut scene = Scene::new();
scene.push(DrawCommand::StrokePath {
path,
stroke: SceneStroke { width: 0.0, ..SceneStroke::default() },
brush: Brush::Solid(Color::from_rgba8(10, 20, 30, 255)),
transform: Affine::IDENTITY,
});
let frame = encode_scene(&scene, viewport(), &mut cache(), None, None, max_depth(), false);
assert!(frame.triangles.is_empty());
}
#[test]
fn quad_line_triangle_batches_interleave_correctly() {
let mut path = BezPath::new();
path.move_to((0.0, 0.0));
path.line_to((20.0, 0.0));
path.line_to((10.0, 20.0));
path.close_path();
let mut scene = Scene::new();
scene.fill_rect_solid(Rect::new(0.0, 0.0, 10.0, 10.0), Color::from_rgba8(255, 0, 0, 255));
scene.line_solid(Vec2 { x: 0.0, y: 0.0 }, Vec2 { x: 10.0, y: 0.0 }, 2.0, Color::from_rgba8(0, 0, 255, 255));
scene.push(DrawCommand::FillPath {
path,
rule: FillRule::NonZero,
brush: Brush::Solid(Color::from_rgba8(10, 20, 30, 255)),
transform: Affine::IDENTITY,
});
let frame = encode_scene(&scene, viewport(), &mut cache(), None, None, max_depth(), false);
assert_eq!(frame.draw_batches().len(), 3);
let kinds: Vec<BatchKind> = frame.draw_batches().iter().map(|b| b.kind).collect();
assert_eq!(kinds, vec![BatchKind::Quad, BatchKind::Line, BatchKind::Triangle]);
}
#[test]
fn linear_gradient_device_params_identity_transform() {
let kind = GradientKind::Linear(LinearGradientPosition {
start: kurbo::Point::new(3.0, 4.0),
end: kurbo::Point::new(13.0, 24.0),
});
let params = transform_gradient_params(&kind, Extend::Pad, &Affine::IDENTITY);
assert_eq!(params.p0, [3.0, 4.0]);
assert_eq!(params.p1, 10.0);
assert_eq!(params.p2, 20.0);
assert_eq!(params.kind_extend & 3, 2, "Linear kind bits must be 2");
}
#[test]
fn linear_gradient_device_params_translated() {
let kind = GradientKind::Linear(LinearGradientPosition {
start: kurbo::Point::new(0.0, 0.0),
end: kurbo::Point::new(10.0, 0.0),
});
let t = Affine::translate((5.0, 7.0));
let params = transform_gradient_params(&kind, Extend::Pad, &t);
assert_eq!(params.p0, [5.0, 7.0], "start must move WITH the translation");
assert_eq!(params.p1, 10.0, "axis is translation-invariant");
assert_eq!(params.p2, 0.0, "axis is translation-invariant");
}
#[test]
fn linear_gradient_device_params_rotated_axis_rotates_with_the_shape() {
let kind = GradientKind::Linear(LinearGradientPosition {
start: kurbo::Point::new(0.0, 0.0),
end: kurbo::Point::new(10.0, 0.0),
});
let t = Affine::rotate(std::f64::consts::FRAC_PI_2);
let params = transform_gradient_params(&kind, Extend::Pad, &t);
let mag = (params.p1 * params.p1 + params.p2 * params.p2).sqrt();
assert!((mag - 10.0).abs() < 0.01, "axis length must be preserved under rotation, got {mag}");
assert!(params.p1.abs() < 0.01, "a 90-degree rotation must turn the (10,0) axis onto the y-axis, got p1={}", params.p1);
assert!(params.p2.abs() > 9.9, "a 90-degree rotation must turn the (10,0) axis onto the y-axis, got p2={}", params.p2);
}
fn two_stop_gradient_stops() -> ColorStops {
ColorStops::from(
&[
peniko::ColorStop::from((0.0f32, Color::from_rgba8(255, 0, 0, 255))),
peniko::ColorStop::from((1.0f32, Color::from_rgba8(0, 0, 255, 255))),
][..],
)
}
fn radial_gradient_brush() -> Brush {
let mut g = Gradient::new_radial(kurbo::Point::new(20.0, 20.0), 15.0);
g.stops = two_stop_gradient_stops();
g.extend = Extend::Pad;
Brush::Gradient(g)
}
fn sweep_gradient_brush() -> Brush {
let mut g = Gradient::new_sweep(kurbo::Point::new(20.0, 20.0), -std::f32::consts::PI, std::f32::consts::PI);
g.stops = two_stop_gradient_stops();
g.extend = Extend::Pad;
Brush::Gradient(g)
}
#[test]
fn radial_gradient_fillrect_falls_back_to_solid_without_a_lut() {
let mut scene = Scene::new();
scene.push(DrawCommand::FillRect {
rect: Rect::new(0.0, 0.0, 40.0, 40.0),
radii: None,
brush: radial_gradient_brush(),
transform: Affine::IDENTITY,
});
let frame = encode_scene(&scene, viewport(), &mut cache(), None, None, max_depth(), false);
assert!(frame.gradients.is_empty(), "no GradientInstance without a real LUT atlas");
assert!(!frame.triangles.is_empty(), "must still render SOMETHING — the solid fallback mesh");
}
#[test]
#[ignore = "needs a headless GPU adapter"]
fn radial_and_sweep_fillrect_emit_gradient_instance_with_plausible_device_params() {
let Some((device, _queue)) = test_device() else { return };
let mut lut = GradientLutAtlas::new(&device, 8);
lut.begin_frame();
let mut scene = Scene::new();
scene.push(DrawCommand::FillRect {
rect: Rect::new(0.0, 0.0, 40.0, 40.0),
radii: None,
brush: radial_gradient_brush(),
transform: Affine::IDENTITY,
});
scene.push(DrawCommand::FillRect {
rect: Rect::new(50.0, 0.0, 90.0, 40.0),
radii: None,
brush: sweep_gradient_brush(),
transform: Affine::IDENTITY,
});
let frame = encode_scene(&scene, viewport(), &mut cache(), None, Some(&mut lut), max_depth(), false);
assert!(frame.triangles.is_empty(), "Radial/Sweep must NOT land on the flat-shaded triangle path");
assert!(!frame.gradients.is_empty(), "a real LUT atlas must produce real GradientInstances");
let radial = frame.gradients[0];
assert_eq!(radial.kind_extend & 3, 0, "first FillRect's gradient is Radial (kind bits 0)");
assert!((radial.p0[0] - 20.0).abs() < 0.5 && (radial.p0[1] - 20.0).abs() < 0.5, "center ~= (20,20), got {:?}", radial.p0);
assert!((radial.p1 - 15.0).abs() < 0.5, "radius ~= 15, got {}", radial.p1);
let sweep = frame.gradients.iter().find(|g| g.kind_extend & 3 == 1).expect("a Sweep instance must exist");
assert!((sweep.p0[0] - 20.0).abs() < 0.5 && (sweep.p0[1] - 20.0).abs() < 0.5, "center ~= (20,20), got {:?}", sweep.p0);
assert!((sweep.p1 + std::f32::consts::PI).abs() < 0.01, "start_angle ~= -PI, got {}", sweep.p1);
assert!((sweep.p2 - std::f32::consts::PI).abs() < 0.01, "end_angle ~= PI, got {}", sweep.p2);
}
#[test]
#[ignore = "needs a headless GPU adapter"]
fn gradient_on_fillpath_strokepath_strokerect_all_emit_gradient_instances() {
let Some((device, _queue)) = test_device() else { return };
let mut triangle = BezPath::new();
triangle.move_to((0.0, 0.0));
triangle.line_to((20.0, 0.0));
triangle.line_to((10.0, 20.0));
triangle.close_path();
let fill_scene = {
let mut scene = Scene::new();
scene.push(DrawCommand::FillPath {
path: triangle.clone(),
rule: FillRule::NonZero,
brush: radial_gradient_brush(),
transform: Affine::IDENTITY,
});
scene
};
let stroke_path_scene = {
let mut scene = Scene::new();
scene.push(DrawCommand::StrokePath {
path: triangle,
stroke: SceneStroke { width: 2.0, ..SceneStroke::default() },
brush: radial_gradient_brush(),
transform: Affine::IDENTITY,
});
scene
};
let stroke_rect_scene = {
let mut scene = Scene::new();
scene.push(DrawCommand::StrokeRect {
rect: Rect::new(0.0, 0.0, 40.0, 40.0),
radii: None,
stroke: SceneStroke { width: 2.0, ..SceneStroke::default() },
brush: radial_gradient_brush(),
transform: Affine::IDENTITY,
});
scene
};
for (label, scene) in [
("FillPath", &fill_scene),
("StrokePath", &stroke_path_scene),
("StrokeRect", &stroke_rect_scene),
] {
let mut lut = GradientLutAtlas::new(&device, 8);
lut.begin_frame();
let frame = encode_scene(scene, viewport(), &mut cache(), None, Some(&mut lut), max_depth(), false);
assert!(frame.triangles.is_empty(), "{label} must route through the Gradient path, not the solid one");
assert!(!frame.gradients.is_empty(), "{label} must emit real GradientInstances");
assert_eq!(
frame.draw_batches().iter().map(|b| b.kind).collect::<Vec<_>>(),
vec![BatchKind::Gradient],
"{label} alone must produce exactly one Gradient batch"
);
}
let mut combined = Scene::new();
for scene in [&fill_scene, &stroke_path_scene, &stroke_rect_scene] {
combined.commands.extend(scene.commands.iter().cloned());
}
let mut lut = GradientLutAtlas::new(&device, 8);
lut.begin_frame();
let frame = encode_scene(&combined, viewport(), &mut cache(), None, Some(&mut lut), max_depth(), false);
assert!(frame.triangles.is_empty(), "combined scene must route through the Gradient path, not the solid one");
let batches = frame.draw_batches();
assert_eq!(batches.len(), 1, "3 back-to-back Gradient draws at the same clip depth must coalesce into 1 batch");
assert_eq!(batches[0].kind, BatchKind::Gradient);
assert_eq!(batches[0].count, frame.gradients.len() as u32, "the coalesced batch must cover every emitted instance");
}
fn register_test_image(w: u32, h: u32, fill: [u8; 4]) -> ImageId {
let mut bytes = vec![0u8; (w * h * 4) as usize];
for px in bytes.chunks_exact_mut(4) {
px.copy_from_slice(&fill);
}
let data = uzor_urx_image::ImageData::from_raw_premul(w, h, bytes).expect("size matches by construction");
uzor_urx_image::register_image(data)
}
#[test]
fn image_batch_key_includes_id_distinct_ids_split_same_id_coalesces() {
let id_a = register_test_image(2, 2, [255, 0, 0, 255]);
let id_b = register_test_image(2, 2, [0, 255, 0, 255]);
let mut scene = Scene::new();
scene.push(DrawCommand::Image {
src: id_a,
src_rect: None,
dest: Rect::new(0.0, 0.0, 10.0, 10.0),
transform: Affine::IDENTITY,
});
scene.push(DrawCommand::Image {
src: id_a,
src_rect: None,
dest: Rect::new(10.0, 0.0, 20.0, 10.0),
transform: Affine::IDENTITY,
});
scene.push(DrawCommand::Image {
src: id_b,
src_rect: None,
dest: Rect::new(20.0, 0.0, 30.0, 10.0),
transform: Affine::IDENTITY,
});
let frame = encode_scene(&scene, viewport(), &mut cache(), None, None, max_depth(), false);
assert_eq!(frame.images.len(), 3, "all 3 draws must still emit their own ImageInstance");
let batches = frame.draw_batches();
assert_eq!(
batches.len(),
2,
"the two SAME-id (id_a) draws must coalesce into ONE batch; the id_b draw must be a SEPARATE batch"
);
assert_eq!(batches[0].kind, BatchKind::Image(id_a));
assert_eq!(batches[0].count, 2, "both id_a draws coalesced");
assert_eq!(batches[1].kind, BatchKind::Image(id_b));
assert_eq!(batches[1].count, 1);
uzor_urx_image::unregister_image(id_a);
uzor_urx_image::unregister_image(id_b);
}
#[test]
fn clip_stack_root_is_full_viewport_and_never_pops() {
let mut clip = ClipStack::new(viewport());
assert_eq!(clip.current(), [0.0, 0.0, 100.0, 100.0]);
clip.pop(); assert_eq!(clip.current(), [0.0, 0.0, 100.0, 100.0]);
}
#[test]
fn clip_stack_push_intersects_with_current_top() {
let mut clip = ClipStack::new(viewport());
clip.push_rect_device([10.0, 10.0, 60.0, 60.0]); assert_eq!(clip.current(), [10.0, 10.0, 60.0, 60.0]);
clip.push_rect_device([40.0, 40.0, 60.0, 60.0]); assert_eq!(clip.current(), [40.0, 40.0, 30.0, 30.0]); clip.pop();
assert_eq!(clip.current(), [10.0, 10.0, 60.0, 60.0], "pop must restore the PREVIOUS top exactly");
}
#[test]
fn clip_stack_disjoint_push_yields_zero_area() {
let mut clip = ClipStack::new(viewport());
clip.push_rect_device([0.0, 0.0, 10.0, 10.0]);
clip.push_rect_device([50.0, 50.0, 10.0, 10.0]); let cur = clip.current();
assert!(cur[2] <= 0.0 || cur[3] <= 0.0, "disjoint push must intersect to zero area: {cur:?}");
}
#[test]
fn push_clip_rect_feeds_every_subsequent_instance() {
let mut scene = Scene::new();
scene.push(DrawCommand::PushClipRect {
rect: Rect::new(10.0, 10.0, 30.0, 30.0),
transform: Affine::IDENTITY,
});
scene.fill_rect_solid(Rect::new(0.0, 0.0, 100.0, 100.0), Color::from_rgba8(255, 0, 0, 255));
scene.line_solid(Vec2 { x: 0.0, y: 0.0 }, Vec2 { x: 50.0, y: 50.0 }, 2.0, Color::from_rgba8(0, 255, 0, 255));
scene.push(DrawCommand::PopClip);
scene.fill_rect_solid(Rect::new(0.0, 0.0, 5.0, 5.0), Color::from_rgba8(0, 0, 255, 255));
let frame = encode_scene(&scene, viewport(), &mut cache(), None, None, max_depth(), false);
assert_eq!(frame.quads[0].clip_rect, [10.0, 10.0, 20.0, 20.0], "rect under the pushed clip");
assert_eq!(frame.lines[0].clip_rect, [10.0, 10.0, 20.0, 20.0], "line under the pushed clip");
assert_eq!(
frame.quads[1].clip_rect,
[0.0, 0.0, 100.0, 100.0],
"rect encoded AFTER PopClip must see the full viewport again"
);
}
#[test]
fn push_clip_rounded_rect_still_feeds_bbox_into_clip_rect() {
let mut scene = Scene::new();
scene.push(DrawCommand::PushClipRoundedRect {
rect: uzor_urx_core::math::RoundedRect::from_rect(
Rect::new(10.0, 10.0, 30.0, 30.0),
uzor_urx_core::math::RoundedRectRadii::new(5.0, 5.0, 5.0, 5.0),
),
transform: Affine::IDENTITY,
});
scene.fill_rect_solid(Rect::new(0.0, 0.0, 100.0, 100.0), Color::from_rgba8(255, 0, 0, 255));
let frame = encode_scene(&scene, viewport(), &mut cache(), None, None, max_depth(), false);
assert_eq!(frame.quads[0].clip_rect, [10.0, 10.0, 20.0, 20.0]);
assert!(frame.has_rounded_clip);
}
#[test]
fn rounded_clip_stencil_ref_sequencing_push_content_pop() {
let mut scene = Scene::new();
scene.fill_rect_solid(Rect::new(0.0, 0.0, 5.0, 5.0), Color::from_rgba8(1, 1, 1, 255)); scene.push(DrawCommand::PushClipRoundedRect {
rect: uzor_urx_core::math::RoundedRect::from_rect(
Rect::new(10.0, 10.0, 30.0, 30.0),
uzor_urx_core::math::RoundedRectRadii::new(5.0, 5.0, 5.0, 5.0),
),
transform: Affine::IDENTITY,
});
scene.fill_rect_solid(Rect::new(0.0, 0.0, 100.0, 100.0), Color::from_rgba8(255, 0, 0, 255)); scene.push(DrawCommand::PopClip);
scene.fill_rect_solid(Rect::new(0.0, 0.0, 5.0, 5.0), Color::from_rgba8(2, 2, 2, 255));
let frame = encode_scene(&scene, viewport(), &mut cache(), None, None, max_depth(), false);
assert!(frame.has_rounded_clip);
assert!(!frame.stencil_masks.is_empty(), "the mask geometry itself must have been emitted");
let kinds: Vec<(BatchKind, Option<u32>)> = frame.draw_batches().iter().map(|b| (b.kind, b.stencil_ref)).collect();
assert_eq!(
kinds,
vec![
(BatchKind::Quad, None), (BatchKind::StencilMask(MaskOp::Increment), Some(0)), (BatchKind::Quad, Some(1)), (BatchKind::StencilMask(MaskOp::Decrement), Some(1)), (BatchKind::Quad, None), ],
"push -> increment(gate=parent), content -> Some(depth), pop -> decrement(gate=own depth)"
);
}
#[test]
fn unbalanced_push_clip_rect_stays_active_for_the_rest_of_the_frame_no_panic() {
let mut scene = Scene::new();
scene.push(DrawCommand::PushClipRect {
rect: Rect::new(10.0, 10.0, 20.0, 20.0),
transform: Affine::IDENTITY,
});
scene.fill_rect_solid(Rect::new(0.0, 0.0, 100.0, 100.0), Color::from_rgba8(255, 0, 0, 255));
let frame = encode_scene(&scene, viewport(), &mut cache(), None, None, max_depth(), false);
assert_eq!(frame.quads.len(), 1);
assert_eq!(frame.quads[0].clip_rect, [10.0, 10.0, 10.0, 10.0]);
}
#[test]
fn zero_area_clip_elides_every_instance_type_without_a_degrade() {
let mut path = BezPath::new();
path.move_to((0.0, 0.0));
path.line_to((20.0, 0.0));
path.line_to((10.0, 20.0));
path.close_path();
let mut scene = Scene::new();
scene.push(DrawCommand::PushClipRect { rect: Rect::new(0.0, 0.0, 0.0, 0.0), transform: Affine::IDENTITY });
scene.fill_rect_solid(Rect::new(0.0, 0.0, 10.0, 10.0), Color::from_rgba8(255, 0, 0, 255));
scene.line_solid(Vec2 { x: 0.0, y: 0.0 }, Vec2 { x: 10.0, y: 0.0 }, 2.0, Color::from_rgba8(0, 0, 255, 255));
scene.push(DrawCommand::FillPath {
path,
rule: FillRule::NonZero,
brush: Brush::Solid(Color::from_rgba8(10, 20, 30, 255)),
transform: Affine::IDENTITY,
});
let frame = encode_scene(&scene, viewport(), &mut cache(), None, None, max_depth(), false);
assert!(frame.quads.is_empty());
assert!(frame.lines.is_empty());
assert!(frame.triangles.is_empty());
assert!(frame.draw_batches().is_empty());
}
#[derive(Debug, PartialEq, Eq)]
enum OpShape {
Draw(BatchKind),
Push(u32),
Pop(u32),
}
fn op_shapes(frame: &EncodedFrame) -> Vec<OpShape> {
frame
.ops
.iter()
.map(|op| match op {
FrameOp::Draw(b) => OpShape::Draw(b.kind),
FrameOp::PushLayer { depth } => OpShape::Push(*depth),
FrameOp::PopLayer { depth, .. } => OpShape::Pop(*depth),
})
.collect()
}
fn push_blend_layer(alpha: f32) -> DrawCommand {
DrawCommand::PushBlendLayer { mode: BlendMode::default(), alpha, transform: Affine::IDENTITY }
}
#[test]
fn push_pop_blend_layer_emits_markers_bracketing_content_in_scan_order() {
let mut scene = Scene::new();
scene.fill_rect_solid(Rect::new(0.0, 0.0, 5.0, 5.0), Color::from_rgba8(1, 1, 1, 255)); scene.push(push_blend_layer(0.5));
scene.fill_rect_solid(Rect::new(0.0, 0.0, 5.0, 5.0), Color::from_rgba8(2, 2, 2, 255)); scene.push(DrawCommand::PopBlendLayer);
scene.fill_rect_solid(Rect::new(0.0, 0.0, 5.0, 5.0), Color::from_rgba8(3, 3, 3, 255));
let frame = encode_scene(&scene, viewport(), &mut cache(), None, None, max_depth(), false);
assert_eq!(
op_shapes(&frame),
vec![
OpShape::Draw(BatchKind::Quad),
OpShape::Push(1),
OpShape::Draw(BatchKind::Quad),
OpShape::Pop(1),
OpShape::Draw(BatchKind::Quad),
],
"before/inside/after content must NOT coalesce across the layer markers"
);
assert_eq!(frame.composites.len(), 1);
assert_eq!(frame.composites[0].alpha, 0.5);
}
#[test]
fn coalescing_refuses_across_empty_push_pop_blend_layer_markers() {
let mut scene = Scene::new();
scene.fill_rect_solid(Rect::new(0.0, 0.0, 5.0, 5.0), Color::from_rgba8(1, 1, 1, 255));
scene.push(push_blend_layer(1.0));
scene.push(DrawCommand::PopBlendLayer);
scene.fill_rect_solid(Rect::new(0.0, 0.0, 5.0, 5.0), Color::from_rgba8(1, 1, 1, 255));
let frame = encode_scene(&scene, viewport(), &mut cache(), None, None, max_depth(), false);
assert_eq!(
op_shapes(&frame),
vec![OpShape::Draw(BatchKind::Quad), OpShape::Push(1), OpShape::Pop(1), OpShape::Draw(BatchKind::Quad)],
);
let batches = frame.draw_batches();
assert_eq!(batches.len(), 2, "the two quad draws must NOT merge across the push/pop layer markers");
assert_eq!(batches[0].count, 1);
assert_eq!(batches[1].count, 1);
}
#[test]
fn blend_layer_depth_cap_suppresses_pushes_beyond_max_and_matching_pops_emit_nothing() {
let mut scene = Scene::new();
scene.push(push_blend_layer(1.0)); scene.push(push_blend_layer(1.0)); scene.push(push_blend_layer(1.0)); scene.fill_rect_solid(Rect::new(0.0, 0.0, 5.0, 5.0), Color::from_rgba8(1, 1, 1, 255));
scene.push(DrawCommand::PopBlendLayer); scene.push(DrawCommand::PopBlendLayer); scene.push(DrawCommand::PopBlendLayer);
let frame = encode_scene(&scene, viewport(), &mut cache(), None, None, 2, false);
assert_eq!(
op_shapes(&frame),
vec![
OpShape::Push(1),
OpShape::Push(2),
OpShape::Draw(BatchKind::Quad),
OpShape::Pop(2),
OpShape::Pop(1),
],
"the 3rd push (cap = 2) must emit no PushLayer, and its matching pop must emit no PopLayer either"
);
assert_eq!(frame.composites.len(), 2, "only the 2 real layers get a composite instance, not the suppressed one");
}
#[test]
fn unbalanced_push_blend_layer_force_closes_at_scene_end_with_synthetic_pop() {
let mut scene = Scene::new();
scene.push(push_blend_layer(0.75));
scene.fill_rect_solid(Rect::new(0.0, 0.0, 5.0, 5.0), Color::from_rgba8(9, 9, 9, 255));
let frame = encode_scene(&scene, viewport(), &mut cache(), None, None, max_depth(), false);
assert_eq!(
op_shapes(&frame),
vec![OpShape::Push(1), OpShape::Draw(BatchKind::Quad), OpShape::Pop(1)],
"a synthetic PopLayer must close the still-open layer at scene end"
);
assert_eq!(frame.composites.len(), 1);
assert_eq!(frame.composites[0].alpha, 0.75);
}
fn test_device() -> Option<(wgpu::Device, wgpu::Queue)> {
let instance = wgpu::Instance::new(wgpu::InstanceDescriptor::new_without_display_handle());
let adapter = pollster::block_on(instance.request_adapter(&wgpu::RequestAdapterOptions {
power_preference: wgpu::PowerPreference::LowPower,
force_fallback_adapter: false,
compatible_surface: None,
}))
.ok()?;
pollster::block_on(adapter.request_device(&wgpu::DeviceDescriptor {
label: Some("uzor-urx-wgpu-encode-test"),
required_features: wgpu::Features::empty(),
required_limits: wgpu::Limits::default(),
memory_hints: wgpu::MemoryHints::default(),
trace: wgpu::Trace::Off,
experimental_features: wgpu::ExperimentalFeatures::default(),
}))
.ok()
}
fn registered_font() -> FontId {
let bytes = std::fs::read(concat!(env!("CARGO_MANIFEST_DIR"), "/../uzor-fonts/fonts/DejaVuSans.ttf"))
.expect("uzor-fonts ships DejaVuSans.ttf for exactly this kind of test-only registration");
uzor_urx_glyph::register_font(bytes).expect("DejaVuSans.ttf is a valid font")
}
#[test]
#[ignore = "needs a headless GPU adapter"]
fn glyphrun_with_registered_font_emits_plausible_instances() {
let Some((device, queue)) = test_device() else { return };
let mut atlas = NativeGlyphAtlas::new(&device, &queue, 256, 256);
atlas.begin_frame();
let font = registered_font();
let mut scene = Scene::new();
scene.push(DrawCommand::GlyphRun {
glyphs: vec![Glyph { glyph_id: 36, x: 0.0, y: 0.0 }, Glyph { glyph_id: 37, x: 20.0, y: 0.0 }],
font,
font_size: 32.0,
brush: Brush::Solid(Color::from_rgba8(255, 255, 255, 255)),
transform: Affine::translate((10.0, 10.0)),
text: None,
});
let frame = encode_scene(&scene, viewport(), &mut cache(), Some(&mut atlas), None, max_depth(), false);
assert_eq!(frame.glyphs.len(), 2, "both glyphs should have rasterised + placed successfully");
for g in &frame.glyphs {
assert!(g.size[0] > 0.0 && g.size[1] > 0.0, "a real glyph bitmap must have a positive size");
assert!(g.uv_size[0] > 0.0 && g.uv_size[1] > 0.0, "a placed glyph must have a non-zero atlas UV rect");
}
assert_eq!(frame.draw_batches().len(), 1);
assert_eq!(frame.draw_batches()[0].kind, BatchKind::Glyph);
}
#[test]
#[ignore = "needs a headless GPU adapter"]
fn glyph_batches_coalesce_and_interleave_with_quad_batches() {
let Some((device, queue)) = test_device() else { return };
let mut atlas = NativeGlyphAtlas::new(&device, &queue, 256, 256);
atlas.begin_frame();
let font = registered_font();
let mut scene = Scene::new();
scene.fill_rect_solid(Rect::new(0.0, 0.0, 10.0, 10.0), Color::from_rgba8(255, 0, 0, 255));
scene.push(DrawCommand::GlyphRun {
glyphs: vec![Glyph { glyph_id: 36, x: 0.0, y: 0.0 }, Glyph { glyph_id: 37, x: 20.0, y: 0.0 }],
font,
font_size: 32.0,
brush: Brush::Solid(Color::from_rgba8(255, 255, 255, 255)),
transform: Affine::translate((0.0, 40.0)),
text: None,
});
scene.fill_rect_solid(Rect::new(50.0, 50.0, 60.0, 60.0), Color::from_rgba8(0, 255, 0, 255));
let frame = encode_scene(&scene, viewport(), &mut cache(), Some(&mut atlas), None, max_depth(), false);
assert_eq!(frame.glyphs.len(), 2, "both glyphs must have placed for this to be a meaningful coalescing test");
assert_eq!(frame.draw_batches().len(), 3, "Quad, then Glyph (both glyphs coalesced into ONE batch), then Quad");
let kinds: Vec<BatchKind> = frame.draw_batches().iter().map(|b| b.kind).collect();
assert_eq!(kinds, vec![BatchKind::Quad, BatchKind::Glyph, BatchKind::Quad]);
assert_eq!(frame.draw_batches()[1].count, 2, "both glyphs from the one GlyphRun must coalesce into a single batch");
}
mod metrics_recorder_proof {
use std::collections::HashMap;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::{Arc, Mutex};
use metrics::{Counter, CounterFn, Gauge, Histogram, Key, KeyName, Metadata, Recorder, SharedString, Unit};
struct RecordedCounter(AtomicU64);
impl CounterFn for RecordedCounter {
fn increment(&self, value: u64) {
self.0.fetch_add(value, Ordering::SeqCst);
}
fn absolute(&self, value: u64) {
self.0.store(value, Ordering::SeqCst);
}
}
#[derive(Default)]
struct TestRecorder {
counters: Mutex<HashMap<Key, Arc<RecordedCounter>>>,
}
impl TestRecorder {
fn value_for(&self, metric_name: &str, label: &str) -> u64 {
let map = self.counters.lock().unwrap_or_else(|e| e.into_inner());
map.iter()
.filter(|(key, _)| {
key.name() == metric_name && key.labels().any(|l| l.key() == "kind" && l.value() == label)
})
.map(|(_, counter)| counter.0.load(Ordering::SeqCst))
.sum()
}
fn total_for(&self, metric_name: &str) -> u64 {
let map = self.counters.lock().unwrap_or_else(|e| e.into_inner());
map.iter()
.filter(|(key, _)| key.name() == metric_name)
.map(|(_, counter)| counter.0.load(Ordering::SeqCst))
.sum()
}
}
impl Recorder for TestRecorder {
fn describe_counter(&self, _key: KeyName, _unit: Option<Unit>, _description: SharedString) {}
fn describe_gauge(&self, _key: KeyName, _unit: Option<Unit>, _description: SharedString) {}
fn describe_histogram(&self, _key: KeyName, _unit: Option<Unit>, _description: SharedString) {}
fn register_counter(&self, key: &Key, _metadata: &Metadata<'_>) -> Counter {
let mut map = self.counters.lock().unwrap_or_else(|e| e.into_inner());
let handle = map.entry(key.clone()).or_insert_with(|| Arc::new(RecordedCounter(AtomicU64::new(0))));
Counter::from_arc(handle.clone())
}
fn register_gauge(&self, _key: &Key, _metadata: &Metadata<'_>) -> Gauge {
Gauge::noop()
}
fn register_histogram(&self, _key: &Key, _metadata: &Metadata<'_>) -> Histogram {
Histogram::noop()
}
}
use super::*;
#[test]
fn unbalanced_push_clip_rounded_rect_force_closes_at_scene_end() {
let recorder = TestRecorder::default();
let frame = metrics::with_local_recorder(&recorder, || {
let mut scene = Scene::new();
scene.push(DrawCommand::PushClipRoundedRect {
rect: uzor_urx_core::math::RoundedRect::from_rect(
Rect::new(10.0, 10.0, 30.0, 30.0),
uzor_urx_core::math::RoundedRectRadii::new(5.0, 5.0, 5.0, 5.0),
),
transform: Affine::IDENTITY,
});
scene.fill_rect_solid(Rect::new(0.0, 0.0, 100.0, 100.0), Color::from_rgba8(255, 0, 0, 255));
encode_scene(&scene, viewport(), &mut cache(), None, None, max_depth(), false)
});
let batches = frame.draw_batches();
let last = batches.last().expect("at least the synthetic decrement batch must exist");
assert_eq!(last.kind, BatchKind::StencilMask(MaskOp::Decrement));
assert_eq!(last.stencil_ref, Some(1), "force-close gates on the still-open scope's own depth");
let value = recorder.value_for(KEY_RENDER_PRIMITIVES, "native_rounded_clip_force_closed_at_scene_end");
assert_eq!(value, 1, "the force-close counter must have actually incremented, not just avoided a panic");
}
fn push_blend_layer(mode: BlendMode, alpha: f32, transform: Affine) -> DrawCommand {
DrawCommand::PushBlendLayer { mode, alpha, transform }
}
#[test]
fn blend_layer_force_closed_at_scene_end_counter_increments_once() {
let recorder = TestRecorder::default();
metrics::with_local_recorder(&recorder, || {
let mut scene = Scene::new();
scene.push(push_blend_layer(BlendMode::default(), 1.0, Affine::IDENTITY));
scene.fill_rect_solid(Rect::new(0.0, 0.0, 5.0, 5.0), Color::from_rgba8(1, 1, 1, 255));
encode_scene(&scene, viewport(), &mut cache(), None, None, max_depth(), false)
});
let value = recorder.value_for(KEY_RENDER_PRIMITIVES, "native_blend_layer_force_closed_at_scene_end");
assert_eq!(value, 1, "the force-close counter must have actually incremented, not just avoided a panic");
}
#[test]
fn blend_layer_depth_exceeded_counts_once_per_suppressed_push() {
let recorder = TestRecorder::default();
metrics::with_local_recorder(&recorder, || {
let mut scene = Scene::new();
scene.push(push_blend_layer(BlendMode::default(), 1.0, Affine::IDENTITY)); scene.push(push_blend_layer(BlendMode::default(), 1.0, Affine::IDENTITY)); scene.push(push_blend_layer(BlendMode::default(), 1.0, Affine::IDENTITY)); scene.push(DrawCommand::PopBlendLayer);
scene.push(DrawCommand::PopBlendLayer);
scene.push(DrawCommand::PopBlendLayer);
encode_scene(&scene, viewport(), &mut cache(), None, None, 1, false)
});
let value = recorder.value_for(KEY_RENDER_PRIMITIVES, "native_blend_layer_depth_exceeded");
assert_eq!(value, 2, "one count per suppressed push, cap = 1 with 3 total pushes");
}
#[test]
fn blend_layer_pop_underflow_is_a_defensive_noop_and_counts() {
let recorder = TestRecorder::default();
let frame = metrics::with_local_recorder(&recorder, || {
let mut scene = Scene::new();
scene.push(DrawCommand::PopBlendLayer); scene.fill_rect_solid(Rect::new(0.0, 0.0, 5.0, 5.0), Color::from_rgba8(1, 1, 1, 255));
encode_scene(&scene, viewport(), &mut cache(), None, None, max_depth(), false)
});
assert_eq!(frame.quads.len(), 1, "content after the stray pop must still render normally, no panic");
assert!(frame.composites.is_empty(), "an underflowing pop has no layer to composite");
let value = recorder.value_for(KEY_RENDER_PRIMITIVES, "native_blend_layer_pop_underflow");
assert_eq!(value, 1);
}
#[test]
fn blend_layer_non_default_mode_counts_mix_and_compose_degrades_independently() {
let recorder = TestRecorder::default();
metrics::with_local_recorder(&recorder, || {
let mut scene = Scene::new();
let mode = BlendMode::new(uzor_urx_core::math::Mix::Multiply, uzor_urx_core::math::Compose::SrcIn);
scene.push(push_blend_layer(mode, 1.0, Affine::IDENTITY));
scene.fill_rect_solid(Rect::new(0.0, 0.0, 5.0, 5.0), Color::from_rgba8(1, 1, 1, 255));
scene.push(DrawCommand::PopBlendLayer);
encode_scene(&scene, viewport(), &mut cache(), None, None, max_depth(), false)
});
assert_eq!(recorder.value_for(KEY_RENDER_PRIMITIVES, "native_blend_layer_mix_to_normal"), 1);
assert_eq!(recorder.value_for(KEY_RENDER_PRIMITIVES, "native_blend_layer_compose_to_srcover"), 1);
}
#[test]
fn blend_layer_non_identity_transform_is_counted() {
let recorder = TestRecorder::default();
metrics::with_local_recorder(&recorder, || {
let mut scene = Scene::new();
scene.push(push_blend_layer(BlendMode::default(), 1.0, Affine::translate((5.0, 5.0))));
scene.push(DrawCommand::PopBlendLayer);
encode_scene(&scene, viewport(), &mut cache(), None, None, max_depth(), false)
});
let value = recorder.value_for(KEY_RENDER_PRIMITIVES, "native_blend_layer_transform_ignored");
assert_eq!(value, 1);
}
#[test]
fn radial_gradient_without_a_lut_counts_the_degrade() {
let recorder = TestRecorder::default();
metrics::with_local_recorder(&recorder, || {
let mut scene = Scene::new();
scene.push(DrawCommand::FillRect {
rect: Rect::new(0.0, 0.0, 40.0, 40.0),
radii: None,
brush: radial_gradient_brush(),
transform: Affine::IDENTITY,
});
encode_scene(&scene, viewport(), &mut cache(), None, None, max_depth(), false)
});
let value = recorder.value_for(KEY_RENDER_PRIMITIVES, "native_gradient_lut_full_this_frame");
assert_eq!(value, 1);
}
#[test]
fn focal_radial_gradient_counts_the_shared_unprefixed_label() {
let recorder = TestRecorder::default();
metrics::with_local_recorder(&recorder, || {
let mut g = Gradient::new_radial(kurbo::Point::new(20.0, 20.0), 15.0);
if let GradientKind::Radial(pos) = &mut g.kind {
pos.start_center = kurbo::Point::new(5.0, 5.0); }
g.stops = two_stop_gradient_stops();
let mut scene = Scene::new();
scene.push(DrawCommand::FillRect {
rect: Rect::new(0.0, 0.0, 40.0, 40.0),
radii: None,
brush: Brush::Gradient(g),
transform: Affine::IDENTITY,
});
encode_scene(&scene, viewport(), &mut cache(), None, None, max_depth(), false)
});
assert_eq!(recorder.value_for(KEY_RENDER_PRIMITIVES, "gradient_radial_focal_degraded"), 1);
}
#[test]
fn sheared_rect_routes_through_triangle_pipeline_and_counts_telemetry() {
let recorder = TestRecorder::default();
let frame = metrics::with_local_recorder(&recorder, || {
let mut scene = Scene::new();
scene.push(DrawCommand::FillRect {
rect: Rect::new(10.0, 10.0, 30.0, 30.0),
radii: None,
brush: Brush::Solid(Color::from_rgba8(255, 0, 0, 255)),
transform: Affine::new([1.0, 0.0, 0.5, 1.0, 0.0, 0.0]), });
encode_scene(&scene, viewport(), &mut cache(), None, None, max_depth(), false)
});
assert!(frame.quads.is_empty(), "a sheared transform must NOT route through Quad SDF");
assert!(!frame.triangles.is_empty(), "must route through the Triangle pipeline instead");
assert_eq!(recorder.value_for(KEY_RENDER_PRIMITIVES, "native_rect_shear_to_triangle_pipeline"), 1);
}
#[test]
fn dashed_line_counts_the_dash_to_triangle_routing_telemetry() {
let recorder = TestRecorder::default();
let frame = metrics::with_local_recorder(&recorder, || {
let mut scene = Scene::new();
scene.push(DrawCommand::Line {
from: Vec2 { x: 0.0, y: 0.0 },
to: Vec2 { x: 100.0, y: 0.0 },
stroke: SceneStroke {
width: 4.0,
dash: Some(uzor_urx_core::scene::Dash { pattern: vec![10.0, 10.0], phase: 0.0 }),
..SceneStroke::default()
},
brush: Brush::Solid(Color::from_rgba8(255, 255, 255, 255)),
transform: Affine::IDENTITY,
});
encode_scene(&scene, viewport(), &mut cache(), None, None, max_depth(), false)
});
assert!(frame.lines.is_empty());
assert!(!frame.triangles.is_empty());
assert_eq!(recorder.value_for(KEY_RENDER_PRIMITIVES, "native_line_dash_to_triangle_pipeline"), 1);
}
#[test]
fn dashed_stroke_rect_counts_the_dash_to_triangle_routing_telemetry() {
let recorder = TestRecorder::default();
let frame = metrics::with_local_recorder(&recorder, || {
let mut scene = Scene::new();
scene.push(DrawCommand::StrokeRect {
rect: Rect::new(0.0, 0.0, 40.0, 40.0),
radii: None,
stroke: SceneStroke {
width: 3.0,
dash: Some(uzor_urx_core::scene::Dash { pattern: vec![8.0, 8.0], phase: 0.0 }),
..SceneStroke::default()
},
brush: Brush::Solid(Color::from_rgba8(0, 255, 0, 255)),
transform: Affine::IDENTITY,
});
encode_scene(&scene, viewport(), &mut cache(), None, None, max_depth(), false)
});
assert!(frame.quads.is_empty());
assert!(!frame.triangles.is_empty());
assert_eq!(recorder.value_for(KEY_RENDER_PRIMITIVES, "native_strokerect_dash_to_triangle_pipeline"), 1);
}
#[test]
fn non_uniform_radii_rect_routes_through_triangle_pipeline_without_any_degrade() {
let recorder = TestRecorder::default();
let frame = metrics::with_local_recorder(&recorder, || {
let mut scene = Scene::new();
scene.push(DrawCommand::FillRect {
rect: Rect::new(0.0, 0.0, 40.0, 40.0),
radii: Some([4.0, 40.0, 4.0, 40.0]), brush: Brush::Solid(Color::from_rgba8(255, 0, 0, 255)),
transform: Affine::IDENTITY,
});
encode_scene(&scene, viewport(), &mut cache(), None, None, max_depth(), false)
});
assert!(frame.quads.is_empty(), "non-uniform radii must NOT route through Quad SDF");
assert!(!frame.triangles.is_empty(), "must route through the Triangle pipeline instead");
assert_eq!(
recorder.value_for(KEY_RENDER_PRIMITIVES, "native_rect_shear_to_triangle_pipeline"),
0,
"non-uniform radii under an IDENTITY (non-shear) transform must not count the shear telemetry"
);
assert_eq!(
recorder.value_for(KEY_RENDER_PRIMITIVES, "native_per_corner_radii_uniform_approx"),
0,
"the approximation counter is fully CLOSED — must never fire again"
);
}
#[test]
fn unregistered_image_counts_miss_and_emits_nothing() {
let recorder = TestRecorder::default();
let frame = metrics::with_local_recorder(&recorder, || {
let mut scene = Scene::new();
scene.push(DrawCommand::Image {
src: ImageId(u64::MAX),
src_rect: None,
dest: Rect::new(0.0, 0.0, 20.0, 20.0),
transform: Affine::IDENTITY,
});
encode_scene(&scene, viewport(), &mut cache(), None, None, max_depth(), false)
});
assert!(frame.images.is_empty(), "no ImageInstance for an unregistered id");
let value = recorder.value_for(KEY_RENDER_PRIMITIVES, "image_id_unknown");
assert_eq!(value, 1);
}
#[test]
fn sheared_image_degrades_to_rotation_only_and_still_renders() {
let id = register_test_image(2, 2, [10, 20, 30, 255]);
let recorder = TestRecorder::default();
let frame = metrics::with_local_recorder(&recorder, || {
let mut scene = Scene::new();
let shear = Affine::new([1.0, 0.0, 0.5, 1.0, 0.0, 0.0]);
scene.push(DrawCommand::Image {
src: id,
src_rect: None,
dest: Rect::new(0.0, 0.0, 10.0, 10.0),
transform: shear,
});
encode_scene(&scene, viewport(), &mut cache(), None, None, max_depth(), false)
});
assert_eq!(frame.images.len(), 1, "a sheared image must still render, approximated");
let value = recorder.value_for(KEY_RENDER_PRIMITIVES, "native_image_shear_to_rotation_approx");
assert_eq!(value, 1);
uzor_urx_image::unregister_image(id);
}
#[test]
fn glyphrun_with_unregistered_font_counts_rasterise_failed_and_emits_nothing() {
let recorder = TestRecorder::default();
metrics::with_local_recorder(&recorder, || {
let mut scene = Scene::new();
scene.push(DrawCommand::GlyphRun {
glyphs: vec![
uzor_urx_core::scene::Glyph { glyph_id: 1, x: 0.0, y: 0.0 },
uzor_urx_core::scene::Glyph { glyph_id: 2, x: 10.0, y: 0.0 },
],
font: uzor_urx_core::scene::FontId(u64::MAX), font_size: 32.0,
brush: Brush::Solid(Color::from_rgba8(255, 255, 255, 255)),
transform: Affine::IDENTITY,
text: None,
});
let frame = encode_scene(&scene, viewport(), &mut cache(), None, None, max_depth(), false);
assert_eq!(frame.glyphs.len(), 0, "no glyph should have been placed — the font was never registered");
});
let value = recorder.value_for(KEY_RENDER_PRIMITIVES, "native_glyph_rasterise_failed");
assert_eq!(value, 2, "one native_glyph_rasterise_failed per glyph in the run");
}
#[test]
fn glyphrun_gradient_brush_degrades_to_solid_without_panicking() {
let recorder = TestRecorder::default();
metrics::with_local_recorder(&recorder, || {
let gradient = Gradient::new_linear((0.0, 0.0), (10.0, 0.0))
.with_stops([(0.0f32, Color::from_rgba8(0, 0, 0, 255)), (1.0f32, Color::from_rgba8(255, 255, 255, 255))]);
let mut scene = Scene::new();
scene.push(DrawCommand::GlyphRun {
glyphs: vec![uzor_urx_core::scene::Glyph { glyph_id: 1, x: 0.0, y: 0.0 }],
font: uzor_urx_core::scene::FontId(u64::MAX - 1), font_size: 32.0,
brush: Brush::Gradient(gradient),
transform: Affine::IDENTITY,
text: None,
});
let _frame = encode_scene(&scene, viewport(), &mut cache(), None, None, max_depth(), false);
});
let value = recorder.value_for(KEY_RENDER_PRIMITIVES, "native_glyphrun_gradient_to_solid");
assert_eq!(value, 1, "the gradient-brush degrade counter must fire exactly once per GlyphRun");
}
#[test]
#[ignore = "needs a headless GPU adapter"]
fn tiny_configured_atlas_forces_full_this_frame_degrade() {
let Some((device, queue)) = test_device() else { return };
let cfg = uzor_urx_core::config::UrxConfig::builder()
.wgpu_glyph_atlas_w(64)
.wgpu_glyph_atlas_h(64)
.build()
.expect("64x64 is a valid atlas dim");
let mut atlas = NativeGlyphAtlas::new(&device, &queue, cfg.wgpu_glyph_atlas_w, cfg.wgpu_glyph_atlas_h);
atlas.begin_frame();
let font = registered_font();
let glyphs: Vec<Glyph> = (3u32..83).map(|id| Glyph { glyph_id: id, x: 0.0, y: 0.0 }).collect();
let recorder = TestRecorder::default();
metrics::with_local_recorder(&recorder, || {
let mut scene = Scene::new();
scene.push(DrawCommand::GlyphRun {
glyphs,
font,
font_size: 24.0,
brush: Brush::Solid(Color::from_rgba8(255, 255, 255, 255)),
transform: Affine::IDENTITY,
text: None,
});
let _frame = encode_scene(&scene, viewport(), &mut cache(), Some(&mut atlas), None, max_depth(), false);
});
let full_this_frame = recorder.value_for(KEY_RENDER_PRIMITIVES, "native_glyph_atlas_full_this_frame");
assert!(full_this_frame > 0, "a 64x64 atlas fed 80 unique glyph identities in ONE frame must overflow");
let stats = atlas.stats();
assert!(
stats.entries < 80,
"a 64x64 atlas cannot hold 80 unique glyph rects — entries ({}) must be well short of 80",
stats.entries
);
}
#[test]
fn nonfinite_fill_path_point_is_skipped_not_panicked_and_counts_the_metric() {
let recorder = TestRecorder::default();
let frame = metrics::with_local_recorder(&recorder, || {
let mut path = BezPath::new();
path.move_to(Point::new(0.0, 0.0));
path.line_to(Point::new(f64::NAN, 10.0));
path.line_to(Point::new(10.0, 10.0));
path.close_path();
let mut scene = Scene::new();
scene.push(DrawCommand::FillPath {
path,
rule: FillRule::NonZero,
brush: Brush::Solid(Color::from_rgba8(255, 0, 0, 255)),
transform: Affine::IDENTITY,
});
encode_scene(&scene, viewport(), &mut cache(), None, None, max_depth(), false)
});
assert!(frame.triangles.is_empty(), "the non-finite path must contribute NO geometry");
assert_eq!(
recorder.total_for(KEY_RENDER_SKIPPED_NONFINITE),
1,
"the shared skip-and-count policy must fire exactly once"
);
}
#[test]
fn nonfinite_stroke_path_point_is_skipped_not_panicked_and_counts_the_metric() {
let recorder = TestRecorder::default();
let frame = metrics::with_local_recorder(&recorder, || {
let mut path = BezPath::new();
path.move_to(Point::new(0.0, 0.0));
path.line_to(Point::new(10.0, f64::INFINITY));
let mut scene = Scene::new();
scene.push(DrawCommand::StrokePath {
path,
stroke: SceneStroke { width: 2.0, ..SceneStroke::default() },
brush: Brush::Solid(Color::from_rgba8(0, 255, 0, 255)),
transform: Affine::IDENTITY,
});
encode_scene(&scene, viewport(), &mut cache(), None, None, max_depth(), false)
});
assert!(frame.triangles.is_empty(), "the non-finite path must contribute NO geometry");
assert_eq!(recorder.total_for(KEY_RENDER_SKIPPED_NONFINITE), 1);
}
#[test]
fn nonfinite_fill_path_does_not_block_a_later_valid_command() {
let recorder = TestRecorder::default();
let frame = metrics::with_local_recorder(&recorder, || {
let mut bad_path = BezPath::new();
bad_path.move_to(Point::new(0.0, 0.0));
bad_path.line_to(Point::new(f64::NAN, 10.0));
let mut scene = Scene::new();
scene.push(DrawCommand::FillPath {
path: bad_path,
rule: FillRule::NonZero,
brush: Brush::Solid(Color::from_rgba8(255, 0, 0, 255)),
transform: Affine::IDENTITY,
});
scene.fill_rect_solid(Rect::new(0.0, 0.0, 10.0, 10.0), Color::from_rgba8(0, 255, 0, 255));
encode_scene(&scene, viewport(), &mut cache(), None, None, max_depth(), false)
});
assert_eq!(frame.quads.len(), 1, "the valid FillRect after the bad path must still render");
assert_eq!(recorder.total_for(KEY_RENDER_SKIPPED_NONFINITE), 1);
}
}
}