#![cfg(not(target_arch = "wasm32"))]
use std::sync::{Arc, Mutex, MutexGuard, PoisonError};
use frust_engine::gpu::atlas::{AtlasArray, AtlasPageBuffers, AtlasRenderer, push_solid_strips};
use frust_engine::gpu::pipelines::atlas_strip_desc;
use frust_engine::gpu::{ATLAS_FORMAT, EngineShaders};
use frust_engine::{EngineDraw, SceneCompiler};
use frust_gpu::{HeadlessTarget, PipelineCache, ShaderLibrary, TierCaps};
use frust_scene::{Scene, SceneBuilder};
use glifo::atlas::PendingClearRect;
use glifo::{AtlasCommand, AtlasCommandRecorder, AtlasPaint, DrawSink, GlyphAtlas};
use kurbo::{Affine, BezPath, Rect};
use peniko::color::palette::css::{RED, WHITE};
use peniko::{Brush, Color};
use vello_common::paint::Paint;
const PAGE: u32 = 256;
const TEXEL_BYTES: u32 = 4;
const COPY_ROW_ALIGNMENT: u32 = 256;
const SLOT: Slot = Slot {
x: 64,
y: 96,
width: 40,
height: 48,
};
const OTHER_SLOT: Slot = Slot {
x: 160,
y: 24,
width: 40,
height: 48,
};
#[derive(Clone, Copy, Debug)]
struct Slot {
x: u32,
y: u32,
width: u32,
height: u32,
}
impl Slot {
fn contains(self, x: u32, y: u32) -> bool {
x >= self.x && x < self.x + self.width && y >= self.y && y < self.y + self.height
}
fn centre(self) -> (u32, u32) {
(self.x + self.width / 2, self.y + self.height / 2)
}
}
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 atlas render adapter: {:?}",
adapter.get_info()
);
let caps = TierCaps::probe(&adapter);
let (device, queue) = adapter
.request_device(&wgpu::DeviceDescriptor {
label: Some("frust-engine atlas render 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)
})
}
struct Harness {
device: wgpu::Device,
queue: wgpu::Queue,
pipeline: wgpu::RenderPipeline,
atlas: AtlasArray,
renderer: AtlasRenderer,
_cache: PipelineCache,
}
impl Harness {
fn new(layers: u32) -> Self {
let (device, queue, caps) = gpu();
let mut library = ShaderLibrary::new();
let shaders = EngineShaders::register(&mut library, &device);
let mut cache = PipelineCache::new(Arc::new(library), None);
let pipeline = cache
.get_or_create(&device, &atlas_strip_desc(&shaders))
.clone();
let atlas = AtlasArray::with_layers(&device, PAGE, PAGE, layers);
let renderer = AtlasRenderer::new(&device, &caps);
Self {
device,
queue,
pipeline,
atlas,
renderer,
_cache: cache,
}
}
fn replay(&mut self, glyphs: &mut GlyphAtlas) -> frust_engine::gpu::atlas::AtlasRenderReport {
self.renderer.render_pending(
&self.device,
&self.queue,
&self.pipeline,
&self.atlas,
glyphs,
frust_engine::FrameTimestamps::inert(),
lower_page,
)
}
fn read_layer(&self, layer: u32) -> Vec<u8> {
let bytes_per_row = (PAGE * TEXEL_BYTES).next_multiple_of(COPY_ROW_ALIGNMENT);
let buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
label: Some("atlas layer readback"),
size: u64::from(bytes_per_row) * u64::from(PAGE),
usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let mut encoder = self
.device
.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("atlas layer readback copy"),
});
encoder.copy_texture_to_buffer(
wgpu::TexelCopyTextureInfo {
texture: self.atlas.texture(),
mip_level: 0,
origin: wgpu::Origin3d {
x: 0,
y: 0,
z: layer,
},
aspect: wgpu::TextureAspect::All,
},
wgpu::TexelCopyBufferInfo {
buffer: &buffer,
layout: wgpu::TexelCopyBufferLayout {
offset: 0,
bytes_per_row: Some(bytes_per_row),
rows_per_image: Some(PAGE),
},
},
wgpu::Extent3d {
width: PAGE,
height: PAGE,
depth_or_array_layers: 1,
},
);
self.queue.submit([encoder.finish()]);
let slice = buffer.slice(..);
let (tx, rx) = std::sync::mpsc::channel();
slice.map_async(wgpu::MapMode::Read, move |result| {
let _ = tx.send(result);
});
self.device
.poll(wgpu::PollType::wait_indefinitely())
.expect("the readback poll must succeed");
rx.recv()
.expect("the readback channel must stay open")
.expect("the readback buffer must map");
let mapped = slice
.get_mapped_range()
.expect("the mapped readback range must be readable");
let row = (PAGE * TEXEL_BYTES) as usize;
let stride = bytes_per_row as usize;
let mut pixels = Vec::with_capacity(row * PAGE as usize);
for y in 0..PAGE as usize {
pixels.extend_from_slice(&mapped[y * stride..y * stride + row]);
}
drop(mapped);
buffer.unmap();
pixels
}
}
fn texel(pixels: &[u8], x: u32, y: u32) -> [u8; 4] {
let index = ((y * PAGE + x) * TEXEL_BYTES) as usize;
pixels[index..index + 4]
.try_into()
.expect("a page row holds four bytes per texel")
}
fn assert_blank_outside(pixels: &[u8], slots: &[Slot], what: &str) {
for y in 0..PAGE {
for x in 0..PAGE {
if slots.iter().any(|slot| slot.contains(x, y)) {
continue;
}
assert_eq!(
texel(pixels, x, y),
[0, 0, 0, 0],
"{what}: ({x}, {y}) is outside every slot but is not transparent"
);
}
}
}
fn covered(pixels: &[u8], slot: Slot) -> u32 {
let mut count = 0;
for y in slot.y..slot.y + slot.height {
for x in slot.x..slot.x + slot.width {
if texel(pixels, x, y)[3] != 0 {
count += 1;
}
}
}
count
}
fn outline(slot: Slot) -> BezPath {
let w = f64::from(slot.width);
let h = f64::from(slot.height);
let mut path = BezPath::new();
path.move_to((w * 0.5, 2.0));
path.curve_to((w - 2.0, h * 0.25), (w - 2.0, h * 0.75), (w * 0.5, h - 2.0));
path.curve_to((2.0, h * 0.75), (2.0, h * 0.25), (w * 0.5, 2.0));
path.close_path();
path
}
fn record_outline(glyphs: &mut GlyphAtlas, page: u32, slot: Slot, color: Color) {
let recorder = glyphs.recorder_for_page(page, PAGE as u16, PAGE as u16);
recorder.set_transform(Affine::translate((f64::from(slot.x), f64::from(slot.y))));
recorder.set_paint(AtlasPaint::Solid(color));
recorder.fill_path(&outline(slot));
}
fn lower_page(recorder: &AtlasCommandRecorder, buffers: &mut AtlasPageBuffers) -> bool {
let mut scene = Scene::new();
{
let mut builder = SceneBuilder::new(&mut scene);
let mut transform = Affine::IDENTITY;
let mut brush = Brush::Solid(Color::BLACK);
for command in &recorder.commands {
match command {
AtlasCommand::SetTransform(next) => transform = *next,
AtlasCommand::SetPaint(AtlasPaint::Solid(color)) => brush = Brush::Solid(*color),
AtlasCommand::FillPath(path) => {
builder.push_transform(transform);
builder.fill_path((**path).clone(), brush.clone());
builder.pop_transform();
}
AtlasCommand::FillRect(rect) => {
builder.push_transform(transform);
builder.fill_rect(*rect, brush.clone());
builder.pop_transform();
}
_ => return false,
}
}
}
let size = (PAGE as u16, PAGE as u16);
let mut compiler = SceneCompiler::new(size.0, size.1);
let Ok(frame) = compiler.compile(&scene, Affine::IDENTITY, size) else {
return false;
};
let strips = frame.strip_buf();
for draw in frame.draws() {
let EngineDraw {
paint,
depth,
strip_range,
} = draw;
let Paint::Solid(color) = paint else {
return false;
};
let Some(run) = strips.get(strip_range.clone()) else {
continue;
};
push_solid_strips(
run,
color.as_premul_rgba8().to_u32(),
*depth,
&mut buffers.instances,
);
}
buffers.alphas.extend_from_slice(frame.alphas());
true
}
#[test]
#[ignore = "requires a GPU (Vulkan/Metal); run with `cargo test -p frust-engine --test atlas_render -- --ignored`"]
fn a_replayed_outline_covers_its_slot_and_leaves_the_rest_of_the_page_blank() {
let _serialized = render_lock();
let mut harness = Harness::new(1);
let scope = harness
.device
.push_error_scope(wgpu::ErrorFilter::Validation);
let mut glyphs = GlyphAtlas::new();
record_outline(&mut glyphs, 0, SLOT, WHITE);
let report = harness.replay(&mut glyphs);
assert_eq!(report.pages, 1, "one dirty page, one pass");
assert_eq!(report.refused, 0, "an outline is a shape this tier lowers");
assert!(report.instances > 0, "the page drew no strips at all");
let pixels = harness.read_layer(0);
let error = drain_error_scope(&harness.device, scope);
assert!(error.is_none(), "the atlas pass raised {error:?}");
let (cx, cy) = SLOT.centre();
assert_eq!(
texel(&pixels, cx, cy),
[255, 255, 255, 255],
"the interior of a white outline must be fully covered"
);
assert!(
covered(&pixels, SLOT) > SLOT.width * SLOT.height / 4,
"the outline covered {} of {} slot texels — the replay drew almost nothing",
covered(&pixels, SLOT),
SLOT.width * SLOT.height
);
assert_blank_outside(&pixels, &[SLOT], "a single replayed outline");
}
#[test]
#[ignore = "requires a GPU (Vulkan/Metal); run with `cargo test -p frust-engine --test atlas_render -- --ignored`"]
fn a_clear_rect_zeroes_exactly_its_rectangle() {
let _serialized = render_lock();
let harness = Harness::new(1);
let scope = harness
.device
.push_error_scope(wgpu::ErrorFilter::Validation);
let filled = frust_engine::AtlasRegion {
layer: 0,
offset: [SLOT.x, SLOT.y],
size: [SLOT.width, SLOT.height],
};
assert!(
harness
.atlas
.write_region(&harness.queue, filled, &vec![0xFF_u8; filled.byte_len()])
);
let cleared = PendingClearRect {
page_index: 0,
x: (SLOT.x + 8) as u16,
y: (SLOT.y + 8) as u16,
width: 16,
height: 16,
};
assert!(
harness
.renderer
.clear_rect(&harness.queue, &harness.atlas, cleared)
);
let pixels = harness.read_layer(0);
let error = drain_error_scope(&harness.device, scope);
assert!(error.is_none(), "the clear raised {error:?}");
for y in 0..PAGE {
for x in 0..PAGE {
let inside_clear = x >= u32::from(cleared.x)
&& x < u32::from(cleared.x) + u32::from(cleared.width)
&& y >= u32::from(cleared.y)
&& y < u32::from(cleared.y) + u32::from(cleared.height);
let expected = if !inside_clear && SLOT.contains(x, y) {
[0xFF; 4]
} else {
[0; 4]
};
assert_eq!(
texel(&pixels, x, y),
expected,
"({x}, {y}): a clear must zero its own rectangle and nothing else"
);
}
}
}
#[test]
#[ignore = "requires a GPU (Vulkan/Metal); run with `cargo test -p frust-engine --test atlas_render -- --ignored`"]
fn a_clear_of_a_rectangle_lands_before_the_replay_that_reuses_it() {
let _serialized = render_lock();
let mut harness = Harness::new(1);
let scope = harness
.device
.push_error_scope(wgpu::ErrorFilter::Validation);
let filled = frust_engine::AtlasRegion {
layer: 0,
offset: [SLOT.x, SLOT.y],
size: [SLOT.width, SLOT.height],
};
assert!(
harness
.atlas
.write_region(&harness.queue, filled, &vec![0xFF_u8; filled.byte_len()])
);
assert!(harness.renderer.clear_rect(
&harness.queue,
&harness.atlas,
PendingClearRect {
page_index: 0,
x: SLOT.x as u16,
y: SLOT.y as u16,
width: SLOT.width as u16,
height: SLOT.height as u16,
}
));
let mut glyphs = GlyphAtlas::new();
record_outline(&mut glyphs, 0, SLOT, RED);
assert_eq!(harness.replay(&mut glyphs).pages, 1);
let pixels = harness.read_layer(0);
let error = drain_error_scope(&harness.device, scope);
assert!(
error.is_none(),
"the ordered clear and replay raised {error:?}"
);
let (cx, cy) = SLOT.centre();
assert_eq!(
texel(&pixels, cx, cy),
[255, 0, 0, 255],
"the replayed glyph must survive the clear that preceded it"
);
assert_eq!(
texel(&pixels, SLOT.x, SLOT.y),
[0, 0, 0, 0],
"the clear must have zeroed the parts of the slot the glyph does not cover"
);
assert_blank_outside(&pixels, &[SLOT], "a cleared and replayed slot");
}
#[test]
#[ignore = "requires a GPU (Vulkan/Metal); run with `cargo test -p frust-engine --test atlas_render -- --ignored`"]
fn two_layers_render_independently() {
let _serialized = render_lock();
let mut harness = Harness::new(2);
let scope = harness
.device
.push_error_scope(wgpu::ErrorFilter::Validation);
let mut glyphs = GlyphAtlas::new();
record_outline(&mut glyphs, 0, SLOT, WHITE);
record_outline(&mut glyphs, 1, OTHER_SLOT, RED);
let report = harness.replay(&mut glyphs);
assert_eq!(report.pages, 2, "one pass per dirty page");
assert_eq!(report.refused, 0);
let zero = harness.read_layer(0);
let one = harness.read_layer(1);
let error = drain_error_scope(&harness.device, scope);
assert!(error.is_none(), "the two-layer replay raised {error:?}");
let (cx, cy) = SLOT.centre();
assert_eq!(texel(&zero, cx, cy), [255, 255, 255, 255]);
assert_eq!(
covered(&zero, OTHER_SLOT),
0,
"layer zero must not carry layer one's glyph"
);
assert_blank_outside(&zero, &[SLOT], "layer zero");
let (ox, oy) = OTHER_SLOT.centre();
assert_eq!(texel(&one, ox, oy), [255, 0, 0, 255]);
assert_eq!(
covered(&one, SLOT),
0,
"layer one must not carry layer zero's glyph"
);
assert_blank_outside(&one, &[OTHER_SLOT], "layer one");
}
#[test]
#[ignore = "requires a GPU (Vulkan/Metal); run with `cargo test -p frust-engine --test atlas_render -- --ignored`"]
fn the_atlas_pass_never_touches_the_scene_target() {
let _serialized = render_lock();
let mut harness = Harness::new(1);
let scope = harness
.device
.push_error_scope(wgpu::ErrorFilter::Validation);
let target = HeadlessTarget::new(&harness.device, PAGE, PAGE, ATLAS_FORMAT);
let mut encoder = harness
.device
.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("scene target paint"),
});
drop(encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("scene target paint pass"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: target.view(),
depth_slice: None,
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Clear(wgpu::Color {
r: 0.0,
g: 1.0,
b: 0.0,
a: 1.0,
}),
store: wgpu::StoreOp::Store,
},
})],
depth_stencil_attachment: None,
timestamp_writes: None,
occlusion_query_set: None,
multiview_mask: None,
}));
harness.queue.submit([encoder.finish()]);
let before = target.read_back(&harness.device, &harness.queue);
let mut glyphs = GlyphAtlas::new();
record_outline(&mut glyphs, 0, SLOT, WHITE);
assert_eq!(harness.replay(&mut glyphs).pages, 1);
let after = target.read_back(&harness.device, &harness.queue);
let error = drain_error_scope(&harness.device, scope);
assert!(error.is_none(), "the atlas pass raised {error:?}");
assert_eq!(
before, after,
"the atlas pass wrote the scene's own target — its only attachment is an atlas layer"
);
assert!(covered(&harness.read_layer(0), SLOT) > 0);
}
#[test]
#[ignore = "requires a GPU (Vulkan/Metal); run with `cargo test -p frust-engine --test atlas_render -- --ignored`"]
fn a_page_the_lowering_declines_is_refused_rather_than_drawn() {
let _serialized = render_lock();
let mut harness = Harness::new(1);
let scope = harness
.device
.push_error_scope(wgpu::ErrorFilter::Validation);
let mut glyphs = GlyphAtlas::new();
{
let recorder = glyphs.recorder_for_page(0, PAGE as u16, PAGE as u16);
recorder.set_transform(Affine::translate((f64::from(SLOT.x), f64::from(SLOT.y))));
recorder.set_paint(AtlasPaint::Solid(WHITE));
recorder.push_blend_layer(peniko::BlendMode::default());
recorder.fill_rect(&Rect::new(0.0, 0.0, 8.0, 8.0));
recorder.pop_layer();
}
let report = harness.replay(&mut glyphs);
assert_eq!(report.pages, 0, "nothing may be drawn from a declined page");
assert_eq!(report.instances, 0);
assert_eq!(report.refused, 1, "the refusal must be counted, not silent");
let pixels = harness.read_layer(0);
let error = drain_error_scope(&harness.device, scope);
assert!(error.is_none(), "a declined page raised {error:?}");
assert_blank_outside(&pixels, &[], "a declined page");
}