#![cfg(not(target_arch = "wasm32"))]
use std::sync::{Arc, Mutex, MutexGuard, PoisonError};
use frust_engine::gpu::depth::{DEPTH_COMPARE, DEPTH_FORMAT, depth_texture_descriptor};
use frust_engine::gpu::pipelines::EnginePipeline;
use frust_engine::{EngineRenderer, EngineTarget, OutputAlpha};
use frust_gpu::{HeadlessTarget, PipelineCache, RenderPipelineDesc, ShaderLibrary, TierCaps};
use frust_scene::{Scene, SceneBuilder};
use kurbo::{Affine, Rect};
use peniko::{Brush, Color};
const SIZE: u32 = 64;
const FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Rgba8Unorm;
const BASE: Color = Color::from_rgb8(0, 0, 255);
const DRAWN: Color = Color::from_rgb8(0, 255, 0);
const BASE_PIXEL: [u8; 4] = [0, 0, 255, 255];
const DRAWN_PIXEL: [u8; 4] = [0, 255, 0, 255];
const NEAR_PIXEL: [u8; 4] = [255, 0, 0, 255];
const LEFT: &[u32] = &[4, 16, 28];
const RIGHT: &[u32] = &[36, 48, 60];
const ROWS: &[u32] = &[8, 32, 56];
const TOLERANCE: u8 = 2;
const WGSL: &str = r#"
const NEAR_Z: f32 = 0.3;
@vertex
fn vs_near(@builtin(vertex_index) index: u32) -> @builtin(position) vec4<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 vec4<f32>(corners[index], NEAR_Z, 1.0);
}
@fragment
fn fs_near() -> @location(0) vec4<f32> {
return vec4<f32>(1.0, 0.0, 0.0, 1.0);
}
"#;
const VERTICES: std::ops::Range<u32> = 0..6;
static RENDER_LOCK: Mutex<()> = Mutex::new(());
fn render_lock() -> MutexGuard<'static, ()> {
RENDER_LOCK.lock().unwrap_or_else(PoisonError::into_inner)
}
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, TierCaps) {
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-engine shared encoder adapter: {:?}",
adapter.get_info()
);
let caps = TierCaps::probe(&adapter);
let (device, queue) = adapter
.request_device(&wgpu::DeviceDescriptor {
label: Some("frust-engine shared encoder device"),
required_features: wgpu::Features::empty(),
required_limits: frust_gpu::test_device_limits(&adapter, &caps),
..Default::default()
})
.await
.expect("failed to create the device");
(device, queue, caps)
})
}
fn shared_depth(device: &wgpu::Device) -> wgpu::TextureView {
let texture = device.create_texture(&depth_texture_descriptor(SIZE, SIZE));
texture.create_view(&wgpu::TextureViewDescriptor::default())
}
fn near_pipeline(device: &wgpu::Device) -> (wgpu::RenderPipeline, PipelineCache) {
let mut library = ShaderLibrary::new();
let shader = library.insert_wgsl(device, "shared-encoder-near", WGSL);
let mut cache = PipelineCache::new(Arc::new(library), None);
let pipeline = cache
.get_or_create(
device,
&RenderPipelineDesc {
depth: Some(wgpu::DepthStencilState {
format: DEPTH_FORMAT,
depth_write_enabled: Some(true),
depth_compare: Some(DEPTH_COMPARE),
stencil: wgpu::StencilState::default(),
bias: wgpu::DepthBiasState::default(),
}),
..RenderPipelineDesc::new(shader, "vs_near", "fs_near", FORMAT)
},
)
.clone();
(pipeline, cache)
}
fn record_near(
encoder: &mut wgpu::CommandEncoder,
pipeline: &wgpu::RenderPipeline,
view: &wgpu::TextureView,
depth: &wgpu::TextureView,
first: bool,
) {
let color_load = if first {
wgpu::LoadOp::Clear(wgpu::Color {
r: 0.0,
g: 0.0,
b: 0.0,
a: 1.0,
})
} else {
wgpu::LoadOp::Load
};
let depth_load = if first {
wgpu::LoadOp::Clear(1.0)
} else {
wgpu::LoadOp::Load
};
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("shared encoder foreign depth pass"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view,
depth_slice: None,
resolve_target: None,
ops: wgpu::Operations {
load: color_load,
store: wgpu::StoreOp::Store,
},
})],
depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
view: depth,
depth_ops: Some(wgpu::Operations {
load: depth_load,
store: wgpu::StoreOp::Store,
}),
stencil_ops: None,
}),
timestamp_writes: None,
occlusion_query_set: None,
multiview_mask: None,
});
pass.set_pipeline(pipeline);
pass.draw(VERTICES, 0..1);
}
fn covering_rect() -> Scene {
let mut scene = Scene::new();
let mut builder = SceneBuilder::new(&mut scene);
builder.fill_rect(
Rect::new(0.0, 0.0, f64::from(SIZE), f64::from(SIZE)),
Brush::Solid(DRAWN),
);
scene
}
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 {
let actual = pixel(pixels, x, y);
let off = (0..4).any(|c| actual[c].abs_diff(expected[c]) > TOLERANCE);
assert!(
!off,
"{what}: pixel ({x}, {y}) is {actual:?}, expected {expected:?} +/- {TOLERANCE}"
);
}
}
}
fn one_encoder(frame_last: bool) -> Vec<u8> {
let _guard = render_lock();
let (device, queue, caps) = gpu();
let scope = device.push_error_scope(wgpu::ErrorFilter::Validation);
let target = HeadlessTarget::new(&device, SIZE, SIZE, FORMAT);
let depth = shared_depth(&device);
let (near, _cache) = near_pipeline(&device);
let mut renderer = EngineRenderer::new(&device, &caps, FORMAT, None)
.expect("the engine builds on this device");
renderer.set_depth_pre_cleared(frame_last);
assert_eq!(renderer.depth_pre_cleared(), frame_last);
let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("frust-engine shared encoder frame"),
});
if frame_last {
record_near(&mut encoder, &near, target.view(), &depth, true);
}
renderer
.encode(
&device,
&queue,
&mut encoder,
&covering_rect(),
EngineTarget {
view: target.view(),
format: FORMAT,
width: SIZE,
height: SIZE,
depth: Some(&depth),
output: OutputAlpha::Premultiplied,
},
BASE,
Affine::IDENTITY,
)
.expect("a well-formed frame encodes");
if !frame_last {
record_near(&mut encoder, &near, target.view(), &depth, false);
}
queue.submit([encoder.finish()]);
renderer.end_frame(&queue);
let error = drain_error_scope(&device, scope);
assert!(error.is_none(), "the frame raised {error:?}");
target.read_back(&device, &queue)
}
#[test]
fn the_shared_comparison_is_the_one_every_depth_testing_engine_pipeline_uses() {
for pipeline in [
EnginePipeline::StripOpaque,
EnginePipeline::StripDepthAlpha,
EnginePipeline::StripDepthDestOut,
] {
let state = pipeline
.depth()
.expect("a depth-testing variant carries depth state");
assert_eq!(state.format, DEPTH_FORMAT, "{pipeline:?}");
assert_eq!(state.depth_compare, Some(DEPTH_COMPARE), "{pipeline:?}");
}
}
#[test]
#[ignore = "requires a GPU (Vulkan/Metal); run with \
`cargo test -p frust-engine --test shared_encoder -- --ignored` \
(pin the adapter on a multi-GPU host with WGPU_BACKEND / WGPU_ADAPTER_NAME)"]
fn a_frame_is_occluded_where_an_earlier_pass_in_the_same_encoder_wrote_nearer_depth() {
let pixels = one_encoder(true);
assert_columns(
&pixels,
LEFT,
BASE_PIXEL,
"the frame's draw must be rejected where nearer depth was already written",
);
assert_columns(
&pixels,
RIGHT,
DRAWN_PIXEL,
"the frame's draw must reach the half the foreign pass left alone",
);
}
#[test]
#[ignore = "requires a GPU (Vulkan/Metal); run with \
`cargo test -p frust-engine --test shared_encoder -- --ignored` \
(pin the adapter on a multi-GPU host with WGPU_BACKEND / WGPU_ADAPTER_NAME)"]
fn a_pass_after_the_frame_draws_over_it_where_it_is_nearer() {
let pixels = one_encoder(false);
assert_columns(
&pixels,
LEFT,
NEAR_PIXEL,
"a nearer pass recorded after the frame must take the pixel",
);
assert_columns(
&pixels,
RIGHT,
DRAWN_PIXEL,
"the half that pass does not cover must keep the frame's draw",
);
}