diff --git a/Cargo.toml b/Cargo.toml index 061ebe6..4c793df 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,7 +28,7 @@ profiling = "1" slab = "0.4" strum = { version = "0.25", features = ["derive"] } web-sys = "0.3.60" -winit = "0.30" +winit = { version="0.30", default-features = false, features=["rwh_06", "x11"]} [lib] @@ -89,8 +89,7 @@ del-msh-core = "=0.1.33" del-geo = "=0.1.29" [target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies] -# see https://github.com/emilk/egui/issues/4270 -egui-winit = "0.29" +egui-winit = { version="0.29", deafult-features=false, features=["links"] } [target.'cfg(target_arch = "wasm32")'.dev-dependencies] console_error_panic_hook = "0.1.7" diff --git a/blade-egui/shader.wgsl b/blade-egui/shader.wgsl index ceff1c6..88be51f 100644 --- a/blade-egui/shader.wgsl +++ b/blade-egui/shader.wgsl @@ -1,3 +1,5 @@ +// NOTE: Borrows heavily from egui-wgpu:s wgsl shaders, used here under the MIT license + struct VertexOutput { @location(0) tex_coord: vec2, @location(1) color: vec4, @@ -6,8 +8,8 @@ struct VertexOutput { struct Uniforms { screen_size: vec2, - convert_to_linear: f32, - padding: f32, + dithering: u32, + padding: u32, }; var r_uniforms: Uniforms; @@ -21,13 +23,6 @@ struct Vertex { } var r_vertex_data: array; -fn linear_from_srgb(srgb: vec3) -> vec3 { - let cutoff = srgb < vec3(10.31475); - let lower = srgb / vec3(3294.6); - let higher = pow((srgb + vec3(14.025)) / vec3(269.025), vec3(2.4)); - return select(higher, lower, cutoff); -} - @vertex fn vs_main( @builtin(vertex_index) v_index: u32, @@ -35,8 +30,9 @@ fn vs_main( let input = r_vertex_data[v_index]; var out: VertexOutput; out.tex_coord = vec2(input.tex_coord_x, input.tex_coord_y); - let color = unpack4x8unorm(input.color); - out.color = vec4(pow(color.xyz, vec3(2.2)), color.a); + // let color = unpack4x8unorm(input.color); + let color = unpack_color(input.color); + out.color = color; out.position = vec4( 2.0 * input.pos_x / r_uniforms.screen_size.x - 1.0, 1.0 - 2.0 * input.pos_y / r_uniforms.screen_size.y, @@ -50,6 +46,96 @@ var r_texture: texture_2d; var r_sampler: sampler; @fragment -fn fs_main(in: VertexOutput) -> @location(0) vec4 { - return in.color * textureSample(r_texture, r_sampler, in.tex_coord); +fn fs_main_linear_framebuffer(in: VertexOutput) -> @location(0) vec4 { + // We always have an sRGB aware texture at the moment. + let tex_linear = textureSample(r_texture, r_sampler, in.tex_coord); + let tex_gamma = gamma_from_linear_rgba(tex_linear); + var out_color_gamma = in.color * tex_gamma; + // Dither the float color down to eight bits to reduce banding. + // This step is optional for egui backends. + // Note that dithering is performed on the gamma encoded values, + // because this function is used together with a srgb converting target. + if r_uniforms.dithering == 1 { + let out_color_gamma_rgb = dither_interleaved(out_color_gamma.rgb, 256.0, in.position); + out_color_gamma = vec4(out_color_gamma_rgb, out_color_gamma.a); + } + let out_color_linear = linear_from_gamma_rgb(out_color_gamma.rgb); + return vec4(out_color_linear, out_color_gamma.a); +} + +@fragment +fn fs_main_gamma_framebuffer(in: VertexOutput) -> @location(0) vec4 { + // We always have an sRGB aware texture at the moment. + let tex_linear = textureSample(r_texture, r_sampler, in.tex_coord); + let tex_gamma = gamma_from_linear_rgba(tex_linear); + var out_color_gamma = in.color * tex_gamma; + // Dither the float color down to eight bits to reduce banding. + // This step is optional for egui backends. + if r_uniforms.dithering == 1 { + let out_color_gamma_rgb = dither_interleaved(out_color_gamma.rgb, 256.0, in.position); + out_color_gamma = vec4(out_color_gamma_rgb, out_color_gamma.a); + } + return out_color_gamma; +} + + +// ----------------------------------------------- +// Adapted from +// https://www.shadertoy.com/view/llVGzG +// Originally presented in: +// Jimenez 2014, "Next Generation Post-Processing in Call of Duty" +// +// A good overview can be found in +// https://blog.demofox.org/2022/01/01/interleaved-gradient-noise-a-different-kind-of-low-discrepancy-sequence/ +// via https://github.com/rerun-io/rerun/ +fn interleaved_gradient_noise(n: vec2) -> f32 { + let f = 0.06711056 * n.x + 0.00583715 * n.y; + return fract(52.9829189 * fract(f)); +} + +fn dither_interleaved(rgb: vec3, levels: f32, frag_coord: vec4) -> vec3 { + var noise = interleaved_gradient_noise(frag_coord.xy); + // scale down the noise slightly to ensure flat colors aren't getting dithered + noise = (noise - 0.5) * 0.95; + return rgb + noise / (levels - 1.0); +} + +// 0-1 linear from 0-1 sRGB gamma +fn linear_from_gamma_rgb(srgb: vec3) -> vec3 { + let cutoff = srgb < vec3(0.04045); + let lower = srgb / vec3(12.92); + let higher = pow((srgb + vec3(0.055)) / vec3(1.055), vec3(2.4)); + return select(higher, lower, cutoff); +} + +// 0-1 sRGB gamma from 0-1 linear +fn gamma_from_linear_rgb(rgb: vec3) -> vec3 { + let cutoff = rgb < vec3(0.0031308); + let lower = rgb * vec3(12.92); + let higher = vec3(1.055) * pow(rgb, vec3(1.0 / 2.4)) - vec3(0.055); + return select(higher, lower, cutoff); +} + +// 0-1 sRGBA gamma from 0-1 linear +fn gamma_from_linear_rgba(linear_rgba: vec4) -> vec4 { + return vec4(gamma_from_linear_rgb(linear_rgba.rgb), linear_rgba.a); +} + +// [u8; 4] SRGB as u32 -> [r, g, b, a] in 0.-1 +fn unpack_color(color: u32) -> vec4 { + return vec4( + f32(color & 255u), + f32((color >> 8u) & 255u), + f32((color >> 16u) & 255u), + f32((color >> 24u) & 255u), + ) / 255.0; +} + +fn position_from_screen(screen_pos: vec2) -> vec4 { + return vec4( + 2.0 * screen_pos.x / r_uniforms.screen_size.x - 1.0, + 1.0 - 2.0 * screen_pos.y / r_uniforms.screen_size.y, + 0.0, + 1.0, + ); } diff --git a/blade-egui/src/lib.rs b/blade-egui/src/lib.rs index 533a269..fde149c 100644 --- a/blade-egui/src/lib.rs +++ b/blade-egui/src/lib.rs @@ -26,19 +26,20 @@ use std::{ #[derive(Clone, Copy, bytemuck::Zeroable, bytemuck::Pod)] struct Uniforms { screen_size: [f32; 2], - padding: [f32; 2], + dithering: u32, + padding: u32, } #[derive(blade_macros::ShaderData)] struct Globals { r_uniforms: Uniforms, - r_sampler: blade_graphics::Sampler, } #[derive(blade_macros::ShaderData)] struct Locals { r_vertex_data: blade_graphics::BufferPiece, r_texture: blade_graphics::TextureView, + r_sampler: blade_graphics::Sampler, } #[derive(Debug, PartialEq)] @@ -58,10 +59,16 @@ impl ScreenDescriptor { struct GuiTexture { allocation: blade_graphics::Texture, view: blade_graphics::TextureView, + sampler: blade_graphics::Sampler, } impl GuiTexture { - fn create(context: &blade_graphics::Context, name: &str, size: blade_graphics::Extent) -> Self { + fn create( + context: &blade_graphics::Context, + name: &str, + size: blade_graphics::Extent, + options: egui::TextureOptions, + ) -> Self { let format = blade_graphics::TextureFormat::Rgba8UnormSrgb; let allocation = context.create_texture(blade_graphics::TextureDesc { name, @@ -81,12 +88,62 @@ impl GuiTexture { subresources: &blade_graphics::TextureSubresources::default(), }, ); - Self { allocation, view } + + let sampler = context.create_sampler(blade_graphics::SamplerDesc { + name, + address_modes: { + let mode = match options.wrap_mode { + egui::TextureWrapMode::ClampToEdge => blade_graphics::AddressMode::ClampToEdge, + egui::TextureWrapMode::Repeat => blade_graphics::AddressMode::Repeat, + egui::TextureWrapMode::MirroredRepeat => { + blade_graphics::AddressMode::MirrorRepeat + } + }; + [mode; 3] + }, + mag_filter: match options.magnification { + egui::TextureFilter::Nearest => blade_graphics::FilterMode::Nearest, + egui::TextureFilter::Linear => blade_graphics::FilterMode::Linear, + }, + min_filter: match options.minification { + egui::TextureFilter::Nearest => blade_graphics::FilterMode::Nearest, + egui::TextureFilter::Linear => blade_graphics::FilterMode::Linear, + }, + mipmap_filter: match options.mipmap_mode { + Some(it) => match it { + egui::TextureFilter::Nearest => blade_graphics::FilterMode::Nearest, + egui::TextureFilter::Linear => blade_graphics::FilterMode::Linear, + }, + None => blade_graphics::FilterMode::Linear, + }, + ..Default::default() + }); + + Self { + allocation, + view, + sampler, + } } fn delete(self, context: &blade_graphics::Context) { context.destroy_texture(self.allocation); context.destroy_texture_view(self.view); + context.destroy_sampler(self.sampler); + } +} + +#[derive(Clone, Copy)] +pub struct GuiPainterOptions { + /// Controls whether to apply dithering to minimize banding artifacts, same as egui-wgpu + /// + /// Defaults to true. + pub dithering: bool, +} + +impl Default for GuiPainterOptions { + fn default() -> Self { + Self { dithering: true } } } @@ -103,7 +160,7 @@ pub struct GuiPainter { //TODO: this could also look better textures_dropped: Vec, textures_to_delete: Vec<(GuiTexture, blade_graphics::SyncPoint)>, - sampler: blade_graphics::Sampler, + options: GuiPainterOptions, } impl GuiPainter { @@ -120,7 +177,6 @@ impl GuiPainter { for (gui_texture, _) in self.textures_to_delete.drain(..) { gui_texture.delete(context); } - context.destroy_sampler(self.sampler); } /// Create a new painter with a given GPU context. @@ -128,7 +184,11 @@ impl GuiPainter { /// It supports renderpasses with only a color attachment, /// and this attachment format must be The `output_format`. #[profiling::function] - pub fn new(info: blade_graphics::SurfaceInfo, context: &blade_graphics::Context) -> Self { + pub fn new( + info: blade_graphics::SurfaceInfo, + context: &blade_graphics::Context, + options: GuiPainterOptions, + ) -> Self { let shader = context.create_shader(blade_graphics::ShaderDesc { source: SHADER_SOURCE, }); @@ -144,7 +204,11 @@ impl GuiPainter { ..Default::default() }, depth_stencil: None, //TODO? - fragment: shader.at("fs_main"), + fragment: if info.format.is_srgb() { + shader.at("fs_main_linear_framebuffer") + } else { + shader.at("fs_main_gamma_framebuffer") + }, color_targets: &[blade_graphics::ColorTargetState { format: info.format, blend: Some(blade_graphics::BlendState::ALPHA_BLENDING), @@ -158,21 +222,13 @@ impl GuiPainter { alignment: 4, }); - let sampler = context.create_sampler(blade_graphics::SamplerDesc { - name: "gui", - address_modes: [blade_graphics::AddressMode::ClampToEdge; 3], - mag_filter: blade_graphics::FilterMode::Linear, - min_filter: blade_graphics::FilterMode::Linear, - ..Default::default() - }); - Self { pipeline, belt, textures: Default::default(), textures_dropped: Vec::new(), textures_to_delete: Vec::new(), - sampler, + options, } } @@ -239,7 +295,8 @@ impl GuiPainter { let texture = match self.textures.entry(texture_id) { Entry::Occupied(mut o) => { if image_delta.pos.is_none() { - let texture = GuiTexture::create(context, &label, extent); + let texture = + GuiTexture::create(context, &label, extent, image_delta.options); command_encoder.init_texture(texture.allocation); let old = o.insert(texture); self.textures_dropped.push(old); @@ -247,7 +304,7 @@ impl GuiPainter { o.into_mut() } Entry::Vacant(v) => { - let texture = GuiTexture::create(context, &label, extent); + let texture = GuiTexture::create(context, &label, extent, image_delta.options); command_encoder.init_texture(texture.allocation); v.insert(texture) } @@ -296,9 +353,9 @@ impl GuiPainter { &Globals { r_uniforms: Uniforms { screen_size: [logical_size.0, logical_size.1], - padding: [0.0; 2], + dithering: if self.options.dithering { 1 } else { 0 }, + padding: 0, }, - r_sampler: self.sampler, }, ); @@ -340,6 +397,7 @@ impl GuiPainter { &Locals { r_vertex_data: vertex_buf, r_texture: texture.view, + r_sampler: texture.sampler, }, ); diff --git a/blade-graphics/src/lib.rs b/blade-graphics/src/lib.rs index b85baf6..35879b7 100644 --- a/blade-graphics/src/lib.rs +++ b/blade-graphics/src/lib.rs @@ -309,6 +309,42 @@ pub enum TextureFormat { Bc5Snorm, } +impl TextureFormat { + /// Returns true if the format is srgb-aware + pub const fn is_srgb(self) -> bool { + match self { + TextureFormat::Rgba8UnormSrgb + | TextureFormat::Bgra8UnormSrgb + | TextureFormat::Bc1UnormSrgb + | TextureFormat::Bc2UnormSrgb + | TextureFormat::Bc3UnormSrgb => true, + + TextureFormat::R8Unorm + | TextureFormat::Rg8Unorm + | TextureFormat::Rg8Snorm + | TextureFormat::Rgba8Unorm + | TextureFormat::Bgra8Unorm + | TextureFormat::Rgba8Snorm + | TextureFormat::R16Float + | TextureFormat::Rgba16Float + | TextureFormat::R32Float + | TextureFormat::Rg32Float + | TextureFormat::Rgba32Float + | TextureFormat::R32Uint + | TextureFormat::Rg32Uint + | TextureFormat::Rgba32Uint + | TextureFormat::Depth32Float + | TextureFormat::Bc1Unorm + | TextureFormat::Bc2Unorm + | TextureFormat::Bc3Unorm + | TextureFormat::Bc4Unorm + | TextureFormat::Bc4Snorm + | TextureFormat::Bc5Unorm + | TextureFormat::Bc5Snorm => false, + } + } +} + #[derive(Clone, Copy, Debug)] pub struct TexelBlockInfo { pub dimensions: (u8, u8), diff --git a/examples/gui/main.rs b/examples/gui/main.rs new file mode 100644 index 0000000..0535d0c --- /dev/null +++ b/examples/gui/main.rs @@ -0,0 +1,236 @@ +#![allow(irrefutable_let_patterns)] + +use blade_graphics as gpu; +use winit::platform::x11::WindowAttributesExtX11; + +struct Example { + command_encoder: gpu::CommandEncoder, + prev_sync_point: Option, + context: gpu::Context, + surface: gpu::Surface, + gui_painter: blade_egui::GuiPainter, +} + +impl Example { + fn new(window: &winit::window::Window) -> Self { + let window_size = window.inner_size(); + let context = unsafe { + gpu::Context::init(gpu::ContextDesc { + presentation: true, + validation: cfg!(debug_assertions), + timing: true, + capture: true, + ..Default::default() + }) + .unwrap() + }; + let surface_config = Self::make_surface_config(window_size); + let surface = context + .create_surface_configured(window, surface_config) + .unwrap(); + let surface_info = surface.info(); + + let gui_painter = blade_egui::GuiPainter::new(surface_info, &context, Default::default()); + + let mut command_encoder = context.create_command_encoder(gpu::CommandEncoderDesc { + name: "main", + buffer_count: 2, + }); + command_encoder.start(); + let sync_point = context.submit(&mut command_encoder); + + Self { + command_encoder, + prev_sync_point: Some(sync_point), + context, + surface, + gui_painter, + } + } + + fn destroy(&mut self) { + if let Some(sp) = self.prev_sync_point.take() { + self.context.wait_for(&sp, !0); + } + self.context + .destroy_command_encoder(&mut self.command_encoder); + self.gui_painter.destroy(&self.context); + self.context.destroy_surface(&mut self.surface); + } + + fn make_surface_config(size: winit::dpi::PhysicalSize) -> gpu::SurfaceConfig { + log::info!("Window size: {:?}", size); + gpu::SurfaceConfig { + size: gpu::Extent { + width: size.width, + height: size.height, + depth: 1, + }, + usage: gpu::TextureUsage::TARGET, + display_sync: gpu::DisplaySync::Block, + color_space: gpu::ColorSpace::Linear, + // color_space: gpu::ColorSpace::Srgb, + ..Default::default() + } + } + + fn resize(&mut self, size: winit::dpi::PhysicalSize) { + let config = Self::make_surface_config(size); + self.context.reconfigure_surface(&mut self.surface, config); + } + + fn render( + &mut self, + gui_primitives: &[egui::ClippedPrimitive], + gui_textures: &egui::TexturesDelta, + screen_desc: &blade_egui::ScreenDescriptor, + ) { + let frame = self.surface.acquire_frame(); + self.command_encoder.start(); + self.command_encoder.init_texture(frame.texture()); + + self.gui_painter + .update_textures(&mut self.command_encoder, gui_textures, &self.context); + + if let mut pass = self.command_encoder.render( + "draw", + gpu::RenderTargetSet { + colors: &[gpu::RenderTarget { + view: frame.texture_view(), + init_op: gpu::InitOp::Clear(gpu::TextureColor::OpaqueBlack), + finish_op: gpu::FinishOp::Store, + }], + depth_stencil: None, + }, + ) { + self.gui_painter + .paint(&mut pass, gui_primitives, screen_desc, &self.context); + } + self.command_encoder.present(frame); + let sync_point = self.context.submit(&mut self.command_encoder); + self.gui_painter.after_submit(&sync_point); + + if let Some(sp) = self.prev_sync_point.take() { + self.context.wait_for(&sp, !0); + } + self.prev_sync_point = Some(sync_point); + } + + fn add_gui(&mut self, ui: &mut egui::Ui) { + ui.heading("Timings"); + for (name, time) in self.command_encoder.timings() { + let millis = time.as_secs_f32() * 1000.0; + ui.horizontal(|ui| { + ui.label(name); + ui.colored_label(egui::Color32::WHITE, format!("{:.2} ms", millis)); + }); + } + } +} + +fn main() { + env_logger::init(); + + use winit::platform::x11::EventLoopBuilderExtX11; + + let event_loop = winit::event_loop::EventLoop::builder() + .with_x11() + .build() + .unwrap(); + let window_attributes = winit::window::Window::default_attributes() + .with_inner_size(winit::dpi::PhysicalSize::new(800, 600)) + .with_title("blade-particle"); + + let window = event_loop.create_window(window_attributes).unwrap(); + + let egui_ctx = egui::Context::default(); + let viewport_id = egui_ctx.viewport_id(); + let mut egui_winit = egui_winit::State::new(egui_ctx, viewport_id, &window, None, None, None); + + let mut example = Example::new(&window); + + event_loop + .run(|event, target| { + target.set_control_flow(winit::event_loop::ControlFlow::Poll); + match event { + winit::event::Event::AboutToWait => { + window.request_redraw(); + } + winit::event::Event::WindowEvent { event, .. } => { + let response = egui_winit.on_window_event(&window, &event); + if response.consumed { + return; + } + if response.repaint { + window.request_redraw(); + } + + match event { + winit::event::WindowEvent::KeyboardInput { + event: + winit::event::KeyEvent { + physical_key: winit::keyboard::PhysicalKey::Code(key_code), + state: winit::event::ElementState::Pressed, + .. + }, + .. + } => match key_code { + winit::keyboard::KeyCode::Escape => { + target.exit(); + } + _ => {} + }, + winit::event::WindowEvent::CloseRequested => { + target.exit(); + } + winit::event::WindowEvent::Resized(new_size) => { + example.resize(new_size); + } + winit::event::WindowEvent::RedrawRequested => { + let raw_input = egui_winit.take_egui_input(&window); + let egui_output = egui_winit.egui_ctx().run(raw_input, |egui_ctx| { + egui::CentralPanel::default().show(egui_ctx, |ui| { + example.add_gui(ui); + if ui.button("Quit").clicked() { + target.exit(); + } + }); + }); + + egui_winit.handle_platform_output(&window, egui_output.platform_output); + let repaint_delay = + egui_output.viewport_output[&viewport_id].repaint_delay; + + let pixels_per_point = + egui_winit::pixels_per_point(egui_winit.egui_ctx(), &window); + let primitives = egui_winit + .egui_ctx() + .tessellate(egui_output.shapes, pixels_per_point); + + let control_flow = if let Some(repaint_after_instant) = + std::time::Instant::now().checked_add(repaint_delay) + { + winit::event_loop::ControlFlow::WaitUntil(repaint_after_instant) + } else { + winit::event_loop::ControlFlow::Wait + }; + target.set_control_flow(control_flow); + + //Note: this will probably look different with proper support for resizing + let window_size = window.inner_size(); + let screen_desc = blade_egui::ScreenDescriptor { + physical_size: (window_size.width, window_size.height), + scale_factor: pixels_per_point, + }; + example.render(&primitives, &egui_output.textures_delta, &screen_desc); + } + _ => {} + } + } + _ => {} + } + }) + .unwrap(); + + example.destroy(); +} diff --git a/examples/particle/main.rs b/examples/particle/main.rs index e6a24c7..cbb26d5 100644 --- a/examples/particle/main.rs +++ b/examples/particle/main.rs @@ -41,7 +41,7 @@ impl Example { .unwrap(); let surface_info = surface.info(); - let gui_painter = blade_egui::GuiPainter::new(surface_info, &context); + let gui_painter = blade_egui::GuiPainter::new(surface_info, &context, Default::default()); let particle_system = particle::System::new( &context, particle::SystemDesc { diff --git a/examples/scene/main.rs b/examples/scene/main.rs index ddae495..f42e296 100644 --- a/examples/scene/main.rs +++ b/examples/scene/main.rs @@ -228,7 +228,7 @@ impl Example { &render_config, ); pacer.end_frame(&context); - let gui_painter = blade_egui::GuiPainter::new(surface_info, &context); + let gui_painter = blade_egui::GuiPainter::new(surface_info, &context, Default::default()); Self { scene_path: PathBuf::new(), diff --git a/src/lib.rs b/src/lib.rs index 2f754a4..1f7880e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -461,7 +461,8 @@ impl Engine { pacer.end_frame(&gpu_context); - let gui_painter = blade_egui::GuiPainter::new(surface_info, &gpu_context); + let gui_painter = + blade_egui::GuiPainter::new(surface_info, &gpu_context, Default::default()); let mut physics = Physics::default(); physics.debug_pipeline.mode = rapier3d::pipeline::DebugRenderMode::empty(); physics.integration_params.dt = config.time_step;