Skip to main content

atrinik_render_api/
lib.rs

1// Copyright 2026 The Atrinik Project
2// SPDX-License-Identifier: MIT
3
4#![forbid(unsafe_code)]
5
6use atrinik_render_resources::ResourceProvider;
7use atrinik_scene::SceneBundle;
8use std::{fmt, sync::Arc};
9
10#[derive(Clone, Copy, Debug, Eq, PartialEq)]
11pub enum BackendPreference {
12    Automatic,
13    Vulkan,
14    Direct3D12,
15}
16
17#[derive(Clone, Copy, Debug, Eq, PartialEq)]
18pub enum TargetKind {
19    Window,
20    Embedded,
21    Offscreen,
22}
23
24#[derive(Clone, Copy, Debug, Eq, PartialEq)]
25pub struct TargetDescriptor {
26    pub kind: TargetKind,
27    pub width: u32,
28    pub height: u32,
29    pub backend: BackendPreference,
30}
31
32#[derive(Clone, Copy, Debug, Eq, PartialEq)]
33pub struct TargetLimits {
34    pub maximum_dimension: u32,
35    pub maximum_pixels: u64,
36}
37
38impl Default for TargetLimits {
39    fn default() -> Self {
40        Self {
41            maximum_dimension: 8_192,
42            maximum_pixels: 16_777_216,
43        }
44    }
45}
46
47impl TargetDescriptor {
48    /// Checks nonzero bounded target dimensions.
49    ///
50    /// # Errors
51    /// Returns [`Error::InvalidTarget`] for dimensions outside the bound.
52    pub fn validate(self, limits: TargetLimits) -> Result<Self, Error> {
53        if self.width == 0
54            || self.height == 0
55            || self.width > limits.maximum_dimension
56            || self.height > limits.maximum_dimension
57            || u64::from(self.width) * u64::from(self.height) > limits.maximum_pixels
58        {
59            return Err(Error::InvalidTarget);
60        }
61        Ok(self)
62    }
63}
64
65#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
66pub struct RecoveryMetrics {
67    pub surface_reconfigurations: u64,
68    pub device_recoveries: u64,
69    pub skipped_frames: u64,
70    pub frames_submitted: u64,
71    pub resource_requests: u64,
72    pub uploaded_bytes: u64,
73    pub vertex_count: u64,
74    pub vertex_allocation_bytes: u64,
75    pub target_allocation_bytes: u64,
76    pub readback_bytes: u64,
77    pub frame_cpu_micros: u64,
78}
79
80#[derive(Clone, Debug, Eq, PartialEq)]
81pub struct FrameOutput {
82    pub width: u32,
83    pub height: u32,
84    pub rgba8: Vec<u8>,
85    pub semantic_ids: Vec<u32>,
86    pub depth: Vec<i16>,
87    pub coverage: Vec<u8>,
88    pub metrics: RecoveryMetrics,
89}
90
91impl FrameOutput {
92    /// Checks that every output plane exactly matches the pixel dimensions.
93    ///
94    /// # Errors
95    /// Returns [`Error::InvalidFrame`] or [`Error::LimitExceeded`] when the
96    /// dimensions and planes cannot form a valid bounded frame.
97    pub fn validate(&self) -> Result<(), Error> {
98        let pixels = usize::try_from(self.width)
99            .ok()
100            .and_then(|width| {
101                usize::try_from(self.height)
102                    .ok()
103                    .and_then(|height| width.checked_mul(height))
104            })
105            .ok_or(Error::LimitExceeded)?;
106        if pixels == 0
107            || self.rgba8.len() != pixels.checked_mul(4).ok_or(Error::LimitExceeded)?
108            || self.semantic_ids.len() != pixels
109            || self.depth.len() != pixels
110            || self.coverage.len() != pixels
111        {
112            return Err(Error::InvalidFrame);
113        }
114        Ok(())
115    }
116}
117
118#[derive(Clone)]
119pub struct RenderRequest<'a> {
120    pub scene: &'a SceneBundle,
121    pub resources: Arc<dyn ResourceProvider>,
122}
123
124pub trait Renderer {
125    fn target(&self) -> TargetDescriptor;
126    /// Changes logical target dimensions without changing target ownership.
127    ///
128    /// # Errors
129    /// Returns a typed backend or target error when resizing cannot complete.
130    fn resize(&mut self, width: u32, height: u32) -> Result<(), Error>;
131    /// Renders exactly one immutable scene revision.
132    ///
133    /// # Errors
134    /// Returns a typed backend, surface, resource, or validation error.
135    fn render(&mut self, request: RenderRequest<'_>) -> Result<FrameOutput, Error>;
136}
137
138#[derive(Clone, Debug, Eq, PartialEq)]
139pub enum Error {
140    InvalidTarget,
141    InvalidFrame,
142    LimitExceeded,
143    BackendUnavailable(String),
144    SurfaceUnavailable,
145    SurfaceLost,
146    DeviceLost,
147    Resource(String),
148    Internal(String),
149}
150
151impl fmt::Display for Error {
152    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
153        write!(formatter, "renderer error: {self:?}")
154    }
155}
156
157impl std::error::Error for Error {}
158
159#[cfg(test)]
160mod tests {
161    use super::{
162        BackendPreference, Error, FrameOutput, RecoveryMetrics, TargetDescriptor, TargetKind,
163        TargetLimits,
164    };
165
166    #[test]
167    fn frame_planes_must_match_dimensions() {
168        let frame = FrameOutput {
169            width: 1,
170            height: 1,
171            rgba8: vec![0; 4],
172            semantic_ids: vec![0],
173            depth: vec![0],
174            coverage: vec![0],
175            metrics: RecoveryMetrics::default(),
176        };
177        assert_eq!(frame.validate(), Ok(()));
178        let mut invalid = frame;
179        invalid.rgba8.pop();
180        assert_eq!(invalid.validate(), Err(Error::InvalidFrame));
181    }
182
183    #[test]
184    fn target_pixel_budget_is_independent_of_axis_limit() {
185        let target = TargetDescriptor {
186            kind: TargetKind::Offscreen,
187            width: 8_192,
188            height: 8_192,
189            backend: BackendPreference::Automatic,
190        };
191        assert_eq!(
192            target.validate(TargetLimits::default()),
193            Err(Error::InvalidTarget)
194        );
195    }
196}