From 6084336b8f01f70b94c9cef294813077231c2af4 Mon Sep 17 00:00:00 2001 From: joe Date: Tue, 21 May 2024 20:03:25 -0600 Subject: [PATCH 01/14] add wgpu support --- Cargo.toml | 7 +- examples/hello_world.rs | 1 + examples/simple_demo.rs | 1 + src/renderer/mod.rs | 5 + src/renderer/wgpu_renderer.rs | 286 ++++++++++++++++++++++++++++++++++ src/window.rs | 12 +- 6 files changed, 305 insertions(+), 7 deletions(-) create mode 100644 src/renderer/wgpu_renderer.rs diff --git a/Cargo.toml b/Cargo.toml index 8161224..f11dad3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,18 +16,23 @@ readme = "README.md" [features] default = ["opengl", "default_fonts"] opengl = ["egui_glow", "baseview/opengl"] +wgpu = ["egui-wgpu", "raw-window-handle-06", "pollster"] default_fonts = ["egui/default_fonts"] ## Enable parallel tessellation using [`rayon`](https://docs.rs/rayon). ## ## This can help performance for graphics-intense applications. rayon = ["egui/rayon"] +pollster = ["dep:pollster"] [dependencies] egui = { version = "0.27", default-features = false, features = ["bytemuck"] } +egui-wgpu = { version = "0.27", optional = true } egui_glow = { version = "0.27", optional = true } keyboard-types = { version = "0.6", default-features = false } baseview = { git = "https://github.com/RustAudio/baseview.git", rev = "45465c5f46abed6c6ce370fffde5edc8e4cd5aa3" } raw-window-handle = "0.5" +raw-window-handle-06 = { package = "raw-window-handle", version = "0.6", optional = true } # TODO: Enable wayland feature when baseview gets wayland support. copypasta = { version = "0.10", default-features = false, features = ["x11"] } -log = "0.4" \ No newline at end of file +log = "0.4" +pollster = { version = "0.3.0", optional = true } diff --git a/examples/hello_world.rs b/examples/hello_world.rs index 3394faa..ad61a04 100644 --- a/examples/hello_world.rs +++ b/examples/hello_world.rs @@ -7,6 +7,7 @@ fn main() { title: String::from("egui-baseview hello world"), size: Size::new(300.0, 110.0), scale: WindowScalePolicy::SystemScaleFactor, + #[cfg(feature = "opengl")] gl_config: Some(Default::default()), }; diff --git a/examples/simple_demo.rs b/examples/simple_demo.rs index dc33873..4c7a6d2 100644 --- a/examples/simple_demo.rs +++ b/examples/simple_demo.rs @@ -7,6 +7,7 @@ fn main() { title: String::from("egui-baseview simple demo"), size: Size::new(400.0, 200.0), scale: WindowScalePolicy::SystemScaleFactor, + #[cfg(feature = "opengl")] gl_config: Some(Default::default()), }; diff --git a/src/renderer/mod.rs b/src/renderer/mod.rs index 9e93321..f2461b0 100644 --- a/src/renderer/mod.rs +++ b/src/renderer/mod.rs @@ -2,3 +2,8 @@ mod opengl_renderer; #[cfg(feature = "opengl")] pub(crate) use opengl_renderer::Renderer; + +#[cfg(feature = "wgpu")] +mod wgpu_renderer; +#[cfg(feature = "wgpu")] +pub(crate) use wgpu_renderer::Renderer; diff --git a/src/renderer/wgpu_renderer.rs b/src/renderer/wgpu_renderer.rs new file mode 100644 index 0000000..350860c --- /dev/null +++ b/src/renderer/wgpu_renderer.rs @@ -0,0 +1,286 @@ +use std::{ + num::{NonZeroIsize, NonZeroU32}, + ptr::NonNull, + sync::Arc, +}; + +use baseview::Window; +use egui_wgpu::{ + wgpu::{ + Color, CommandEncoderDescriptor, Extent3d, Instance, InstanceDescriptor, + RenderPassColorAttachment, RenderPassDescriptor, Surface, SurfaceConfiguration, + SurfaceTargetUnsafe, TextureDescriptor, TextureDimension, TextureUsages, TextureView, + TextureViewDescriptor, + }, + RenderState, ScreenDescriptor, WgpuConfiguration, +}; +use raw_window_handle::{HasRawDisplayHandle, HasRawWindowHandle}; +use raw_window_handle_06::{ + AppKitDisplayHandle, AppKitWindowHandle, Win32WindowHandle, WindowsDisplayHandle, + XcbDisplayHandle, XcbWindowHandle, XlibDisplayHandle, XlibWindowHandle, +}; + +const MSAA_SAMPLES: u32 = 4; + +pub struct Renderer { + render_state: Arc, + surface: Surface<'static>, + configuration: WgpuConfiguration, + msaa_texture_view: Option, + width: u32, + height: u32, +} + +impl Renderer { + pub fn new(window: &Window) -> Self { + let instance = Instance::new(InstanceDescriptor::default()); + + let raw_display_handle = window.raw_display_handle(); + let raw_window_handle = window.raw_window_handle(); + + let target = SurfaceTargetUnsafe::RawHandle { + raw_display_handle: match raw_display_handle { + raw_window_handle::RawDisplayHandle::AppKit(_) => { + raw_window_handle_06::RawDisplayHandle::AppKit(AppKitDisplayHandle::new()) + } + raw_window_handle::RawDisplayHandle::Xlib(handle) => { + raw_window_handle_06::RawDisplayHandle::Xlib(XlibDisplayHandle::new( + NonNull::new(handle.display), + handle.screen, + )) + } + raw_window_handle::RawDisplayHandle::Xcb(handle) => { + raw_window_handle_06::RawDisplayHandle::Xcb(XcbDisplayHandle::new( + NonNull::new(handle.connection), + handle.screen, + )) + } + raw_window_handle::RawDisplayHandle::Windows(_) => { + raw_window_handle_06::RawDisplayHandle::Windows(WindowsDisplayHandle::new()) + } + _ => todo!(), + }, + raw_window_handle: match raw_window_handle { + raw_window_handle::RawWindowHandle::AppKit(handle) => { + raw_window_handle_06::RawWindowHandle::AppKit(AppKitWindowHandle::new( + NonNull::new(handle.ns_view).unwrap(), + )) + } + raw_window_handle::RawWindowHandle::Xlib(handle) => { + raw_window_handle_06::RawWindowHandle::Xlib(XlibWindowHandle::new( + handle.window, + )) + } + raw_window_handle::RawWindowHandle::Xcb(handle) => { + raw_window_handle_06::RawWindowHandle::Xcb(XcbWindowHandle::new( + NonZeroU32::new(handle.window).unwrap(), + )) + } + // will this work? i have no idea! + raw_window_handle::RawWindowHandle::Win32(handle) => { + raw_window_handle_06::RawWindowHandle::Win32(Win32WindowHandle::new( + NonZeroIsize::new(unsafe { *handle.hwnd.cast() }).unwrap(), + )) + } + _ => todo!(), + }, + }; + + let surface = unsafe { instance.create_surface_unsafe(target) }.unwrap(); + let configuration = WgpuConfiguration::default(); + + let state = Arc::new( + pollster::block_on(RenderState::create( + &configuration, + &instance, + &surface, + None, + MSAA_SAMPLES, + )) + .unwrap(), + ); + + Self { + render_state: state, + surface, + configuration, + msaa_texture_view: None, + width: 0, + height: 0, + } + } + + pub fn max_texture_side(&self) -> usize { + self.render_state + .as_ref() + .device + .limits() + .max_texture_dimension_2d as usize + } + + fn configure_surface(&self, width: u32, height: u32) { + let usage = TextureUsages::RENDER_ATTACHMENT; + + let mut surf_config = SurfaceConfiguration { + usage, + format: self.render_state.target_format, + present_mode: self.configuration.present_mode, + view_formats: vec![self.render_state.target_format], + ..self + .surface + .get_default_config(&self.render_state.adapter, width, height) + .expect("Unsupported surface") + }; + + if let Some(desired_maximum_frame_latency) = + self.configuration.desired_maximum_frame_latency + { + surf_config.desired_maximum_frame_latency = desired_maximum_frame_latency; + } + + self.surface + .configure(&self.render_state.device, &surf_config); + } + + fn resize_and_generate_msaa_view(&mut self, width: u32, height: u32) { + let render_state = self.render_state.as_ref(); + + self.width = width; + self.height = height; + + self.configure_surface(width, height); + + let texture_format = render_state.target_format; + self.msaa_texture_view = Some( + render_state + .device + .create_texture(&TextureDescriptor { + label: Some("egui_msaa_texture"), + size: Extent3d { + width, + height, + depth_or_array_layers: 1, + }, + mip_level_count: 1, + sample_count: MSAA_SAMPLES, + dimension: TextureDimension::D2, + format: texture_format, + usage: TextureUsages::RENDER_ATTACHMENT, + view_formats: &[texture_format], + }) + .create_view(&TextureViewDescriptor::default()), + ); + } + + pub fn render( + &mut self, + _window: &Window, + bg_color: egui::Rgba, + canvas_width: u32, + canvas_height: u32, + pixels_per_point: f32, + egui_ctx: &mut egui::Context, + shapes: &mut Vec, + textures_delta: &mut egui::TexturesDelta, + ) { + let shapes = std::mem::take(shapes); + + let clipped_primitives = egui_ctx.tessellate(shapes, pixels_per_point); + + let mut encoder = + self.render_state + .device + .create_command_encoder(&CommandEncoderDescriptor { + label: Some("encoder"), + }); + + let screen_descriptor = ScreenDescriptor { + size_in_pixels: [canvas_width, canvas_height], + pixels_per_point, + }; + + let user_cmd_bufs = { + let mut renderer = self.render_state.renderer.write(); + for (id, image_delta) in &textures_delta.set { + renderer.update_texture( + &self.render_state.device, + &self.render_state.queue, + *id, + image_delta, + ); + } + + renderer.update_buffers( + &self.render_state.device, + &self.render_state.queue, + &mut encoder, + &clipped_primitives, + &screen_descriptor, + ) + }; + + if self.width != canvas_width + || self.height != canvas_height + || self.msaa_texture_view.is_none() + { + self.resize_and_generate_msaa_view(canvas_width, canvas_height); + } + + let output_frame = { self.surface.get_current_texture() }; + + let output_frame = match output_frame { + Ok(frame) => frame, + Err(err) => match (self.configuration.on_surface_error)(err) { + egui_wgpu::SurfaceErrorAction::SkipFrame => return, + egui_wgpu::SurfaceErrorAction::RecreateSurface => { + self.configure_surface(self.width, self.height); + return; + } + }, + }; + + { + let renderer = self.render_state.renderer.read(); + let frame_view = output_frame + .texture + .create_view(&TextureViewDescriptor::default()); + + let mut render_pass = encoder.begin_render_pass(&RenderPassDescriptor { + label: Some("egui_render"), + color_attachments: &[Some(RenderPassColorAttachment { + view: self.msaa_texture_view.as_ref().unwrap(), + resolve_target: Some(&frame_view), + ops: egui_wgpu::wgpu::Operations { + load: egui_wgpu::wgpu::LoadOp::Clear(Color { + r: bg_color[0] as f64, + g: bg_color[1] as f64, + b: bg_color[2] as f64, + a: bg_color[3] as f64, + }), + store: egui_wgpu::wgpu::StoreOp::Store, + }, + })], + depth_stencil_attachment: None, + timestamp_writes: None, + occlusion_query_set: None, + }); + + renderer.render(&mut render_pass, &clipped_primitives, &screen_descriptor); + } + + { + let mut renderer = self.render_state.renderer.write(); + for id in &textures_delta.free { + renderer.free_texture(id); + } + } + + let encoded = encoder.finish(); + + self.render_state + .queue + .submit(user_cmd_bufs.into_iter().chain([encoded])); + + output_frame.present(); + } +} diff --git a/src/window.rs b/src/window.rs index e93b6c6..d589530 100644 --- a/src/window.rs +++ b/src/window.rs @@ -215,9 +215,9 @@ where B: FnMut(&egui::Context, &mut Queue, &mut State), B: 'static + Send, { - if settings.gl_config.is_none() { - settings.gl_config = Some(Default::default()); - } + // if settings.gl_config.is_none() { + // settings.gl_config = Some(Default::default()); + // } let open_settings = OpenSettings::new(&settings); @@ -243,9 +243,9 @@ where B: FnMut(&egui::Context, &mut Queue, &mut State), B: 'static + Send, { - if settings.gl_config.is_none() { - settings.gl_config = Some(Default::default()); - } + // if settings.gl_config.is_none() { + // settings.gl_config = Some(Default::default()); + // } let open_settings = OpenSettings::new(&settings); From ba0f509df99c63b6c33d160372b5abc226d68745 Mon Sep 17 00:00:00 2001 From: joe Date: Tue, 21 May 2024 20:10:23 -0600 Subject: [PATCH 02/14] i forgot that i commented this out --- src/window.rs | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/window.rs b/src/window.rs index d589530..f50fad9 100644 --- a/src/window.rs +++ b/src/window.rs @@ -215,9 +215,10 @@ where B: FnMut(&egui::Context, &mut Queue, &mut State), B: 'static + Send, { - // if settings.gl_config.is_none() { - // settings.gl_config = Some(Default::default()); - // } + #[cfg(feature = "opengl")] + if settings.gl_config.is_none() { + settings.gl_config = Some(Default::default()); + } let open_settings = OpenSettings::new(&settings); @@ -243,9 +244,10 @@ where B: FnMut(&egui::Context, &mut Queue, &mut State), B: 'static + Send, { - // if settings.gl_config.is_none() { - // settings.gl_config = Some(Default::default()); - // } + #[cfg(feature = "opengl")] + if settings.gl_config.is_none() { + settings.gl_config = Some(Default::default()); + } let open_settings = OpenSettings::new(&settings); From 92a1743ec5aa1640c94e7aa20adb75aea869a1f9 Mon Sep 17 00:00:00 2001 From: joe Date: Tue, 21 May 2024 20:11:09 -0600 Subject: [PATCH 03/14] cargo fmt --- src/window.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/window.rs b/src/window.rs index f50fad9..40595b2 100644 --- a/src/window.rs +++ b/src/window.rs @@ -217,7 +217,7 @@ where { #[cfg(feature = "opengl")] if settings.gl_config.is_none() { - settings.gl_config = Some(Default::default()); + settings.gl_config = Some(Default::default()); } let open_settings = OpenSettings::new(&settings); From f6aaeca6c7f286b71810da97a74beb22a905a395 Mon Sep 17 00:00:00 2001 From: joe Date: Wed, 22 May 2024 13:11:07 -0600 Subject: [PATCH 04/14] trigger resize on resize event --- src/renderer/wgpu_renderer.rs | 2 +- src/window.rs | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/renderer/wgpu_renderer.rs b/src/renderer/wgpu_renderer.rs index 350860c..cd056bd 100644 --- a/src/renderer/wgpu_renderer.rs +++ b/src/renderer/wgpu_renderer.rs @@ -142,7 +142,7 @@ impl Renderer { .configure(&self.render_state.device, &surf_config); } - fn resize_and_generate_msaa_view(&mut self, width: u32, height: u32) { + pub fn resize_and_generate_msaa_view(&mut self, width: u32, height: u32) { let render_state = self.render_state.as_ref(); self.width = width; diff --git a/src/window.rs b/src/window.rs index 40595b2..7976dc0 100644 --- a/src/window.rs +++ b/src/window.rs @@ -536,6 +536,9 @@ where viewport_info.native_pixels_per_point = Some(self.pixels_per_point as f32); viewport_info.inner_rect = Some(screen_rect); + #[cfg(feature = "wgpu")] + self.renderer.resize_and_generate_msaa_view(self.physical_width, self.physical_height); + // Schedule to repaint on the next frame. self.repaint_after = Some(Instant::now()); } From b00e331be47f25d5a50fd89269a33952200311d2 Mon Sep 17 00:00:00 2001 From: joe Date: Wed, 22 May 2024 13:31:55 -0600 Subject: [PATCH 05/14] pointer nonsense :) --- src/renderer/wgpu_renderer.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/renderer/wgpu_renderer.rs b/src/renderer/wgpu_renderer.rs index cd056bd..2b579bc 100644 --- a/src/renderer/wgpu_renderer.rs +++ b/src/renderer/wgpu_renderer.rs @@ -79,7 +79,7 @@ impl Renderer { // will this work? i have no idea! raw_window_handle::RawWindowHandle::Win32(handle) => { raw_window_handle_06::RawWindowHandle::Win32(Win32WindowHandle::new( - NonZeroIsize::new(unsafe { *handle.hwnd.cast() }).unwrap(), + NonZeroIsize::new(unsafe { handle.hwnd.read_unaligned() as isize }).unwrap(), )) } _ => todo!(), From fcb8abe7675fe49af8b40975f5089b4f288e5996 Mon Sep 17 00:00:00 2001 From: joe Date: Wed, 22 May 2024 13:32:06 -0600 Subject: [PATCH 06/14] Revert "trigger resize on resize event" This reverts commit f6aaeca6c7f286b71810da97a74beb22a905a395. this was a false alarm, resizing logic should already work correctly --- src/renderer/wgpu_renderer.rs | 2 +- src/window.rs | 3 --- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/src/renderer/wgpu_renderer.rs b/src/renderer/wgpu_renderer.rs index 2b579bc..00f1b40 100644 --- a/src/renderer/wgpu_renderer.rs +++ b/src/renderer/wgpu_renderer.rs @@ -142,7 +142,7 @@ impl Renderer { .configure(&self.render_state.device, &surf_config); } - pub fn resize_and_generate_msaa_view(&mut self, width: u32, height: u32) { + fn resize_and_generate_msaa_view(&mut self, width: u32, height: u32) { let render_state = self.render_state.as_ref(); self.width = width; diff --git a/src/window.rs b/src/window.rs index 7976dc0..40595b2 100644 --- a/src/window.rs +++ b/src/window.rs @@ -536,9 +536,6 @@ where viewport_info.native_pixels_per_point = Some(self.pixels_per_point as f32); viewport_info.inner_rect = Some(screen_rect); - #[cfg(feature = "wgpu")] - self.renderer.resize_and_generate_msaa_view(self.physical_width, self.physical_height); - // Schedule to repaint on the next frame. self.repaint_after = Some(Instant::now()); } From 9cc4ec49ed2ab36ae1ecc73ecfc1e70eb462d304 Mon Sep 17 00:00:00 2001 From: joe Date: Wed, 22 May 2024 14:21:58 -0600 Subject: [PATCH 07/14] attempt using direct cast --- src/renderer/wgpu_renderer.rs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/renderer/wgpu_renderer.rs b/src/renderer/wgpu_renderer.rs index 00f1b40..02701ff 100644 --- a/src/renderer/wgpu_renderer.rs +++ b/src/renderer/wgpu_renderer.rs @@ -78,9 +78,12 @@ impl Renderer { } // will this work? i have no idea! raw_window_handle::RawWindowHandle::Win32(handle) => { - raw_window_handle_06::RawWindowHandle::Win32(Win32WindowHandle::new( - NonZeroIsize::new(unsafe { handle.hwnd.read_unaligned() as isize }).unwrap(), - )) + let mut raw_handle = Win32WindowHandle::new(NonZeroIsize::new(handle.hwnd as isize).unwrap()); + + raw_handle.hinstance = handle.hinstance.is_null().then_some(NonZeroIsize::new(handle.hinstance as isize).unwrap()); + + + raw_window_handle_06::RawWindowHandle::Win32(raw_handle) } _ => todo!(), }, From cec7cd69ac6731ed6d141256072684a6605cfe00 Mon Sep 17 00:00:00 2001 From: joe Date: Thu, 23 May 2024 19:34:16 -0600 Subject: [PATCH 08/14] resolve clippy warnings, refactor backend, and add opengl fallback in case wgpu dies --- Cargo.toml | 3 +- examples/hello_world.rs | 2 +- examples/simple_demo.rs | 2 +- src/renderer.rs | 60 +++++++++++++++++++++++++++++++++ src/renderer/mod.rs | 9 ----- src/renderer/opengl_renderer.rs | 17 +++++----- src/renderer/wgpu_renderer.rs | 40 +++++++++++----------- src/window.rs | 39 +++++++++------------ 8 files changed, 109 insertions(+), 63 deletions(-) create mode 100644 src/renderer.rs delete mode 100644 src/renderer/mod.rs diff --git a/Cargo.toml b/Cargo.toml index f11dad3..8102c6f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,7 +16,7 @@ readme = "README.md" [features] default = ["opengl", "default_fonts"] opengl = ["egui_glow", "baseview/opengl"] -wgpu = ["egui-wgpu", "raw-window-handle-06", "pollster"] +wgpu = ["egui-wgpu", "raw-window-handle-06", "pollster", "dep:wgpu", "opengl"] default_fonts = ["egui/default_fonts"] ## Enable parallel tessellation using [`rayon`](https://docs.rs/rayon). ## @@ -28,6 +28,7 @@ pollster = ["dep:pollster"] egui = { version = "0.27", default-features = false, features = ["bytemuck"] } egui-wgpu = { version = "0.27", optional = true } egui_glow = { version = "0.27", optional = true } +wgpu = {version = "0.19", optional = true} keyboard-types = { version = "0.6", default-features = false } baseview = { git = "https://github.com/RustAudio/baseview.git", rev = "45465c5f46abed6c6ce370fffde5edc8e4cd5aa3" } raw-window-handle = "0.5" diff --git a/examples/hello_world.rs b/examples/hello_world.rs index ad61a04..3e2402f 100644 --- a/examples/hello_world.rs +++ b/examples/hello_world.rs @@ -18,7 +18,7 @@ fn main() { state, |_egui_ctx: &Context, _queue: &mut Queue, _state: &mut ()| {}, |egui_ctx: &Context, _queue: &mut Queue, _state: &mut ()| { - egui::Window::new("egui-baseview hello world").show(&egui_ctx, |ui| { + egui::Window::new("egui-baseview hello world").show(egui_ctx, |ui| { ui.label("Hello World!"); }); }, diff --git a/examples/simple_demo.rs b/examples/simple_demo.rs index 4c7a6d2..18788a4 100644 --- a/examples/simple_demo.rs +++ b/examples/simple_demo.rs @@ -22,7 +22,7 @@ fn main() { // Called before each frame. Here you should update the state of your // application and build the UI. |egui_ctx: &Context, queue: &mut Queue, state: &mut State| { - egui::Window::new("egui-baseview simple demo").show(&egui_ctx, |ui| { + egui::Window::new("egui-baseview simple demo").show(egui_ctx, |ui| { ui.heading("My Egui Application"); ui.horizontal(|ui| { ui.label("Your name: "); diff --git a/src/renderer.rs b/src/renderer.rs new file mode 100644 index 0000000..cc76ea3 --- /dev/null +++ b/src/renderer.rs @@ -0,0 +1,60 @@ +use baseview::Window; +use egui::FullOutput; + +#[cfg(feature = "opengl")] +mod opengl_renderer; + +#[cfg(feature = "wgpu")] +mod wgpu_renderer; + +pub(crate) enum Renderer { + #[cfg(feature = "opengl")] + OpenGL(opengl_renderer::Renderer), + #[cfg(feature = "wgpu")] + Wgpu(wgpu_renderer::Renderer) +} + +impl Renderer { + pub(crate) fn render(&mut self, window: &Window, bg_color: egui::Rgba, + dimensions: (u32, u32), + pixels_per_point: f32, + egui_ctx: &mut egui::Context, + full_output: &mut FullOutput) { + match self { + #[cfg(feature = "opengl")] + Renderer::OpenGL(renderer) => renderer.render(window, bg_color, dimensions, pixels_per_point, egui_ctx, full_output), + #[cfg(feature = "wgpu")] + Renderer::Wgpu(renderer) => renderer.render(bg_color, dimensions, pixels_per_point, egui_ctx, full_output), + #[allow(unreachable_patterns)] + _ => unreachable!() + } + } + + pub(crate) fn max_texture_side(&self) -> usize { + match self { + #[cfg(feature = "opengl")] + Renderer::OpenGL(renderer) => renderer.max_texture_side(), + #[cfg(feature = "wgpu")] + Renderer::Wgpu(renderer) => renderer.max_texture_side(), + #[allow(unreachable_patterns)] + _ => unreachable!() + } + } +} + +// unreacable!() being here suppresses a compiler error, so the user hopefully realizes that they're supposed to enable a backend +#[allow(unreachable_code)] +pub(crate) fn get_renderer(window: &Window) -> Renderer { + #[cfg(not(any(feature = "opengl", feature = "wgpu")))] + compile_error!("No renderer present. Please enable either opengl or wgpu in the crate's features"); + + #[cfg(feature = "wgpu")] + if let Ok(renderer) = wgpu_renderer::Renderer::new(window) { + return Renderer::Wgpu(renderer); + } + + #[cfg(feature = "opengl")] + return Renderer::OpenGL(opengl_renderer::Renderer::new(window)); + + unreachable!() +} diff --git a/src/renderer/mod.rs b/src/renderer/mod.rs deleted file mode 100644 index f2461b0..0000000 --- a/src/renderer/mod.rs +++ /dev/null @@ -1,9 +0,0 @@ -#[cfg(feature = "opengl")] -mod opengl_renderer; -#[cfg(feature = "opengl")] -pub(crate) use opengl_renderer::Renderer; - -#[cfg(feature = "wgpu")] -mod wgpu_renderer; -#[cfg(feature = "wgpu")] -pub(crate) use wgpu_renderer::Renderer; diff --git a/src/renderer/opengl_renderer.rs b/src/renderer/opengl_renderer.rs index e997317..01d50fb 100644 --- a/src/renderer/opengl_renderer.rs +++ b/src/renderer/opengl_renderer.rs @@ -1,4 +1,5 @@ use baseview::Window; +use egui::FullOutput; use egui_glow::Painter; use std::sync::Arc; @@ -16,6 +17,7 @@ impl Renderer { context.make_current(); } + #[allow(clippy::arc_with_non_send_sync)] let glow_context = Arc::new(unsafe { egui_glow::glow::Context::from_loader_function(|s| context.get_proc_address(s)) }); @@ -44,15 +46,14 @@ impl Renderer { &mut self, window: &Window, bg_color: egui::Rgba, - canvas_width: u32, - canvas_height: u32, + dimensions: (u32, u32), pixels_per_point: f32, egui_ctx: &mut egui::Context, - shapes: &mut Vec, - textures_delta: &mut egui::TexturesDelta, + full_output: &mut FullOutput ) { - let shapes = std::mem::take(shapes); - let mut textures_delta = std::mem::take(textures_delta); + let (canvas_width, canvas_height) = dimensions; + let shapes = std::mem::take(&mut full_output.shapes); + let textures_delta = &mut full_output.textures_delta; let context = window .gl_context() @@ -68,8 +69,8 @@ impl Renderer { self.glow_context.clear(egui_glow::glow::COLOR_BUFFER_BIT); } - for (id, image_delta) in textures_delta.set { - self.painter.set_texture(id, &image_delta); + for (id, image_delta) in &textures_delta.set { + self.painter.set_texture(*id, image_delta); } let clipped_primitives = egui_ctx.tessellate(shapes, pixels_per_point); diff --git a/src/renderer/wgpu_renderer.rs b/src/renderer/wgpu_renderer.rs index 02701ff..4c75e2f 100644 --- a/src/renderer/wgpu_renderer.rs +++ b/src/renderer/wgpu_renderer.rs @@ -5,6 +5,7 @@ use std::{ }; use baseview::Window; +use egui::FullOutput; use egui_wgpu::{ wgpu::{ Color, CommandEncoderDescriptor, Extent3d, Instance, InstanceDescriptor, @@ -12,7 +13,7 @@ use egui_wgpu::{ SurfaceTargetUnsafe, TextureDescriptor, TextureDimension, TextureUsages, TextureView, TextureViewDescriptor, }, - RenderState, ScreenDescriptor, WgpuConfiguration, + RenderState, ScreenDescriptor, WgpuConfiguration, WgpuError, }; use raw_window_handle::{HasRawDisplayHandle, HasRawWindowHandle}; use raw_window_handle_06::{ @@ -32,7 +33,7 @@ pub struct Renderer { } impl Renderer { - pub fn new(window: &Window) -> Self { + pub fn new(window: &Window) -> Result { let instance = Instance::new(InstanceDescriptor::default()); let raw_display_handle = window.raw_display_handle(); @@ -99,18 +100,19 @@ impl Renderer { &surface, None, MSAA_SAMPLES, - )) - .unwrap(), + ))?, ); - Self { - render_state: state, - surface, - configuration, - msaa_texture_view: None, - width: 0, - height: 0, - } + Ok( + Self { + render_state: state, + surface, + configuration, + msaa_texture_view: None, + width: 0, + height: 0, + } + ) } pub fn max_texture_side(&self) -> usize { @@ -177,16 +179,14 @@ impl Renderer { pub fn render( &mut self, - _window: &Window, bg_color: egui::Rgba, - canvas_width: u32, - canvas_height: u32, + dimensions: (u32, u32), pixels_per_point: f32, egui_ctx: &mut egui::Context, - shapes: &mut Vec, - textures_delta: &mut egui::TexturesDelta, + full_output: &mut FullOutput ) { - let shapes = std::mem::take(shapes); + let (canvas_width, canvas_height) = dimensions; + let shapes = std::mem::take(&mut full_output.shapes); let clipped_primitives = egui_ctx.tessellate(shapes, pixels_per_point); @@ -204,7 +204,7 @@ impl Renderer { let user_cmd_bufs = { let mut renderer = self.render_state.renderer.write(); - for (id, image_delta) in &textures_delta.set { + for (id, image_delta) in &full_output.textures_delta.set { renderer.update_texture( &self.render_state.device, &self.render_state.queue, @@ -273,7 +273,7 @@ impl Renderer { { let mut renderer = self.render_state.renderer.write(); - for id in &textures_delta.free { + for id in &full_output.textures_delta.free { renderer.free_texture(id); } } diff --git a/src/window.rs b/src/window.rs index 40595b2..8e2dd1b 100644 --- a/src/window.rs +++ b/src/window.rs @@ -2,12 +2,12 @@ use baseview::{ Event, EventStatus, Window, WindowHandle, WindowHandler, WindowOpenOptions, WindowScalePolicy, }; use copypasta::ClipboardProvider; -use egui::{pos2, vec2, FullOutput, Pos2, Rect, Rgba, ViewportCommand}; +use egui::{pos2, vec2, Pos2, Rect, Rgba, ViewportCommand}; use keyboard_types::Modifiers; use raw_window_handle::HasRawWindowHandle; use std::time::Instant; -use crate::renderer::Renderer; +use crate::renderer::{get_renderer, Renderer}; pub struct Queue<'a> { bg_color: &'a mut Rgba, @@ -52,8 +52,8 @@ impl OpenSettings { Self { scale_policy, - logical_width: settings.size.width as f64, - logical_height: settings.size.height as f64, + logical_width: settings.size.width, + logical_height: settings.size.height, title: settings.title.clone(), } } @@ -89,8 +89,6 @@ where bg_color: Rgba, close_requested: bool, repaint_after: Option, - - full_output: egui::FullOutput, } impl EguiWindow @@ -110,7 +108,7 @@ where B: FnMut(&egui::Context, &mut Queue, &mut State), B: 'static + Send, { - let renderer = Renderer::new(window); + let renderer = get_renderer(window); let egui_ctx = egui::Context::default(); // Assume scale for now until there is an event with a new one. @@ -189,8 +187,6 @@ where bg_color, close_requested, repaint_after: Some(start_time), - - full_output: FullOutput::default(), } } @@ -205,7 +201,7 @@ where /// application and build the UI. pub fn open_parented( parent: &P, - mut settings: WindowOpenOptions, + #[allow(unused_mut)] mut settings: WindowOpenOptions, state: State, build: B, update: U, @@ -239,7 +235,7 @@ where /// call `ctx.set_fonts()`. Optional. /// * `update` - Called before each frame. Here you should update the state of your /// application and build the UI. - pub fn open_blocking(mut settings: WindowOpenOptions, state: State, build: B, update: U) + pub fn open_blocking(#[allow(unused_mut)] mut settings: WindowOpenOptions, state: State, build: B, update: U) where B: FnMut(&egui::Context, &mut Queue, &mut State), B: 'static + Send, @@ -292,9 +288,9 @@ where // Prevent data from being allocated every frame by storing this // in a member field. - self.full_output = self.egui_ctx.end_frame(); + let mut full_output = self.egui_ctx.end_frame(); - let Some(viewport_output) = self.full_output.viewport_output.get(&self.viewport_id) else { + let Some(viewport_output) = full_output.viewport_output.get(&self.viewport_id) else { // The main window was closed by egui. window.close(); return; @@ -324,12 +320,10 @@ where self.renderer.render( window, self.bg_color, - self.physical_width, - self.physical_height, + (self.physical_width, self.physical_height), self.pixels_per_point, &mut self.egui_ctx, - &mut self.full_output.shapes, - &mut self.full_output.textures_delta, + &mut full_output ); self.repaint_after = None; @@ -338,19 +332,18 @@ where self.repaint_after = Some(repaint_after); } - if !self.full_output.platform_output.copied_text.is_empty() { + if !full_output.platform_output.copied_text.is_empty() { if let Some(clipboard_ctx) = &mut self.clipboard_ctx { if let Err(err) = - clipboard_ctx.set_contents(self.full_output.platform_output.copied_text.clone()) + clipboard_ctx.set_contents(full_output.platform_output.copied_text.clone()) { log::error!("Copy/Cut error: {}", err); } } - self.full_output.platform_output.copied_text.clear(); } let cursor_icon = - crate::translate::translate_cursor_icon(self.full_output.platform_output.cursor_icon); + crate::translate::translate_cursor_icon(full_output.platform_output.cursor_icon); if self.current_cursor_icon != cursor_icon { self.current_cursor_icon = cursor_icon; @@ -463,7 +456,7 @@ where self.egui_input.modifiers.mac_cmd = pressed; self.egui_input.modifiers.command = pressed; } - () // prevent `rustfmt` from breaking this + // prevent `rustfmt` from breaking this } _ => (), } @@ -533,7 +526,7 @@ where .viewports .get_mut(&self.viewport_id) .unwrap(); - viewport_info.native_pixels_per_point = Some(self.pixels_per_point as f32); + viewport_info.native_pixels_per_point = Some(self.pixels_per_point); viewport_info.inner_rect = Some(screen_rect); // Schedule to repaint on the next frame. From 976a044a81443db1ecd59a4f7145e218ae8753c1 Mon Sep 17 00:00:00 2001 From: joe Date: Thu, 23 May 2024 20:56:34 -0600 Subject: [PATCH 09/14] dodge potential eager evaluation panic --- Cargo.toml | 2 +- src/renderer/wgpu_renderer.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 8102c6f..3393698 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,7 +14,7 @@ readme = "README.md" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [features] -default = ["opengl", "default_fonts"] +default = ["wgpu", "default_fonts"] opengl = ["egui_glow", "baseview/opengl"] wgpu = ["egui-wgpu", "raw-window-handle-06", "pollster", "dep:wgpu", "opengl"] default_fonts = ["egui/default_fonts"] diff --git a/src/renderer/wgpu_renderer.rs b/src/renderer/wgpu_renderer.rs index 4c75e2f..b8cb920 100644 --- a/src/renderer/wgpu_renderer.rs +++ b/src/renderer/wgpu_renderer.rs @@ -81,7 +81,7 @@ impl Renderer { raw_window_handle::RawWindowHandle::Win32(handle) => { let mut raw_handle = Win32WindowHandle::new(NonZeroIsize::new(handle.hwnd as isize).unwrap()); - raw_handle.hinstance = handle.hinstance.is_null().then_some(NonZeroIsize::new(handle.hinstance as isize).unwrap()); + raw_handle.hinstance = handle.hinstance.is_null().then(|| { NonZeroIsize::new(handle.hinstance as isize).unwrap() }); raw_window_handle_06::RawWindowHandle::Win32(raw_handle) From a660b0548e5a908c4484a4e97c1bfa5a89732140 Mon Sep 17 00:00:00 2001 From: joe Date: Mon, 27 May 2024 16:04:31 -0600 Subject: [PATCH 10/14] add render test --- examples/render_test.rs | 671 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 671 insertions(+) create mode 100644 examples/render_test.rs diff --git a/examples/render_test.rs b/examples/render_test.rs new file mode 100644 index 0000000..466ac1b --- /dev/null +++ b/examples/render_test.rs @@ -0,0 +1,671 @@ +use baseview::{Size, WindowOpenOptions, WindowScalePolicy}; +use egui::Context; +use egui_baseview::{EguiWindow, Queue}; + +use std::collections::HashMap; + +use egui::{widgets::color_picker::show_color, TextureOptions, *}; + +const GRADIENT_SIZE: Vec2 = vec2(256.0, 18.0); + +const BLACK: Color32 = Color32::BLACK; +const GREEN: Color32 = Color32::GREEN; +const RED: Color32 = Color32::RED; +const TRANSPARENT: Color32 = Color32::TRANSPARENT; +const WHITE: Color32 = Color32::WHITE; + +/// A test for sanity-checking and diagnosing egui rendering backends. +pub struct ColorTest { + tex_mngr: TextureManager, + vertex_gradients: bool, + texture_gradients: bool, +} + +impl Default for ColorTest { + fn default() -> Self { + Self { + tex_mngr: Default::default(), + vertex_gradients: true, + texture_gradients: true, + } + } +} + +impl ColorTest { + pub fn ui(&mut self, ui: &mut Ui) { + ui.horizontal_wrapped(|ui|{ + ui.label("This is made to test that the egui rendering backend is set up correctly."); + ui.add(egui::Label::new("❓").sense(egui::Sense::click())) + .on_hover_text("The texture sampling should be sRGB-aware, and every other color operation should be done in gamma-space (sRGB). All colors should use pre-multiplied alpha"); + }); + + ui.separator(); + + pixel_test(ui); + + ui.separator(); + + ui.collapsing("Color test", |ui| { + self.color_test(ui); + }); + + ui.separator(); + + ui.heading("Text rendering"); + + text_on_bg(ui, Color32::from_gray(200), Color32::from_gray(230)); // gray on gray + text_on_bg(ui, Color32::from_gray(140), Color32::from_gray(28)); // dark mode normal text + + // Matches Mac Font book (useful for testing): + text_on_bg(ui, Color32::from_gray(39), Color32::from_gray(255)); + text_on_bg(ui, Color32::from_gray(220), Color32::from_gray(30)); + + ui.separator(); + + blending_and_feathering_test(ui); + } + + fn color_test(&mut self, ui: &mut Ui) { + ui.label("If the rendering is done right, all groups of gradients will look uniform."); + + ui.horizontal(|ui| { + ui.checkbox(&mut self.vertex_gradients, "Vertex gradients"); + ui.checkbox(&mut self.texture_gradients, "Texture gradients"); + }); + + ui.heading("sRGB color test"); + ui.label("Use a color picker to ensure this color is (255, 165, 0) / #ffa500"); + ui.scope(|ui| { + ui.spacing_mut().item_spacing.y = 0.0; // No spacing between gradients + let g = Gradient::one_color(Color32::from_rgb(255, 165, 0)); + self.vertex_gradient(ui, "orange rgb(255, 165, 0) - vertex", WHITE, &g); + self.tex_gradient(ui, "orange rgb(255, 165, 0) - texture", WHITE, &g); + }); + + ui.separator(); + + ui.label("Test that vertex color times texture color is done in gamma space:"); + ui.scope(|ui| { + ui.spacing_mut().item_spacing.y = 0.0; // No spacing between gradients + + let tex_color = Color32::from_rgb(64, 128, 255); + let vertex_color = Color32::from_rgb(128, 196, 196); + let ground_truth = mul_color_gamma(tex_color, vertex_color); + + ui.horizontal(|ui| { + let color_size = ui.spacing().interact_size; + ui.label("texture"); + show_color(ui, tex_color, color_size); + ui.label(" * "); + show_color(ui, vertex_color, color_size); + ui.label(" vertex color ="); + }); + { + let g = Gradient::one_color(ground_truth); + self.vertex_gradient(ui, "Ground truth (vertices)", WHITE, &g); + self.tex_gradient(ui, "Ground truth (texture)", WHITE, &g); + } + + ui.horizontal(|ui| { + let g = Gradient::one_color(tex_color); + let tex = self.tex_mngr.get(ui.ctx(), &g); + let texel_offset = 0.5 / (g.0.len() as f32); + let uv = Rect::from_min_max(pos2(texel_offset, 0.0), pos2(1.0 - texel_offset, 1.0)); + ui.add( + Image::from_texture((tex.id(), GRADIENT_SIZE)) + .tint(vertex_color) + .uv(uv), + ) + .on_hover_text(format!("A texture that is {} texels wide", g.0.len())); + ui.label("GPU result"); + }); + }); + + ui.separator(); + + // TODO(emilk): test color multiplication (image tint), + // to make sure vertex and texture color multiplication is done in linear space. + + ui.label("Gamma interpolation:"); + self.show_gradients(ui, WHITE, (RED, GREEN), Interpolation::Gamma); + + ui.separator(); + + self.show_gradients(ui, RED, (TRANSPARENT, GREEN), Interpolation::Gamma); + + ui.separator(); + + self.show_gradients(ui, WHITE, (TRANSPARENT, GREEN), Interpolation::Gamma); + + ui.separator(); + + self.show_gradients(ui, BLACK, (BLACK, WHITE), Interpolation::Gamma); + ui.separator(); + self.show_gradients(ui, WHITE, (BLACK, TRANSPARENT), Interpolation::Gamma); + ui.separator(); + self.show_gradients(ui, BLACK, (TRANSPARENT, WHITE), Interpolation::Gamma); + ui.separator(); + + ui.label("Additive blending: add more and more blue to the red background:"); + self.show_gradients( + ui, + RED, + (TRANSPARENT, Color32::from_rgb_additive(0, 0, 255)), + Interpolation::Gamma, + ); + + ui.separator(); + + ui.label("Linear interpolation (texture sampling):"); + self.show_gradients(ui, WHITE, (RED, GREEN), Interpolation::Linear); + } + + fn show_gradients( + &mut self, + ui: &mut Ui, + bg_fill: Color32, + (left, right): (Color32, Color32), + interpolation: Interpolation, + ) { + let is_opaque = left.is_opaque() && right.is_opaque(); + + ui.horizontal(|ui| { + let color_size = ui.spacing().interact_size; + if !is_opaque { + ui.label("Background:"); + show_color(ui, bg_fill, color_size); + } + ui.label("gradient"); + show_color(ui, left, color_size); + ui.label("-"); + show_color(ui, right, color_size); + }); + + ui.scope(|ui| { + ui.spacing_mut().item_spacing.y = 0.0; // No spacing between gradients + if is_opaque { + let g = Gradient::ground_truth_gradient(left, right, interpolation); + self.vertex_gradient(ui, "Ground Truth (CPU gradient) - vertices", bg_fill, &g); + self.tex_gradient(ui, "Ground Truth (CPU gradient) - texture", bg_fill, &g); + } else { + let g = Gradient::ground_truth_gradient(left, right, interpolation) + .with_bg_fill(bg_fill); + self.vertex_gradient( + ui, + "Ground Truth (CPU gradient, CPU blending) - vertices", + bg_fill, + &g, + ); + self.tex_gradient( + ui, + "Ground Truth (CPU gradient, CPU blending) - texture", + bg_fill, + &g, + ); + let g = Gradient::ground_truth_gradient(left, right, interpolation); + self.vertex_gradient(ui, "CPU gradient, GPU blending - vertices", bg_fill, &g); + self.tex_gradient(ui, "CPU gradient, GPU blending - texture", bg_fill, &g); + } + + let g = Gradient::endpoints(left, right); + + match interpolation { + Interpolation::Linear => { + // texture sampler is sRGBA aware, and should therefore be linear + self.tex_gradient(ui, "Texture of width 2 (test texture sampler)", bg_fill, &g); + } + Interpolation::Gamma => { + // vertex shader uses gamma + self.vertex_gradient( + ui, + "Triangle mesh of width 2 (test vertex decode and interpolation)", + bg_fill, + &g, + ); + } + } + }); + } + + fn tex_gradient(&mut self, ui: &mut Ui, label: &str, bg_fill: Color32, gradient: &Gradient) { + if !self.texture_gradients { + return; + } + ui.horizontal(|ui| { + let tex = self.tex_mngr.get(ui.ctx(), gradient); + let texel_offset = 0.5 / (gradient.0.len() as f32); + let uv = Rect::from_min_max(pos2(texel_offset, 0.0), pos2(1.0 - texel_offset, 1.0)); + ui.add( + Image::from_texture((tex.id(), GRADIENT_SIZE)) + .bg_fill(bg_fill) + .uv(uv), + ) + .on_hover_text(format!( + "A texture that is {} texels wide", + gradient.0.len() + )); + ui.label(label); + }); + } + + fn vertex_gradient(&mut self, ui: &mut Ui, label: &str, bg_fill: Color32, gradient: &Gradient) { + if !self.vertex_gradients { + return; + } + ui.horizontal(|ui| { + vertex_gradient(ui, bg_fill, gradient).on_hover_text(format!( + "A triangle mesh that is {} vertices wide", + gradient.0.len() + )); + ui.label(label); + }); + } +} + +fn vertex_gradient(ui: &mut Ui, bg_fill: Color32, gradient: &Gradient) -> Response { + use egui::epaint::*; + let (rect, response) = ui.allocate_at_least(GRADIENT_SIZE, Sense::hover()); + if bg_fill != Default::default() { + let mut mesh = Mesh::default(); + mesh.add_colored_rect(rect, bg_fill); + ui.painter().add(Shape::mesh(mesh)); + } + { + let n = gradient.0.len(); + assert!(n >= 2); + let mut mesh = Mesh::default(); + for (i, &color) in gradient.0.iter().enumerate() { + let t = i as f32 / (n as f32 - 1.0); + let x = lerp(rect.x_range(), t); + mesh.colored_vertex(pos2(x, rect.top()), color); + mesh.colored_vertex(pos2(x, rect.bottom()), color); + if i < n - 1 { + let i = i as u32; + mesh.add_triangle(2 * i, 2 * i + 1, 2 * i + 2); + mesh.add_triangle(2 * i + 1, 2 * i + 2, 2 * i + 3); + } + } + ui.painter().add(Shape::mesh(mesh)); + } + response +} + +#[derive(Clone, Copy)] +enum Interpolation { + Linear, + Gamma, +} + +#[derive(Clone, Hash, PartialEq, Eq)] +struct Gradient(pub Vec); + +impl Gradient { + pub fn one_color(srgba: Color32) -> Self { + Self(vec![srgba, srgba]) + } + + pub fn endpoints(left: Color32, right: Color32) -> Self { + Self(vec![left, right]) + } + + pub fn ground_truth_gradient( + left: Color32, + right: Color32, + interpolation: Interpolation, + ) -> Self { + match interpolation { + Interpolation::Linear => Self::ground_truth_linear_gradient(left, right), + Interpolation::Gamma => Self::ground_truth_gamma_gradient(left, right), + } + } + + pub fn ground_truth_linear_gradient(left: Color32, right: Color32) -> Self { + let left = Rgba::from(left); + let right = Rgba::from(right); + + let n = 255; + Self( + (0..=n) + .map(|i| { + let t = i as f32 / n as f32; + Color32::from(lerp(left..=right, t)) + }) + .collect(), + ) + } + + pub fn ground_truth_gamma_gradient(left: Color32, right: Color32) -> Self { + let n = 255; + Self( + (0..=n) + .map(|i| { + let t = i as f32 / n as f32; + lerp_color_gamma(left, right, t) + }) + .collect(), + ) + } + + /// Do premultiplied alpha-aware blending of the gradient on top of the fill color + /// in gamma-space. + pub fn with_bg_fill(self, bg: Color32) -> Self { + Self( + self.0 + .into_iter() + .map(|fg| { + let a = fg.a() as f32 / 255.0; + Color32::from_rgba_premultiplied( + (bg[0] as f32 * (1.0 - a) + fg[0] as f32).round() as u8, + (bg[1] as f32 * (1.0 - a) + fg[1] as f32).round() as u8, + (bg[2] as f32 * (1.0 - a) + fg[2] as f32).round() as u8, + (bg[3] as f32 * (1.0 - a) + fg[3] as f32).round() as u8, + ) + }) + .collect(), + ) + } + + pub fn to_pixel_row(&self) -> Vec { + self.0.clone() + } +} + +#[derive(Default)] +struct TextureManager(HashMap); + +impl TextureManager { + fn get(&mut self, ctx: &egui::Context, gradient: &Gradient) -> &TextureHandle { + self.0.entry(gradient.clone()).or_insert_with(|| { + let pixels = gradient.to_pixel_row(); + let width = pixels.len(); + let height = 1; + ctx.load_texture( + "color_test_gradient", + epaint::ColorImage { + size: [width, height], + pixels, + }, + TextureOptions::LINEAR, + ) + }) + } +} + +/// A visual test that the rendering is correctly aligned on the physical pixel grid. +/// +/// Requires eyes and a magnifying glass to verify. +pub fn pixel_test(ui: &mut Ui) { + ui.heading("Pixel alignment test"); + ui.label("If anything is blurry, then everything will be blurry, including text."); + ui.label("You might need a magnifying glass to check this test."); + + if cfg!(target_arch = "wasm32") { + ui.label("Make sure these test pass even when you zoom in/out and resize the browser."); + } + + ui.add_space(4.0); + + pixel_test_lines(ui); + + ui.add_space(4.0); + + pixel_test_squares(ui); +} + +fn pixel_test_squares(ui: &mut Ui) { + ui.label("The first square should be exactly one physical pixel big."); + ui.label("They should be exactly one physical pixel apart."); + ui.label("Each subsequent square should be one physical pixel larger than the previous."); + ui.label("They should be perfectly aligned to the physical pixel grid."); + + let color = if ui.style().visuals.dark_mode { + egui::Color32::WHITE + } else { + egui::Color32::BLACK + }; + + let pixels_per_point = ui.ctx().pixels_per_point(); + + let num_squares = (pixels_per_point * 10.0).round().max(10.0) as u32; + let size_pixels = vec2( + ((num_squares + 1) * (num_squares + 2) / 2) as f32, + num_squares as f32, + ); + let size_points = size_pixels / pixels_per_point + Vec2::splat(2.0); + let (response, painter) = ui.allocate_painter(size_points, Sense::hover()); + + let mut cursor_pixel = Pos2::new( + response.rect.min.x * pixels_per_point, + response.rect.min.y * pixels_per_point, + ) + .ceil(); + for size in 1..=num_squares { + let rect_points = Rect::from_min_size( + Pos2::new(cursor_pixel.x, cursor_pixel.y), + Vec2::splat(size as f32), + ); + painter.rect_filled(rect_points / pixels_per_point, 0.0, color); + cursor_pixel.x += (1 + size) as f32; + } +} + +fn pixel_test_lines(ui: &mut Ui) { + let pixels_per_point = ui.ctx().pixels_per_point(); + let n = (96.0 * pixels_per_point) as usize; + + ui.label("The lines should be exactly one physical pixel wide, one physical pixel apart."); + ui.label("They should be perfectly white and black."); + + let hspace_px = pixels_per_point * 4.0; + + let size_px = Vec2::new(2.0 * n as f32 + hspace_px, n as f32); + let size_points = size_px / pixels_per_point + Vec2::splat(2.0); + let (response, painter) = ui.allocate_painter(size_points, Sense::hover()); + + let mut cursor_px = Pos2::new( + response.rect.min.x * pixels_per_point, + response.rect.min.y * pixels_per_point, + ) + .ceil(); + + // Vertical stripes: + for x in 0..n / 2 { + let rect_px = Rect::from_min_size( + Pos2::new(cursor_px.x + 2.0 * x as f32, cursor_px.y), + Vec2::new(1.0, n as f32), + ); + painter.rect_filled(rect_px / pixels_per_point, 0.0, egui::Color32::WHITE); + let rect_px = rect_px.translate(vec2(1.0, 0.0)); + painter.rect_filled(rect_px / pixels_per_point, 0.0, egui::Color32::BLACK); + } + + cursor_px.x += n as f32 + hspace_px; + + // Horizontal stripes: + for y in 0..n / 2 { + let rect_px = Rect::from_min_size( + Pos2::new(cursor_px.x, cursor_px.y + 2.0 * y as f32), + Vec2::new(n as f32, 1.0), + ); + painter.rect_filled(rect_px / pixels_per_point, 0.0, egui::Color32::WHITE); + let rect_px = rect_px.translate(vec2(0.0, 1.0)); + painter.rect_filled(rect_px / pixels_per_point, 0.0, egui::Color32::BLACK); + } +} + +fn blending_and_feathering_test(ui: &mut Ui) { + ui.label("The left side shows how lines of different widths look."); + ui.label("The right side tests text rendering at different opacities and sizes."); + ui.label("The top and bottom images should look symmetrical in their intensities."); + + let size = vec2(512.0, 512.0); + let (response, painter) = ui.allocate_painter(size, Sense::hover()); + let rect = response.rect; + + let mut top_half = rect; + top_half.set_bottom(top_half.center().y); + painter.rect_filled(top_half, 0.0, Color32::BLACK); + paint_fine_lines_and_text(&painter, top_half, Color32::WHITE); + + let mut bottom_half = rect; + bottom_half.set_top(bottom_half.center().y); + painter.rect_filled(bottom_half, 0.0, Color32::WHITE); + paint_fine_lines_and_text(&painter, bottom_half, Color32::BLACK); +} + +fn text_on_bg(ui: &mut egui::Ui, fg: Color32, bg: Color32) { + assert!(fg.is_opaque()); + assert!(bg.is_opaque()); + + ui.horizontal(|ui| { + ui.label( + RichText::from("▣ The quick brown fox jumps over the lazy dog and runs away.") + .background_color(bg) + .color(fg), + ); + ui.label(format!( + "({} {} {}) on ({} {} {})", + fg.r(), + fg.g(), + fg.b(), + bg.r(), + bg.g(), + bg.b(), + )); + }); +} + +fn paint_fine_lines_and_text(painter: &egui::Painter, mut rect: Rect, color: Color32) { + { + let mut y = 0.0; + for opacity in [1.00, 0.50, 0.25, 0.10, 0.05, 0.02, 0.01, 0.00] { + painter.text( + rect.center_top() + vec2(0.0, y), + Align2::LEFT_TOP, + format!("{:.0}% white", 100.0 * opacity), + FontId::proportional(14.0), + Color32::WHITE.gamma_multiply(opacity), + ); + painter.text( + rect.center_top() + vec2(80.0, y), + Align2::LEFT_TOP, + format!("{:.0}% gray", 100.0 * opacity), + FontId::proportional(14.0), + Color32::GRAY.gamma_multiply(opacity), + ); + painter.text( + rect.center_top() + vec2(160.0, y), + Align2::LEFT_TOP, + format!("{:.0}% black", 100.0 * opacity), + FontId::proportional(14.0), + Color32::BLACK.gamma_multiply(opacity), + ); + y += 20.0; + } + + for font_size in [6.0, 7.0, 8.0, 9.0, 10.0, 12.0, 14.0] { + painter.text( + rect.center_top() + vec2(0.0, y), + Align2::LEFT_TOP, + format!( + "{font_size}px - The quick brown fox jumps over the lazy dog and runs away." + ), + FontId::proportional(font_size), + color, + ); + y += font_size + 1.0; + } + } + + rect.max.x = rect.center().x; + + rect = rect.shrink(16.0); + for width in [0.05, 0.1, 0.25, 0.5, 1.0, 2.0, 4.0] { + painter.text( + rect.left_top(), + Align2::CENTER_CENTER, + width.to_string(), + FontId::monospace(12.0), + color, + ); + + painter.add(egui::epaint::CubicBezierShape::from_points_stroke( + [ + rect.left_top() + vec2(16.0, 0.0), + rect.right_top(), + rect.right_center(), + rect.right_bottom(), + ], + false, + Color32::TRANSPARENT, + Stroke::new(width, color), + )); + + rect.min.y += 24.0; + rect.max.x -= 24.0; + } + + rect.min.y += 16.0; + painter.text( + rect.left_top(), + Align2::LEFT_CENTER, + "transparent --> opaque", + FontId::monospace(10.0), + color, + ); + rect.min.y += 12.0; + let mut mesh = Mesh::default(); + mesh.colored_vertex(rect.left_bottom(), Color32::TRANSPARENT); + mesh.colored_vertex(rect.left_top(), Color32::TRANSPARENT); + mesh.colored_vertex(rect.right_bottom(), color); + mesh.colored_vertex(rect.right_top(), color); + mesh.add_triangle(0, 1, 2); + mesh.add_triangle(1, 2, 3); + painter.add(mesh); +} + +fn mul_color_gamma(left: Color32, right: Color32) -> Color32 { + Color32::from_rgba_premultiplied( + (left.r() as f32 * right.r() as f32 / 255.0).round() as u8, + (left.g() as f32 * right.g() as f32 / 255.0).round() as u8, + (left.b() as f32 * right.b() as f32 / 255.0).round() as u8, + (left.a() as f32 * right.a() as f32 / 255.0).round() as u8, + ) +} + +fn lerp_color_gamma(left: Color32, right: Color32, t: f32) -> Color32 { + Color32::from_rgba_premultiplied( + lerp((left[0] as f32)..=(right[0] as f32), t).round() as u8, + lerp((left[1] as f32)..=(right[1] as f32), t).round() as u8, + lerp((left[2] as f32)..=(right[2] as f32), t).round() as u8, + lerp((left[3] as f32)..=(right[3] as f32), t).round() as u8, + ) +} + + +fn main() { + let settings = WindowOpenOptions { + title: String::from("egui-baseview hello world"), + size: Size::new(1280.0, 720.0), + scale: WindowScalePolicy::SystemScaleFactor, + #[cfg(feature = "opengl")] + gl_config: Some(Default::default()), + }; + + let state = ColorTest::default(); + + EguiWindow::open_blocking( + settings, + state, + |_egui_ctx: &Context, _queue: &mut Queue, _state: &mut ColorTest| {}, + |egui_ctx: &Context, _queue: &mut Queue, state: &mut ColorTest| { + egui::Window::new("egui-baseview hello world").show(egui_ctx, |ui| { + ui.label("uwu"); + }); + + egui::Window::new("rendering test").scroll2(true).show(egui_ctx, |ui| { + state.ui(ui); + }); + }, + ); +} From 70629531767b6230e0cf537300686202769b26a3 Mon Sep 17 00:00:00 2001 From: joe Date: Tue, 28 May 2024 10:23:27 -0600 Subject: [PATCH 11/14] revert glow fallback, and disallow both renderers being active the glow fallback's not necesarry, since wgpu will do that itself. if you try to have both enabled, it can cause runtime troubles, so disallow at compile time --- Cargo.toml | 2 +- src/renderer.rs | 10 +++++++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 3393698..45ca4ad 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,7 +16,7 @@ readme = "README.md" [features] default = ["wgpu", "default_fonts"] opengl = ["egui_glow", "baseview/opengl"] -wgpu = ["egui-wgpu", "raw-window-handle-06", "pollster", "dep:wgpu", "opengl"] +wgpu = ["egui-wgpu", "raw-window-handle-06", "pollster", "dep:wgpu"] default_fonts = ["egui/default_fonts"] ## Enable parallel tessellation using [`rayon`](https://docs.rs/rayon). ## diff --git a/src/renderer.rs b/src/renderer.rs index cc76ea3..c371ccc 100644 --- a/src/renderer.rs +++ b/src/renderer.rs @@ -15,14 +15,14 @@ pub(crate) enum Renderer { } impl Renderer { - pub(crate) fn render(&mut self, window: &Window, bg_color: egui::Rgba, + pub(crate) fn render(&mut self, _window: &Window, bg_color: egui::Rgba, dimensions: (u32, u32), pixels_per_point: f32, egui_ctx: &mut egui::Context, full_output: &mut FullOutput) { match self { #[cfg(feature = "opengl")] - Renderer::OpenGL(renderer) => renderer.render(window, bg_color, dimensions, pixels_per_point, egui_ctx, full_output), + Renderer::OpenGL(renderer) => renderer.render(_window, bg_color, dimensions, pixels_per_point, egui_ctx, full_output), #[cfg(feature = "wgpu")] Renderer::Wgpu(renderer) => renderer.render(bg_color, dimensions, pixels_per_point, egui_ctx, full_output), #[allow(unreachable_patterns)] @@ -48,8 +48,12 @@ pub(crate) fn get_renderer(window: &Window) -> Renderer { #[cfg(not(any(feature = "opengl", feature = "wgpu")))] compile_error!("No renderer present. Please enable either opengl or wgpu in the crate's features"); + #[cfg(all(feature = "wgpu", feature = "opengl"))] + compile_error!("Both renderers enabled, which is unnesecarry. Either use just wgpu, or opengl"); + #[cfg(feature = "wgpu")] - if let Ok(renderer) = wgpu_renderer::Renderer::new(window) { + { + let renderer = wgpu_renderer::Renderer::new(window).expect("wgpu backend failed to initalize"); return Renderer::Wgpu(renderer); } From 2d59de586d6ca7f29abccb02b9dd5c16887566b8 Mon Sep 17 00:00:00 2001 From: joe Date: Tue, 28 May 2024 10:27:08 -0600 Subject: [PATCH 12/14] resolve compiler error --- src/window.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/window.rs b/src/window.rs index 20ddd6a..4c5f8e2 100644 --- a/src/window.rs +++ b/src/window.rs @@ -342,7 +342,7 @@ where } } - if let Some(open_url) = &self.full_output.platform_output.open_url { + if let Some(open_url) = &full_output.platform_output.open_url { if let Err(err) = open::that_detached(&open_url.url) { log::error!("Open error: {}", err); } From b08a0d315141db50db7648bb72fd06b66a22587b Mon Sep 17 00:00:00 2001 From: joe Date: Thu, 13 Jun 2024 09:25:44 -0600 Subject: [PATCH 13/14] officially updated yippie (also there was a change in how scrolling's handled) --- Cargo.toml | 7 ++++++- src/window.rs | 20 ++++---------------- 2 files changed, 10 insertions(+), 17 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index c96c618..b9a8db4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,7 +28,7 @@ pollster = ["dep:pollster"] egui = { version = "0.27", default-features = false, features = ["bytemuck"] } egui-wgpu = { version = "0.27", optional = true } egui_glow = { version = "0.27", optional = true } -wgpu = {version = "0.19", optional = true} +wgpu = {version = "0.20", optional = true} keyboard-types = { version = "0.6", default-features = false } baseview = { git = "https://github.com/RustAudio/baseview.git", rev = "45465c5f46abed6c6ce370fffde5edc8e4cd5aa3" } raw-window-handle = "0.5" @@ -38,3 +38,8 @@ copypasta = { version = "0.10", default-features = false, features = ["x11"] } log = "0.4" open = "5.1" pollster = { version = "0.3.0", optional = true } + +[patch.crates-io] +egui = {git = "https://github.com/emilk/egui", rev = "814ad0783cf8b826a258e29ed4c50ae6daa2e890"} +egui-wgpu = {git = "https://github.com/emilk/egui", rev = "814ad0783cf8b826a258e29ed4c50ae6daa2e890"} +egui_glow = {git = "https://github.com/emilk/egui", rev = "814ad0783cf8b826a258e29ed4c50ae6daa2e890"} diff --git a/src/window.rs b/src/window.rs index 4c5f8e2..a7cd624 100644 --- a/src/window.rs +++ b/src/window.rs @@ -406,12 +406,12 @@ where } => { self.update_modifiers(modifiers); - let mut delta = match scroll_delta { + let (unit, mut delta) = match scroll_delta { baseview::ScrollDelta::Lines { x, y } => { - egui::vec2(*x, *y) * self.points_per_scroll_line + (egui::MouseWheelUnit::Line, egui::vec2(*x, *y) * self.points_per_scroll_line) } baseview::ScrollDelta::Pixels { x, y } => { - egui::vec2(*x, *y) * self.points_per_pixel + (egui::MouseWheelUnit::Point, egui::vec2(*x, *y) * self.points_per_pixel) } }; if cfg!(target_os = "macos") { @@ -420,19 +420,7 @@ where delta.x *= -1.0; } - if self.egui_input.modifiers.ctrl || self.egui_input.modifiers.command { - // Treat as zoom instead: - let factor = (delta.y / 200.0).exp(); - self.egui_input.events.push(egui::Event::Zoom(factor)); - } else if self.egui_input.modifiers.shift { - // Treat as horizontal scrolling. - // Note: one Mac we already get horizontal scroll events when shift is down. - self.egui_input - .events - .push(egui::Event::Scroll(egui::vec2(delta.x + delta.y, 0.0))); - } else { - self.egui_input.events.push(egui::Event::Scroll(delta)); - } + self.egui_input.events.push(egui::Event::MouseWheel { unit, delta, modifiers: self.egui_input.modifiers }) } baseview::MouseEvent::CursorLeft => { self.pointer_pos_in_points = None; From 275cb4c3ed86d98ea557259383d71d2847b8babf Mon Sep 17 00:00:00 2001 From: Joe Sorensen Date: Thu, 13 Jun 2024 09:39:24 -0600 Subject: [PATCH 14/14] fix crash when hinstance isnt set --- src/renderer/wgpu_renderer.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/renderer/wgpu_renderer.rs b/src/renderer/wgpu_renderer.rs index b8cb920..f4a4ebb 100644 --- a/src/renderer/wgpu_renderer.rs +++ b/src/renderer/wgpu_renderer.rs @@ -81,7 +81,7 @@ impl Renderer { raw_window_handle::RawWindowHandle::Win32(handle) => { let mut raw_handle = Win32WindowHandle::new(NonZeroIsize::new(handle.hwnd as isize).unwrap()); - raw_handle.hinstance = handle.hinstance.is_null().then(|| { NonZeroIsize::new(handle.hinstance as isize).unwrap() }); + raw_handle.hinstance = NonZeroIsize::new(handle.hinstance as isize); raw_window_handle_06::RawWindowHandle::Win32(raw_handle)