Skip to main content

alpha2_render/
error.rs

1use std::fmt;
2
3/// Render input or output failure.
4#[derive(Copy, Clone, Debug, Eq, PartialEq)]
5#[non_exhaustive]
6pub enum RenderError {
7    /// Render targets must have non-zero dimensions.
8    ZeroDimension,
9    /// Render dimensions exceed the configured or physical device limit.
10    DimensionTooLarge,
11    /// Colors must be finite, inside the unit interval, and premultiplied:
12    /// no color component above alpha.
13    InvalidColor,
14    /// A layer transform is non-finite, has zero scale, or invalid opacity.
15    InvalidVisual,
16    /// An effect parameter is non-finite or outside its supported range.
17    InvalidEffectParameter,
18    /// No effect with the requested identity is registered.
19    UnknownEffect,
20    /// Supplied parameter names or types do not match the effect schema.
21    InvalidEffectParams,
22    /// An uploaded effect resource has an invalid identity, shape, or payload.
23    InvalidEffectResource,
24    /// An effect invocation omitted a required registered resource.
25    MissingEffectResource,
26    /// A resource identity resolves to a different representation than required.
27    EffectResourceKindMismatch,
28    /// A transition invocation omitted its second texture.
29    MissingEffectInput,
30    /// A frame shows a caption, text generator or layout its host did not
31    /// prepare at the frame's size.
32    MissingText,
33    /// RGBA input byte length does not match the declared image size.
34    InvalidImageData,
35    /// A rendered texture could not be read back to host memory, and where.
36    TextureReadback(ReadbackFailure),
37    /// An internally constructed render graph was structurally invalid.
38    InvalidGraph,
39    /// A native texture could not synchronize with the render submission,
40    /// and what the platform's status for that said about the device, when
41    /// it said anything.
42    ExternalTextureSync(Option<alpha2_media::GpuFault>),
43    /// Caller-supplied NV12 planes are not an R8 luma and an RG8 chroma
44    /// render attachment at the output size and half of it.
45    InvalidPlaneTarget,
46    /// GPU work did not complete within the renderer's wait timeout.
47    GpuTimeout,
48    /// The device completed a submission without running it: a readback came
49    /// back without the bytes its submission copied. The renderer refuses
50    /// work from then on; only a new process gets a device that runs it.
51    GpuWorkDropped,
52    /// The GPU rejected this frame's work, for this kind of error: an
53    /// allocation it could not make, or invalid use (often of what failed to
54    /// allocate). Nothing in the submission ran; the device is fine, and the
55    /// renderer kept nothing of the frame.
56    Gpu {
57        /// The class of error the GPU reported.
58        kind: alpha2_gpu::GpuErrorKind,
59    },
60    /// The device is lost: it can no longer make the resources a frame
61    /// needs. The renderer refuses work from then on.
62    GpuDeviceLost,
63    /// An HDR (PQ or HLG) source met [`crate::ColorPolicy::RejectHdr`]: the
64    /// project's color pipeline refuses it rather than show it.
65    UnsupportedHdr,
66}
67
68impl fmt::Display for RenderError {
69    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
70        match self {
71            Self::ZeroDimension => formatter.write_str("render dimensions must be non-zero"),
72            Self::DimensionTooLarge => {
73                formatter.write_str("render dimensions exceed the supported texture budget")
74            }
75            Self::InvalidColor => {
76                formatter.write_str(
77                    "color components must be finite values from zero to one, premultiplied: none above alpha",
78                )
79            }
80            Self::InvalidVisual => formatter.write_str("layer presentation values are invalid"),
81            Self::InvalidEffectParameter => formatter.write_str("effect parameter is invalid"),
82            Self::UnknownEffect => formatter.write_str("effect is not registered"),
83            Self::InvalidEffectParams => {
84                formatter.write_str("effect parameters do not match schema")
85            }
86            Self::InvalidEffectResource => formatter.write_str("effect resource is invalid"),
87            Self::MissingEffectResource => {
88                formatter.write_str("effect resource is missing or not registered")
89            }
90            Self::MissingText => {
91                formatter.write_str("text was not prepared for this frame and size")
92            }
93            Self::EffectResourceKindMismatch => {
94                formatter.write_str("effect resource kind does not match schema")
95            }
96            Self::MissingEffectInput => formatter.write_str("effect input texture is missing"),
97            Self::InvalidImageData => {
98                formatter.write_str("RGBA byte length does not match the image size")
99            }
100            Self::TextureReadback(failure) => {
101                write!(formatter, "GPU texture readback failed: {failure}")
102            }
103            Self::InvalidGraph => formatter.write_str("render graph is structurally invalid"),
104            Self::ExternalTextureSync(None) => {
105                formatter.write_str("external texture synchronization failed")
106            }
107            Self::ExternalTextureSync(Some(fault)) => write!(
108                formatter,
109                "external texture synchronization failed ({})",
110                fault.as_str()
111            ),
112            Self::InvalidPlaneTarget => formatter.write_str(
113                "NV12 plane targets must be an R8 luma and an RG8 chroma render attachment at the output size and half of it",
114            ),
115            Self::GpuTimeout => formatter.write_str(GPU_TIMEOUT_MESSAGE),
116            Self::GpuWorkDropped => formatter.write_str(GPU_WORK_DROPPED_MESSAGE),
117            Self::Gpu { kind } => write!(
118                formatter,
119                "the GPU rejected this frame's work with a {} error",
120                kind.as_str()
121            ),
122            Self::GpuDeviceLost => formatter.write_str(GPU_DEVICE_LOST_MESSAGE),
123            Self::UnsupportedHdr => {
124                formatter.write_str("the color policy refuses HDR (PQ or HLG) sources")
125            }
126        }
127    }
128}
129
130impl std::error::Error for RenderError {}
131
132/// Where a texture readback failed.
133///
134/// A failed buffer mapping carries no cause in `wgpu` 30
135/// (`BufferAsyncError` is a unit), so [`Self::MapFailed`] says only that the
136/// mapping was refused. Whether the device was lost or out of memory at the
137/// time is for the device's diagnostics (`alpha2_gpu::GpuDiagnosticLog`) to
138/// show; none of these is itself evidence of memory exhaustion.
139#[derive(Copy, Clone, Debug, Eq, PartialEq)]
140#[non_exhaustive]
141pub enum ReadbackFailure {
142    /// The rendered texture's format or size is not one the readback packs.
143    UnsupportedTarget,
144    /// A row, plane, or buffer size overflowed or could not be represented.
145    Layout,
146    /// Mapping was requested twice for one readback.
147    DuplicateMapRequest,
148    /// The GPU refused to map the staging buffer.
149    MapFailed,
150    /// The mapping callback was dropped without reporting.
151    CallbackDropped,
152    /// The staging buffer was read before its mapping succeeded.
153    NotMapped,
154    /// A caller-supplied plane destination is too small for the frame.
155    DestinationTooSmall,
156    /// The device was polled for a submission it did not recognise.
157    SubmissionMismatch,
158}
159
160impl ReadbackFailure {
161    /// Stable lowercase name for logs and protocols.
162    #[must_use]
163    pub const fn as_str(self) -> &'static str {
164        match self {
165            Self::UnsupportedTarget => "unsupported-target",
166            Self::Layout => "layout",
167            Self::DuplicateMapRequest => "duplicate-map-request",
168            Self::MapFailed => "map-failed",
169            Self::CallbackDropped => "callback-dropped",
170            Self::NotMapped => "not-mapped",
171            Self::DestinationTooSmall => "destination-too-small",
172            Self::SubmissionMismatch => "submission-mismatch",
173        }
174    }
175}
176
177impl fmt::Display for ReadbackFailure {
178    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
179        formatter.write_str(self.as_str())
180    }
181}
182
183/// The message every GPU wait timeout carries, so a host that sees only error
184/// text can still classify it: one sentence for the renderer, the native
185/// interop helpers, and encoder inputs alike.
186pub use alpha2_media::GPU_TIMEOUT_MESSAGE;
187
188/// The message of [`RenderError::GpuWorkDropped`]. It names a lost device,
189/// which is what the host classifies it as.
190pub const GPU_WORK_DROPPED_MESSAGE: &str = "GPU device lost: it completed submitted work without \
191     running it (a readback lacked its canary), so this process can no longer render";
192
193/// The message of [`RenderError::GpuDeviceLost`].
194pub const GPU_DEVICE_LOST_MESSAGE: &str = "GPU device lost: it can no longer make the resources \
195     a frame needs, so this process can no longer render";
196
197/// How long a native caller waits for GPU work before giving up, unless the
198/// renderer is told otherwise.
199pub const DEFAULT_GPU_WAIT_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(30);
200
201#[cfg(not(target_arch = "wasm32"))]
202#[allow(clippy::needless_pass_by_value)] // Shaped for `map_err`.
203pub(crate) fn poll_error(error: wgpu::PollError) -> RenderError {
204    match error {
205        wgpu::PollError::Timeout => RenderError::GpuTimeout,
206        wgpu::PollError::WrongSubmissionIndex(..) => {
207            RenderError::TextureReadback(ReadbackFailure::SubmissionMismatch)
208        }
209    }
210}