diff --git a/crates/egui-wgpu/src/egui.wgsl b/crates/egui-wgpu/src/egui.wgsl index 39210841b..5b4b8240f 100644 --- a/crates/egui-wgpu/src/egui.wgsl +++ b/crates/egui-wgpu/src/egui.wgsl @@ -98,20 +98,31 @@ fn vs_main( @group(1) @binding(0) var r_tex_color: texture_2d; @group(1) @binding(1) var r_tex_sampler: sampler; +/// 1 if the texture sampler uses nearest filtering, 0 if linear. +/// Only read when `predictable_texture_filtering` is on. +@group(1) @binding(2) var r_tex_nearest_filtering: u32; + fn sample_texture(in: VertexOutput) -> vec4 { if r_locals.predictable_texture_filtering == 0 { // Hardware filtering: fast, but varies across GPUs and drivers. return textureSample(r_tex_color, r_tex_sampler, in.tex_coord); } else { - // Manual bilinear filtering with four taps at pixel centers using textureLoad let texture_size = vec2(textureDimensions(r_tex_color, 0)); let texture_size_f = vec2(texture_size); + let max_coord = texture_size - vec2(1, 1); + + if r_tex_nearest_filtering == 1 { + // Nearest filtering: load the texel under the sample position. + let texel = clamp(vec2(in.tex_coord * texture_size_f), vec2(0, 0), max_coord); + return textureLoad(r_tex_color, texel, 0); + } + + // Manual bilinear filtering with four taps at pixel centers using textureLoad let pixel_coord = in.tex_coord * texture_size_f - 0.5; let pixel_fract = fract(pixel_coord); let pixel_floor = vec2(floor(pixel_coord)); // Manual texture clamping - let max_coord = texture_size - vec2(1, 1); let p00 = clamp(pixel_floor + vec2(0, 0), vec2(0, 0), max_coord); let p10 = clamp(pixel_floor + vec2(1, 0), vec2(0, 0), max_coord); let p01 = clamp(pixel_floor + vec2(0, 1), vec2(0, 0), max_coord); diff --git a/crates/egui-wgpu/src/renderer.rs b/crates/egui-wgpu/src/renderer.rs index de8808de3..267363591 100644 --- a/crates/egui-wgpu/src/renderer.rs +++ b/crates/egui-wgpu/src/renderer.rs @@ -245,6 +245,12 @@ pub struct Renderer { uniform_bind_group: wgpu::BindGroup, texture_bind_group_layout: wgpu::BindGroupLayout, + /// Uniform buffers each holding a single `u32`: + /// 1 if the texture sampler uses nearest filtering, 0 otherwise. + /// Indexed by that flag value. + /// Read by the shader when `predictable_texture_filtering` is on. + nearest_filtering_flag_buffers: [wgpu::Buffer; 2], + /// Map of egui texture IDs to textures and their associated bindgroups (texture view + /// sampler). The texture may be None if the `TextureId` is just a handle to a user-provided /// sampler. @@ -347,10 +353,28 @@ impl Renderer { ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), count: None, }, + wgpu::BindGroupLayoutEntry { + binding: 2, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Buffer { + has_dynamic_offset: false, + min_binding_size: NonZeroU64::new(core::mem::size_of::() as _), + ty: wgpu::BufferBindingType::Uniform, + }, + count: None, + }, ], }) }; + let nearest_filtering_flag_buffers = [0_u32, 1_u32].map(|flag| { + device.create_buffer_init(&wgpu::util::BufferInitDescriptor { + label: Some(&format!("egui_nearest_filtering_flag_{flag}")), + contents: bytemuck::bytes_of(&flag), + usage: wgpu::BufferUsages::UNIFORM, + }) + }); + let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { label: Some("egui_pipeline_layout"), bind_group_layouts: &[ @@ -458,6 +482,7 @@ impl Renderer { previous_uniform_buffer_content: UniformBuffer::zeroed(), uniform_bind_group, texture_bind_group_layout, + nearest_filtering_flag_buffers, textures: HashMap::default(), next_user_texture_id: 0, samplers: HashMap::default(), @@ -709,6 +734,8 @@ impl Renderer { }; let bind_group = bind_group.unwrap_or_else(|| { + let nearest = + image_delta.options.magnification == epaint::textures::TextureFilter::Nearest; let sampler = self .samplers .entry(image_delta.options) @@ -727,6 +754,11 @@ impl Renderer { binding: 1, resource: wgpu::BindingResource::Sampler(sampler), }, + wgpu::BindGroupEntry { + binding: 2, + resource: self.nearest_filtering_flag_buffers[usize::from(nearest)] + .as_entire_binding(), + }, ], }) }); @@ -829,6 +861,7 @@ impl Renderer { ) -> epaint::TextureId { profiling::function_scope!(); + let nearest = sampler_descriptor.mag_filter == wgpu::FilterMode::Nearest; let sampler = device.create_sampler(&wgpu::SamplerDescriptor { compare: None, ..sampler_descriptor @@ -846,6 +879,11 @@ impl Renderer { binding: 1, resource: wgpu::BindingResource::Sampler(&sampler), }, + wgpu::BindGroupEntry { + binding: 2, + resource: self.nearest_filtering_flag_buffers[usize::from(nearest)] + .as_entire_binding(), + }, ], }); @@ -885,6 +923,7 @@ impl Renderer { .get_mut(&id) .expect("Tried to update a texture that has not been allocated yet."); + let nearest = sampler_descriptor.mag_filter == wgpu::FilterMode::Nearest; let sampler = device.create_sampler(&wgpu::SamplerDescriptor { compare: None, ..sampler_descriptor @@ -902,6 +941,11 @@ impl Renderer { binding: 1, resource: wgpu::BindingResource::Sampler(&sampler), }, + wgpu::BindGroupEntry { + binding: 2, + resource: self.nearest_filtering_flag_buffers[usize::from(nearest)] + .as_entire_binding(), + }, ], }); diff --git a/crates/egui_demo_lib/tests/misc.rs b/crates/egui_demo_lib/tests/misc.rs index 427710629..07b61215a 100644 --- a/crates/egui_demo_lib/tests/misc.rs +++ b/crates/egui_demo_lib/tests/misc.rs @@ -1,6 +1,41 @@ use egui::{Color32, accesskit::Role}; use egui_kittest::{Harness, kittest::Queryable as _}; +/// Textures with [`egui::TextureOptions::NEAREST`] should render crisp, +/// also with kittest's predictable texture filtering. +#[test] +fn test_nearest_texture_filtering() { + let mut texture: Option = None; + let mut harness = Harness::builder() + .with_size(egui::Vec2::new(80.0, 48.0)) + .build_ui(move |ui| { + let texture = texture.get_or_insert_with(|| { + let pixels = [ + Color32::BLACK, + Color32::WHITE, + Color32::BLACK, + Color32::WHITE, + Color32::WHITE, + Color32::BLACK, + Color32::WHITE, + Color32::BLACK, + ]; + let image = egui::ColorImage::new([4, 2], pixels.to_vec()); + ui.ctx() + .load_texture("checkerboard", image, egui::TextureOptions::NEAREST) + }); + + let rect = egui::Rect::from_min_size(egui::pos2(8.0, 8.0), egui::vec2(64.0, 32.0)); + let uv = egui::Rect::from_min_max(egui::pos2(0.0, 0.0), egui::pos2(1.0, 1.0)); + ui.painter().add( + egui::epaint::RectShape::filled(rect, 0, Color32::WHITE) + .with_texture(texture.id(), uv), + ); + }); + harness.run(); + harness.snapshot("nearest_texture_filtering"); +} + #[test] fn test_kerning() { let mut results = egui_kittest::SnapshotResults::new(); diff --git a/crates/egui_demo_lib/tests/snapshots/nearest_texture_filtering.png b/crates/egui_demo_lib/tests/snapshots/nearest_texture_filtering.png new file mode 100644 index 000000000..fedbc8f13 --- /dev/null +++ b/crates/egui_demo_lib/tests/snapshots/nearest_texture_filtering.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:72e133c853ab933d37711665ce1278f8f23ce35b8a74a46559f269d0dfd0bf40 +size 353