use std::sync::{Arc, Mutex};
use bevy::{
asset::uuid_handle,
camera::visibility::RenderLayers,
mesh::{MeshVertexBufferLayoutRef, VertexBufferLayout},
prelude::*,
render::{
extract_component::ExtractComponent,
render_resource::{
AsBindGroup, RenderPipelineDescriptor, SpecializedMeshPipelineError, VertexFormat,
VertexStepMode,
},
renderer::RenderDevice,
},
shader::ShaderRef,
sprite_render::{Material2d, Material2dKey},
};
use vello::{AaConfig, AaSupport, kurbo::Affine};
mod plugin;
mod systems;
pub(crate) mod extract;
pub(crate) mod prepare;
pub(crate) use plugin::VelloRenderPlugin;
pub mod diagnostics;
pub const RT_SHADER_HANDLE: Handle<Shader> = uuid_handle!("e7235b72-1181-4e18-a9f2-93b32026a820");
#[derive(Component, Debug, Clone, Copy, ExtractComponent)]
#[require(Camera2d)]
pub struct VelloView;
#[derive(AsBindGroup, TypePath, Asset, Clone)]
pub struct VelloCanvasMaterial {
#[texture(0)]
#[sampler(1)]
pub texture: Handle<Image>,
}
impl Material2d for VelloCanvasMaterial {
fn vertex_shader() -> ShaderRef {
RT_SHADER_HANDLE.into()
}
fn fragment_shader() -> ShaderRef {
RT_SHADER_HANDLE.into()
}
fn specialize(
descriptor: &mut RenderPipelineDescriptor,
_layout: &MeshVertexBufferLayoutRef,
_key: Material2dKey<Self>,
) -> Result<(), SpecializedMeshPipelineError> {
if let Some(target) = descriptor.fragment.as_mut() {
let mut_targets = &mut target.targets;
if let Some(Some(target)) = mut_targets.get_mut(0) {
target.blend = Some(vello::wgpu::BlendState::ALPHA_BLENDING);
}
}
let formats = vec![
VertexFormat::Float32x3,
VertexFormat::Float32x2,
];
let vertex_layout =
VertexBufferLayout::from_vertex_formats(VertexStepMode::Vertex, formats);
descriptor.vertex.buffers = vec![vertex_layout];
Ok(())
}
}
#[derive(Resource, Deref, DerefMut)]
pub struct VelloRenderer(Arc<Mutex<vello::Renderer>>);
impl VelloRenderer {
pub fn try_new(
device: &vello::wgpu::Device,
settings: &VelloRenderSettings,
) -> Result<Self, vello::Error> {
vello::Renderer::new(
device,
vello::RendererOptions {
use_cpu: settings.use_cpu,
antialiasing_support: AaSupport::all(),
num_init_threads: None,
pipeline_cache: None,
},
)
.map(Mutex::new)
.map(Arc::new)
.map(VelloRenderer)
}
}
impl FromWorld for VelloRenderer {
fn from_world(world: &mut World) -> Self {
match VelloRenderer::try_new(
world.get_resource::<RenderDevice>().unwrap().wgpu_device(),
world.get_resource::<VelloRenderSettings>().unwrap(),
) {
Ok(r) => r,
Err(e) => {
tracing::error!(
"Attempting safe-mode fallback, failed to initialize renderer: {e:}"
);
{
let mut settings = world.get_resource_mut::<VelloRenderSettings>().unwrap();
settings.use_cpu = true;
settings.antialiasing = AaConfig::Area;
}
match VelloRenderer::try_new(
world.get_resource::<RenderDevice>().unwrap().wgpu_device(),
world.get_resource::<VelloRenderSettings>().unwrap(),
) {
Ok(r) => r,
Err(e) => panic!("Failed to start vello: {e}"),
}
}
}
}
}
#[derive(Resource, Deref, DerefMut, Default)]
#[cfg(feature = "lottie")]
pub struct VelatoRenderer(velato::Renderer);
#[derive(Resource, Clone)]
pub struct VelloRenderSettings {
pub use_cpu: bool,
pub antialiasing: AaConfig,
}
impl Default for VelloRenderSettings {
fn default() -> Self {
Self {
use_cpu: false,
antialiasing: AaConfig::Area,
}
}
}
#[derive(Resource, Clone, Debug, Default, PartialEq)]
pub(crate) struct VelloCanvasSettings {
pub render_layers: RenderLayers,
}
#[derive(Clone)]
#[allow(clippy::large_enum_variant, reason = "Many feature gates")]
pub(crate) enum VelloWorldRenderItem {
Scene {
affine: Affine,
item: crate::integrations::scene::render::ExtractedVelloScene2d,
},
#[cfg(feature = "svg")]
Svg {
affine: Affine,
item: crate::integrations::svg::render::ExtractedVelloSvg2d,
},
#[cfg(feature = "lottie")]
Lottie {
affine: Affine,
item: crate::integrations::lottie::render::ExtractedVelloLottie2d,
},
#[cfg(feature = "text")]
Text {
affine: Affine,
item: crate::integrations::text::render::ExtractedVelloText2d,
},
}
#[derive(Clone)]
#[allow(clippy::large_enum_variant, reason = "Many feature gates")]
pub(crate) enum VelloUiRenderItem {
Scene {
affine: Affine,
clip: Option<vello::kurbo::Rect>,
item: crate::integrations::scene::render::ExtractedUiVelloScene,
},
#[cfg(feature = "svg")]
Svg {
affine: Affine,
clip: Option<vello::kurbo::Rect>,
item: crate::integrations::svg::render::ExtractedUiVelloSvg,
},
#[cfg(feature = "lottie")]
Lottie {
affine: Affine,
clip: Option<vello::kurbo::Rect>,
item: crate::integrations::lottie::render::ExtractedUiVelloLottie,
},
#[cfg(feature = "text")]
Text {
affine: Affine,
clip: Option<vello::kurbo::Rect>,
item: crate::integrations::text::render::ExtractedUiVelloText,
},
}
#[derive(Resource, Default)]
pub(crate) struct VelloRenderQueue {
world: Vec<VelloWorldRenderItem>,
ui: Vec<VelloUiRenderItem>,
}
#[derive(Resource, Default, Debug, Clone, Reflect)]
pub(crate) struct VelloEntityCountData {
pub n_world_scenes: u32,
pub n_ui_scenes: u32,
#[cfg(feature = "text")]
pub n_world_texts: u32,
#[cfg(feature = "text")]
pub n_ui_texts: u32,
#[cfg(feature = "svg")]
pub n_world_svgs: u32,
#[cfg(feature = "svg")]
pub n_ui_svgs: u32,
#[cfg(feature = "lottie")]
pub n_world_lotties: u32,
#[cfg(feature = "lottie")]
pub n_ui_lotties: u32,
}
#[derive(Resource, Default, Debug, Clone, Reflect)]
pub(crate) struct VelloFrameProfileData {
pub n_paths: u32,
pub n_path_segs: u32,
pub n_clips: u32,
pub n_open_clips: u32,
#[cfg(feature = "text")]
pub n_glyphs: u32,
#[cfg(feature = "text")]
pub n_glyph_runs: u32,
}
#[cfg(test)]
mod tests {
use super::*;
use bevy::math::Rect;
use systems::to_kurbo_clip;
#[test]
fn clip_converts_to_kurbo_without_scaling() {
let bevy_clip = Rect::new(10.0, 20.0, 100.0, 200.0);
let kurbo_clip = to_kurbo_clip(Some(bevy_clip)).unwrap();
assert_eq!(kurbo_clip.x0, 10.0);
assert_eq!(kurbo_clip.y0, 20.0);
assert_eq!(kurbo_clip.x1, 100.0);
assert_eq!(kurbo_clip.y1, 200.0);
}
#[test]
fn clip_none_returns_none() {
assert!(to_kurbo_clip(None).is_none());
}
#[test]
fn clip_with_infinite_x_produces_finite_rect() {
let clip = Rect::new(f32::NEG_INFINITY, 35.0, f32::INFINITY, 772.0);
let kurbo = to_kurbo_clip(Some(clip));
let kurbo = kurbo.expect("infinite-x clip should still produce a rect");
assert!(kurbo.x0.is_finite(), "x0 must be finite, got {}", kurbo.x0);
assert!(kurbo.x1.is_finite(), "x1 must be finite, got {}", kurbo.x1);
assert_eq!(kurbo.y0, 35.0, "finite y0 must be preserved");
assert_eq!(kurbo.y1, 772.0, "finite y1 must be preserved");
assert!(kurbo.x0 < kurbo.x1, "clamped rect must have positive width");
}
#[test]
fn clip_with_nan_returns_none() {
let clip = Rect::new(f32::NAN, 35.0, f32::NAN, 772.0);
assert!(
to_kurbo_clip(Some(clip)).is_none(),
"NaN clip should return None"
);
}
#[test]
fn clip_with_one_infinite_coord_produces_finite_rect() {
let clip = Rect::new(f32::NEG_INFINITY, 0.0, 1280.0, 800.0);
let kurbo = to_kurbo_clip(Some(clip)).expect("should produce a rect");
assert!(kurbo.x0.is_finite(), "x0 must be finite");
assert_eq!(kurbo.x1, 1280.0);
assert_eq!(kurbo.y0, 0.0);
assert_eq!(kurbo.y1, 800.0);
}
#[test]
fn ui_sort_preserves_insertion_order_for_equal_keys() {
let items: Vec<(u32, char)> = vec![(5, 'A'), (5, 'B'), (5, 'C'), (5, 'D'), (5, 'E')];
let mut sorted = items.clone();
sorted.sort_by_key(|(i, _)| *i);
let order: Vec<char> = sorted.iter().map(|(_, c)| *c).collect();
assert_eq!(order, vec!['A', 'B', 'C', 'D', 'E']);
}
}