#![cfg(feature = "wgpu")]
#![allow(dead_code)]
use super::compare::{LumaImageBuffer, compare_image_with_threshold};
use super::{dist_to_color, dump_image, to_dyn_image};
use klyff_msdf::{
AtlasRegionSize, GpuAtlasRegion, MsdfGenerator, MtsdfGpuWriter, OutlineProvider,
SkrifaOutlineProvider, median,
};
use skrifa::MetadataProvider;
pub struct GpuCtx {
pub device: wgpu::Device,
pub queue: wgpu::Queue,
}
impl GpuCtx {
pub fn new() -> Option<Self> {
let instance = wgpu::Instance::default();
let adapter = pollster::block_on(instance.request_adapter(&wgpu::RequestAdapterOptions {
power_preference: wgpu::PowerPreference::HighPerformance,
force_fallback_adapter: false,
compatible_surface: None,
}))
.ok()?;
let (device, queue) = pollster::block_on(adapter.request_device(&wgpu::DeviceDescriptor {
label: Some("klyff_msdf test device"),
required_features: wgpu::Features::empty(),
required_limits: wgpu::Limits::default(),
memory_hints: wgpu::MemoryHints::Performance,
trace: wgpu::Trace::Off,
experimental_features: wgpu::ExperimentalFeatures::disabled(),
}))
.ok()?;
Some(Self { device, queue })
}
}
pub fn compare_font_render_gpu(
gpu: &GpuCtx,
writer: &mut MtsdfGpuWriter,
name: &str,
font_data: &[u8],
character: char,
ref_img_path: &std::path::Path,
output: &std::path::Path,
) {
let font_ref = skrifa::FontRef::new(font_data).expect("Can read font");
let glyph_id = font_ref
.charmap()
.map(character)
.unwrap_or_else(|| panic!("{character} is not valid character in font"));
let skrifa_provider = SkrifaOutlineProvider::new(font_ref.clone(), glyph_id);
const PPEM: f32 = 100.;
const PADDING_X: usize = 10;
const PADDING_Y: usize = 10;
let mut generator = MsdfGenerator::new();
let em_size = skrifa_provider.length_per_em();
let outline = match generator.read_glyph(&skrifa_provider) {
Ok(outline) => outline,
Err(klyff_msdf::ReadGlyphOutlineError::EmptyGlyph)
| Err(klyff_msdf::ReadGlyphOutlineError::ShouldRasterize) => {
return;
}
Err(klyff_msdf::ReadGlyphOutlineError::NotFound) => {
panic!("Glyph not found for {glyph_id}");
}
Err(klyff_msdf::ReadGlyphOutlineError::Other(e)) => {
panic!("Error encountered with glyph {glyph_id}: {e}");
}
};
let size = AtlasRegionSize {
inner_width: (outline.width_em() * PPEM).round() as usize,
inner_height: (outline.height_em() * PPEM).round() as usize,
padding_x: PADDING_X,
padding_y: PADDING_Y,
};
let width = size.total_width();
let height = size.total_height();
let inner_width = size.inner_width;
let inner_height = size.inner_height;
let texture = gpu.device.create_texture(&wgpu::TextureDescriptor {
label: Some("klyff_msdf test atlas"),
size: wgpu::Extent3d {
width: width as u32,
height: height as u32,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: wgpu::TextureFormat::Rgba8Unorm,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT
| wgpu::TextureUsages::COPY_SRC
| wgpu::TextureUsages::TEXTURE_BINDING,
view_formats: &[],
});
let region = GpuAtlasRegion {
layer: 0,
min_x: 0,
min_y: 0,
width: width as u32,
height: height as u32,
padding_x: PADDING_X as u32,
padding_y: PADDING_Y as u32,
};
writer.add_glyph(outline, region);
let bytes_per_pixel = 4u32;
let unaligned = width as u32 * bytes_per_pixel;
let padded_bytes_per_row = (unaligned + 255) & !255;
let readback_size = padded_bytes_per_row as u64 * height as u64;
let readback = gpu.device.create_buffer(&wgpu::BufferDescriptor {
label: Some("klyff_msdf test readback"),
size: readback_size,
usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
mapped_at_creation: false,
});
let mut encoder = gpu
.device
.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("klyff_msdf test encoder"),
});
writer.write_glyphs(&gpu.device, &gpu.queue, &mut encoder, &texture);
encoder.copy_texture_to_buffer(
wgpu::TexelCopyTextureInfo {
texture: &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 as u32),
},
},
wgpu::Extent3d {
width: width as u32,
height: height as u32,
depth_or_array_layers: 1,
},
);
gpu.queue.submit([encoder.finish()]);
let slice = readback.slice(..);
slice.map_async(wgpu::MapMode::Read, |_| {});
gpu.device
.poll(wgpu::PollType::wait_indefinitely())
.unwrap();
let mapped = slice.get_mapped_range();
let mut tight = vec![0u8; width * height * 4];
for y in 0..height {
let src = y * padded_bytes_per_row as usize;
let dst = y * width * 4;
tight[dst..dst + width * 4].copy_from_slice(&mapped[src..src + width * 4]);
}
drop(mapped);
readback.unmap();
let mtsdf_source_image =
image::RgbaImage::from_raw(width as u32, height as u32, tight).expect("Valid image data");
assert_ne!(mtsdf_source_image.width(), 0);
assert_ne!(mtsdf_source_image.height(), 0);
let (mtsdf_pseudo_content, mtsdf_true_content) = {
let mut pseudo = vec![0; inner_width * inner_height];
let mut true_dist = vec![0; inner_width * inner_height];
for (j, y) in (size.padding_y..size.padding_y + inner_height).enumerate() {
for (i, x) in (size.padding_x..size.padding_x + inner_width).enumerate() {
let [r, g, b, a] = image::imageops::sample_bilinear(
&mtsdf_source_image,
(x as f32 + 0.5) / (width as f32),
(y as f32 + 0.5) / (height as f32),
)
.expect("Valid pixel")
.0;
let m = median(r, g, b);
pseudo[j * inner_width + i] = dist_to_color(m, em_size);
true_dist[j * inner_width + i] = dist_to_color(a, em_size);
}
}
(pseudo, true_dist)
};
let w = inner_width as u32;
let h = inner_height as u32;
let mtsdf_pseudo_cmp_image = LumaImageBuffer {
width: inner_width,
height: inner_height,
buffer: &mtsdf_pseudo_content,
};
let mtsdf_true_cmp_image = LumaImageBuffer {
width: inner_width,
height: inner_height,
buffer: &mtsdf_true_content,
};
let Some(ref_image) = std::fs::File::open(ref_img_path)
.map(std::io::BufReader::new)
.ok()
.and_then(|r| image::load(r, image::ImageFormat::Png).ok())
else {
println!(
"No existing reference image at {}. Skipping GPU comparison.",
ref_img_path.display()
);
return;
};
let ref_image = LumaImageBuffer {
width: inner_width,
height: inner_height,
buffer: &ref_image
.resize(w, h, image::imageops::FilterType::Triangle)
.to_luma8(),
};
const GPU_MAX_DEVIATION_RATIO: f32 = 0.05;
let (mtsdf_pseudo_valid, mtsdf_pseudo_deviation) =
compare_image_with_threshold(&mtsdf_pseudo_cmp_image, &ref_image, GPU_MAX_DEVIATION_RATIO);
let (mtsdf_true_valid, mtsdf_true_deviation) =
compare_image_with_threshold(&mtsdf_true_cmp_image, &ref_image, GPU_MAX_DEVIATION_RATIO);
if !mtsdf_pseudo_valid || !mtsdf_true_valid {
dump_image(
output,
name,
glyph_id.to_u32(),
&[
image::DynamicImage::ImageRgba8(mtsdf_source_image),
to_dyn_image(mtsdf_pseudo_content, w, h),
to_dyn_image(mtsdf_true_content, w, h),
],
);
panic!(
"GPU regression with rendering glyph {glyph_id}. Check dumped images at {}.
Deviation ratio: MTSDF Pseudo = {mtsdf_pseudo_deviation}, MTSDF True = {mtsdf_true_deviation}",
output.display()
)
}
}