1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
//! Backend-independent scene diagnostics and bounded structural traversal.
use std::fmt;
/// A machine-readable reason for scene preflight failure.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum ValidationErrorKind {
/// A property is nonfinite, out of range, or otherwise invalid.
InvalidValue,
/// The scene exceeds its node or nesting-depth allowance.
StructuralLimit,
/// A required rendering capability is unavailable.
Unsupported,
}
/// A property-level diagnostic, independent of fonts, GPU initialization and I/O.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ValidationError {
/// Category suitable for programmatic handling.
pub kind: ValidationErrorKind,
/// Structural path, such as `layers[2].mask.group.layers[0]`.
pub path: String,
/// The invalid property or exhausted allowance.
pub field: String,
/// The specific constraint that failed.
pub message: String,
/// The requested frame index, when supplied by composition preflight or rendering.
pub frame: Option<u32>,
}
impl ValidationError {
/// Creates a property diagnostic without binding it to an output frame.
pub fn new(
kind: ValidationErrorKind,
path: impl Into<String>,
field: impl Into<String>,
message: impl Into<String>,
) -> Self {
Self {
kind,
path: path.into(),
field: field.into(),
message: message.into(),
frame: None,
}
}
/// Creates an invalid-value diagnostic for a rejected property.
pub fn invalid(
path: impl Into<String>,
field: impl Into<String>,
message: impl Into<String>,
) -> Self {
Self::new(ValidationErrorKind::InvalidValue, path, field, message)
}
/// Creates a structural, work or storage allowance diagnostic.
pub fn limit(
path: impl Into<String>,
field: impl Into<String>,
message: impl Into<String>,
) -> Self {
Self::new(ValidationErrorKind::StructuralLimit, path, field, message)
}
/// Creates a diagnostic for input that is valid but not supported.
pub fn unsupported(
path: impl Into<String>,
field: impl Into<String>,
message: impl Into<String>,
) -> Self {
Self::new(ValidationErrorKind::Unsupported, path, field, message)
}
}
impl fmt::Display for ValidationError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
if let Some(frame) = self.frame {
write!(formatter, "frame {frame}: ")?;
}
write!(formatter, "{}.{}: {}", self.path, self.field, self.message)
}
}
impl std::error::Error for ValidationError {}
/// Scene structure allowances, separate from pixel and kernel-work budgets.
///
/// Defaults allow 100,000 layers and 64 levels. All content branches and masks
/// count, including invisible layers and inactive transition endpoints. The root
/// layers have depth one. Custom limits must be positive, with at most 1,000,000
/// nodes and 128 levels, so preflight itself always has bounded recursion.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct ValidationLimits {
/// Maximum total layer count across all branches.
pub max_nodes: usize,
/// Maximum layer nesting depth, including masks.
pub max_depth: usize,
}
impl Default for ValidationLimits {
fn default() -> Self {
Self {
max_nodes: 100_000,
max_depth: 64,
}
}
}
/// Structural requirements measured without initializing a renderer.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct SceneStats {
/// Number of logical layers, including masks, all temporal samples and the
/// wrapper/primitive charges of retained particle batches.
pub nodes: usize,
/// Deepest layer, or zero for an empty scene.
pub max_depth: usize,
}
/// A bounded structural traversal shared by compilation and preparation.
pub struct ValidationState {
limits: ValidationLimits,
/// Cumulative node/depth statistics for the currently validated structure.
pub stats: SceneStats,
}
impl ValidationState {
/// Validates allowances and starts an empty traversal.
pub fn new(limits: ValidationLimits) -> Result<Self, ValidationError> {
if limits.max_nodes == 0
|| limits.max_nodes > 1_000_000
|| limits.max_depth == 0
|| limits.max_depth > 128
{
return Err(ValidationError::new(
ValidationErrorKind::InvalidValue,
"scene",
"limits",
"Require 1..=1000000 nodes and 1..=128 levels",
));
}
Ok(Self {
limits,
stats: SceneStats::default(),
})
}
/// Charges one structural node before descending into its children.
pub fn enter(&mut self, path: &str, depth: usize) -> Result<(), ValidationError> {
self.enter_with(|| path.to_owned(), depth)
}
/// Like [`Self::enter`], but formats the path only when a limit is exceeded.
pub fn enter_with(
&mut self,
path: impl FnOnce() -> String,
depth: usize,
) -> Result<(), ValidationError> {
if depth > self.limits.max_depth || self.stats.nodes >= self.limits.max_nodes {
let (field, allowed) = if depth > self.limits.max_depth {
("max_depth", self.limits.max_depth)
} else {
("max_nodes", self.limits.max_nodes)
};
return Err(ValidationError::limit(
path(),
field,
format!("Scene exceeds {field}={allowed}"),
));
}
self.stats.nodes += 1;
self.stats.max_depth = self.stats.max_depth.max(depth);
Ok(())
}
}