daegun 1.0.0

Your all-in-one text engine. Rust, no_std, zero dependencies.
Documentation
use std::time::{Duration, Instant};

use daegun::daerizer::daegpu::backend::Backend;
use daegun::daerizer::daegpu::{GpuBatch, Mode, SubpixelParams};

const SIZES: [(u32, u32); 5] = [(64, 64), (256, 256), (512, 512), (1024, 1024), (2048, 1024)];

const ITERS: usize = 200;

fn median(samples: &mut [Duration]) -> Duration {
    samples.sort_unstable();
    samples[samples.len() / 2]
}

fn fit(points: &[(f64, f64)]) -> (f64, f64) {
    let n = points.len() as f64;
    let (sx, sy): (f64, f64) = points.iter().fold((0.0, 0.0), |(a, b), (x, y)| (a + x, b + y));
    let sxx: f64 = points.iter().map(|(x, _)| x * x).sum();
    let sxy: f64 = points.iter().map(|(x, y)| x * y).sum();
    let slope = (n * sxy - sx * sy) / (n * sxx - sx * sx);
    (sy / n - slope * sx / n, slope)
}

fn draw_cost<B: Backend>() {
    let Ok(r) = B::new() else {
        eprintln!("{}: no device", B::NAME);
        return;
    };
    let batch = GpuBatch::new();
    let Ok(geometry) = r.geometry(&batch) else {
        eprintln!("{}: geometry failed", B::NAME);
        return;
    };
    let (sub, mode) = (SubpixelParams::default(), Mode::Grayscale);

    eprintln!("{} — empty draw, median of {ITERS}", B::NAME);
    let (mut submit_fit, mut wait_fit, mut read_fit) = (Vec::new(), Vec::new(), Vec::new());

    for (w, h) in SIZES {
        let Ok(mut t) = r.target(w, h) else { continue };
        for _ in 0..8 {
            r.draw(&mut t, &geometry, &[], &sub, mode).expect("warm draw");
        }
        r.read_pixels(&mut t).expect("warm read");

        let mut submit = Vec::with_capacity(ITERS);
        for _ in 0..ITERS {
            let at = Instant::now();
            r.draw(&mut t, &geometry, &[], &sub, mode).expect("draw");
            submit.push(at.elapsed());
        }
        r.wait(&mut t).expect("drain");

        let mut waited = Vec::with_capacity(ITERS);
        for _ in 0..ITERS {
            let at = Instant::now();
            r.draw(&mut t, &geometry, &[], &sub, mode).expect("draw");
            r.wait(&mut t).expect("wait");
            waited.push(at.elapsed());
        }

        let mut read = Vec::with_capacity(ITERS);
        for _ in 0..ITERS {
            let at = Instant::now();
            r.draw(&mut t, &geometry, &[], &sub, mode).expect("draw");
            r.read_pixels(&mut t).expect("read");
            read.push(at.elapsed());
        }

        let (s, wt, rd) = (median(&mut submit), median(&mut waited), median(&mut read));
        let px = f64::from(w) * f64::from(h);
        submit_fit.push((px, s.as_secs_f64() * 1e6));
        wait_fit.push((px, wt.as_secs_f64() * 1e6));
        read_fit.push((px, rd.as_secs_f64() * 1e6));

        eprintln!(
            "  {w:>4}x{h:<4}  submit {:>9.1?}   +wait {:>9.1?}   +read {:>9.1?}   readback {:>9.1?}",
            s, wt, rd, rd.saturating_sub(wt),
        );
    }

    for (label, pts) in [("submit", &submit_fit), ("+wait", &wait_fit), ("+read", &read_fit)] {
        if pts.len() >= 2 {
            let (fixed, slope) = fit(pts);
            eprintln!("  fit {label:<7} {fixed:8.1} us fixed  + {:.3} us per 1000 px", slope * 1000.0);
        }
    }
}

#[test]
#[ignore = "latency measurement, not a pass/fail property"]
fn draw_cost_by_stage() {
    #[cfg(target_vendor = "apple")]
    draw_cost::<daegun::daerizer::daegpu::ffi::Renderer>();
    draw_cost::<daegun::daerizer::daegpu::vk::Renderer>();
    #[cfg(windows)]
    draw_cost::<daegun::daerizer::daegpu::d3d11::Renderer>();
    #[cfg(windows)]
    draw_cost::<daegun::daerizer::daegpu::d3d12::Renderer>();
}

#[test]
#[ignore = "latency measurement, not a pass/fail property"]
fn a_read_is_never_cheaper_than_the_wait_inside_it() {
    fn check<B: Backend>() {
        let Ok(r) = B::new() else { return };
        let batch = GpuBatch::new();
        let Ok(g) = r.geometry(&batch) else { return };
        let Ok(mut t) = r.target(512, 512) else { return };
        let (sub, mode) = (SubpixelParams::default(), Mode::Grayscale);
        for _ in 0..8 {
            r.draw(&mut t, &g, &[], &sub, mode).expect("warm");
        }
        r.read_pixels(&mut t).expect("warm read");

        let mut waited = Vec::new();
        let mut read = Vec::new();
        for _ in 0..64 {
            let at = Instant::now();
            r.draw(&mut t, &g, &[], &sub, mode).expect("draw");
            r.wait(&mut t).expect("wait");
            waited.push(at.elapsed());

            let at = Instant::now();
            r.draw(&mut t, &g, &[], &sub, mode).expect("draw");
            r.read_pixels(&mut t).expect("read");
            read.push(at.elapsed());
        }
        let (w, rd) = (median(&mut waited), median(&mut read));
        eprintln!("{}: wait {w:.1?}  read {rd:.1?}", B::NAME);
        assert!(
            rd * 2 >= w,
            "{}: a read ({rd:?}) came back in under half the wait it contains ({w:?}), so the wait \
             inside it is not happening",
            B::NAME,
        );
    }
    #[cfg(target_vendor = "apple")]
    check::<daegun::daerizer::daegpu::ffi::Renderer>();
    check::<daegun::daerizer::daegpu::vk::Renderer>();
    #[cfg(windows)]
    check::<daegun::daerizer::daegpu::d3d11::Renderer>();
    #[cfg(windows)]
    check::<daegun::daerizer::daegpu::d3d12::Renderer>();
}