#![forbid(unsafe_code)]
use atrinik_render_resources::ResourceProvider;
use atrinik_scene::SceneBundle;
use std::{fmt, sync::Arc};
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum BackendPreference {
Automatic,
Vulkan,
Direct3D12,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum TargetKind {
Window,
Embedded,
Offscreen,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct TargetDescriptor {
pub kind: TargetKind,
pub width: u32,
pub height: u32,
pub backend: BackendPreference,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct TargetLimits {
pub maximum_dimension: u32,
pub maximum_pixels: u64,
}
impl Default for TargetLimits {
fn default() -> Self {
Self {
maximum_dimension: 8_192,
maximum_pixels: 16_777_216,
}
}
}
impl TargetDescriptor {
pub fn validate(self, limits: TargetLimits) -> Result<Self, Error> {
if self.width == 0
|| self.height == 0
|| self.width > limits.maximum_dimension
|| self.height > limits.maximum_dimension
|| u64::from(self.width) * u64::from(self.height) > limits.maximum_pixels
{
return Err(Error::InvalidTarget);
}
Ok(self)
}
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub struct RecoveryMetrics {
pub surface_reconfigurations: u64,
pub device_recoveries: u64,
pub skipped_frames: u64,
pub frames_submitted: u64,
pub resource_requests: u64,
pub uploaded_bytes: u64,
pub vertex_count: u64,
pub vertex_allocation_bytes: u64,
pub target_allocation_bytes: u64,
pub readback_bytes: u64,
pub frame_cpu_micros: u64,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct FrameOutput {
pub width: u32,
pub height: u32,
pub rgba8: Vec<u8>,
pub semantic_ids: Vec<u32>,
pub depth: Vec<i16>,
pub coverage: Vec<u8>,
pub metrics: RecoveryMetrics,
}
impl FrameOutput {
pub fn validate(&self) -> Result<(), Error> {
let pixels = usize::try_from(self.width)
.ok()
.and_then(|width| {
usize::try_from(self.height)
.ok()
.and_then(|height| width.checked_mul(height))
})
.ok_or(Error::LimitExceeded)?;
if pixels == 0
|| self.rgba8.len() != pixels.checked_mul(4).ok_or(Error::LimitExceeded)?
|| self.semantic_ids.len() != pixels
|| self.depth.len() != pixels
|| self.coverage.len() != pixels
{
return Err(Error::InvalidFrame);
}
Ok(())
}
}
#[derive(Clone)]
pub struct RenderRequest<'a> {
pub scene: &'a SceneBundle,
pub resources: Arc<dyn ResourceProvider>,
}
pub trait Renderer {
fn target(&self) -> TargetDescriptor;
fn resize(&mut self, width: u32, height: u32) -> Result<(), Error>;
fn render(&mut self, request: RenderRequest<'_>) -> Result<FrameOutput, Error>;
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum Error {
InvalidTarget,
InvalidFrame,
LimitExceeded,
BackendUnavailable(String),
SurfaceUnavailable,
SurfaceLost,
DeviceLost,
Resource(String),
Internal(String),
}
impl fmt::Display for Error {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(formatter, "renderer error: {self:?}")
}
}
impl std::error::Error for Error {}
#[cfg(test)]
mod tests {
use super::{
BackendPreference, Error, FrameOutput, RecoveryMetrics, TargetDescriptor, TargetKind,
TargetLimits,
};
#[test]
fn frame_planes_must_match_dimensions() {
let frame = FrameOutput {
width: 1,
height: 1,
rgba8: vec![0; 4],
semantic_ids: vec![0],
depth: vec![0],
coverage: vec![0],
metrics: RecoveryMetrics::default(),
};
assert_eq!(frame.validate(), Ok(()));
let mut invalid = frame;
invalid.rgba8.pop();
assert_eq!(invalid.validate(), Err(Error::InvalidFrame));
}
#[test]
fn target_pixel_budget_is_independent_of_axis_limit() {
let target = TargetDescriptor {
kind: TargetKind::Offscreen,
width: 8_192,
height: 8_192,
backend: BackendPreference::Automatic,
};
assert_eq!(
target.validate(TargetLimits::default()),
Err(Error::InvalidTarget)
);
}
}