atrinik-render-testkit 0.1.0

Deterministic renderer doubles and conformance scenes for Atrinik
Documentation
// Copyright 2026 The Atrinik Project
// SPDX-License-Identifier: MIT

use atrinik_render_api::{RenderRequest, Renderer};
use atrinik_render_testkit::{
    ReferenceRenderer, dense_scene, offscreen_target, structural_scene, synthetic_provider,
    synthetic_scene,
};
use sha2::{Digest, Sha256};
use std::{fmt::Write as _, time::Instant};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    println!("{{\"schema_version\":1,\"cases\":[");
    let cases = [
        ("overlap-square", synthetic_scene(16, 16)?, 50_000_u64),
        ("unaligned-wide", synthetic_scene(31, 17)?, 50_000),
        ("structural-fog-ui", structural_scene(48, 48)?, 75_000),
        ("high-density-1024", dense_scene(128, 128)?, 250_000),
    ];
    for (index, (name, scene, maximum_cpu_micros)) in cases.into_iter().enumerate() {
        let viewport = scene.viewport();
        let started = Instant::now();
        let frame = ReferenceRenderer::new(offscreen_target(viewport.width, viewport.height))?
            .render(RenderRequest {
                scene: &scene,
                resources: synthetic_provider()?,
            })?;
        let elapsed_micros = u64::try_from(started.elapsed().as_micros()).unwrap_or(u64::MAX);
        if elapsed_micros > maximum_cpu_micros {
            return Err(format!(
                "scenario {name} exceeded CPU budget: {elapsed_micros} > {maximum_cpu_micros} microseconds"
            )
            .into());
        }
        let semantic = frame
            .semantic_ids
            .iter()
            .flat_map(|value| value.to_le_bytes())
            .collect::<Vec<_>>();
        let depth = frame
            .depth
            .iter()
            .flat_map(|value| value.to_le_bytes())
            .collect::<Vec<_>>();
        println!(
            "{}{{\"name\":\"{}\",\"width\":{},\"height\":{},\"clock_millis\":{},\"maximum_rgba_channel_difference\":1,\"performance\":{{\"maximum_cpu_micros\":{},\"sprite_count\":{},\"resource_requests\":{},\"uploaded_bytes\":{},\"vertex_count\":{},\"vertex_allocation_bytes\":{},\"target_allocation_bytes\":{}}},\"digests\":{{\"rgba8\":\"{}\",\"semantic_u32le\":\"{}\",\"depth_i16le\":\"{}\",\"coverage_u8\":\"{}\"}}}}",
            if index == 0 { "" } else { "," },
            name,
            viewport.width,
            viewport.height,
            scene.clock_millis(),
            maximum_cpu_micros,
            scene.sprites().len(),
            frame.metrics.resource_requests,
            frame.metrics.uploaded_bytes,
            frame.metrics.vertex_count,
            frame.metrics.vertex_allocation_bytes,
            frame.metrics.target_allocation_bytes,
            digest(&frame.rgba8),
            digest(&semantic),
            digest(&depth),
            digest(&frame.coverage),
        );
    }
    println!("]}}");
    Ok(())
}

fn digest(bytes: &[u8]) -> String {
    let mut output = String::with_capacity(64);
    for byte in Sha256::digest(bytes) {
        write!(&mut output, "{byte:02x}").expect("writing to a String cannot fail");
    }
    output
}