use repose_core::{Brush, Color, Px, Rect, Scene, SceneNode};
use repose_render_wgpu::{
Callback, CallbackRenderPass, CallbackResources, DepthComposite, ScreenDescriptor,
WgpuCallback, offscreen::OffscreenRenderer,
};
const SHADER: &str = r#"
struct Camera {
view_proj: mat4x4<f32>,
};
@group(0) @binding(0) var<uniform> camera: Camera;
struct VsOut {
@builtin(position) pos: vec4<f32>,
@location(0) color: vec3<f32>,
};
@vertex
fn vs_main(@location(0) pos: vec3<f32>, @location(1) color: vec3<f32>) -> VsOut {
var out: VsOut;
out.pos = camera.view_proj * vec4<f32>(pos, 1.0);
out.color = color;
return out;
}
@fragment
fn fs_main(@location(0) color: vec3<f32>) -> @location(0) vec4<f32> {
return vec4<f32>(color, 1.0);
}
"#;
#[repr(C)]
#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
struct Vert {
pos: [f32; 3],
color: [f32; 3],
}
struct Probe;
impl WgpuCallback for Probe {
fn prepare(
&self,
device: &wgpu::Device,
queue: &wgpu::Queue,
encoder: &mut wgpu::CommandEncoder,
screen: &ScreenDescriptor,
resources: &mut CallbackResources,
) -> Vec<wgpu::CommandBuffer> {
if let Err(e) =
DepthComposite::get(resources).ensure(device, screen, "test.composite", 64, 64)
{
log::warn!("depth composite target unavailable: {e:#}");
}
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("test-composite-scene"),
source: wgpu::ShaderSource::Wgsl(SHADER.into()),
});
let camera = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("test-composite-cam"),
size: 64,
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let ident: [[f32; 4]; 4] = [
[1.0, 0.0, 0.0, 0.0],
[0.0, 1.0, 0.0, 0.0],
[0.0, 0.0, 1.0, 0.0],
[0.0, 0.0, 0.0, 1.0],
];
queue.write_buffer(&camera, 0, bytemuck::cast_slice(&[ident]));
let cam_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("test-composite-bgl"),
entries: &[wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::VERTEX,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
}],
});
let cam_bind = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("test-composite-bg"),
layout: &cam_layout,
entries: &[wgpu::BindGroupEntry {
binding: 0,
resource: camera.as_entire_binding(),
}],
});
let pipe_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("test-composite-pl"),
bind_group_layouts: &[Some(&cam_layout)],
immediate_size: 0,
});
let depth_format = DepthComposite::get(resources)
.depth_format("test.composite")
.unwrap_or(wgpu::TextureFormat::Depth24PlusStencil8);
let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("test-composite-pipe"),
layout: Some(&pipe_layout),
vertex: wgpu::VertexState {
module: &shader,
entry_point: Some("vs_main"),
buffers: &[Some(wgpu::VertexBufferLayout {
array_stride: size_of::<Vert>() as u64,
step_mode: wgpu::VertexStepMode::Vertex,
attributes: &[
wgpu::VertexAttribute {
format: wgpu::VertexFormat::Float32x3,
offset: 0,
shader_location: 0,
},
wgpu::VertexAttribute {
format: wgpu::VertexFormat::Float32x3,
offset: 12,
shader_location: 1,
},
],
})],
compilation_options: Default::default(),
},
fragment: Some(wgpu::FragmentState {
module: &shader,
entry_point: Some("fs_main"),
targets: &[Some(wgpu::ColorTargetState {
format: screen.target_format,
blend: Some(wgpu::BlendState::REPLACE),
write_mask: wgpu::ColorWrites::ALL,
})],
compilation_options: Default::default(),
}),
primitive: wgpu::PrimitiveState {
topology: wgpu::PrimitiveTopology::TriangleList,
..Default::default()
},
depth_stencil: Some(wgpu::DepthStencilState {
format: depth_format,
depth_write_enabled: Some(true),
depth_compare: Some(wgpu::CompareFunction::Less),
stencil: wgpu::StencilState::default(),
bias: wgpu::DepthBiasState::default(),
}),
multisample: wgpu::MultisampleState {
count: 1,
mask: !0,
alpha_to_coverage_enabled: false,
},
multiview_mask: None,
cache: None,
});
let quad = |z: f32, c: [f32; 3]| {
[
Vert {
pos: [-1.0, -1.0, z],
color: c,
},
Vert {
pos: [1.0, -1.0, z],
color: c,
},
Vert {
pos: [1.0, 1.0, z],
color: c,
},
Vert {
pos: [-1.0, -1.0, z],
color: c,
},
Vert {
pos: [1.0, 1.0, z],
color: c,
},
Vert {
pos: [-1.0, 1.0, z],
color: c,
},
]
};
let verts: Vec<Vert> = quad(0.9, [1.0, 0.0, 0.0])
.into_iter()
.chain(quad(0.1, [0.0, 1.0, 0.0]))
.collect();
let vbuf = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("test-composite-verts"),
size: (verts.len() * size_of::<Vert>()) as u64,
usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
queue.write_buffer(&vbuf, 0, bytemuck::cast_slice(&verts));
let composite = DepthComposite::get(resources);
if let Some(mut pass) =
composite.begin_scene("test.composite", encoder, [0.0, 0.0, 0.0, 1.0])
{
pass.set_pipeline(&pipeline);
pass.set_bind_group(0, &cam_bind, &[]);
pass.set_vertex_buffer(0, vbuf.slice(..));
pass.draw(0..12, 0..1);
}
Vec::new()
}
fn paint(
&self,
_info: repose_core::PaintCallbackInfo,
rpass: &mut CallbackRenderPass<'_, '_>,
resources: &CallbackResources,
) {
if let Some(composite) = resources.get::<DepthComposite>() {
composite.blit("test.composite", rpass);
}
}
}
#[test]
fn depth_composite_resolves_near_over_far() {
let mut renderer = match OffscreenRenderer::new_blocking(64, 64, 1) {
Ok(r) => r,
Err(e) => {
eprintln!("SKIP composite test (no GPU): {e}");
return;
}
};
let scene = Scene {
clear_color: Color::from_rgba(0, 0, 0, 255),
nodes: vec![SceneNode::Callback {
rect: Rect {
x: 0.0,
y: 0.0,
w: 64.0,
h: 64.0,
},
payload: Callback::new(Probe),
}],
};
let px = renderer
.render_rgba(&scene, Some([0.0, 0.0, 0.0, 1.0]))
.expect("render");
let at = |x: u32, y: u32| -> [u8; 4] {
let i = ((y * 64 + x) * 4) as usize;
[px[i], px[i + 1], px[i + 2], px[i + 3]]
};
assert_eq!(at(32, 32), [0, 255, 0, 255], "near quad wins by depth");
assert_eq!(at(4, 4), [0, 255, 0, 255], "near quad fills the view");
}
struct TransparentProbe;
impl WgpuCallback for TransparentProbe {
fn prepare(
&self,
device: &wgpu::Device,
_queue: &wgpu::Queue,
encoder: &mut wgpu::CommandEncoder,
screen: &ScreenDescriptor,
resources: &mut CallbackResources,
) -> Vec<wgpu::CommandBuffer> {
if let Err(e) =
DepthComposite::get(resources).ensure(device, screen, "test.transparent", 64, 64)
{
log::warn!("depth composite target unavailable: {e:#}");
return Vec::new();
}
let composite = DepthComposite::get(resources);
if let Some(pass) = composite.begin_scene("test.transparent", encoder, [0.0; 4]) {
drop(pass);
}
Vec::new()
}
fn paint(
&self,
_info: repose_core::PaintCallbackInfo,
rpass: &mut CallbackRenderPass<'_, '_>,
resources: &CallbackResources,
) {
if let Some(composite) = resources.get::<DepthComposite>() {
composite.blit("test.transparent", rpass);
}
}
}
struct ClobberingProbe;
impl WgpuCallback for ClobberingProbe {
fn paint(
&self,
_info: repose_core::PaintCallbackInfo,
rpass: &mut CallbackRenderPass<'_, '_>,
_resources: &CallbackResources,
) {
rpass.set_viewport(0.0, 0.0, 1.0, 1.0, 0.0, 1.0);
rpass.set_scissor_rect(0, 0, 1, 1);
rpass.set_stencil_reference(3);
}
}
fn full_rect() -> SceneNode {
SceneNode::Rect {
rect: Rect {
x: 0.0,
y: 0.0,
w: 64.0,
h: 64.0,
},
brush: Brush::Solid(Color::from_rgba(0, 0, 255, 255)),
radius: [Px::ZERO; 4],
}
}
fn try_renderer() -> Option<OffscreenRenderer> {
match OffscreenRenderer::new_blocking(64, 64, 1) {
Ok(r) => Some(r),
Err(e) => {
eprintln!("SKIP composite test (no GPU): {e}");
None
}
}
}
fn pixel(px: &[u8], x: u32, y: u32) -> [u8; 4] {
let i = ((y * 64 + x) * 4) as usize;
[px[i], px[i + 1], px[i + 2], px[i + 3]]
}
#[test]
fn transparent_depth_composite_keeps_the_ui_beneath() {
let Some(mut renderer) = try_renderer() else {
return;
};
let scene = Scene {
clear_color: Color::from_rgba(0, 0, 0, 255),
nodes: vec![
full_rect(),
SceneNode::Callback {
rect: Rect {
x: 0.0,
y: 0.0,
w: 64.0,
h: 64.0,
},
payload: Callback::new(TransparentProbe),
},
],
};
let px = renderer
.render_rgba(&scene, Some([0.0, 0.0, 0.0, 1.0]))
.expect("render");
assert_eq!(
pixel(&px, 32, 32),
[0, 0, 255, 255],
"the blue UI under a transparent viewport must survive the blit"
);
}
#[test]
fn callback_state_is_restored_for_later_draws() {
let Some(mut renderer) = try_renderer() else {
return;
};
let scene = Scene {
clear_color: Color::from_rgba(0, 0, 0, 255),
nodes: vec![
SceneNode::Callback {
rect: Rect {
x: 0.0,
y: 0.0,
w: 8.0,
h: 8.0,
},
payload: Callback::new(ClobberingProbe),
},
SceneNode::Rect {
rect: Rect {
x: 0.0,
y: 0.0,
w: 64.0,
h: 64.0,
},
brush: Brush::Solid(Color::from_rgba(0, 255, 0, 255)),
radius: [Px::ZERO; 4],
},
],
};
let px = renderer
.render_rgba(&scene, Some([0.0, 0.0, 0.0, 1.0]))
.expect("render");
for (x, y) in [(0, 0), (16, 16), (32, 32), (48, 48), (56, 8), (8, 56)] {
assert_eq!(
pixel(&px, x, y),
[0, 255, 0, 255],
"rect after the callback must fill the frame at ({x},{y})"
);
}
}
struct PanickingProbe;
impl WgpuCallback for PanickingProbe {
fn paint(
&self,
_info: repose_core::PaintCallbackInfo,
rpass: &mut CallbackRenderPass<'_, '_>,
_resources: &CallbackResources,
) {
rpass.set_viewport(0.0, 0.0, 1.0, 1.0, 0.0, 1.0);
rpass.set_scissor_rect(0, 0, 1, 1);
panic!("callback failure");
}
}
#[test]
fn panicking_callback_does_not_break_the_rest_of_the_frame() {
let Some(mut renderer) = try_renderer() else {
return;
};
let previous_hook = std::panic::take_hook();
std::panic::set_hook(Box::new(|_| {}));
let rendered = renderer.render_rgba(
&Scene {
clear_color: Color::from_rgba(0, 0, 0, 255),
nodes: vec![
SceneNode::Callback {
rect: Rect {
x: 0.0,
y: 0.0,
w: 8.0,
h: 8.0,
},
payload: Callback::new(PanickingProbe),
},
SceneNode::Rect {
rect: Rect {
x: 0.0,
y: 0.0,
w: 64.0,
h: 64.0,
},
brush: Brush::Solid(Color::from_rgba(0, 255, 0, 255)),
radius: [Px::ZERO; 4],
},
],
},
Some([0.0, 0.0, 0.0, 1.0]),
);
std::panic::set_hook(previous_hook);
let px = rendered.expect("render");
for (x, y) in [(16, 16), (32, 32), (48, 48)] {
assert_eq!(
pixel(&px, x, y),
[0, 255, 0, 255],
"rect after a panicking callback must still fill ({x},{y})"
);
}
}