From 6809f64a17a7512e91c29e611e9c54457a8cf140 Mon Sep 17 00:00:00 2001 From: Ian Douglas Scott Date: Wed, 26 Oct 2022 15:33:51 -0700 Subject: [PATCH] WIP benchmarking and testing for renderers This shows `iced_glow` outperforming `iced_wgpu`. Probably not accurate, some something may be wrong in the rendering and timing here? It should also test with more primitivies. Tests pass when combined with https://github.com/iced-rs/iced/pull/1485 and https://github.com/iced-rs/iced/pull/1491. --- Cargo.toml | 1 + bench/Cargo.toml | 26 ++++ bench/benches/renderer_bench.rs | 135 +++++++++++++++++++++ bench/examples/render.rs | 1 + bench/src/glow.rs | 132 +++++++++++++++++++++ bench/src/lib.rs | 39 ++++++ bench/src/wgpu.rs | 203 ++++++++++++++++++++++++++++++++ bench/tests/render-test.rs | 74 ++++++++++++ 8 files changed, 611 insertions(+) create mode 100644 bench/Cargo.toml create mode 100644 bench/benches/renderer_bench.rs create mode 100644 bench/examples/render.rs create mode 100644 bench/src/glow.rs create mode 100644 bench/src/lib.rs create mode 100644 bench/src/wgpu.rs create mode 100644 bench/tests/render-test.rs diff --git a/Cargo.toml b/Cargo.toml index 9c6a435a99..34916cbce6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -48,6 +48,7 @@ maintenance = { status = "actively-developed" } [workspace] members = [ + "bench", "core", "futures", "graphics", diff --git a/bench/Cargo.toml b/bench/Cargo.toml new file mode 100644 index 0000000000..8d561ca850 --- /dev/null +++ b/bench/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "iced_bench" +version = "0.1.0" +edition = "2021" + +[dependencies] +iced = { path = "..", features = ["image"] } +iced_glow = { path = "../glow" } +iced_glutin = { path = "../glutin" } +iced_graphics = { path = "../graphics" } +iced_native = { path = "../native" } +iced_wgpu = { path = "../wgpu" } + +[dependencies.image_rs] +version = "0.23" +package = "image" +features = ["png"] +default-features = false + +[dev-dependencies] +criterion = "0.4.0" +rand = "0.8.5" + +[[bench]] +name = "renderer_bench" +harness = false diff --git a/bench/benches/renderer_bench.rs b/bench/benches/renderer_bench.rs new file mode 100644 index 0000000000..2510d24730 --- /dev/null +++ b/bench/benches/renderer_bench.rs @@ -0,0 +1,135 @@ +use criterion::{black_box, criterion_group, criterion_main, Criterion}; + +use iced_native::{widget, Renderer}; + +use iced_bench::{glow::GlowBench, render_widget, wgpu::WgpuBench, Bench}; + +static LOREM_IPSUM: &str = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."; + +fn image(size: u32) -> iced_native::image::Handle { + let mut bytes = Vec::with_capacity(size as usize * size as usize * 4); + for _y in 0..size { + for _x in 0..size { + let r = rand::random(); + let g = rand::random(); + let b = rand::random(); + bytes.extend_from_slice(&[r, g, b, 255]); + } + } + iced_native::image::Handle::from_pixels(size, size, bytes) +} + +fn text_primitive< + R: iced_native::Renderer + iced_native::text::Renderer, +>( + renderer: &R, +) -> iced_graphics::Primitive { + iced_graphics::Primitive::Text { + content: LOREM_IPSUM.to_string(), + bounds: iced::Rectangle::with_size(iced::Size::new(1024.0, 1024.0)), + color: iced_native::Color::BLACK, + size: f32::from(renderer.default_size()), + font: Default::default(), + horizontal_alignment: iced_native::alignment::Horizontal::Left, + vertical_alignment: iced_native::alignment::Vertical::Top, + } +} + +fn text_widget< + R: iced_native::Renderer + iced_native::text::Renderer, +>() -> widget::Text<'static, R> { + widget::helpers::text(LOREM_IPSUM) +} + +fn iter_render< + B: Bench, + F: FnMut(&mut iced_graphics::Renderer), +>( + b: &mut criterion::Bencher, + bench: &mut B, + mut draw_cb: F, +) { + b.iter(|| { + bench.renderer().clear(); + let state = bench.clear(); + draw_cb(bench.renderer()); + bench.present(state); + }) +} + +fn bench_function< + B: Bench, + F: FnMut(&mut iced_graphics::Renderer), +>( + c: &mut Criterion, + bench: &mut B, + id: &str, + mut draw_cb: F, +) { + c.bench_function(&format!("{} {}", B::BACKEND_NAME, id), |b| { + iter_render(b, bench, |backend| draw_cb(backend)); + }); + + // Write output to file, so there's a way to see that generated + // image is correct. + let dir = std::path::Path::new(env!("CARGO_TARGET_TMPDIR")) + .join(format!("bench-renderers/{}", B::BACKEND_NAME)); + std::fs::create_dir_all(&dir).unwrap(); + bench + .read_pixels() + .save(&dir.join(&format!("{}.png", id))) + .unwrap(); +} + +fn generic_benchmark(c: &mut Criterion, bench: &mut B) +where + B::Backend: iced_graphics::backend::Text, +{ + bench_function(c, bench, "draw no primitive", |_renderer| {}); + bench_function(c, bench, "draw quad primitive", |renderer| { + renderer.draw_primitive(black_box(iced_graphics::Primitive::Quad { + bounds: iced::Rectangle::with_size(iced::Size::new(256.0, 256.0)), + background: iced_native::Background::Color( + iced_native::Color::BLACK, + ), + border_radius: 0., + border_width: 0., + border_color: Default::default(), + })); + }); + bench_function(c, bench, "draw text primitive", |renderer| { + renderer.draw_primitive(black_box(text_primitive(renderer))); + }); + let widget = text_widget(); + bench_function(c, bench, "render text", |renderer| { + render_widget(&widget, renderer); + }); + let handle = image(1024); + let bounds = iced::Rectangle::with_size(iced::Size::new(1024.0, 1024.0)); + bench_function(c, bench, "draw image primitive", |renderer| { + renderer.draw_primitive(iced_graphics::Primitive::Image { + handle: handle.clone(), + bounds, + }); + }); +} + +fn glow_benchmark(c: &mut Criterion) { + let mut bench = GlowBench::new(1024, 1024); + + generic_benchmark(c, &mut bench); +} + +fn wgpu_benchmark(c: &mut Criterion) { + let mut bench = WgpuBench::new(1024, 1024); + + generic_benchmark(c, &mut bench); + + let widget = widget::helpers::image(image(1024)); + bench_function(c, &mut bench, "render image", |renderer| { + render_widget(&widget, renderer); + }); +} + +criterion_group!(benches, glow_benchmark, wgpu_benchmark); +criterion_main!(benches); diff --git a/bench/examples/render.rs b/bench/examples/render.rs new file mode 100644 index 0000000000..f328e4d9d0 --- /dev/null +++ b/bench/examples/render.rs @@ -0,0 +1 @@ +fn main() {} diff --git a/bench/src/glow.rs b/bench/src/glow.rs new file mode 100644 index 0000000000..1166926cf1 --- /dev/null +++ b/bench/src/glow.rs @@ -0,0 +1,132 @@ +use iced_glow::glow::{self, HasContext}; +use iced_glutin::glutin; + +fn glutin_context( + width: u32, + height: u32, +) -> glutin::Context { + let el = glutin::event_loop::EventLoop::new(); + #[cfg(target_os = "linux")] + use glutin::platform::unix::HeadlessContextExt; + #[cfg(target_os = "linux")] + if let Ok(context) = glutin::ContextBuilder::new() + .with_gl(glutin::GlRequest::Specific(glutin::Api::OpenGlEs, (2, 0))) + .build_surfaceless(&el) + { + return context; + } + glutin::ContextBuilder::new() + .with_gl(glutin::GlRequest::Specific(glutin::Api::OpenGlEs, (2, 0))) + .build_headless(&el, glutin::dpi::PhysicalSize::new(width, height)) + .unwrap() +} + +pub struct GlowBench { + _context: glutin::Context, + gl: glow::Context, + renderer: iced_glow::Renderer, + viewport: iced_graphics::Viewport, + _framebuffer: glow::NativeFramebuffer, + _renderbuffer: glow::NativeRenderbuffer, + width: u32, + height: u32, +} + +impl GlowBench { + pub fn new(width: u32, height: u32) -> Self { + let context = + unsafe { glutin_context(width, height).make_current().unwrap() }; + let (gl, framebuffer, renderbuffer); + unsafe { + gl = glow::Context::from_loader_function(|name| { + context.get_proc_address(name) + }); + gl.viewport(0, 0, width as i32, height as i32); + + renderbuffer = gl.create_renderbuffer().unwrap(); + gl.bind_renderbuffer(glow::RENDERBUFFER, Some(renderbuffer)); + gl.renderbuffer_storage( + glow::RENDERBUFFER, + glow::RGBA8, + width as i32, + height as i32, + ); + gl.bind_renderbuffer(glow::RENDERBUFFER, None); + + framebuffer = gl.create_framebuffer().unwrap(); + gl.bind_framebuffer(glow::FRAMEBUFFER, Some(framebuffer)); + gl.framebuffer_renderbuffer( + glow::FRAMEBUFFER, + glow::COLOR_ATTACHMENT0, + glow::RENDERBUFFER, + Some(renderbuffer), + ); + assert_eq!( + gl.check_framebuffer_status(glow::FRAMEBUFFER), + glow::FRAMEBUFFER_COMPLETE + ); + }; + let renderer = iced_glow::Renderer::::new( + iced_glow::Backend::new(&gl, Default::default()), + ); + let viewport = iced_graphics::Viewport::with_physical_size( + iced::Size::new(width, height), + 1.0, + ); + Self { + _context: context, + gl, + renderer, + viewport, + _framebuffer: framebuffer, + _renderbuffer: renderbuffer, + width, + height, + } + } +} + +impl super::Bench for GlowBench { + type Backend = iced_glow::Backend; + type RenderState = (); + const BACKEND_NAME: &'static str = "glow"; + + fn clear(&self) { + unsafe { + self.gl.clear_color(1., 1., 1., 1.); + self.gl.clear(glow::COLOR_BUFFER_BIT); + } + } + + fn present(&mut self, _state: ()) { + self.renderer.with_primitives(|backend, primitive| { + backend.present::<&str>(&self.gl, primitive, &self.viewport, &[]); + }); + unsafe { self.gl.finish() }; + } + + fn read_pixels(&self) -> image_rs::RgbaImage { + let mut pixels = image_rs::RgbaImage::new(self.width, self.height); + unsafe { + self.gl.read_pixels( + 0, + 0, + self.width as i32, + self.height as i32, + glow::RGBA, + glow::UNSIGNED_BYTE, + glow::PixelPackData::Slice(&mut pixels), + ); + } + image_rs::imageops::flip_vertical_in_place(&mut pixels); + pixels + } + + fn size(&self) -> (u32, u32) { + (self.width, self.height) + } + + fn renderer(&mut self) -> &mut iced_glow::Renderer { + &mut self.renderer + } +} diff --git a/bench/src/lib.rs b/bench/src/lib.rs new file mode 100644 index 0000000000..564ae35a8c --- /dev/null +++ b/bench/src/lib.rs @@ -0,0 +1,39 @@ +pub mod glow; +pub mod wgpu; + +pub enum Msg {} + +pub fn render_widget< + R: iced_native::Renderer, + W: iced_native::Widget, +>( + widget: &W, + renderer: &mut R, +) { + let size = iced::Size::new(1024.0, 1024.0); + let node = iced_native::layout::Node::new(size); + let layout = iced_native::Layout::new(&node); + widget.draw( + &iced_native::widget::Tree::empty(), + renderer, + &iced::Theme::Light, + &Default::default(), + layout, + iced::Point::new(0.0, 0.0), + &iced::Rectangle::with_size(size), + ); +} + +pub trait Bench { + type Backend: iced_graphics::Backend; + type RenderState; + const BACKEND_NAME: &'static str; + + fn clear(&self) -> Self::RenderState; + fn present(&mut self, state: Self::RenderState); + fn read_pixels(&self) -> image_rs::RgbaImage; + fn size(&self) -> (u32, u32); + fn renderer( + &mut self, + ) -> &mut iced_graphics::Renderer; +} diff --git a/bench/src/wgpu.rs b/bench/src/wgpu.rs new file mode 100644 index 0000000000..9628c05a6a --- /dev/null +++ b/bench/src/wgpu.rs @@ -0,0 +1,203 @@ +use iced::futures::executor::block_on; +use iced_wgpu::wgpu; +use std::num::NonZeroU32; + +fn wgpu_device() -> (wgpu::Device, wgpu::Queue) { + let instance = wgpu::Instance::new(wgpu::Backends::PRIMARY); + let adapter = block_on(wgpu::util::initialize_adapter_from_env_or_default( + &instance, + wgpu::Backends::PRIMARY, + None, + )) + .unwrap(); + block_on(adapter.request_device( + &wgpu::DeviceDescriptor { + label: None, + features: adapter.features() & wgpu::Features::default(), + limits: wgpu::Limits::default(), + }, + None, + )) + .unwrap() +} + +fn create_wgpu_texture( + device: &wgpu::Device, + width: u32, + height: u32, +) -> wgpu::Texture { + let texture_desc = wgpu::TextureDescriptor { + size: wgpu::Extent3d { + width, + height, + depth_or_array_layers: 1, + }, + mip_level_count: 1, + sample_count: 1, + dimension: wgpu::TextureDimension::D2, + format: wgpu::TextureFormat::Rgba8UnormSrgb, + usage: wgpu::TextureUsages::COPY_SRC + | wgpu::TextureUsages::RENDER_ATTACHMENT, + label: None, + }; + device.create_texture(&texture_desc) +} + +pub struct WgpuBench { + device: wgpu::Device, + queue: wgpu::Queue, + texture: wgpu::Texture, + renderer: iced_wgpu::Renderer, + viewport: iced_graphics::Viewport, + staging_belt: wgpu::util::StagingBelt, + width: u32, + height: u32, +} + +impl WgpuBench { + pub fn new(width: u32, height: u32) -> Self { + let (device, queue) = wgpu_device(); + let texture = create_wgpu_texture(&device, width, height); + let renderer = + iced_wgpu::Renderer::::new(iced_wgpu::Backend::new( + &device, + Default::default(), + wgpu::TextureFormat::Rgba8UnormSrgb, + )); + let viewport = iced_graphics::Viewport::with_physical_size( + iced::Size::new(width, height), + 1.0, + ); + let staging_belt = wgpu::util::StagingBelt::new(5 * 1024); + Self { + device, + queue, + texture, + renderer, + viewport, + staging_belt, + width, + height, + } + } +} + +impl super::Bench for WgpuBench { + type Backend = iced_wgpu::Backend; + type RenderState = RenderState; + const BACKEND_NAME: &'static str = "wgpu"; + + fn clear(&self) -> RenderState { + let mut encoder = self.device.create_command_encoder( + &wgpu::CommandEncoderDescriptor { label: None }, + ); + let view = self + .texture + .create_view(&wgpu::TextureViewDescriptor::default()); + encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label: None, + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view: &view, + resolve_target: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Clear(wgpu::Color { + r: 1.0, + g: 1.0, + b: 1.0, + a: 1.0, + }), + store: true, + }, + })], + depth_stencil_attachment: None, + }); + RenderState { encoder, view } + } + + fn present(&mut self, mut state: RenderState) { + self.renderer.with_primitives(|backend, primitive| { + backend.present::<&str>( + &self.device, + &mut self.staging_belt, + &mut state.encoder, + &state.view, + primitive, + &self.viewport, + &[], + ); + }); + + self.staging_belt.finish(); + self.queue.submit(Some(state.encoder.finish())); + self.staging_belt.recall(); + + self.device.poll(wgpu::MaintainBase::Wait); + } + + fn read_pixels(&self) -> image_rs::RgbaImage { + let mut encoder = self.device.create_command_encoder( + &wgpu::CommandEncoderDescriptor { label: None }, + ); + let real_bytes_per_row = self.width * 4; + let bytes_per_row = + if (real_bytes_per_row % wgpu::COPY_BYTES_PER_ROW_ALIGNMENT) == 0 { + real_bytes_per_row + } else { + (real_bytes_per_row / wgpu::COPY_BYTES_PER_ROW_ALIGNMENT + 1) + * wgpu::COPY_BYTES_PER_ROW_ALIGNMENT + }; + let buffer = self.device.create_buffer(&wgpu::BufferDescriptor { + label: None, + size: u64::from(bytes_per_row * self.height), + usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST, + mapped_at_creation: false, + }); + let layout = wgpu::ImageDataLayout { + offset: 0, + bytes_per_row: NonZeroU32::new(bytes_per_row), + rows_per_image: None, + }; + encoder.copy_texture_to_buffer( + self.texture.as_image_copy(), + wgpu::ImageCopyBuffer { + buffer: &buffer, + layout, + }, + wgpu::Extent3d { + width: self.width, + height: self.height, + depth_or_array_layers: 1, + }, + ); + self.queue.submit(Some(encoder.finish())); + + let slice = buffer.slice(..); + slice.map_async(wgpu::MapMode::Read, |res| res.unwrap()); + self.device.poll(wgpu::MaintainBase::Wait); + let range = slice.get_mapped_range(); + let pixels = if bytes_per_row == real_bytes_per_row { + range.to_owned() + } else { + range + .chunks(bytes_per_row as usize) + .map(|chunk| chunk.iter().take(real_bytes_per_row as usize)) + .flatten() + .copied() + .collect() + }; + image_rs::RgbaImage::from_vec(self.width, self.height, pixels).unwrap() + } + + fn size(&self) -> (u32, u32) { + (self.width, self.height) + } + + fn renderer(&mut self) -> &mut iced_wgpu::Renderer { + &mut self.renderer + } +} + +pub struct RenderState { + encoder: wgpu::CommandEncoder, + view: wgpu::TextureView, +} diff --git a/bench/tests/render-test.rs b/bench/tests/render-test.rs new file mode 100644 index 0000000000..657925b847 --- /dev/null +++ b/bench/tests/render-test.rs @@ -0,0 +1,74 @@ +use iced_bench::{glow::GlowBench, wgpu::WgpuBench, Bench}; + +fn rand_pixels(size: u32) -> Vec { + let mut bytes = Vec::with_capacity(size as usize * size as usize * 4); + for _y in 0..size { + for _x in 0..size { + let b = rand::random(); + let g = rand::random(); + let r = rand::random(); + bytes.extend_from_slice(&[b, g, r, 255]); + } + } + bytes +} + +#[test] +fn render_image_primitive_wgpu() { + let size: u16 = 8; + + let mut bench = WgpuBench::new(size.into(), size.into()); + + let pixels = rand_pixels(size.into()); + let handle = iced_native::image::Handle::from_pixels( + size.into(), + size.into(), + pixels.clone(), + ); + let bounds = + iced::Rectangle::with_size(iced::Size::new(size.into(), size.into())); + + let state = bench.clear(); + bench + .renderer() + .draw_primitive(iced_graphics::Primitive::Image { + handle: handle.clone(), + bounds, + }); + bench.present(state); + + let output_pixels = + image_rs::DynamicImage::ImageRgba8(bench.read_pixels()).to_bgra8(); + + assert_eq!(pixels, *output_pixels); +} + +#[test] +fn render_image_primitive_glow() { + let size: u16 = 16; + + let mut bench = GlowBench::new(size.into(), size.into()); + + let pixels = rand_pixels(size.into()); + let handle = iced_native::image::Handle::from_pixels( + size.into(), + size.into(), + pixels.clone(), + ); + let bounds = + iced::Rectangle::with_size(iced::Size::new(size.into(), size.into())); + + let state = bench.clear(); + bench + .renderer() + .draw_primitive(iced_graphics::Primitive::Image { + handle: handle.clone(), + bounds, + }); + bench.present(state); + + let output_pixels = + image_rs::DynamicImage::ImageRgba8(bench.read_pixels()).to_bgra8(); + + assert_eq!(pixels, *output_pixels); +}