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
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
//! Configuration validation.
//!
//! Provides [`ValidationError`] plus `validate()` methods on every
//! public `Config` type. Existing encode/decode entry points keep
//! their clamping behaviour — `validate()` is a fail-fast option
//! callers can opt into for batch jobs that want hard rejection on
//! out-of-range values rather than silent clamping.
//!
//! ```no_run
//! # #[cfg(feature = "encode")] {
//! use zenavif::EncoderConfig;
//!
//! let cfg = EncoderConfig::new().quality(150.0);
//! assert!(cfg.validate().is_err()); // out of 0.0..=100.0
//! # }
//! ```
//!
//! Validation never mutates the config and never reports a "fixed"
//! value — it reports the issue and lets the caller decide.
use core::ops::RangeInclusive;
/// Reasons a [`crate::EncoderConfig`], [`crate::DecoderConfig`], or
/// [`crate::expert::InternalParams`] fails validation.
///
/// `#[non_exhaustive]` — variants may be added in any patch release as
/// new configuration knobs are introduced.
#[non_exhaustive]
#[derive(Debug, Clone, thiserror::Error)]
pub enum ValidationError {
// --- EncoderConfig ---
/// Encoder `quality` must be within `0.0..=100.0`.
#[error("encoder quality {value} out of valid range {valid:?}")]
QualityOutOfRange {
/// The offending value.
value: f32,
/// The valid range.
valid: RangeInclusive<f32>,
},
/// Encoder `alpha_quality` must be within `0.0..=100.0`.
#[error("encoder alpha_quality {value} out of valid range {valid:?}")]
AlphaQualityOutOfRange {
/// The offending value.
value: f32,
/// The valid range.
valid: RangeInclusive<f32>,
},
/// Encoder `speed` must be within `1..=10`.
#[error("encoder speed {value} out of valid range {valid:?}")]
SpeedOutOfRange {
/// The offending value.
value: u8,
/// The valid range.
valid: RangeInclusive<u8>,
},
/// Encoder `threads`, when `Some`, must be greater than zero.
/// Use `None` for the rayon default.
#[error("encoder threads must be > 0 when Some(_); got Some(0)")]
EncoderThreadsZero,
/// Encoder `rotation` must be one of `0`, `90`, `180`, `270`.
#[error("encoder rotation {value} invalid: must be one of {{0, 90, 180, 270}}")]
RotationInvalid {
/// The offending value.
value: u8,
},
/// Encoder `mirror` axis must be `0` (vertical) or `1` (horizontal).
#[error("encoder mirror {value} invalid: must be 0 (vertical) or 1 (horizontal)")]
MirrorInvalid {
/// The offending value.
value: u8,
},
/// CICP code-point fields (color_primaries, transfer_characteristics,
/// matrix_coefficients) must fit ITU-T H.273. The validator rejects
/// the reserved value `3`.
#[error("CICP {field} value {value} is reserved (3 is reserved per ITU-T H.273)")]
CicpReserved {
/// Which CICP field.
field: &'static str,
/// The offending value.
value: u8,
},
/// VAQ strength must be within `0.0..=4.0`.
#[error("VAQ strength {value} out of valid range {valid:?}")]
VaqStrengthOutOfRange {
/// The offending value.
value: f64,
/// The valid range.
valid: RangeInclusive<f64>,
},
/// Segmentation boost out of valid range. zenravif accepts
/// `0.5..=4.0`; `1.0` is "off" and `>1.0` widens deltas. Values
/// `<0.5` or `>4.0` are rejected.
#[error("seg_boost {value} out of valid range {valid:?}")]
SegBoostOutOfRange {
/// The offending value.
value: f64,
/// The valid range.
valid: RangeInclusive<f64>,
},
/// Two parameters that cannot both be set / both be true.
#[error("mutually exclusive: {a} and {b} cannot both be set")]
MutuallyExclusive {
/// First parameter name.
a: &'static str,
/// Second parameter name.
b: &'static str,
},
// --- DecoderConfig ---
/// Decoder `frame_size_limit` cannot use a sentinel reserved by
/// the validator. The current decoder treats `0` as "no limit"
/// at runtime, but for validation purposes a non-zero positive
/// limit must be supplied if the caller wants a bound. Use
/// `frame_size_limit(0)` plus skipping `validate()` to opt out.
/// Reserved for future use; not currently emitted.
#[error("decoder frame size limit {value} cannot be zero")]
DecoderFrameSizeLimitZero {
/// The offending value.
value: u64,
},
// --- expert::InternalParams ---
/// `partition_range` must satisfy `min <= max` and both bounds
/// must be in `{4, 8, 16, 32, 64}`. zenrav1e debug-asserts on
/// `128`, so it is rejected here.
#[error(
"partition_range {min}..{max} invalid: \
must satisfy min <= max and both ∈ {{4, 8, 16, 32, 64}}"
)]
PartitionRangeInvalid {
/// The offending lower bound.
min: u8,
/// The offending upper bound.
max: u8,
},
}
/// Returns true if `v` is a valid AV1 partition block-size bound.
/// zenrav1e accepts `{4, 8, 16, 32, 64}`; `128` is reserved for
/// future large-superblock support and triggers a debug-assert today.
#[cfg(feature = "__expert")]
fn partition_bound_ok(v: u8) -> bool {
matches!(v, 4 | 8 | 16 | 32 | 64)
}
#[cfg(feature = "__expert")]
impl crate::expert::InternalParams {
/// Validate this `InternalParams` value.
///
/// Returns `Err` if any `Some(_)` field is outside its accepted
/// range. The most relevant invariant is `partition_range`: both
/// bounds must be in `{4, 8, 16, 32, 64}` and `min <= max`. The
/// `128` superblock size is reserved for future AV1 large-superblock
/// support and triggers a zenrav1e debug-assert today.
pub fn validate(&self) -> Result<(), ValidationError> {
if let Some((min, max)) = self.partition_range
&& (!partition_bound_ok(min) || !partition_bound_ok(max) || min > max)
{
return Err(ValidationError::PartitionRangeInvalid { min, max });
}
// complex_prediction_modes / lrf / fast_deblock are bool
// overrides — every value of `Option<bool>` is well-formed.
Ok(())
}
}
#[cfg(feature = "encode")]
impl crate::EncoderConfig {
/// Validate this `EncoderConfig` value.
///
/// Returns `Err` on the first failed invariant. Validation does
/// not mutate the config; existing encode entry points still
/// clamp out-of-range values silently. Use this method when you
/// want hard rejection (batch jobs, calibration sweeps, public
/// HTTP endpoints) instead of silent clamping.
pub fn validate(&self) -> Result<(), ValidationError> {
let q_range: RangeInclusive<f32> = 0.0..=100.0;
// quality
if !q_range.contains(&self.quality) || !self.quality.is_finite() {
return Err(ValidationError::QualityOutOfRange {
value: self.quality,
valid: q_range.clone(),
});
}
// alpha_quality
if let Some(aq) = self.alpha_quality
&& (!q_range.contains(&aq) || !aq.is_finite())
{
return Err(ValidationError::AlphaQualityOutOfRange {
value: aq,
valid: q_range.clone(),
});
}
// speed: 1..=10. zenravif documents "1 = slowest/best, 10 = fastest/worst"
// and SpeedSettings::from_preset clamps; we reject 0 and >10.
let speed_range: RangeInclusive<u8> = 1..=10;
if !speed_range.contains(&self.speed) {
return Err(ValidationError::SpeedOutOfRange {
value: self.speed,
valid: speed_range,
});
}
// threads: Option<usize>. None = rayon default, Some(0) is meaningless.
if let Some(0) = self.threads {
return Err(ValidationError::EncoderThreadsZero);
}
// rotation: AVIF irot box stores the angle as a 2-bit
// quarter-turn code (0=0°, 1=90°, 2=180°, 3=270°). The
// serializer masks the input to `& 0x03`, so passing
// degrees (90, 180, 270) silently maps to wrong rotations.
// We require the irot code-point form {0, 1, 2, 3} for
// forwarding parity with zenravif's validator, which uses
// `ROTATION_RANGE = 0..=3`.
if let Some(angle) = self.rotation
&& angle > 3
{
return Err(ValidationError::RotationInvalid { value: angle });
}
// mirror axis: AVIF imir spec — 0 = vertical, 1 = horizontal.
if let Some(axis) = self.mirror
&& axis > 1
{
return Err(ValidationError::MirrorInvalid { value: axis });
}
// CICP code points (ITU-T H.273): 3 is reserved across all three fields.
if let Some(cp) = self.color_primaries
&& cp == 3
{
return Err(ValidationError::CicpReserved {
field: "color_primaries",
value: cp,
});
}
if let Some(tc) = self.transfer_characteristics
&& tc == 3
{
return Err(ValidationError::CicpReserved {
field: "transfer_characteristics",
value: tc,
});
}
if let Some(mc) = self.matrix_coefficients
&& mc == 3
{
return Err(ValidationError::CicpReserved {
field: "matrix_coefficients",
value: mc,
});
}
// encode-imazen knobs.
#[cfg(feature = "encode-imazen")]
{
// VAQ strength range matches zenravif/zenrav1e's accepted
// band: 0.0 (off) through 4.0 (aggressive).
let vaq_range: RangeInclusive<f64> = 0.0..=4.0;
if !vaq_range.contains(&self.vaq_strength) || !self.vaq_strength.is_finite() {
return Err(ValidationError::VaqStrengthOutOfRange {
value: self.vaq_strength,
valid: vaq_range,
});
}
// seg_boost: zenravif's SEG_BOOST_RANGE = 0.5..=4.0.
let seg_range: RangeInclusive<f64> = 0.5..=4.0;
if let Some(b) = self.seg_boost
&& (!b.is_finite() || !seg_range.contains(&b))
{
return Err(ValidationError::SegBoostOutOfRange {
value: b,
valid: seg_range,
});
}
// Cross-param: lossless overrides quality and is incompatible
// with VAQ enabled (zenravif disables QM internally for lossless;
// VAQ on top of quantizer=0 has no defined meaning).
if self.lossless && self.enable_vaq {
return Err(ValidationError::MutuallyExclusive {
a: "lossless",
b: "vaq",
});
}
// Cross-param: lossless + tune_still_image — still-image tuning
// changes deblock/CDEF tradeoffs that have no effect at q=0
// but the combination is conceptually nonsensical and the
// zenrav1e benchmark notes call out tune_still_image as a no-op
// at high q. We allow it; only the quantizer=0 vs VAQ pair is
// rejected because VAQ actively conflicts with the quantizer.
}
// expert::InternalParams forwarded fields. We re-validate the
// partition_range bounds at the EncoderConfig level so callers
// who set the field via `with_internal_params` get a single
// call site for validation.
#[cfg(feature = "__expert")]
{
if let Some((min, max)) = self.override_partition_range
&& (!partition_bound_ok(min) || !partition_bound_ok(max) || min > max)
{
return Err(ValidationError::PartitionRangeInvalid { min, max });
}
}
Ok(())
}
}
impl crate::DecoderConfig {
/// Validate this `DecoderConfig` value.
///
/// Returns `Err` on the first failed invariant. The decoder
/// itself accepts `frame_size_limit = 0` as "no limit"; this
/// method does **not** reject zero (no caller should be forced
/// to pick an arbitrary cap). It validates positively-set
/// numeric fields where a wrong value would silently misconfigure
/// the decoder.
///
/// `threads = 0` is the documented "auto-detect" sentinel and is
/// accepted; positive values are also accepted. There is no
/// invalid threads value at the moment.
pub fn validate(&self) -> Result<(), ValidationError> {
// Currently no DecoderConfig field has an invalid range:
// - threads: 0 = auto, any u32 accepted.
// - apply_grain: bool, every value valid.
// - frame_size_limit: 0 = no limit, any u32 accepted.
// - cpu_flags_mask: any u32 valid (0 = scalar-only).
// - parser_*_limit: Option<_>, every value valid.
// - prefer_8bit: bool, every value valid.
//
// The variant `DecoderFrameSizeLimitZero` is reserved for
// future use if a stricter mode is added.
Ok(())
}
}