use uzor::render::RenderContext;
use uzor_urx_core::scene::Scene;
use crate::urx_degrade_recorder::DegradeRecorder;
fn panic_message(payload: &(dyn std::any::Any + Send)) -> String {
if let Some(s) = payload.downcast_ref::<&str>() {
(*s).to_string()
} else if let Some(s) = payload.downcast_ref::<String>() {
s.clone()
} else {
"<non-string panic payload>".to_string()
}
}
pub fn render_tiny_skia(width: u32, height: u32, draw: impl FnOnce(&mut dyn RenderContext)) -> Vec<u8> {
let mut ctx = uzor_render_tiny_skia::TinySkiaCpuRenderContext::new(width, height, 1.0);
draw(&mut ctx);
ctx.pixels().to_vec()
}
pub fn render_vello_cpu(width: u32, height: u32, draw: impl FnOnce(&mut dyn RenderContext)) -> Vec<u8> {
let mut ctx = uzor_render_vello_cpu::VelloCpuRenderContext::new(1.0);
ctx.begin_frame(width, height);
draw(&mut ctx);
let mut buf = vec![0u8; (width as usize) * (height as usize) * 4];
ctx.render_to_pixmap_rgba8(&mut buf, width as u16, height as u16);
buf
}
pub fn render_urx_cpu(width: u32, height: u32, draw: impl FnOnce(&mut dyn RenderContext)) -> Vec<u8> {
let mut rec_ctx = uzor_render_urx::UrxRenderContext::new(1.0);
rec_ctx.begin_frame(width, height);
draw(&mut rec_ctx);
let scene: Scene = rec_ctx.take_scene();
let mut pixmap = uzor_urx_cpu::Pixmap::new(width, height);
let backend = uzor_urx_cpu::CpuBackend::new();
if let Err(e) = backend.render(&scene, &mut pixmap) {
eprintln!("[uzor-proof-harness] urx-cpu render error: {e:?}");
}
pixmap.pixels().to_vec()
}
fn init_headless_device(pick_features: impl FnOnce(&wgpu::Adapter) -> wgpu::Features) -> 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()?;
let required_features = pick_features(&adapter);
pollster::block_on(adapter.request_device(&wgpu::DeviceDescriptor {
label: Some("uzor-proof-harness-gpu-leg"),
required_features,
required_limits: wgpu::Limits::default(),
memory_hints: wgpu::MemoryHints::default(),
trace: wgpu::Trace::Off,
experimental_features: wgpu::ExperimentalFeatures::default(),
}))
.ok()
}
fn readback_rgba(device: &wgpu::Device, queue: &wgpu::Queue, texture: &wgpu::Texture, width: u32, height: u32) -> Vec<u8> {
let aligned_stride = (width * 4 + 255) & !255;
let buf_size = (aligned_stride * height) as u64;
let staging = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("uzor-proof-harness-gpu-readback"),
size: buf_size,
usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let mut enc = device.create_command_encoder(&wgpu::CommandEncoderDescriptor::default());
enc.copy_texture_to_buffer(
wgpu::TexelCopyTextureInfo { texture, mip_level: 0, origin: wgpu::Origin3d::ZERO, aspect: wgpu::TextureAspect::All },
wgpu::TexelCopyBufferInfo {
buffer: &staging,
layout: wgpu::TexelCopyBufferLayout { offset: 0, bytes_per_row: Some(aligned_stride), rows_per_image: Some(height) },
},
wgpu::Extent3d { width, height, depth_or_array_layers: 1 },
);
queue.submit(Some(enc.finish()));
let slice = staging.slice(..);
let (tx, rx) = std::sync::mpsc::channel();
slice.map_async(wgpu::MapMode::Read, move |r| {
let _ = tx.send(r);
});
let _ = device.poll(wgpu::PollType::Wait { submission_index: None, timeout: None });
let map_result = rx.recv().unwrap_or(Err(wgpu::BufferAsyncError));
if let Err(e) = map_result {
eprintln!("[uzor-proof-harness] gpu readback: staging buffer map failed: {e}");
return vec![0u8; (width as usize) * (height as usize) * 4];
}
let raw = slice.get_mapped_range();
let mut out = Vec::with_capacity((width as usize) * (height as usize) * 4);
for row in 0..height as usize {
let row_start = row * aligned_stride as usize;
let row_end = row_start + (width * 4) as usize;
out.extend_from_slice(&raw[row_start..row_end]);
}
drop(raw);
staging.unmap();
out
}
fn init_vello_gpu_device() -> Option<(wgpu::Device, wgpu::Queue)> {
init_headless_device(|adapter| adapter.features() & (wgpu::Features::CLEAR_TEXTURE | wgpu::Features::PIPELINE_CACHE))
}
fn init_urx_wgpu_device() -> Option<(wgpu::Device, wgpu::Queue)> {
init_headless_device(|_adapter| wgpu::Features::empty())
}
pub fn render_vello_gpu(width: u32, height: u32, draw: impl FnOnce(&mut dyn RenderContext)) -> Option<Vec<u8>> {
let (device, queue) = init_vello_gpu_device()?;
let mut renderer = match vello::Renderer::new(
&device,
vello::RendererOptions {
use_cpu: false,
antialiasing_support: vello::AaSupport::area_only(),
num_init_threads: std::num::NonZeroUsize::new(1),
pipeline_cache: None,
},
) {
Ok(r) => r,
Err(e) => {
eprintln!("[uzor-proof-harness] vello::Renderer::new failed: {e}; skipping vello-gpu leg");
return None;
}
};
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
let mut scene = vello::Scene::new();
{
let mut ctx = uzor_render_vello_gpu::VelloGpuRenderContext::new(&mut scene, 0.0, 0.0);
let ctx_dyn: &mut dyn RenderContext = &mut ctx;
draw(ctx_dyn);
}
let target = device.create_texture(&wgpu::TextureDescriptor {
label: Some("uzor-proof-harness-vello-gpu-target"),
size: wgpu::Extent3d { width, height, depth_or_array_layers: 1 },
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: wgpu::TextureFormat::Rgba8Unorm,
usage: wgpu::TextureUsages::STORAGE_BINDING | wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_SRC,
view_formats: &[],
});
let view = target.create_view(&wgpu::TextureViewDescriptor::default());
if let Err(e) = renderer.render_to_texture(
&device,
&queue,
&scene,
&view,
&vello::RenderParams {
base_color: vello::peniko::Color::TRANSPARENT,
width,
height,
antialiasing_method: vello::AaConfig::Area,
},
) {
eprintln!("[uzor-proof-harness] vello render_to_texture error: {e}");
}
readback_rgba(&device, &queue, &target, width, height)
}));
match result {
Ok(pixels) => Some(pixels),
Err(payload) => {
eprintln!("[uzor-proof-harness] vello-gpu leg panicked: {}; skipping vello-gpu leg", panic_message(payload.as_ref()));
None
}
}
}
pub struct UrxWgpuRender {
pub pixels: Vec<u8>,
pub degrades: Vec<(String, u64)>,
}
pub fn render_urx_wgpu(width: u32, height: u32, draw: impl FnOnce(&mut dyn RenderContext)) -> Option<UrxWgpuRender> {
let mut rec_ctx = uzor_render_urx::UrxRenderContext::new(1.0);
rec_ctx.begin_frame(width, height);
draw(&mut rec_ctx);
let scene: Scene = rec_ctx.take_scene();
let (device, queue) = init_urx_wgpu_device()?;
const FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Rgba8Unorm;
let mut renderer = uzor_urx_wgpu::NativeUrxRenderer::new(device.clone(), queue.clone(), FORMAT);
let texture = device.create_texture(&wgpu::TextureDescriptor {
label: Some("uzor-proof-harness-urx-wgpu-target"),
size: wgpu::Extent3d { width, height, depth_or_array_layers: 1 },
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: FORMAT,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
view_formats: &[],
});
let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
let recorder = DegradeRecorder::default();
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor::default());
metrics::with_local_recorder(&recorder, || {
if let Err(e) = renderer.render_into_encoder(&scene, &mut encoder, &view, uzor_urx_wgpu::Viewport { width, height }) {
eprintln!("[uzor-proof-harness] urx-wgpu render error: {e:?}");
}
});
queue.submit(Some(encoder.finish()));
readback_rgba(&device, &queue, &texture, width, height)
}));
match result {
Ok(pixels) => Some(UrxWgpuRender { pixels, degrades: recorder.fired() }),
Err(payload) => {
eprintln!("[uzor-proof-harness] urx-gpu leg panicked: {}; skipping urx-gpu leg", panic_message(payload.as_ref()));
None
}
}
}
pub struct MultiLegRender {
pub width: u32,
pub height: u32,
pub tiny_skia: Vec<u8>,
pub vello_cpu: Vec<u8>,
pub vello_gpu: Option<Vec<u8>>,
pub urx_cpu: Vec<u8>,
pub urx_gpu: Option<UrxWgpuRender>,
}
impl MultiLegRender {
pub fn capture(width: u32, height: u32, draw: impl Fn(&mut dyn RenderContext)) -> Self {
let vello_gpu = render_vello_gpu(width, height, &draw);
if vello_gpu.is_none() {
eprintln!("[uzor-proof-harness] vello-gpu leg: no GPU/software adapter available (or renderer init failed); skipping");
}
let urx_gpu = render_urx_wgpu(width, height, &draw);
if urx_gpu.is_none() {
eprintln!("[uzor-proof-harness] urx-gpu leg: no GPU/software adapter available; skipping");
}
Self {
width,
height,
tiny_skia: render_tiny_skia(width, height, &draw),
vello_cpu: render_vello_cpu(width, height, &draw),
vello_gpu,
urx_cpu: render_urx_cpu(width, height, &draw),
urx_gpu,
}
}
pub fn legs(&self) -> Vec<(&'static str, &[u8])> {
let mut out = vec![("tiny-skia", self.tiny_skia.as_slice()), ("vello-cpu", self.vello_cpu.as_slice())];
if let Some(ref px) = self.vello_gpu {
out.push(("vello-gpu", px.as_slice()));
}
out.push(("urx-cpu", self.urx_cpu.as_slice()));
if let Some(ref r) = self.urx_gpu {
out.push(("urx-gpu", r.pixels.as_slice()));
}
out
}
}
#[cfg(test)]
mod tests {
use super::*;
fn solid_red_rect(ctx: &mut dyn RenderContext) {
ctx.set_fill_color("#ff0000");
ctx.fill_rect(0.0, 0.0, 4.0, 4.0);
}
#[test]
fn all_three_cpu_legs_render_a_solid_fill_correctly_sized_and_opaque_red() {
let render = MultiLegRender::capture(4, 4, solid_red_rect);
let center = ((2 * 4 + 2) * 4) as usize;
for (label, pixels) in [("tiny-skia", &render.tiny_skia), ("vello-cpu", &render.vello_cpu), ("urx-cpu", &render.urx_cpu)] {
assert_eq!(pixels.len(), 4 * 4 * 4, "{label}: unexpected buffer length");
assert_eq!(&pixels[center..center + 4], &[255, 0, 0, 255], "{label}: center pixel should be opaque red");
}
}
#[test]
fn legs_lists_cpu_legs_always_and_gpu_legs_iff_present() {
let render = MultiLegRender::capture(4, 4, solid_red_rect);
let labels: Vec<&str> = render.legs().into_iter().map(|(label, _)| label).collect();
assert!(labels.contains(&"tiny-skia"));
assert!(labels.contains(&"vello-cpu"));
assert!(labels.contains(&"urx-cpu"));
assert_eq!(labels.contains(&"vello-gpu"), render.vello_gpu.is_some());
assert_eq!(labels.contains(&"urx-gpu"), render.urx_gpu.is_some());
}
#[test]
#[ignore = "needs a GPU/software adapter; run with --ignored"]
fn gpu_legs_render_a_solid_fill_correctly_sized_and_opaque_red_when_available() {
const SIZE: u32 = 64;
let render = MultiLegRender::capture(SIZE, SIZE, |ctx| {
ctx.set_fill_color("#ff0000");
ctx.fill_rect(0.0, 0.0, SIZE as f64, SIZE as f64);
});
let center = (((SIZE / 2) * SIZE + SIZE / 2) * 4) as usize;
let Some(ref vello_gpu) = render.vello_gpu else {
eprintln!("gpu_legs_render_a_solid_fill_correctly_sized_and_opaque_red_when_available: vello-gpu skipped, no adapter");
return;
};
assert_eq!(vello_gpu.len(), (SIZE * SIZE * 4) as usize, "vello-gpu: unexpected buffer length");
assert_eq!(&vello_gpu[center..center + 4], &[255, 0, 0, 255], "vello-gpu: center pixel should be opaque red");
let Some(ref urx_gpu) = render.urx_gpu else {
eprintln!("gpu_legs_render_a_solid_fill_correctly_sized_and_opaque_red_when_available: urx-gpu skipped, no adapter");
return;
};
assert_eq!(urx_gpu.pixels.len(), (SIZE * SIZE * 4) as usize, "urx-gpu: unexpected buffer length");
assert_eq!(&urx_gpu.pixels[center..center + 4], &[255, 0, 0, 255], "urx-gpu: center pixel should be opaque red");
assert!(urx_gpu.degrades.is_empty(), "urx-gpu: a bare solid fill must not trip any degrade counter: {:?}", urx_gpu.degrades);
}
}