use std::time::Instant;
use uzor::render::{GradientPainter, Masking, Painter, ShapeHelpers, TextRenderer};
use uzor_render_wgpu_instanced::InstancedRenderContext;
use uzor_urx_core::math::{Affine, BezPath, Brush, Color, GradientKind};
use uzor_urx_core::scene::{DrawCommand, Scene};
use kurbo::PathEl;
pub fn adapt_scene_into(scene: &Scene, ctx: &mut InstancedRenderContext) {
use uzor_urx_core::metrics_keys::{
KEY_RENDER_PRIMITIVES, render_submit_count_key,
};
let t0 = Instant::now();
let mut clip_depth: u32 = 0;
for cmd in &scene.commands {
match cmd {
DrawCommand::FillRect { rect, radii, brush, transform } => {
apply_transform(ctx, transform);
let (x, y, w, h) = (rect.x0, rect.y0, rect.width(), rect.height());
match brush {
Brush::Solid(c) => {
ctx.set_fill_color(&color_to_css(*c));
if let Some(r) = radii.filter(|r| r.iter().any(|v| *v > 0.0)) {
ctx.fill_rounded_rect(x, y, w, h, r[0] as f64);
} else {
ctx.fill_rect(x, y, w, h);
}
}
Brush::Gradient(grad) => match &grad.kind {
GradientKind::Linear(pos) => {
let stops: Vec<(f32, String)> = grad.stops.iter().map(|s| {
let c = s.color.to_alpha_color::<peniko::color::Srgb>();
(s.offset, color_to_css(c))
}).collect();
let stop_refs: Vec<(f32, &str)> =
stops.iter().map(|(t, s)| (*t, s.as_str())).collect();
ctx.fill_linear_gradient(
&stop_refs,
pos.start.x, pos.start.y,
pos.end.x, pos.end.y,
);
degrade_radii(radii, "wgpu_fill_rect_gradient_radii_dropped");
}
GradientKind::Radial(_) | GradientKind::Sweep(_) => {
let c = grad.stops.first()
.map(|s| s.color.to_alpha_color::<peniko::color::Srgb>())
.unwrap_or(Color::from_rgba8(0,0,0,0));
ctx.set_fill_color(&color_to_css(c));
if let Some(r) = radii.filter(|r| r.iter().any(|v| *v > 0.0)) {
ctx.fill_rounded_rect(x, y, w, h, r[0] as f64);
} else {
ctx.fill_rect(x, y, w, h);
}
metrics::counter!(
KEY_RENDER_PRIMITIVES,
"kind" => "wgpu_fill_rect_radial_or_sweep_to_solid",
).increment(1);
}
},
Brush::Image(_) => {
metrics::counter!(
KEY_RENDER_PRIMITIVES,
"kind" => "wgpu_fill_rect_image_dropped",
).increment(1);
}
}
unapply_transform(ctx, transform);
}
DrawCommand::StrokeRect { rect, radii, stroke, brush, transform } => {
apply_transform(ctx, transform);
let color = brush_to_solid_color(brush);
ctx.set_stroke_color(&color_to_css(color));
ctx.set_stroke_width(stroke.width as f64);
let (x, y, w, h) = (rect.x0, rect.y0, rect.width(), rect.height());
if let Some(r) = radii.filter(|r| r.iter().any(|v| *v > 0.0)) {
ctx.stroke_rounded_rect(x, y, w, h, r[0] as f64);
} else {
ctx.stroke_rect(x, y, w, h);
}
unapply_transform(ctx, transform);
}
DrawCommand::Line { from, to, stroke, brush, transform } => {
let color = brush_to_solid_color(brush);
ctx.set_stroke_color(&color_to_css(color));
ctx.set_stroke_width(stroke.width as f64);
apply_transform(ctx, transform);
ctx.begin_path();
ctx.move_to(from.x, from.y);
ctx.line_to(to.x, to.y);
ctx.stroke();
unapply_transform(ctx, transform);
}
DrawCommand::LineBatch { segments, stroke, brush, transform } => {
let color = brush_to_solid_color(brush);
ctx.set_stroke_color(&color_to_css(color));
ctx.set_stroke_width(stroke.width as f64);
apply_transform(ctx, transform);
ctx.begin_path();
for segment in segments {
ctx.move_to(segment.from.x, segment.from.y);
ctx.line_to(segment.to.x, segment.to.y);
}
ctx.stroke();
unapply_transform(ctx, transform);
}
DrawCommand::FillPath { path, rule: _rule, brush, transform } => {
let color = brush_to_solid_color(brush);
ctx.set_fill_color(&color_to_css(color));
apply_transform(ctx, transform);
ctx.begin_path();
emit_path_into_ctx(path, ctx);
ctx.fill();
unapply_transform(ctx, transform);
}
DrawCommand::StrokePath { path, stroke, brush, transform } => {
let color = brush_to_solid_color(brush);
ctx.set_stroke_color(&color_to_css(color));
ctx.set_stroke_width(stroke.width as f64);
apply_transform(ctx, transform);
ctx.begin_path();
emit_path_into_ctx(path, ctx);
ctx.stroke();
unapply_transform(ctx, transform);
}
DrawCommand::GlyphRun { glyphs: _, font: _, font_size, brush, transform, text } => {
match text {
Some(s) if !s.is_empty() => {
let color = brush_to_solid_color(brush);
ctx.set_fill_color(&color_to_css(color));
ctx.set_font(&format!("{font_size}px sans-serif"));
apply_transform(ctx, transform);
ctx.fill_text(s, 0.0, 0.0);
unapply_transform(ctx, transform);
}
_ => {
metrics::counter!(
uzor_urx_core::metrics_keys::KEY_RENDER_PRIMITIVES,
"kind" => "wgpu_glyphrun_dropped",
).increment(1);
}
}
}
DrawCommand::Image { .. } => {
metrics::counter!(
uzor_urx_core::metrics_keys::KEY_RENDER_PRIMITIVES,
"kind" => "wgpu_image_dropped",
).increment(1);
}
DrawCommand::PushClipRect { rect, transform } => {
apply_transform(ctx, transform);
ctx.save();
ctx.begin_path();
ctx.move_to(rect.x0, rect.y0);
ctx.line_to(rect.x1, rect.y0);
ctx.line_to(rect.x1, rect.y1);
ctx.line_to(rect.x0, rect.y1);
ctx.close_path();
ctx.clip();
clip_depth += 1;
unapply_transform(ctx, transform);
}
DrawCommand::PushClipRoundedRect { rect, transform } => {
apply_transform(ctx, transform);
ctx.save();
ctx.begin_path();
let r = rect.rect();
ctx.move_to(r.x0, r.y0);
ctx.line_to(r.x1, r.y0);
ctx.line_to(r.x1, r.y1);
ctx.line_to(r.x0, r.y1);
ctx.close_path();
ctx.clip();
clip_depth += 1;
metrics::counter!(
uzor_urx_core::metrics_keys::KEY_RENDER_PRIMITIVES,
"kind" => "wgpu_rounded_clip_to_rect_bbox",
).increment(1);
unapply_transform(ctx, transform);
}
DrawCommand::PopClip => {
if clip_depth > 0 {
ctx.restore();
clip_depth -= 1;
}
}
DrawCommand::PushBlendLayer { .. } | DrawCommand::PopBlendLayer => {
metrics::counter!(
uzor_urx_core::metrics_keys::KEY_RENDER_PRIMITIVES,
"kind" => "wgpu_blend_layer_dropped",
).increment(1);
}
}
}
while clip_depth > 0 {
ctx.restore();
clip_depth -= 1;
}
let elapsed_us = t0.elapsed().as_micros() as u64;
metrics::histogram!(render_submit_count_key("urx_wgpu_adapt")).record(elapsed_us as f64);
metrics::counter!(KEY_RENDER_PRIMITIVES).increment(scene.commands.len() as u64);
}
#[inline]
fn degrade_radii(radii: &Option<[f32; 4]>, label: &'static str) {
if let Some(r) = radii {
if r.iter().any(|v| *v > 0.0) {
metrics::counter!(
uzor_urx_core::metrics_keys::KEY_RENDER_PRIMITIVES,
"kind" => label,
).increment(1);
}
}
}
fn emit_path_into_ctx(path: &BezPath, ctx: &mut InstancedRenderContext) {
kurbo::flatten(path.elements().iter().copied(), 0.25, |el| {
match el {
PathEl::MoveTo(p) => { ctx.move_to(p.x, p.y); }
PathEl::LineTo(p) => { ctx.line_to(p.x, p.y); }
PathEl::ClosePath => { ctx.close_path(); }
_ => {}
}
});
}
#[inline]
fn brush_to_solid_color(brush: &Brush) -> Color {
match brush {
Brush::Solid(c) => *c,
Brush::Gradient(g) => g.stops.first()
.map(|s| s.color.to_alpha_color::<peniko::color::Srgb>())
.unwrap_or(Color::from_rgba8(0, 0, 0, 0)),
Brush::Image(_) => Color::from_rgba8(0, 0, 0, 0),
}
}
#[inline]
fn color_to_css(c: Color) -> String {
let p = c.to_rgba8();
format!("rgba({},{},{},{})", p.r, p.g, p.b, (p.a as f64) / 255.0)
}
fn apply_transform(ctx: &mut InstancedRenderContext, t: &Affine) {
if *t == Affine::IDENTITY { return; }
ctx.save();
let c = t.as_coeffs();
let (sx, sy, tx, ty) = (c[0], c[3], c[4], c[5]);
if tx != 0.0 || ty != 0.0 { ctx.translate(tx, ty); }
if sx != 1.0 || sy != 1.0 { ctx.scale(sx, sy); }
}
fn unapply_transform(ctx: &mut InstancedRenderContext, t: &Affine) {
if *t == Affine::IDENTITY { return; }
ctx.restore();
}
#[cfg(test)]
mod tests {
use super::*;
use uzor_urx_core::math::{Rect, Vec2};
use uzor_urx_core::scene::Stroke;
#[test]
fn adapt_emits_quad_for_fill_rect() {
let mut ctx = InstancedRenderContext::new(100.0, 100.0, 0.0, 0.0);
let mut scene = Scene::new();
scene.push(DrawCommand::FillRect {
rect: Rect::new(10.0, 10.0, 50.0, 50.0),
radii: None,
brush: Brush::Solid(Color::from_rgba8(255, 0, 0, 255)),
transform: Affine::IDENTITY,
});
adapt_scene_into(&scene, &mut ctx);
assert!(!ctx.draw_commands.is_empty());
}
#[test]
fn adapt_emits_rounded_rect_for_radii() {
let mut ctx = InstancedRenderContext::new(100.0, 100.0, 0.0, 0.0);
let mut scene = Scene::new();
scene.push(DrawCommand::FillRect {
rect: Rect::new(10.0, 10.0, 50.0, 50.0),
radii: Some([8.0, 8.0, 8.0, 8.0]),
brush: Brush::Solid(Color::from_rgba8(255, 0, 0, 255)),
transform: Affine::IDENTITY,
});
adapt_scene_into(&scene, &mut ctx);
assert!(!ctx.draw_commands.is_empty());
}
#[test]
fn adapt_emits_for_line() {
let mut ctx = InstancedRenderContext::new(100.0, 100.0, 0.0, 0.0);
let mut scene = Scene::new();
scene.push(DrawCommand::Line {
from: Vec2 { x: 5.0, y: 5.0 },
to: Vec2 { x: 50.0, y: 50.0 },
stroke: Stroke { width: 2.0, ..Stroke::default() },
brush: Brush::Solid(Color::from_rgba8(0, 255, 0, 255)),
transform: Affine::IDENTITY,
});
adapt_scene_into(&scene, &mut ctx);
assert!(!ctx.draw_commands.is_empty());
}
#[test]
fn adapt_routes_glyph_run_with_text_companion_to_the_atlas_text_path() {
let mut ctx = InstancedRenderContext::new(200.0, 100.0, 0.0, 0.0);
let mut scene = Scene::new();
scene.push(DrawCommand::GlyphRun {
glyphs: vec![],
font: uzor_urx_core::scene::FontId(0),
font_size: 24.0,
brush: Brush::Solid(Color::from_rgba8(255, 255, 255, 255)),
transform: Affine::translate((15.0, 60.0)),
text: Some("hello".to_owned()),
});
adapt_scene_into(&scene, &mut ctx);
let text_cmds: Vec<_> = ctx
.draw_commands
.iter()
.filter_map(|c| match c {
uzor_render_wgpu_instanced::DrawCmd::Text(t) => Some(t),
_ => None,
})
.collect();
assert_eq!(text_cmds.len(), 1, "exactly one Text draw command must be emitted");
assert_eq!(text_cmds[0].text, "hello");
assert_eq!(text_cmds[0].font_size, 24.0);
assert!(
(text_cmds[0].x - 15.0).abs() < 0.01 && (text_cmds[0].y - 60.0).abs() < 0.01,
"run origin must come from the transform translation, got ({}, {})",
text_cmds[0].x,
text_cmds[0].y
);
}
#[test]
fn adapt_still_degrades_a_glyph_id_only_run_without_text() {
let mut ctx = InstancedRenderContext::new(200.0, 100.0, 0.0, 0.0);
let mut scene = Scene::new();
scene.push(DrawCommand::GlyphRun {
glyphs: vec![uzor_urx_core::scene::Glyph { glyph_id: 36, x: 0.0, y: 0.0 }],
font: uzor_urx_core::scene::FontId(0),
font_size: 24.0,
brush: Brush::Solid(Color::from_rgba8(255, 255, 255, 255)),
transform: Affine::IDENTITY,
text: None,
});
adapt_scene_into(&scene, &mut ctx);
assert!(
!ctx.draw_commands.iter().any(|c| matches!(c, uzor_render_wgpu_instanced::DrawCmd::Text(_))),
"a glyph-id-only run must not fabricate a Text command"
);
}
#[test]
fn adapt_balances_clip_stack() {
let mut ctx = InstancedRenderContext::new(100.0, 100.0, 0.0, 0.0);
let mut scene = Scene::new();
scene.push(DrawCommand::PushClipRect {
rect: Rect::new(0.0, 0.0, 50.0, 50.0),
transform: Affine::IDENTITY,
});
adapt_scene_into(&scene, &mut ctx);
}
}