1#![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 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 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 fn resize(&mut self, width: u32, height: u32) -> Result<(), Error>;
131 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}