use wgpu::TextureFormat;
pub const DEPTH_FORMAT: TextureFormat = TextureFormat::Depth32Float;
pub const OFFSCREEN_FORMAT: TextureFormat = TextureFormat::Rgba8Unorm;
pub const BLIT_WGSL: &str = r#"
@group(0) @binding(0) var blit_tex: texture_2d<f32>;
@group(0) @binding(1) var blit_smp: sampler;
struct BlitOut {
@builtin(position) clip: vec4<f32>,
@location(0) uv: vec2<f32>,
};
@vertex
fn blit_vs(@builtin(vertex_index) vi: u32) -> BlitOut {
var p = array<vec2<f32>, 3>(
vec2<f32>(-1.0, -1.0),
vec2<f32>( 3.0, -1.0),
vec2<f32>(-1.0, 3.0),
);
var out: BlitOut;
let xy = p[vi];
out.clip = vec4<f32>(xy, 0.0, 1.0);
out.uv = vec2<f32>(xy.x * 0.5 + 0.5, 1.0 - (xy.y * 0.5 + 0.5));
return out;
}
@fragment
fn blit_fs(in: BlitOut) -> @location(0) vec4<f32> {
return textureSample(blit_tex, blit_smp, in.uv);
}
"#;
pub struct OffscreenColorDepth {
blit_pipeline: wgpu::RenderPipeline,
blit_bgl: wgpu::BindGroupLayout,
sampler: wgpu::Sampler,
color_tex: Option<wgpu::Texture>,
color_view: Option<wgpu::TextureView>,
depth_view: Option<wgpu::TextureView>,
blit_bind: Option<wgpu::BindGroup>,
size: (u32, u32),
}
impl OffscreenColorDepth {
pub fn new(device: &wgpu::Device, target_format: TextureFormat) -> Self {
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("l0_offscreen_blit"),
source: wgpu::ShaderSource::Wgsl(BLIT_WGSL.into()),
});
let blit_bgl = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("l0_offscreen_blit_bgl"),
entries: &[
wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
sample_type: wgpu::TextureSampleType::Float { filterable: true },
view_dimension: wgpu::TextureViewDimension::D2,
multisampled: false,
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 1,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
count: None,
},
],
});
let blit_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("l0_offscreen_blit_pipeline"),
layout: Some(&device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("l0_offscreen_blit_pll"),
bind_group_layouts: &[Some(&blit_bgl)],
immediate_size: 0,
})),
vertex: wgpu::VertexState {
module: &shader,
entry_point: Some("blit_vs"),
compilation_options: Default::default(),
buffers: &[],
},
primitive: wgpu::PrimitiveState { topology: wgpu::PrimitiveTopology::TriangleList, ..Default::default() },
depth_stencil: None,
multisample: wgpu::MultisampleState::default(),
fragment: Some(wgpu::FragmentState {
module: &shader,
entry_point: Some("blit_fs"),
compilation_options: Default::default(),
targets: &[Some(wgpu::ColorTargetState {
format: target_format,
blend: Some(wgpu::BlendState::ALPHA_BLENDING),
write_mask: wgpu::ColorWrites::ALL,
})],
}),
multiview_mask: None,
cache: None,
});
let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
label: Some("l0_offscreen_blit_sampler"),
mag_filter: wgpu::FilterMode::Linear,
min_filter: wgpu::FilterMode::Linear,
..Default::default()
});
Self {
blit_pipeline,
blit_bgl,
sampler,
color_tex: None,
color_view: None,
depth_view: None,
blit_bind: None,
size: (0, 0),
}
}
pub fn ensure(&mut self, device: &wgpu::Device, w: u32, h: u32) {
let w = w.max(1);
let h = h.max(1);
if self.size == (w, h) && self.color_view.is_some() {
return;
}
let color = device.create_texture(&wgpu::TextureDescriptor {
label: Some("l0_offscreen_color"),
size: wgpu::Extent3d { width: w, height: h, depth_or_array_layers: 1 },
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: OFFSCREEN_FORMAT,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING,
view_formats: &[],
});
let depth = device.create_texture(&wgpu::TextureDescriptor {
label: Some("l0_offscreen_depth"),
size: wgpu::Extent3d { width: w, height: h, depth_or_array_layers: 1 },
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: DEPTH_FORMAT,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
view_formats: &[],
});
let color_view = color.create_view(&Default::default());
let depth_view = depth.create_view(&Default::default());
let blit_bind = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("l0_offscreen_blit_bind"),
layout: &self.blit_bgl,
entries: &[
wgpu::BindGroupEntry { binding: 0, resource: wgpu::BindingResource::TextureView(&color_view) },
wgpu::BindGroupEntry { binding: 1, resource: wgpu::BindingResource::Sampler(&self.sampler) },
],
});
self.color_tex = Some(color);
self.color_view = Some(color_view);
self.depth_view = Some(depth_view);
self.blit_bind = Some(blit_bind);
self.size = (w, h);
}
pub fn color_view(&self) -> Option<&wgpu::TextureView> {
self.color_view.as_ref()
}
pub fn depth_view(&self) -> Option<&wgpu::TextureView> {
self.depth_view.as_ref()
}
pub fn ready(&self) -> bool {
self.blit_bind.is_some()
}
pub fn size(&self) -> (u32, u32) {
self.size
}
pub fn blit(&self, info: &egui::PaintCallbackInfo, render_pass: &mut wgpu::RenderPass<'static>) -> ScissorPx {
let Some(blit_bind) = &self.blit_bind else { return ScissorPx { x: 0, y: 0, w: 0, h: 0 } };
let s = gpu_scissor_px(info);
if s.w == 0 || s.h == 0 {
return s; }
render_pass.set_scissor_rect(s.x, s.y, s.w, s.h);
render_pass.set_pipeline(&self.blit_pipeline);
render_pass.set_bind_group(0, blit_bind, &[]);
render_pass.draw(0..3, 0..1);
s
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct ScissorPx {
pub x: u32,
pub y: u32,
pub w: u32,
pub h: u32,
}
pub fn gpu_scissor_px(info: &egui::PaintCallbackInfo) -> ScissorPx {
let vp = info.viewport_in_pixels();
let cl = info.clip_rect_in_pixels();
scissor_intersection(
(vp.left_px, vp.top_px, vp.width_px, vp.height_px),
(cl.left_px, cl.top_px, cl.width_px, cl.height_px),
info.screen_size_px,
)
}
pub fn scissor_intersection(
viewport: (i32, i32, i32, i32),
clip: (i32, i32, i32, i32),
screen: [u32; 2],
) -> ScissorPx {
let (vx, vy, vw, vh) = viewport;
let (cx, cy, cw, ch) = clip;
let left = vx.max(cx).max(0);
let top = vy.max(cy).max(0);
let right = (vx + vw).min(cx + cw).min(screen[0] as i32);
let bottom = (vy + vh).min(cy + ch).min(screen[1] as i32);
let w = (right - left).max(0) as u32;
let h = (bottom - top).max(0) as u32;
ScissorPx { x: left.max(0) as u32, y: top.max(0) as u32, w, h }
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn scissor_equals_widget_when_clip_covers_it() {
let s = scissor_intersection((50, 40, 200, 150), (50, 40, 200, 150), [640, 480]);
assert_eq!(s, ScissorPx { x: 50, y: 40, w: 200, h: 150 });
}
#[test]
fn scissor_pinned_to_widget_not_panel_clip() {
let s = scissor_intersection((300, 200, 120, 90), (0, 0, 640, 480), [640, 480]);
assert_eq!(s, ScissorPx { x: 300, y: 200, w: 120, h: 90 });
assert!(s.x + s.w <= 420 && s.y + s.h <= 290, "no ink outside the widget rect");
}
#[test]
fn scissor_intersection_clamped() {
let s = scissor_intersection((500, 400, 300, 300), (550, 420, 300, 300), [640, 480]);
assert_eq!(s, ScissorPx { x: 550, y: 420, w: 90, h: 60 });
assert!(s.x + s.w <= 640 && s.y + s.h <= 480);
}
#[test]
fn scissor_zero_area_when_disjoint() {
let s = scissor_intersection((0, 0, 100, 100), (300, 300, 50, 50), [640, 480]);
assert_eq!(s.w, 0);
}
#[test]
fn gpu_scissor_px_from_callback_info() {
let info = egui::PaintCallbackInfo {
viewport: egui::Rect::from_min_size(egui::pos2(40.0, 30.0), egui::vec2(200.0, 150.0)),
clip_rect: egui::Rect::from_min_size(egui::pos2(0.0, 0.0), egui::vec2(800.0, 600.0)),
pixels_per_point: 1.0,
screen_size_px: [800, 600],
};
let s = gpu_scissor_px(&info);
assert_eq!(s, ScissorPx { x: 40, y: 30, w: 200, h: 150 });
}
#[test]
fn offscreen_formats_are_the_depth_tested_pair() {
assert_eq!(DEPTH_FORMAT, TextureFormat::Depth32Float);
assert_eq!(OFFSCREEN_FORMAT, TextureFormat::Rgba8Unorm);
assert!(BLIT_WGSL.contains("blit_vs") && BLIT_WGSL.contains("blit_fs"));
}
}