use std::path::Path;
use hydrolysis::{HydrolysisRenderer, OffscreenGpuContext, OffscreenWindow, PlatformWindow};
use waterui::graphics::SceneViewMergeToParent;
use waterui_core::{AnyView, Environment, View};
use crate::artifacts::{CapturedSnapshot, TestArtifacts};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Snapshot {
pub width: u32,
pub height: u32,
pub rgba8: Vec<u8>,
}
impl Snapshot {
pub fn save_png(&self, path: impl AsRef<Path>) -> image::ImageResult<()> {
let path = path.as_ref();
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent).map_err(image::ImageError::IoError)?;
}
let image = image::RgbaImage::from_raw(self.width, self.height, self.rgba8.clone())
.expect("Snapshot::save_png: rgba buffer shape must match dimensions");
image.save(path)
}
}
#[derive(Debug)]
pub struct TestHost {
env: Environment,
gpu: OffscreenGpuContext,
width: u32,
height: u32,
}
impl TestHost {
#[must_use]
pub fn new(env: Environment, width: u32, height: u32) -> Self {
Self {
env,
gpu: OffscreenGpuContext::new_for_tests_blocking(),
width,
height,
}
}
pub fn render<V: View>(&self, view: V) -> Snapshot {
let mut platform = OffscreenWindow::on_context(
self.gpu.clone(),
self.width.max(1),
self.height.max(1),
wgpu::TextureFormat::Rgba8Unorm,
);
let mut renderer = {
let surface = platform.surface();
HydrolysisRenderer::new(surface.device())
};
let bounds = vello::kurbo::Rect::new(
0.0,
0.0,
f64::from(self.width.max(1)),
f64::from(self.height.max(1)),
);
let surface = platform.surface();
renderer.set_frame_resources(surface.adapter(), surface.device(), surface.queue());
renderer.reset_scene();
renderer.begin_rebuild_frame();
let env = self.env.clone().extending(SceneViewMergeToParent);
renderer.capture_window_tree(
AnyView::new(view),
&env,
bounds,
vello::kurbo::Affine::IDENTITY,
vello::kurbo::Affine::IDENTITY,
);
renderer.finish_rebuild_frame();
let frame = surface
.acquire()
.expect("waterui-testing failed to acquire offscreen frame");
renderer.render_scene_to_texture(hydrolysis::HydrolysisRenderTarget {
adapter: surface.adapter(),
device: surface.device(),
queue: surface.queue(),
texture: Some(frame.texture()),
view: frame.view(),
format: surface.format(),
width: self.width.max(1),
height: self.height.max(1),
base_color: vello::peniko::Color::TRANSPARENT,
});
let rgba8 = readback_texture_rgba8(
surface.device(),
surface.queue(),
frame.texture(),
self.width.max(1),
self.height.max(1),
);
renderer.clear_frame_resources();
surface.present(frame);
drop(renderer);
drop(platform);
self.gpu.reclaim();
Snapshot {
width: self.width.max(1),
height: self.height.max(1),
rgba8,
}
}
pub fn capture_snapshot_with<V: View>(
&self,
view: V,
artifacts: &TestArtifacts,
case: impl AsRef<str>,
stage: impl AsRef<str>,
) -> CapturedSnapshot {
artifacts.capture_snapshot(case, stage, self.render(view))
}
#[must_use]
pub fn artifacts(&self, suite: impl AsRef<str>) -> TestArtifacts {
TestArtifacts::new(suite.as_ref())
}
pub fn capture_snapshot<V: View>(
&self,
view: V,
suite: impl AsRef<str>,
case: impl AsRef<str>,
stage: impl AsRef<str>,
) -> CapturedSnapshot {
let artifacts = self.artifacts(suite);
artifacts.capture_snapshot(case, stage, self.render(view))
}
}
pub fn readback_texture_rgba8(
device: &wgpu::Device,
queue: &wgpu::Queue,
texture: &wgpu::Texture,
width: u32,
height: u32,
) -> Vec<u8> {
const BYTES_PER_PIXEL: u32 = 4;
const COPY_ALIGNMENT: u32 = wgpu::COPY_BYTES_PER_ROW_ALIGNMENT;
let unpadded_bytes_per_row = width * BYTES_PER_PIXEL;
let padded_bytes_per_row = unpadded_bytes_per_row.div_ceil(COPY_ALIGNMENT) * COPY_ALIGNMENT;
let readback = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("waterui-testing-readback"),
size: u64::from(padded_bytes_per_row) * u64::from(height),
usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
mapped_at_creation: false,
});
let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("waterui-testing-readback-encoder"),
});
encoder.copy_texture_to_buffer(
wgpu::TexelCopyTextureInfo {
texture,
mip_level: 0,
origin: wgpu::Origin3d::ZERO,
aspect: wgpu::TextureAspect::All,
},
wgpu::TexelCopyBufferInfo {
buffer: &readback,
layout: wgpu::TexelCopyBufferLayout {
offset: 0,
bytes_per_row: Some(padded_bytes_per_row),
rows_per_image: Some(height),
},
},
wgpu::Extent3d {
width,
height,
depth_or_array_layers: 1,
},
);
queue.submit([encoder.finish()]);
let slice = readback.slice(..);
let (sender, receiver) = std::sync::mpsc::channel();
slice.map_async(wgpu::MapMode::Read, move |result| {
sender
.send(result)
.expect("waterui-testing readback channel receiver dropped");
});
let _ = device.poll(wgpu::PollType::wait_indefinitely());
receiver
.recv()
.expect("waterui-testing readback callback dropped")
.expect("waterui-testing failed to map readback buffer");
let mapped = slice.get_mapped_range();
let mut pixels = vec![0_u8; (width * height * BYTES_PER_PIXEL) as usize];
for row in 0..height as usize {
let source_start = row * padded_bytes_per_row as usize;
let source_end = source_start + unpadded_bytes_per_row as usize;
let destination_start = row * unpadded_bytes_per_row as usize;
let destination_end = destination_start + unpadded_bytes_per_row as usize;
pixels[destination_start..destination_end]
.copy_from_slice(&mapped[source_start..source_end]);
}
drop(mapped);
readback.unmap();
pixels
}