#![allow(unsafe_code)]
use crate::geometry::{PhysicalSize, Point, Rect, ScaleFactor, Size};
use crate::render::{Color, DrawCmd, Font, Pixmap, Scene, SoftwareRenderer};
#[cfg(all(target_os = "linux", feature = "gl"))]
mod gl;
#[cfg(all(target_os = "linux", feature = "vk"))]
mod vulkan;
#[cfg(all(target_os = "windows", feature = "d3d"))]
mod d3d;
#[cfg(all(target_os = "macos", feature = "mtl"))]
mod metal;
pub fn vulkan_devices() -> Result<Vec<String>, String> {
#[cfg(all(target_os = "linux", feature = "vk"))]
{
vulkan::devices()
}
#[cfg(not(all(target_os = "linux", feature = "vk")))]
{
Err("stipple::gpu: built without the `vk` feature (Linux-only Vulkan FFI)".to_string())
}
}
pub fn vulkan_init_device() -> Result<String, String> {
#[cfg(all(target_os = "linux", feature = "vk"))]
{
vulkan::init_device()
}
#[cfg(not(all(target_os = "linux", feature = "vk")))]
{
Err("stipple::gpu: built without the `vk` feature (Linux-only Vulkan FFI)".to_string())
}
}
pub fn vulkan_init_image(width: u32, height: u32) -> Result<String, String> {
#[cfg(all(target_os = "linux", feature = "vk"))]
{
vulkan::init_image(width, height)
}
#[cfg(not(all(target_os = "linux", feature = "vk")))]
{
let _ = (width, height);
Err("stipple::gpu: built without the `vk` feature (Linux-only Vulkan FFI)".to_string())
}
}
pub fn vulkan_init_framebuffer(width: u32, height: u32) -> Result<String, String> {
#[cfg(all(target_os = "linux", feature = "vk"))]
{
vulkan::init_framebuffer(width, height)
}
#[cfg(not(all(target_os = "linux", feature = "vk")))]
{
let _ = (width, height);
Err("stipple::gpu: built without the `vk` feature (Linux-only Vulkan FFI)".to_string())
}
}
pub fn vulkan_clear(width: u32, height: u32) -> Result<String, String> {
#[cfg(all(target_os = "linux", feature = "vk"))]
{
vulkan::init_clear(width, height)
}
#[cfg(not(all(target_os = "linux", feature = "vk")))]
{
let _ = (width, height);
Err("stipple::gpu: built without the `vk` feature (Linux-only Vulkan FFI)".to_string())
}
}
pub fn vulkan_render_clear(width: u32, height: u32) -> Result<Vec<u8>, String> {
#[cfg(all(target_os = "linux", feature = "vk"))]
{
vulkan::render_clear(width, height)
}
#[cfg(not(all(target_os = "linux", feature = "vk")))]
{
let _ = (width, height);
Err("stipple::gpu: built without the `vk` feature (Linux-only Vulkan FFI)".to_string())
}
}
pub fn vulkan_render_triangle(width: u32, height: u32) -> Result<Vec<u8>, String> {
#[cfg(all(target_os = "linux", feature = "vk"))]
{
vulkan::render_triangle(width, height)
}
#[cfg(not(all(target_os = "linux", feature = "vk")))]
{
let _ = (width, height);
Err("stipple::gpu: built without the `vk` feature (Linux-only Vulkan FFI)".to_string())
}
}
pub fn d3d11_device() -> Result<String, String> {
#[cfg(all(target_os = "windows", feature = "d3d"))]
{
d3d::device()
}
#[cfg(not(all(target_os = "windows", feature = "d3d")))]
{
Err("stipple::gpu: built without the `d3d` feature (Windows-only Direct3D FFI)".to_string())
}
}
pub fn d3d11_export_shared_handle(width: u32, height: u32) -> Result<String, String> {
#[cfg(all(target_os = "windows", feature = "d3d"))]
{
d3d::export_shared_handle(width, height)
}
#[cfg(not(all(target_os = "windows", feature = "d3d")))]
{
let _ = (width, height);
Err("stipple::gpu: built without the `d3d` feature (Windows-only Direct3D FFI)".to_string())
}
}
pub fn d3d11_render_clear(width: u32, height: u32) -> Result<Vec<u8>, String> {
#[cfg(all(target_os = "windows", feature = "d3d"))]
{
d3d::render_clear(width, height)
}
#[cfg(not(all(target_os = "windows", feature = "d3d")))]
{
let _ = (width, height);
Err("stipple::gpu: built without the `d3d` feature (Windows-only Direct3D FFI)".to_string())
}
}
pub fn d3d11_render_triangle(width: u32, height: u32) -> Result<Vec<u8>, String> {
#[cfg(all(target_os = "windows", feature = "d3d"))]
{
d3d::render_triangle(width, height)
}
#[cfg(not(all(target_os = "windows", feature = "d3d")))]
{
let _ = (width, height);
Err("stipple::gpu: built without the `d3d` feature (Windows-only Direct3D FFI)".to_string())
}
}
pub fn metal_device() -> Result<String, String> {
#[cfg(all(target_os = "macos", feature = "mtl"))]
{
metal::device()
}
#[cfg(not(all(target_os = "macos", feature = "mtl")))]
{
Err("stipple::gpu: built without the `mtl` feature (macOS-only Metal FFI)".to_string())
}
}
pub fn metal_export_iosurface(width: u32, height: u32) -> Result<String, String> {
#[cfg(all(target_os = "macos", feature = "mtl"))]
{
metal::export_iosurface(width, height)
}
#[cfg(not(all(target_os = "macos", feature = "mtl")))]
{
let _ = (width, height);
Err("stipple::gpu: built without the `mtl` feature (macOS-only Metal FFI)".to_string())
}
}
pub fn metal_render_clear(width: u32, height: u32) -> Result<Vec<u8>, String> {
#[cfg(all(target_os = "macos", feature = "mtl"))]
{
metal::render_clear(width, height)
}
#[cfg(not(all(target_os = "macos", feature = "mtl")))]
{
let _ = (width, height);
Err("stipple::gpu: built without the `mtl` feature (macOS-only Metal FFI)".to_string())
}
}
pub fn metal_render_triangle(width: u32, height: u32) -> Result<Vec<u8>, String> {
#[cfg(all(target_os = "macos", feature = "mtl"))]
{
metal::render_triangle(width, height)
}
#[cfg(not(all(target_os = "macos", feature = "mtl")))]
{
let _ = (width, height);
Err("stipple::gpu: built without the `mtl` feature (macOS-only Metal FFI)".to_string())
}
}
pub fn present_offscreen(input: &Pixmap) -> Result<Pixmap, String> {
#[cfg(all(target_os = "linux", feature = "gl"))]
{
gl::present_offscreen(input)
}
#[cfg(not(all(target_os = "linux", feature = "gl")))]
{
let _ = input;
Err("stipple::gpu: built without the `gl` feature (Linux-only GLES backend)".to_string())
}
}
pub fn dmabuf_extensions() -> Result<String, String> {
#[cfg(all(target_os = "linux", feature = "gl"))]
{
gl::dmabuf_extensions()
}
#[cfg(not(all(target_os = "linux", feature = "gl")))]
{
Err("stipple::gpu: built without the `gl` feature (Linux-only GLES backend)".to_string())
}
}
pub fn dmabuf_export_import_self_test() -> Result<Vec<u8>, String> {
#[cfg(all(target_os = "linux", feature = "gl"))]
{
gl::dmabuf_export_import_self_test()
}
#[cfg(not(all(target_os = "linux", feature = "gl")))]
{
Err("stipple::gpu: built without the `gl` feature (Linux-only GLES backend)".to_string())
}
}
pub fn dmabuf_self_test_on_device(drm_fd: i32) -> Result<Vec<u8>, String> {
#[cfg(all(target_os = "linux", feature = "gl"))]
{
gl::dmabuf_self_test_on_device(drm_fd)
}
#[cfg(not(all(target_os = "linux", feature = "gl")))]
{
let _ = drm_fd;
Err("stipple::gpu: built without the `gl` feature (Linux-only GLES backend)".to_string())
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct DmabufExport {
pub fd: i32,
pub width: u32,
pub height: u32,
pub stride: u32,
pub offset: u32,
pub modifier: u64,
pub fourcc: u32,
pub bpp: u8,
}
pub fn export_dmabuf_on_device(
drm_fd: i32,
width: u32,
height: u32,
) -> Result<DmabufExport, String> {
#[cfg(all(target_os = "linux", feature = "gl"))]
{
gl::export_dmabuf_on_device(drm_fd, width, height)
}
#[cfg(not(all(target_os = "linux", feature = "gl")))]
{
let _ = (drm_fd, width, height);
Err("stipple::gpu: built without the `gl` feature (Linux-only GLES backend)".to_string())
}
}
pub fn fill_rects_offscreen(
size: PhysicalSize,
background: Color,
rects: &[(Rect, Color, f32, f32)],
) -> Result<Pixmap, String> {
#[cfg(all(target_os = "linux", feature = "gl"))]
{
gl::fill_rects_offscreen(size, background, rects)
}
#[cfg(not(all(target_os = "linux", feature = "gl")))]
{
let _ = (size, background, rects);
Err("stipple::gpu: built without the `gl` feature (Linux-only GLES backend)".to_string())
}
}
pub fn render_offscreen(
size: PhysicalSize,
background: Color,
rects: &[(Rect, Color, f32, f32)],
texts: &[(&Pixmap, Rect, Color)],
) -> Result<Pixmap, String> {
#[cfg(all(target_os = "linux", feature = "gl"))]
{
gl::render_offscreen(size, background, rects, texts)
}
#[cfg(not(all(target_os = "linux", feature = "gl")))]
{
let _ = (size, background, rects, texts);
Err("stipple::gpu: built without the `gl` feature (Linux-only GLES backend)".to_string())
}
}
fn size_key(size: f64) -> u32 {
(size * 4.0).round() as u32
}
struct GlyphAtlas {
#[cfg_attr(not(all(target_os = "linux", feature = "gl")), allow(dead_code))]
atlas: Pixmap,
uvs: std::collections::BTreeMap<(char, u32), Rect>,
}
impl GlyphAtlas {
fn build(font: &Font, wanted: &[(char, f64)]) -> Self {
use std::collections::BTreeMap;
let mut keys: BTreeMap<(char, u32), f64> = BTreeMap::new();
for (ch, size) in wanted {
if !ch.is_whitespace() {
keys.insert((*ch, size_key(*size)), *size);
}
}
let cells: Vec<((char, u32), Pixmap)> = keys
.iter()
.map(|((ch, sk), size)| {
let s = ch.to_string();
let cw = font.measure(&s, *size).width.ceil().max(1.0);
let ch_h = font.line_height(*size).ceil().max(1.0);
let mut sc = Scene::new(Size::new(cw, ch_h));
sc.fill_text(font, &s, Point::new(0.0, 0.0), *size, Color::WHITE);
let pm = SoftwareRenderer::new()
.with_background(Color::BLACK)
.render(sc, ScaleFactor::IDENTITY);
((*ch, *sk), pm)
})
.collect();
let total_w: u32 = cells
.iter()
.map(|(_, pm)| pm.size().width)
.sum::<u32>()
.max(1);
let height: u32 = cells
.iter()
.map(|(_, pm)| pm.size().height)
.max()
.unwrap_or(1)
.max(1);
let mut atlas = Pixmap::new(PhysicalSize::new(total_w, height));
let astride = atlas.stride();
let mut uvs = std::collections::BTreeMap::new();
let mut x = 0u32;
for (key, pm) in &cells {
let (pw, ph) = (pm.size().width, pm.size().height);
let pstride = pm.stride();
for row in 0..ph as usize {
let s = &pm.as_bytes()[row * pstride..row * pstride + pw as usize * 4];
let off = row * astride + x as usize * 4;
atlas.as_bytes_mut()[off..off + pw as usize * 4].copy_from_slice(s);
}
uvs.insert(*key, Rect::from_xywh(x as f64, 0.0, pw as f64, ph as f64));
x += pw;
}
GlyphAtlas { atlas, uvs }
}
fn uv(&self, ch: char, size: f64) -> Option<Rect> {
self.uvs.get(&(ch, size_key(size))).copied()
}
}
pub fn render_scene(scene: &Scene, background: Color, font: &Font) -> Result<Pixmap, String> {
let ls = scene.logical_size();
let size = ScaleFactor::IDENTITY.to_physical(ls);
let mut rects: Vec<(Rect, Color, f32, f32)> = Vec::new();
let mut wanted: Vec<(char, f64)> = Vec::new();
for cmd in scene.commands() {
if let DrawCmd::Text { text, size: px, .. } = cmd {
wanted.extend(text.chars().map(|c| (c, *px)));
}
}
let atlas = GlyphAtlas::build(font, &wanted);
let mut glyphs: Vec<(Rect, Rect, Color)> = Vec::new();
for cmd in scene.commands() {
match cmd {
DrawCmd::Rect {
rect,
color,
radius,
border,
} => rects.push((*rect, *color, *radius as f32, *border as f32)),
DrawCmd::Text {
text,
origin,
size: px,
color,
} => {
for (i, ch) in text.char_indices() {
if let Some(src) = atlas.uv(ch, *px) {
let x = origin.x + font.measure(&text[..i], *px).width;
let dst = Rect::from_xywh(x, origin.y, src.width(), src.height());
glyphs.push((src, dst, *color));
}
}
}
DrawCmd::Viewport { rect, .. } => {
rects.push((*rect, Color::rgb(0x20, 0x24, 0x2c), 0.0, 0.0))
}
DrawCmd::PushClip(_) | DrawCmd::PopClip => {}
}
}
#[cfg(all(target_os = "linux", feature = "gl"))]
{
gl::render_scene_gl(size, background, &rects, &[], Some((&atlas.atlas, &glyphs)))
}
#[cfg(not(all(target_os = "linux", feature = "gl")))]
{
let _ = (size, background, glyphs);
Err("stipple::gpu: built without the `gl` feature (Linux-only GLES backend)".to_string())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn glyph_atlas_dedups_and_packs() {
let Some(font) = Font::system_default() else {
eprintln!("skipping: no system font found");
return;
};
let wanted: Vec<(char, f64)> = "hello".chars().map(|c| (c, 16.0)).collect();
let atlas = GlyphAtlas::build(&font, &wanted);
assert_eq!(atlas.uvs.len(), 4, "repeated 'l' should share one slot");
for ch in ['h', 'e', 'l', 'o'] {
assert!(atlas.uv(ch, 16.0).is_some(), "missing glyph {ch}");
}
let ws: Vec<(char, f64)> = vec![(' ', 16.0), ('a', 16.0), ('a', 24.0)];
let atlas2 = GlyphAtlas::build(&font, &ws);
assert_eq!(atlas2.uvs.len(), 2, "space dropped; 'a' at two sizes kept");
assert!(atlas2.atlas.size().width >= 1);
}
}