use std::sync::Arc;
use frust_gpu::{
ColorAttachment, CommandBuffer, DepthAttachment, HeadlessTarget, PipelineCache,
RenderPipelineDesc, RenderTarget, ShaderLibrary, TierCaps, test_device_limits,
};
const SIZE: u32 = 64;
const FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Rgba8Unorm;
const DEPTH: wgpu::TextureFormat = wgpu::TextureFormat::Depth24Plus;
const FAR_PLANE: f32 = 1.0;
const BACKDROP: wgpu::Color = wgpu::Color {
r: 0.0,
g: 0.0,
b: 0.0,
a: 1.0,
};
const NEAR_PIXEL: [u8; 4] = [255, 0, 0, 255];
const FAR_PIXEL: [u8; 4] = [0, 255, 0, 255];
const LEFT: &[u32] = &[4, 16, 28];
const RIGHT: &[u32] = &[36, 48, 60];
const ROWS: &[u32] = &[8, 32, 56];
const WGSL: &str = r#"
const NEAR_Z: f32 = 0.3;
const FAR_Z: f32 = 0.9;
fn left_half(index: u32) -> vec2<f32> {
var corners = array<vec2<f32>, 6>(
vec2<f32>(-1.0, -1.0),
vec2<f32>( 0.0, -1.0),
vec2<f32>( 0.0, 1.0),
vec2<f32>(-1.0, -1.0),
vec2<f32>( 0.0, 1.0),
vec2<f32>(-1.0, 1.0)
);
return corners[index];
}
fn whole_target(index: u32) -> vec2<f32> {
var corners = array<vec2<f32>, 6>(
vec2<f32>(-1.0, -1.0),
vec2<f32>( 1.0, -1.0),
vec2<f32>( 1.0, 1.0),
vec2<f32>(-1.0, -1.0),
vec2<f32>( 1.0, 1.0),
vec2<f32>(-1.0, 1.0)
);
return corners[index];
}
@vertex
fn vs_near(@builtin(vertex_index) index: u32) -> @builtin(position) vec4<f32> {
return vec4<f32>(left_half(index), NEAR_Z, 1.0);
}
@vertex
fn vs_far(@builtin(vertex_index) index: u32) -> @builtin(position) vec4<f32> {
return vec4<f32>(whole_target(index), FAR_Z, 1.0);
}
@fragment
fn fs_near() -> @location(0) vec4<f32> {
return vec4<f32>(1.0, 0.0, 0.0, 1.0);
}
@fragment
fn fs_far() -> @location(0) vec4<f32> {
return vec4<f32>(0.0, 1.0, 0.0, 1.0);
}
"#;
const VERTICES: std::ops::Range<u32> = 0..6;
fn block_on<F: std::future::Future>(future: F) -> F::Output {
use std::task::{Context, Poll, Waker};
let waker = Waker::noop();
let mut cx = Context::from_waker(waker);
let mut future = std::pin::pin!(future);
loop {
match future.as_mut().poll(&mut cx) {
Poll::Ready(value) => return value,
Poll::Pending => std::thread::yield_now(),
}
}
}
fn drain_error_scope(device: &wgpu::Device, scope: wgpu::ErrorScopeGuard) -> Option<wgpu::Error> {
use std::task::{Context, Poll, Waker};
let waker = Waker::noop();
let mut cx = Context::from_waker(waker);
let mut future = std::pin::pin!(scope.pop());
loop {
match future.as_mut().poll(&mut cx) {
Poll::Ready(error) => return error,
Poll::Pending => {
let _ = device.poll(wgpu::PollType::wait_indefinitely());
}
}
}
}
fn gpu() -> (wgpu::Device, wgpu::Queue) {
block_on(async {
let instance =
wgpu::Instance::new(wgpu::InstanceDescriptor::new_without_display_handle_from_env());
let adapter = wgpu::util::initialize_adapter_from_env_or_default(&instance, None)
.await
.expect("no compatible GPU adapter");
println!("frust-gpu shared encoder adapter: {:?}", adapter.get_info());
let caps = TierCaps::probe(&adapter);
adapter
.request_device(&wgpu::DeviceDescriptor {
label: Some("frust-gpu shared encoder device"),
required_features: wgpu::Features::empty(),
required_limits: test_device_limits(&adapter, &caps),
..Default::default()
})
.await
.expect("failed to create the device")
})
}
fn depth_texture(device: &wgpu::Device, width: u32, height: u32) -> wgpu::TextureView {
let texture = device.create_texture(&wgpu::TextureDescriptor {
label: Some("frust-gpu shared encoder depth"),
size: wgpu::Extent3d {
width,
height,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: DEPTH,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
view_formats: &[],
});
texture.create_view(&wgpu::TextureViewDescriptor::default())
}
fn pipelines(device: &wgpu::Device) -> (wgpu::RenderPipeline, wgpu::RenderPipeline, PipelineCache) {
let mut library = ShaderLibrary::new();
let shader = library.insert_wgsl(device, "shared-encoder-depth", WGSL);
let mut cache = PipelineCache::new(Arc::new(library), None);
let depth = wgpu::DepthStencilState {
format: DEPTH,
depth_write_enabled: Some(true),
depth_compare: Some(wgpu::CompareFunction::LessEqual),
stencil: wgpu::StencilState::default(),
bias: wgpu::DepthBiasState::default(),
};
let near = cache
.get_or_create(
device,
&RenderPipelineDesc {
depth: Some(depth.clone()),
..RenderPipelineDesc::new(shader, "vs_near", "fs_near", FORMAT)
},
)
.clone();
let far = cache
.get_or_create(
device,
&RenderPipelineDesc {
depth: Some(depth),
..RenderPipelineDesc::new(shader, "vs_far", "fs_far", FORMAT)
},
)
.clone();
(near, far, cache)
}
fn render_through_one_encoder(
device: &wgpu::Device,
queue: &wgpu::Queue,
target: &HeadlessTarget,
depth: &wgpu::TextureView,
passes: [(&str, &wgpu::RenderPipeline); 2],
) -> Vec<u8> {
let mut buffer = CommandBuffer::new(device, Some("frust-gpu shared encoder"));
for (index, (label, pipeline)) in passes.iter().enumerate() {
let first = index == 0;
let render_target = RenderTarget {
color: vec![ColorAttachment {
view: target.view(),
load: if first {
wgpu::LoadOp::Clear(BACKDROP)
} else {
wgpu::LoadOp::Load
},
store: wgpu::StoreOp::Store,
}],
depth: Some(DepthAttachment {
view: depth,
load: if first {
wgpu::LoadOp::Clear(FAR_PLANE)
} else {
wgpu::LoadOp::Load
},
store: wgpu::StoreOp::Store,
}),
};
let mut pass = buffer.render_pass(&render_target, Some(label));
pass.set_pipeline(pipeline);
pass.draw(VERTICES, 0..1);
}
queue.submit([buffer.finish()]);
target.read_back(device, queue)
}
fn pixel(pixels: &[u8], x: u32, y: u32) -> [u8; 4] {
let at = ((y * SIZE + x) * 4) as usize;
pixels[at..at + 4]
.try_into()
.expect("a read-back row holds four bytes per pixel")
}
fn assert_columns(pixels: &[u8], columns: &[u32], expected: [u8; 4], what: &str) {
for &x in columns {
for &y in ROWS {
assert_eq!(
pixel(pixels, x, y),
expected,
"{what}: pixel ({x}, {y}) is wrong"
);
}
}
}
#[test]
#[ignore = "requires a GPU (Vulkan/Metal); run with \
`cargo test -p frust-gpu --test shared_encoder -- --ignored` \
(pin the adapter on a multi-GPU host with WGPU_BACKEND / WGPU_ADAPTER_NAME)"]
fn a_pass_is_rejected_where_an_earlier_pass_in_the_same_encoder_wrote_nearer_depth() {
let (device, queue) = gpu();
let scope = device.push_error_scope(wgpu::ErrorFilter::Validation);
let target = HeadlessTarget::new(&device, SIZE, SIZE, FORMAT);
let depth = depth_texture(&device, SIZE, SIZE);
let (near, far, _cache) = pipelines(&device);
let pixels = render_through_one_encoder(
&device,
&queue,
&target,
&depth,
[("near half", &near), ("whole target", &far)],
);
assert_columns(&pixels, LEFT, NEAR_PIXEL, "the near half must survive");
assert_columns(
&pixels,
RIGHT,
FAR_PIXEL,
"the far draw must reach the untouched half",
);
let error = drain_error_scope(&device, scope);
assert!(error.is_none(), "the frame raised {error:?}");
}
#[test]
#[ignore = "requires a GPU (Vulkan/Metal); run with \
`cargo test -p frust-gpu --test shared_encoder -- --ignored` \
(pin the adapter on a multi-GPU host with WGPU_BACKEND / WGPU_ADAPTER_NAME)"]
fn a_pass_drawing_nearer_than_what_the_encoder_already_holds_takes_the_pixel_back() {
let (device, queue) = gpu();
let scope = device.push_error_scope(wgpu::ErrorFilter::Validation);
let target = HeadlessTarget::new(&device, SIZE, SIZE, FORMAT);
let depth = depth_texture(&device, SIZE, SIZE);
let (near, far, _cache) = pipelines(&device);
let pixels = render_through_one_encoder(
&device,
&queue,
&target,
&depth,
[("whole target", &far), ("near half", &near)],
);
assert_columns(&pixels, LEFT, NEAR_PIXEL, "the near half must win");
assert_columns(
&pixels,
RIGHT,
FAR_PIXEL,
"the half the near pass does not cover must keep the far draw",
);
let error = drain_error_scope(&device, scope);
assert!(error.is_none(), "the frame raised {error:?}");
}
#[test]
#[ignore = "requires a GPU (Vulkan/Metal); run with \
`cargo test -p frust-gpu --test shared_encoder -- --ignored` \
(pin the adapter on a multi-GPU host with WGPU_BACKEND / WGPU_ADAPTER_NAME)"]
fn a_depth_view_whose_extent_differs_from_the_colour_target_is_refused() {
let (device, queue) = gpu();
let target = HeadlessTarget::new(&device, SIZE, SIZE, FORMAT);
let depth = depth_texture(&device, SIZE / 2, SIZE / 2);
let (near, _far, _cache) = pipelines(&device);
let scope = device.push_error_scope(wgpu::ErrorFilter::Validation);
let mut buffer = CommandBuffer::new(&device, Some("frust-gpu mismatched depth"));
{
let render_target = RenderTarget {
color: vec![ColorAttachment {
view: target.view(),
load: wgpu::LoadOp::Clear(BACKDROP),
store: wgpu::StoreOp::Store,
}],
depth: Some(DepthAttachment {
view: &depth,
load: wgpu::LoadOp::Clear(FAR_PLANE),
store: wgpu::StoreOp::Store,
}),
};
let mut pass = buffer.render_pass(&render_target, Some("mismatched extents"));
pass.set_pipeline(&near);
pass.draw(VERTICES, 0..1);
}
drop(buffer.finish());
let error = drain_error_scope(&device, scope);
assert!(
error.is_some(),
"a depth attachment sized differently from the colour target must be refused"
);
drop(queue);
}