Skip to main content

codec/encode/
tuning.rs

1//! AV1 encoder tuning adapter.
2//!
3//! Translates a single backend-agnostic perceptual quality target into
4//! per-encoder parameters so identical inputs yield visually consistent
5//! output across rav1e, NVENC AV1, and future backends (SVT-AV1, AMF,
6//! QSV).
7//!
8//! See `docs/av1-tuning-research.md` for the source tables and
9//! `docs/av1-tuning-methodology.md` for how to re-calibrate when a new
10//! encoder lands.
11//!
12//! # Design
13//!
14//! The user picks two things:
15//! 1. A `QualityTarget` — perceptual goal expressed in VMAF/SSIMULACRA2
16//!    bands, not encoder-native CRF. Every backend must reach roughly
17//!    the same VMAF for a given target (±2 VMAF band).
18//! 2. A `SpeedTier` — how much wallclock to spend getting there. Maps
19//!    to encoder-native speed presets.
20//!
21//! The adapter also takes `(width, height)` because tile grid and
22//! lookahead sizing depend on frame size.
23
24// ─── Public types ────────────────────────────────────────────────
25
26/// A single perceptual quality target, backend-agnostic.
27///
28/// Maps to VMAF / SSIMULACRA2 bands, NOT encoder CRF values:
29///
30/// | Variant             | Target VMAF | Target SSIMULACRA2 | Use case                         |
31/// |---------------------|:-----------:|:------------------:|----------------------------------|
32/// | `VisuallyLossless`  | ~98         | ~90                | Archive, master                  |
33/// | `High`              | ~95         | ~80                | Premium OTT / top ABR rung       |
34/// | `Standard`          | ~90         | ~70                | Default web / streaming          |
35/// | `Low`               | ~85         | ~60                | Mobile / bandwidth-constrained   |
36/// | `Vmaf(u8)`          | explicit    | n/a                | A/B testing escape hatch         |
37#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
38pub enum QualityTarget {
39    VisuallyLossless,
40    High,
41    #[default]
42    Standard,
43    Low,
44    Vmaf(u8),
45}
46
47/// User-facing speed tier — maps to encoder-native speed presets.
48///
49/// | Variant    | rav1e | NVENC preset | SVT-AV1 preset | libaom cpu-used |
50/// |------------|:-----:|:------------:|:--------------:|:---------------:|
51/// | `Draft`    | 8     | P5           | 12             | 8               |
52/// | `Standard` | 6     | P6           | 8              | 6               |
53/// | `Archive`  | 4     | P7           | 4              | 4               |
54#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
55pub enum SpeedTier {
56    Draft,
57    #[default]
58    Standard,
59    Archive,
60}
61
62// ─── Per-encoder parameter structs ───────────────────────────────
63
64/// Concrete parameters for rav1e's `EncoderConfig`.
65///
66/// Consumed in `crates/codec/src/encode/rav1e_enc.rs::build_rav1e_config`.
67#[derive(Debug, Clone, Copy, PartialEq, Eq)]
68pub struct Rav1eParams {
69    /// rav1e quantizer: 0–255, lower = higher quality. Default 100.
70    pub quantizer: usize,
71    /// rav1e speed preset 0 (slowest/best) – 10 (fastest). Archive=4,
72    /// Standard=6, Draft=8.
73    pub speed_preset: u8,
74    /// Number of tile rows (literal, not log2). Resolution-dependent.
75    pub tile_rows: usize,
76    /// Number of tile columns (literal). Resolution-dependent.
77    pub tile_cols: usize,
78}
79
80/// Concrete parameters for NVENC AV1 (NV_ENC_CONFIG + NV_ENC_RC_PARAMS).
81///
82/// Consumed in `crates/codec/src/encode/nvenc.rs` when populating
83/// `NV_ENC_INITIALIZE_PARAMS.encode_config` (currently null — see
84/// `reviews/codec-review-3.md` issues 1-3).
85///
86/// GUID is returned as its raw 16-byte form so the caller can splat
87/// it into the SDK's `#[repr(C)] Guid { data1: u32, data2: u16,
88/// data3: u16, data4: [u8;8] }` without this module depending on the
89/// FFI struct definitions.
90#[derive(Debug, Clone, Copy, PartialEq, Eq)]
91pub struct NvencAv1Params {
92    /// Rate control mode. Values are the SDK constants
93    /// `NV_ENC_PARAMS_RC_CONSTQP = 0`, `NV_ENC_PARAMS_RC_VBR = 1`,
94    /// `NV_ENC_PARAMS_RC_CBR = 2`. We only emit CONSTQP (archive) or
95    /// VBR+targetQuality (all other tiers) — CBR is never used by
96    /// this service.
97    pub rc_mode: NvencRateControl,
98    /// AV1 CQ target (for VBR mode) or constant QP (for CONSTQP mode).
99    /// Range 0–63 for AV1 (NOT 0-51 — that range is H.264/HEVC).
100    pub cq: u8,
101    /// Preset GUID raw bytes, ready to splat into a `#[repr(C)] Guid`.
102    /// Order: data1 (4 bytes, u32 LE), data2 (2 bytes u16 LE),
103    /// data3 (2 bytes u16 LE), data4 (8 raw bytes).
104    pub preset_guid: [u8; 16],
105    /// `NV_ENC_TUNING_INFO` — always `HIGH_QUALITY (1)` for this
106    /// service; never low-latency.
107    pub tuning_info: u32,
108    /// Adaptive quantization strength 0–15. 0 disables AQ. ~8 is
109    /// a reasonable default under HIGH_QUALITY tuning.
110    pub aq_strength: u8,
111    /// Lookahead depth (frames). 0 disables. Higher = better quality
112    /// bias at cost of latency.
113    pub lookahead_depth: u32,
114    /// `NV_ENC_CONFIG_AV1.numTileColumns`.
115    pub num_tile_columns: u32,
116    /// `NV_ENC_CONFIG_AV1.numTileRows`.
117    pub num_tile_rows: u32,
118    /// `NV_ENC_CONFIG_AV1.outputAnnexBFormat`. Always 0 (LOB) for
119    /// MP4 muxing — AV1-ISOBMFF requires `obu_has_size_field = 1`.
120    pub output_annex_b_format: u32,
121    /// `NV_ENC_CONFIG_AV1.repeatSeqHdr`. Always 1 so every IDR
122    /// carries a sequence header for seeking.
123    pub repeat_seq_hdr: u32,
124}
125
126/// NVENC rate control modes actually used by this service. The numeric
127/// value matches the SDK's `NV_ENC_PARAMS_RC_MODE`.
128#[derive(Debug, Clone, Copy, PartialEq, Eq)]
129#[repr(u32)]
130pub enum NvencRateControl {
131    /// `NV_ENC_PARAMS_RC_CONSTQP = 0`. Every frame gets the same QP.
132    /// Strict archival mode — bitrate floats.
133    ConstQp = 0,
134    /// `NV_ENC_PARAMS_RC_VBR = 1` with `targetQuality` set. NVENC's
135    /// CQ mode — quality-stable across content.
136    VbrTargetQuality = 1,
137}
138
139/// Concrete parameters for AMD AMF AV1 (VCN on RDNA3+).
140///
141/// AMF is property-driven: every knob is set via
142/// `AMFComponent::SetProperty(name, value)` using wide-string names
143/// defined in `vendor/amd/VideoEncoderAV1.h`. The adapter emits integer
144/// ranges that exactly match the property-value ranges the AMF runtime
145/// accepts — out-of-range values return `AMF_INVALID_ARG`.
146///
147/// Consumed in `crates/codec/src/encode/amf.rs::AmfEncoder::new`.
148#[derive(Debug, Clone, Copy, PartialEq, Eq)]
149pub struct AmfAv1Params {
150    /// `AMF_VIDEO_ENCODER_AV1_RATE_CONTROL_METHOD`. CQP for archive,
151    /// QVBR (quality-VBR) for the common quality-target tiers.
152    pub rc_mode: AmfRateControl,
153    /// `AMF_VIDEO_ENCODER_AV1_Q_INDEX_INTRA`. AV1 QP index 0..255 (the
154    /// full AV1 quantizer range — NOT 0..63; that's NVENC's scale).
155    pub q_index_intra: u8,
156    /// `AMF_VIDEO_ENCODER_AV1_Q_INDEX_INTER`. Usually +8 on intra so
157    /// P-frames spend fewer bits.
158    pub q_index_inter: u8,
159    /// `AMF_VIDEO_ENCODER_AV1_QVBR_QUALITY_LEVEL`. 1..100 when
160    /// rc_mode == `QualityVbr`; ignored for CQP. Higher = better.
161    pub qvbr_quality: u8,
162    /// `AMF_VIDEO_ENCODER_AV1_QUALITY_PRESET`. Lower = better quality.
163    pub quality_preset: AmfQualityPreset,
164    /// `AMF_VIDEO_ENCODER_AV1_GOP_SIZE`. Frames between keyframes.
165    pub gop_size: u32,
166    /// `AMF_VIDEO_ENCODER_AV1_AQ_MODE`. 0=off, 1=CAQ (content-adaptive).
167    pub aq_mode: u32,
168    /// `AMF_VIDEO_ENCODER_AV1_TILES_PER_FRAME`. AMF picks the grid;
169    /// we specify the total. 1 tile at ≤1080p, 4 at 1080p+, 4 at 4K
170    /// (VCN is less tile-parallel than rav1e — more tiles hurts HQ).
171    pub tiles_per_frame: u32,
172}
173
174/// AMF AV1 quality presets. Values match `AMF_VIDEO_ENCODER_AV1_QUALITY_PRESET_*`
175/// constants from the GPUOpen AMF wiki. Lower = better quality / more wall-clock.
176/// The transcode service never picks `Speed` (same rationale as NVENC: no
177/// low-latency presets in this service — see research §2.4).
178#[derive(Debug, Clone, Copy, PartialEq, Eq)]
179#[repr(i64)]
180pub enum AmfQualityPreset {
181    HighQuality = 10,
182    Quality = 30,
183    Balanced = 50,
184    /// Not used by this service; kept in the enum so the mapping table
185    /// stays complete for any future ultra-low-latency path.
186    #[allow(dead_code)]
187    Speed = 70,
188}
189
190/// AMF AV1 rate control modes actually used by this service.
191/// Values match `AMF_VIDEO_ENCODER_AV1_RATE_CONTROL_METHOD_*`.
192#[derive(Debug, Clone, Copy, PartialEq, Eq)]
193#[repr(i64)]
194pub enum AmfRateControl {
195    /// `AMF_VIDEO_ENCODER_AV1_RATE_CONTROL_METHOD_CQP = 1`. Every
196    /// frame gets the same q-index. Archival.
197    Cqp = 1,
198    /// `AMF_VIDEO_ENCODER_AV1_RATE_CONTROL_METHOD_QUALITY_VBR = 5`.
199    /// Quality-target VBR — bitrate floats to hit a perceptual level.
200    QualityVbr = 5,
201}
202
203/// Concrete parameters for Intel QSV AV1 (oneVPL on Arc / Meteor Lake+).
204///
205/// oneVPL is struct-driven: `mfxVideoParam` carries every knob in fixed
206/// fields (no property bag). The adapter produces the exact values we
207/// splat into the struct in `crates/codec/src/encode/qsv.rs`.
208#[derive(Debug, Clone, Copy, PartialEq, Eq)]
209pub struct QsvAv1Params {
210    /// `mfxVideoParam.mfx.RateControlMethod`. ICQ for the common
211    /// quality targets; CQP for archive.
212    pub rc_mode: QsvRateControl,
213    /// `mfxVideoParam.mfx.ICQQuality` (ICQ mode) — 1..51 for AV1 per
214    /// oneVPL 2.8+ dispatcher. 1=best, 51=worst. Mapped from libaom CQ.
215    pub icq_quality: u16,
216    /// `mfxVideoParam.mfx.QPI` (CQP mode) — AV1 q-index 0..255.
217    pub qp_i: u16,
218    /// `mfxVideoParam.mfx.QPP` (CQP mode) — inter-frame QP.
219    pub qp_p: u16,
220    /// `mfxVideoParam.mfx.TargetUsage`. 1=best quality, 7=best speed.
221    pub target_usage: u16,
222    /// `mfxVideoParam.mfx.GopPicSize`. Frames between keyframes.
223    pub gop_pic_size: u16,
224    /// Tile grid — `mfxExtAV1TileParam.NumTileColumns` / `NumTileRows`.
225    pub num_tile_columns: u8,
226    pub num_tile_rows: u8,
227    /// `mfxVideoParam.mfx.LowPower`. Always
228    /// `MFX_CODINGOPTION_OFF = 32` for this service — the low-power
229    /// path on older Arc silicon has documented quality regressions;
230    /// leaving it explicitly OFF sidesteps that.
231    pub low_power: u16,
232}
233
234/// oneVPL tri-state option values (from `MFX_CODINGOPTION_*`).
235/// Used for `LowPower` and a handful of other `mfxU16` toggles.
236pub const MFX_CODINGOPTION_OFF: u16 = 32;
237/// Not currently used but named so the value shows up next to `OFF`
238/// whenever a future code path wants explicit on-switching.
239#[allow(dead_code)]
240pub const MFX_CODINGOPTION_ON: u16 = 16;
241
242/// QSV AV1 rate control mode values match `MFX_RATECONTROL_*`
243/// in `oneVPL/include/vpl/mfxstructs.h`.
244#[derive(Debug, Clone, Copy, PartialEq, Eq)]
245#[repr(u16)]
246pub enum QsvRateControl {
247    /// `MFX_RATECONTROL_CQP = 3`.
248    Cqp = 3,
249    /// `MFX_RATECONTROL_ICQ = 8`. Intelligent constant quality — the
250    /// QSV equivalent of CRF. Best match for a perceptual target.
251    Icq = 8,
252}
253
254// NVENC preset GUIDs from Video Codec SDK 12.2 headers. Bytes are the
255// raw #[repr(C)] serialization: u32 LE, u16 LE, u16 LE, [u8;8].
256//
257// Only P5, P6, P7 are exposed — the transcode service has no use for
258// the low-latency presets P1-P4.
259
260// NVENC SDK 13.0 preset GUIDs (vendor/nvidia/nvEncodeAPI.h:226-251).
261//
262// CRITICAL: SDK 12.2 had ENTIRELY DIFFERENT preset GUIDs for P5/P6/P7
263// — when we vendored SDK 13's nvEncodeAPI.h on 2026-05-01 we updated
264// the NvEncFunctionList ordering + struct layouts but missed that the
265// preset-GUID values themselves were also reshuffled. Sending SDK
266// 12.2 P5/P6/P7 GUIDs to a SDK 13 driver returned NV_ENC_ERR_UNSUPPORTED_PARAM
267// (rc=12) from NvEncGetEncodePresetConfigEx (the driver doesn't
268// recognise the old GUIDs and rejects the lookup). For reference, the
269// 12.2 → 13 GUID rotation:
270//   P5: d0918ee2-a509-4681-af96-e9c3c45b7aa7 → 21c6e6b4-297a-4cba-998f-b6cbde72ade3
271//   P6: fc8ebf15-6e19-47b4-8ea7-b1917f379eed → 8e75c279-6299-4ab6-8302-0b215a335cf5
272//   P7: 84bdda58-33cb-4895-a372-ddeddb013ac4 → 84848c12-6f71-4c13-931b-53e283f57974
273const NV_ENC_PRESET_P5_GUID_BYTES: [u8; 16] = [
274    0xb4, 0xe6, 0xc6, 0x21, // data1 = 0x21c6e6b4
275    0x7a, 0x29, // data2 = 0x297a
276    0xba, 0x4c, // data3 = 0x4cba
277    0x99, 0x8f, 0xb6, 0xcb, 0xde, 0x72, 0xad, 0xe3,
278];
279
280const NV_ENC_PRESET_P6_GUID_BYTES: [u8; 16] = [
281    0x79, 0xc2, 0x75, 0x8e, // data1 = 0x8e75c279
282    0x99, 0x62, // data2 = 0x6299
283    0xb6, 0x4a, // data3 = 0x4ab6
284    0x83, 0x02, 0x0b, 0x21, 0x5a, 0x33, 0x5c, 0xf5,
285];
286
287const NV_ENC_PRESET_P7_GUID_BYTES: [u8; 16] = [
288    0x12, 0x8c, 0x84, 0x84, // data1 = 0x84848c12
289    0x71, 0x6f, // data2 = 0x6f71
290    0x13, 0x4c, // data3 = 0x4c13
291    0x93, 0x1b, 0x53, 0xe2, 0x83, 0xf5, 0x79, 0x74,
292];
293
294/// SDK constant `NV_ENC_TUNING_INFO_HIGH_QUALITY = 1`.
295pub const NVENC_TUNING_HIGH_QUALITY: u32 = 1;
296
297// ─── Adapter functions ───────────────────────────────────────────
298
299/// Derive rav1e params for a given quality target + speed tier +
300/// resolution.
301pub fn rav1e_params(
302    target: QualityTarget,
303    tier: SpeedTier,
304    width: u32,
305    height: u32,
306) -> Rav1eParams {
307    // rav1e quantizer ≈ 4 × libaom cq-level (well-known rule of thumb;
308    // see docs/av1-tuning-research.md §2.3).
309    let libaom_cq = libaom_cq_for_target(target);
310    let quantizer = (libaom_cq as usize) * 4;
311
312    let speed_preset = match tier {
313        SpeedTier::Archive => 4,
314        SpeedTier::Standard => 6,
315        SpeedTier::Draft => 8,
316    };
317
318    // rav1e has high per-tile overhead and benefits from parallelism;
319    // use the generous tile grid at 4K (4x4 = 16 tiles).
320    let (tile_cols, tile_rows) = tile_grid_rav1e(width, height);
321
322    Rav1eParams {
323        quantizer,
324        speed_preset,
325        tile_rows,
326        tile_cols,
327    }
328}
329
330/// Derive NVENC AV1 params for a given quality target + speed tier +
331/// resolution.
332pub fn nvenc_av1_params(
333    target: QualityTarget,
334    tier: SpeedTier,
335    width: u32,
336    height: u32,
337) -> NvencAv1Params {
338    // Calibrated CQ values: NVENC AV1 needs ~3-4 lower CQ to hit the
339    // same VMAF as libaom, compensating for its lower compression
340    // efficiency. See research §2.4.
341    let cq = nvenc_cq_for_target(target);
342
343    let (preset_guid, lookahead_depth, aq_strength) = match tier {
344        SpeedTier::Archive => (NV_ENC_PRESET_P7_GUID_BYTES, 32, 10),
345        SpeedTier::Standard => (NV_ENC_PRESET_P6_GUID_BYTES, 16, 8),
346        SpeedTier::Draft => (NV_ENC_PRESET_P5_GUID_BYTES, 0, 6),
347    };
348
349    // Archive tier uses CONSTQP for reproducible bitstreams; every
350    // other tier uses VBR with targetQuality so bitrate floats by
351    // content complexity.
352    let rc_mode = match target {
353        QualityTarget::VisuallyLossless => NvencRateControl::ConstQp,
354        _ => NvencRateControl::VbrTargetQuality,
355    };
356
357    // NVENC AV1 HQ tuning: fewer tiles = better compression because
358    // tile boundaries break loop-filter continuity and AV1 tiles are
359    // independently entropy-coded. Published measurements show ~0.6%
360    // VMAF loss at 2 tiles, ~1.3% at 4+ tiles on libaom; NVENC HQ
361    // exhibits the same scaling. NVENC has enough internal parallelism
362    // that it doesn't need 16-tile grids for throughput the way rav1e
363    // does — cap at 2x2 even at 4K.
364    //   Reference: research §3 and
365    //   https://streaminglearningcenter.com/codecs/av1-encoding-and-4k.html
366    let (num_tile_columns, num_tile_rows) = tile_grid_nvenc(width, height);
367
368    NvencAv1Params {
369        rc_mode,
370        cq,
371        preset_guid,
372        tuning_info: NVENC_TUNING_HIGH_QUALITY,
373        aq_strength,
374        lookahead_depth,
375        num_tile_columns: num_tile_columns as u32,
376        num_tile_rows: num_tile_rows as u32,
377        output_annex_b_format: 0, // LOB for MP4
378        repeat_seq_hdr: 1,
379    }
380}
381
382/// Derive AMD AMF AV1 params for a given quality target + speed tier +
383/// resolution.
384///
385/// AMF's AV1 q-index scale is 0..255 (the full AV1 quantizer range, not
386/// the NVENC-style 0..63 CQ band). Start point is rav1e's `4 × libaom_cq`
387/// rule, then apply an 8-point calibration shift down to compensate for
388/// VCN's documented compression-efficiency gap vs libaom (same goughlui
389/// study that calibrated NVENC's 3-4-point CQ shift tested AMF VCN and
390/// reported an analogous ~2-point CQ-equivalent shift; 2 points × 4 ≈ 8
391/// in the 0..255 space).
392///
393/// TODO(calibrate): replace these seed anchors with calibrated values
394/// once av1-tuning-eng runs the offline VMAF pass on RDNA3 hardware.
395/// See `docs/av1-tuning-research.md` §2.5 for the calibration protocol.
396pub fn amf_av1_params(
397    target: QualityTarget,
398    tier: SpeedTier,
399    width: u32,
400    height: u32,
401) -> AmfAv1Params {
402    let q_index_intra = amf_q_index_for_target(target);
403    // Inter-frames get a slightly higher QP so P/B frames spend fewer
404    // bits — biases bit allocation toward keyframes, which matches how
405    // rav1e and NVENC CONSTQP mode behave.
406    let q_index_inter = q_index_intra.saturating_add(8);
407
408    // QVBR quality 1..100; higher = better. Map our VMAF-band targets
409    // to the AMF-native band: VL=95, High=85, Standard=70, Low=55.
410    let qvbr_quality = match target {
411        QualityTarget::VisuallyLossless => 95,
412        QualityTarget::High => 85,
413        QualityTarget::Standard => 70,
414        QualityTarget::Low => 55,
415        QualityTarget::Vmaf(v) => vmaf_to_qvbr_quality(v),
416    };
417
418    // AMF quality preset per SpeedTier. Archive → HighQuality (best
419    // but slowest), Standard → Quality, Draft → Balanced. `Speed`
420    // preset deliberately unused — same rule as NVENC's P1-P4
421    // exclusion (see research §2.4: no low-latency tunings for batch
422    // transcode).
423    let quality_preset = match tier {
424        SpeedTier::Archive => AmfQualityPreset::HighQuality,
425        SpeedTier::Standard => AmfQualityPreset::Quality,
426        SpeedTier::Draft => AmfQualityPreset::Balanced,
427    };
428
429    // CQP for archival-lossless runs (reproducible bitstream); QVBR
430    // for everything else — matches the NVENC branch structure.
431    let rc_mode = match target {
432        QualityTarget::VisuallyLossless => AmfRateControl::Cqp,
433        _ => AmfRateControl::QualityVbr,
434    };
435
436    // AMF VCN tile parallelism is similar to NVENC — fewer tiles =
437    // better compression. Share the NVENC 2×2 cap via `tile_grid_hw`
438    // (both are "HQ-equivalent HW encoders that don't need aggressive
439    // tiling for throughput"). Total tiles = cols × rows; at 1×1 that's
440    // one, at 2×2 that's 4.
441    let (tile_cols, tile_rows) = tile_grid_hw(width, height);
442    let tiles_per_frame = (tile_cols * tile_rows) as u32;
443
444    AmfAv1Params {
445        rc_mode,
446        q_index_intra,
447        q_index_inter,
448        qvbr_quality,
449        quality_preset,
450        gop_size: 0, // caller fills from keyframe_interval
451        aq_mode: 1,  // CAQ — content-adaptive QP on
452        tiles_per_frame,
453    }
454}
455
456/// Derive Intel QSV AV1 params for a given quality target + speed tier +
457/// resolution.
458///
459/// oneVPL exposes two sensible modes for quality-driven encoding: ICQ
460/// (intelligent constant quality, 1..51 for AV1 — 1=best) and CQP
461/// (constant q-index, 0..255). ICQ is the default; CQP is the archival
462/// path. ICQ quality maps near-linearly to libaom cq-level at the range
463/// we care about (research §2.6, calibrated from Intel's public
464/// oneVPL sample_encode benchmarks).
465pub fn qsv_av1_params(
466    target: QualityTarget,
467    tier: SpeedTier,
468    width: u32,
469    height: u32,
470) -> QsvAv1Params {
471    // ICQ quality 1..51; 1=best. QSV maps AV1's native 0..63 CQ range
472    // into the 0..51 scale for API parity with H.264/HEVC (oneVPL
473    // idiosyncrasy), so we scale libaom cq-level by 51/63 ≈ 0.81.
474    //   VL: libaom 20 × 51/63 ≈ 16
475    //   Hi: libaom 27 × 51/63 ≈ 22
476    //   Std: libaom 32 × 51/63 ≈ 26
477    //   Low: libaom 38 × 51/63 ≈ 31
478    let icq_quality = match target {
479        QualityTarget::VisuallyLossless => 16,
480        QualityTarget::High => 22,
481        QualityTarget::Standard => 26,
482        QualityTarget::Low => 31,
483        QualityTarget::Vmaf(v) => vmaf_to_qsv_icq(v),
484    };
485    // CQP q-index for archival — QSV uses the full AV1 0..255 range
486    // via `mfx.QPI`. Same 4× libaom mapping as rav1e/AMF.
487    let libaom_cq = libaom_cq_for_target(target);
488    let qp_i = (libaom_cq as u16 * 4).min(255);
489    let qp_p = qp_i.saturating_add(8).min(255);
490
491    // oneVPL TargetUsage: 1=best quality, 7=best speed. Per
492    // av1-tuning-eng review: Archive=1, Standard=4, Draft=6
493    // (not 7 — 6 still leaves headroom for the driver's
494    // "adaptive speed" selections without falling into the explicit
495    // "worst-quality" bucket).
496    let target_usage = match tier {
497        SpeedTier::Archive => 1,
498        SpeedTier::Standard => 4,
499        SpeedTier::Draft => 6,
500    };
501
502    let rc_mode = match target {
503        QualityTarget::VisuallyLossless => QsvRateControl::Cqp,
504        _ => QsvRateControl::Icq,
505    };
506
507    let (num_tile_columns, num_tile_rows) = tile_grid_hw(width, height);
508
509    QsvAv1Params {
510        rc_mode,
511        icq_quality,
512        qp_i,
513        qp_p,
514        target_usage,
515        gop_pic_size: 0, // caller fills from keyframe_interval
516        num_tile_columns: num_tile_columns as u8,
517        num_tile_rows: num_tile_rows as u8,
518        // AV1 QSV encode is VDENC (low-power) only on Arc / Meteor Lake+.
519        low_power: MFX_CODINGOPTION_ON,
520    }
521}
522
523// ─── Internal helpers ────────────────────────────────────────────
524
525/// libaom `cq-level` that corresponds to a given QualityTarget. libaom
526/// is the cross-encoder reference: we equalize other encoders *to*
527/// libaom's VMAF at each CQ.
528///
529/// Exposed `pub` so the FFmpeg-wrapper encoder path
530/// (`encode::ffmpeg_enc`) can route `libsvtav1` / `libaom-av1` through
531/// the same adapter tables as the native encoders.
532pub fn libaom_cq_for_target(target: QualityTarget) -> u8 {
533    match target {
534        QualityTarget::VisuallyLossless => 20,
535        QualityTarget::High => 27,
536        QualityTarget::Standard => 32,
537        QualityTarget::Low => 38,
538        QualityTarget::Vmaf(v) => vmaf_to_libaom_cq(v),
539    }
540}
541
542/// NVENC CQ that hits the same VMAF as `libaom_cq_for_target`, per
543/// the research doc §2.4.
544fn nvenc_cq_for_target(target: QualityTarget) -> u8 {
545    match target {
546        QualityTarget::VisuallyLossless => 19,
547        QualityTarget::High => 25,
548        QualityTarget::Standard => 30,
549        QualityTarget::Low => 36,
550        QualityTarget::Vmaf(v) => vmaf_to_nvenc_cq(v),
551    }
552}
553
554/// Anchor points for libaom VMAF↔cq-level (research §2.1). Must stay
555/// in descending VMAF order; `piecewise_cq` below depends on it.
556const LIBAOM_ANCHORS: &[(i32, i32)] = &[
557    (100, 10), // asymptote beyond VisuallyLossless
558    (98, 20),
559    (95, 27),
560    (90, 32),
561    (85, 38),
562    (70, 55), // low-quality extrapolation
563];
564
565/// Anchor points for NVENC AV1 VMAF↔CQ. Calibrated down from libaom
566/// to compensate for NVENC's documented compression-efficiency gap
567/// (research §2.4). Same VMAF → lower CQ than libaom.
568const NVENC_ANCHORS: &[(i32, i32)] = &[(100, 10), (98, 19), (95, 25), (90, 30), (85, 36), (70, 52)];
569
570/// Piecewise-linear interpolation between anchors. Anchors are
571/// `(vmaf, cq)` pairs in descending VMAF order. Out-of-range VMAF
572/// values clamp to the nearest anchor's CQ.
573fn piecewise_cq(vmaf: u8, anchors: &[(i32, i32)]) -> u8 {
574    let v = vmaf as i32;
575    // Above the top anchor: return its CQ (asymptote).
576    if v >= anchors[0].0 {
577        return anchors[0].1.clamp(0, 63) as u8;
578    }
579    // Below the bottom anchor: return its CQ.
580    let last = anchors.len() - 1;
581    if v <= anchors[last].0 {
582        return anchors[last].1.clamp(0, 63) as u8;
583    }
584    // Linear interpolation between surrounding anchors.
585    for pair in anchors.windows(2) {
586        let (v_hi, cq_hi) = pair[0];
587        let (v_lo, cq_lo) = pair[1];
588        if v <= v_hi && v >= v_lo {
589            let span = v_hi - v_lo;
590            if span == 0 {
591                return cq_hi.clamp(0, 63) as u8;
592            }
593            let t = v_hi - v; // 0 at high anchor, span at low anchor
594            let cq = cq_hi + (cq_lo - cq_hi) * t / span;
595            return cq.clamp(0, 63) as u8;
596        }
597    }
598    anchors[last].1.clamp(0, 63) as u8
599}
600
601fn vmaf_to_libaom_cq(vmaf: u8) -> u8 {
602    piecewise_cq(vmaf, LIBAOM_ANCHORS)
603}
604
605fn vmaf_to_nvenc_cq(vmaf: u8) -> u8 {
606    piecewise_cq(vmaf, NVENC_ANCHORS)
607}
608
609/// Tile grid for rav1e (CPU). Returns `(columns, rows)`, literal counts.
610/// rav1e is memory-bandwidth-limited and benefits from aggressive tiling
611/// even at the cost of a small quality hit, because tile parallelism is
612/// most of its throughput story at 4K+.
613fn tile_grid_rav1e(width: u32, height: u32) -> (usize, usize) {
614    let max_dim = width.max(height);
615    if max_dim >= 3840 {
616        (4, 4) // 16 tiles at 4K — rav1e fans out across cores
617    } else if max_dim >= 1920 {
618        (2, 2)
619    } else {
620        (1, 1)
621    }
622}
623
624/// Tile grid for NVENC AV1. Returns `(columns, rows)`. NVENC has enough
625/// internal parallelism that it does not need large tile grids for
626/// throughput — and its HIGH_QUALITY tuning is sensitive to the ~1%
627/// quality cost per extra tile row/column (tile boundaries break loop
628/// filter continuity, and AV1 tiles are entropy-coded independently).
629/// Cap at 2×2 even at 4K.
630fn tile_grid_nvenc(width: u32, height: u32) -> (usize, usize) {
631    let max_dim = width.max(height);
632    if max_dim >= 1920 { (2, 2) } else { (1, 1) }
633}
634
635/// Shared HW-encoder tile grid. Used by NVENC, AMF, and QSV — all
636/// three are "HQ-equivalent hardware encoders" that don't need rav1e's
637/// aggressive tiling for throughput and are sensitive to the ~1%
638/// quality cost per extra tile row/column. Cap at 2×2 even at 4K.
639///
640/// This is an alias over `tile_grid_nvenc` so the shared rule is
641/// explicit at call sites. Changing the shared cap is a one-line
642/// change here.
643fn tile_grid_hw(width: u32, height: u32) -> (usize, usize) {
644    tile_grid_nvenc(width, height)
645}
646
647/// AMF CQP q-index (0..255) for a given QualityTarget. Starts from
648/// `libaom_cq × 4` and subtracts an 8-point calibration shift to
649/// compensate for VCN's compression-efficiency gap — analogous to
650/// NVENC's 3-4-point CQ shift in 0..63 space.
651///
652/// TODO(calibrate): replace with anchors from the offline VMAF pass
653/// on RDNA3 hardware. Seed values come from av1-tuning-eng's research
654/// doc §2.5 and GPUOpen AMF tuning guide.
655fn amf_q_index_for_target(target: QualityTarget) -> u8 {
656    let base = match target {
657        QualityTarget::VisuallyLossless => 72, // libaom 20 × 4 - 8
658        QualityTarget::High => 100,            // libaom 27 × 4 - 8
659        QualityTarget::Standard => 120,        // libaom 32 × 4 - 8
660        QualityTarget::Low => 144,             // libaom 38 × 4 - 8
661        QualityTarget::Vmaf(v) => vmaf_to_amf_q_index(v),
662    };
663    base.min(255) as u8
664}
665
666/// Anchors for AMF q-index interpolation when a caller passes an
667/// explicit Vmaf target. Descending VMAF → ascending q-index.
668const AMF_Q_INDEX_ANCHORS: &[(i32, i32)] = &[
669    (100, 50), // asymptote below VisuallyLossless
670    (98, 72),
671    (95, 100),
672    (90, 120),
673    (85, 144),
674    (70, 200),
675];
676
677fn vmaf_to_amf_q_index(vmaf: u8) -> u16 {
678    piecewise_quality(vmaf, AMF_Q_INDEX_ANCHORS, 0, 255) as u16
679}
680
681/// AMF anchors: AMF's QVBR quality scale is 1..100 (higher = better).
682/// Calibrated from research §2.5 against libaom at matched VMAF.
683const AMF_QVBR_ANCHORS: &[(i32, i32)] =
684    &[(100, 100), (98, 95), (95, 85), (90, 70), (85, 55), (70, 35)];
685
686fn vmaf_to_qvbr_quality(vmaf: u8) -> u8 {
687    piecewise_quality(vmaf, AMF_QVBR_ANCHORS, 1, 100)
688}
689
690/// QSV ICQ scale is 1..51 (lower = better), inverted from AMF's QVBR.
691/// Anchor table reflects Intel's public oneVPL sample benchmarks.
692const QSV_ICQ_ANCHORS: &[(i32, i32)] =
693    &[(100, 8), (98, 18), (95, 24), (90, 30), (85, 36), (70, 48)];
694
695fn vmaf_to_qsv_icq(vmaf: u8) -> u16 {
696    piecewise_quality(vmaf, QSV_ICQ_ANCHORS, 1, 51) as u16
697}
698
699/// Generic piecewise-linear interpolator for non-CQ scales. Mirrors
700/// `piecewise_cq` but with configurable clamp bounds so the same logic
701/// serves AMF's 1..100 and QSV's 1..51.
702fn piecewise_quality(vmaf: u8, anchors: &[(i32, i32)], lo: i32, hi: i32) -> u8 {
703    let v = vmaf as i32;
704    if v >= anchors[0].0 {
705        return anchors[0].1.clamp(lo, hi) as u8;
706    }
707    let last = anchors.len() - 1;
708    if v <= anchors[last].0 {
709        return anchors[last].1.clamp(lo, hi) as u8;
710    }
711    for pair in anchors.windows(2) {
712        let (v_hi, q_hi) = pair[0];
713        let (v_lo, q_lo) = pair[1];
714        if v <= v_hi && v >= v_lo {
715            let span = v_hi - v_lo;
716            if span == 0 {
717                return q_hi.clamp(lo, hi) as u8;
718            }
719            let t = v_hi - v;
720            let q = q_hi + (q_lo - q_hi) * t / span;
721            return q.clamp(lo, hi) as u8;
722        }
723    }
724    anchors[last].1.clamp(lo, hi) as u8
725}
726
727// ─── Unit tests ──────────────────────────────────────────────────
728
729#[cfg(test)]
730mod tests {
731    use super::*;
732
733    const RESOLUTIONS: &[(u32, u32)] = &[
734        (640, 360),   // 360p — single tile
735        (854, 480),   // 480p — single tile
736        (1280, 720),  // 720p — single tile
737        (1920, 1080), // 1080p — 2x2
738        (2560, 1440), // 1440p — 2x2
739        (3840, 2160), // 4K — 4x4
740    ];
741
742    const TARGETS: &[QualityTarget] = &[
743        QualityTarget::VisuallyLossless,
744        QualityTarget::High,
745        QualityTarget::Standard,
746        QualityTarget::Low,
747    ];
748
749    const TIERS: &[SpeedTier] = &[SpeedTier::Draft, SpeedTier::Standard, SpeedTier::Archive];
750
751    #[test]
752    fn rav1e_every_combination_returns_valid_params() {
753        for (w, h) in RESOLUTIONS {
754            for target in TARGETS {
755                for tier in TIERS {
756                    let p = rav1e_params(*target, *tier, *w, *h);
757                    assert!(p.quantizer <= 255, "quantizer {} oob", p.quantizer);
758                    assert!(p.speed_preset <= 10, "speed_preset {} oob", p.speed_preset);
759                    assert!(p.tile_rows >= 1);
760                    assert!(p.tile_cols >= 1);
761                }
762            }
763        }
764    }
765
766    #[test]
767    fn nvenc_every_combination_returns_valid_params() {
768        for (w, h) in RESOLUTIONS {
769            for target in TARGETS {
770                for tier in TIERS {
771                    let p = nvenc_av1_params(*target, *tier, *w, *h);
772                    assert!(p.cq <= 63, "cq {} exceeds AV1 max 63", p.cq);
773                    assert_eq!(p.tuning_info, NVENC_TUNING_HIGH_QUALITY);
774                    assert_eq!(p.output_annex_b_format, 0, "must be LOB for MP4");
775                    assert_eq!(p.repeat_seq_hdr, 1, "every IDR needs seq hdr");
776                    assert!(p.aq_strength <= 15);
777                }
778            }
779        }
780    }
781
782    #[test]
783    fn rav1e_quantizer_monotonic_in_quality() {
784        // Higher-quality targets must produce lower (stricter) quantizer.
785        let sd1080 = (1920, 1080);
786        let vl = rav1e_params(
787            QualityTarget::VisuallyLossless,
788            SpeedTier::Standard,
789            sd1080.0,
790            sd1080.1,
791        );
792        let hi = rav1e_params(QualityTarget::High, SpeedTier::Standard, sd1080.0, sd1080.1);
793        let std = rav1e_params(
794            QualityTarget::Standard,
795            SpeedTier::Standard,
796            sd1080.0,
797            sd1080.1,
798        );
799        let lo = rav1e_params(QualityTarget::Low, SpeedTier::Standard, sd1080.0, sd1080.1);
800        assert!(vl.quantizer < hi.quantizer);
801        assert!(hi.quantizer < std.quantizer);
802        assert!(std.quantizer < lo.quantizer);
803    }
804
805    #[test]
806    fn nvenc_cq_monotonic_in_quality() {
807        let sd = (1920, 1080);
808        let vl = nvenc_av1_params(
809            QualityTarget::VisuallyLossless,
810            SpeedTier::Standard,
811            sd.0,
812            sd.1,
813        );
814        let hi = nvenc_av1_params(QualityTarget::High, SpeedTier::Standard, sd.0, sd.1);
815        let std = nvenc_av1_params(QualityTarget::Standard, SpeedTier::Standard, sd.0, sd.1);
816        let lo = nvenc_av1_params(QualityTarget::Low, SpeedTier::Standard, sd.0, sd.1);
817        assert!(vl.cq < hi.cq);
818        assert!(hi.cq < std.cq);
819        assert!(std.cq < lo.cq);
820    }
821
822    #[test]
823    fn rav1e_speed_preset_monotonic_in_tier() {
824        let vl = QualityTarget::Standard;
825        let (w, h) = (1920, 1080);
826        let arc = rav1e_params(vl, SpeedTier::Archive, w, h);
827        let std = rav1e_params(vl, SpeedTier::Standard, w, h);
828        let drf = rav1e_params(vl, SpeedTier::Draft, w, h);
829        // Faster tiers -> higher preset number in rav1e.
830        assert!(arc.speed_preset < std.speed_preset);
831        assert!(std.speed_preset < drf.speed_preset);
832    }
833
834    #[test]
835    fn tile_grid_rav1e_by_resolution() {
836        assert_eq!(tile_grid_rav1e(640, 360), (1, 1));
837        assert_eq!(tile_grid_rav1e(1280, 720), (1, 1));
838        assert_eq!(tile_grid_rav1e(1920, 1080), (2, 2));
839        assert_eq!(tile_grid_rav1e(2560, 1440), (2, 2));
840        assert_eq!(tile_grid_rav1e(3840, 2160), (4, 4));
841        assert_eq!(tile_grid_rav1e(4096, 2160), (4, 4));
842        // Portrait 1080x1920 still deserves tiling — use max dim.
843        assert_eq!(tile_grid_rav1e(1080, 1920), (2, 2));
844    }
845
846    #[test]
847    fn tile_grid_nvenc_caps_at_2x2() {
848        // NVENC HQ prefers fewer tiles — no 4x4 even at 4K.
849        assert_eq!(tile_grid_nvenc(640, 360), (1, 1));
850        assert_eq!(tile_grid_nvenc(1280, 720), (1, 1));
851        assert_eq!(tile_grid_nvenc(1920, 1080), (2, 2));
852        assert_eq!(tile_grid_nvenc(2560, 1440), (2, 2));
853        assert_eq!(tile_grid_nvenc(3840, 2160), (2, 2));
854        assert_eq!(tile_grid_nvenc(4096, 2160), (2, 2));
855        assert_eq!(tile_grid_nvenc(1080, 1920), (2, 2));
856    }
857
858    #[test]
859    fn archive_tier_uses_constqp_at_lossless() {
860        let p = nvenc_av1_params(
861            QualityTarget::VisuallyLossless,
862            SpeedTier::Archive,
863            1920,
864            1080,
865        );
866        assert_eq!(p.rc_mode, NvencRateControl::ConstQp);
867    }
868
869    #[test]
870    fn non_archive_tiers_use_vbr_cq() {
871        for target in [
872            QualityTarget::High,
873            QualityTarget::Standard,
874            QualityTarget::Low,
875        ] {
876            for tier in TIERS {
877                let p = nvenc_av1_params(target, *tier, 1920, 1080);
878                assert_eq!(
879                    p.rc_mode,
880                    NvencRateControl::VbrTargetQuality,
881                    "target={:?} tier={:?} should use VBR+CQ",
882                    target,
883                    tier
884                );
885            }
886        }
887    }
888
889    #[test]
890    fn vmaf_escape_hatch_matches_named_targets() {
891        // VMAF 98 should map to roughly VisuallyLossless's CQ.
892        let vl = nvenc_cq_for_target(QualityTarget::VisuallyLossless);
893        let v98 = nvenc_cq_for_target(QualityTarget::Vmaf(98));
894        assert!(
895            (vl as i32 - v98 as i32).abs() <= 2,
896            "VMAF 98 escape hatch CQ={} should be within 2 of named VL CQ={}",
897            v98,
898            vl
899        );
900
901        // VMAF 90 should map near Standard's CQ.
902        let std = nvenc_cq_for_target(QualityTarget::Standard);
903        let v90 = nvenc_cq_for_target(QualityTarget::Vmaf(90));
904        assert!((std as i32 - v90 as i32).abs() <= 2);
905    }
906
907    #[test]
908    fn vmaf_escape_hatch_clamps_oob() {
909        // 0 and 255 shouldn't panic; clamp to valid CQ range.
910        let lo_cq = nvenc_cq_for_target(QualityTarget::Vmaf(0));
911        let hi_cq = nvenc_cq_for_target(QualityTarget::Vmaf(255));
912        assert!(lo_cq <= 63);
913        assert!(hi_cq <= 63);
914        // Low VMAF target -> high CQ. High VMAF target -> low CQ.
915        assert!(lo_cq > hi_cq);
916    }
917
918    #[test]
919    fn preset_guids_are_distinct() {
920        assert_ne!(NV_ENC_PRESET_P5_GUID_BYTES, NV_ENC_PRESET_P6_GUID_BYTES);
921        assert_ne!(NV_ENC_PRESET_P6_GUID_BYTES, NV_ENC_PRESET_P7_GUID_BYTES);
922        assert_ne!(NV_ENC_PRESET_P5_GUID_BYTES, NV_ENC_PRESET_P7_GUID_BYTES);
923    }
924
925    #[test]
926    fn rav1e_quantizer_matches_libaom_4x_rule() {
927        // docs rule: rav1e quantizer ≈ 4 × libaom cq-level.
928        let p = rav1e_params(QualityTarget::High, SpeedTier::Standard, 1920, 1080);
929        assert_eq!(p.quantizer, 27 * 4); // libaom cq-level for High = 27
930        let p = rav1e_params(QualityTarget::Standard, SpeedTier::Standard, 1920, 1080);
931        assert_eq!(p.quantizer, 32 * 4);
932    }
933
934    #[test]
935    fn default_quality_is_standard() {
936        let q: QualityTarget = Default::default();
937        assert_eq!(q, QualityTarget::Standard);
938        let t: SpeedTier = Default::default();
939        assert_eq!(t, SpeedTier::Standard);
940    }
941
942    #[test]
943    fn amf_every_combination_returns_valid_params() {
944        for (w, h) in RESOLUTIONS {
945            for target in TARGETS {
946                for tier in TIERS {
947                    let p = amf_av1_params(*target, *tier, *w, *h);
948                    // AV1 QP range is 0..255 — q_index_inter uses
949                    // saturating add so it never wraps.
950                    // q_index fields are u8, so the <=255 bound is
951                    // structurally guaranteed — the meaningful check is
952                    // that inter is at least as large as intra.
953                    assert!(p.q_index_inter >= p.q_index_intra);
954                    assert!((1..=100).contains(&p.qvbr_quality));
955                    assert!(p.tiles_per_frame >= 1);
956                    // Speed preset is not used by this service — any
957                    // combination must stay in the HighQuality..Balanced
958                    // band.
959                    assert!(matches!(
960                        p.quality_preset,
961                        AmfQualityPreset::HighQuality
962                            | AmfQualityPreset::Quality
963                            | AmfQualityPreset::Balanced
964                    ));
965                }
966            }
967        }
968    }
969
970    #[test]
971    fn qsv_every_combination_returns_valid_params() {
972        for (w, h) in RESOLUTIONS {
973            for target in TARGETS {
974                for tier in TIERS {
975                    let p = qsv_av1_params(*target, *tier, *w, *h);
976                    // oneVPL ICQ for AV1 is 1..51.
977                    assert!((1..=51).contains(&p.icq_quality));
978                    // AV1 q-index 0..255.
979                    assert!(p.qp_i <= 255);
980                    assert!(p.qp_p <= 255);
981                    // TargetUsage is 1..7 per mfxstructs.h; we cap at
982                    // 6 for Draft (av1-tuning-eng recommendation).
983                    assert!((1..=6).contains(&p.target_usage));
984                    // LowPower must be ON — AV1 QSV encode is VDENC-only on
985                    // Intel (the only AV1 encode entry point the iHD driver
986                    // exposes); OFF makes Query reject with MFX_ERR_UNSUPPORTED.
987                    assert_eq!(p.low_power, MFX_CODINGOPTION_ON);
988                    assert!(p.num_tile_columns >= 1);
989                    assert!(p.num_tile_rows >= 1);
990                }
991            }
992        }
993    }
994
995    #[test]
996    fn amf_q_index_monotonic_in_quality() {
997        let (w, h) = (1920, 1080);
998        let vl = amf_av1_params(QualityTarget::VisuallyLossless, SpeedTier::Standard, w, h);
999        let hi = amf_av1_params(QualityTarget::High, SpeedTier::Standard, w, h);
1000        let std = amf_av1_params(QualityTarget::Standard, SpeedTier::Standard, w, h);
1001        let lo = amf_av1_params(QualityTarget::Low, SpeedTier::Standard, w, h);
1002        assert!(vl.q_index_intra < hi.q_index_intra);
1003        assert!(hi.q_index_intra < std.q_index_intra);
1004        assert!(std.q_index_intra < lo.q_index_intra);
1005    }
1006
1007    #[test]
1008    fn qsv_icq_monotonic_in_quality() {
1009        let (w, h) = (1920, 1080);
1010        let vl = qsv_av1_params(QualityTarget::VisuallyLossless, SpeedTier::Standard, w, h);
1011        let hi = qsv_av1_params(QualityTarget::High, SpeedTier::Standard, w, h);
1012        let std = qsv_av1_params(QualityTarget::Standard, SpeedTier::Standard, w, h);
1013        let lo = qsv_av1_params(QualityTarget::Low, SpeedTier::Standard, w, h);
1014        // Lower ICQ quality = higher visual quality, so values should
1015        // increase as the requested target drops.
1016        assert!(vl.icq_quality < hi.icq_quality);
1017        assert!(hi.icq_quality < std.icq_quality);
1018        assert!(std.icq_quality < lo.icq_quality);
1019    }
1020
1021    #[test]
1022    fn amf_archive_at_visually_lossless_uses_cqp() {
1023        let p = amf_av1_params(
1024            QualityTarget::VisuallyLossless,
1025            SpeedTier::Archive,
1026            1920,
1027            1080,
1028        );
1029        assert_eq!(p.rc_mode, AmfRateControl::Cqp);
1030    }
1031
1032    #[test]
1033    fn amf_non_vl_uses_quality_vbr() {
1034        for target in [
1035            QualityTarget::High,
1036            QualityTarget::Standard,
1037            QualityTarget::Low,
1038        ] {
1039            for tier in TIERS {
1040                let p = amf_av1_params(target, *tier, 1920, 1080);
1041                assert_eq!(p.rc_mode, AmfRateControl::QualityVbr);
1042            }
1043        }
1044    }
1045
1046    #[test]
1047    fn qsv_archive_at_visually_lossless_uses_cqp() {
1048        let p = qsv_av1_params(
1049            QualityTarget::VisuallyLossless,
1050            SpeedTier::Archive,
1051            1920,
1052            1080,
1053        );
1054        assert_eq!(p.rc_mode, QsvRateControl::Cqp);
1055    }
1056
1057    #[test]
1058    fn amf_quality_preset_tier_mapping() {
1059        // Archive → HighQuality, Standard → Quality, Draft → Balanced.
1060        // Speed preset is never selected by this service.
1061        let (w, h) = (1920, 1080);
1062        let arc = amf_av1_params(QualityTarget::Standard, SpeedTier::Archive, w, h);
1063        let std = amf_av1_params(QualityTarget::Standard, SpeedTier::Standard, w, h);
1064        let drf = amf_av1_params(QualityTarget::Standard, SpeedTier::Draft, w, h);
1065        assert_eq!(arc.quality_preset, AmfQualityPreset::HighQuality);
1066        assert_eq!(std.quality_preset, AmfQualityPreset::Quality);
1067        assert_eq!(drf.quality_preset, AmfQualityPreset::Balanced);
1068    }
1069
1070    #[test]
1071    fn qsv_target_usage_tier_ordering() {
1072        // Archive → TU 1 (best quality); Draft → TU 7 (best speed).
1073        let (w, h) = (1920, 1080);
1074        let arc = qsv_av1_params(QualityTarget::Standard, SpeedTier::Archive, w, h);
1075        let std = qsv_av1_params(QualityTarget::Standard, SpeedTier::Standard, w, h);
1076        let drf = qsv_av1_params(QualityTarget::Standard, SpeedTier::Draft, w, h);
1077        assert!(arc.target_usage < std.target_usage);
1078        assert!(std.target_usage < drf.target_usage);
1079    }
1080
1081    #[test]
1082    fn amf_tile_count_caps_at_4() {
1083        // RDNA3 VCN prefers few tiles — research §3 caps at 4 via tile_grid_hw.
1084        // tiles_per_frame = cols * rows on the shared 2×2-at-4K grid.
1085        fn tiles(w: u32, h: u32) -> usize {
1086            let (c, r) = tile_grid_hw(w, h);
1087            c * r
1088        }
1089        assert_eq!(tiles(640, 360), 1);
1090        assert_eq!(tiles(1280, 720), 1);
1091        assert_eq!(tiles(1920, 1080), 4);
1092        assert_eq!(tiles(3840, 2160), 4);
1093    }
1094
1095    #[test]
1096    fn qsv_tile_grid_caps_at_2x2() {
1097        // QSV AV1 shares `tile_grid_hw` with AMF/NVENC — capped at 2×2.
1098        assert_eq!(tile_grid_hw(640, 360), (1, 1));
1099        assert_eq!(tile_grid_hw(1280, 720), (1, 1));
1100        assert_eq!(tile_grid_hw(1920, 1080), (2, 2));
1101        assert_eq!(tile_grid_hw(3840, 2160), (2, 2));
1102    }
1103
1104    /// Regression test from codec-spec-reviewer's task #49 review: every
1105    /// tile grid produced by the adapter must fit inside AV1 Level 5.1
1106    /// limits (AV1 spec Annex A.3): ≤8 tile columns, ≤64 total tiles,
1107    /// per-tile width ≤4096 luma samples, per-tile area ≤4,230,144.
1108    /// Level 5.1 covers every resolution we ship up to 4096×2176.
1109    #[test]
1110    fn tile_grid_fits_av1_level_5_1() {
1111        const MAX_TILE_COLS_L51: u32 = 8;
1112        const MAX_TILES_L51: u32 = 64;
1113        const MAX_TILE_WIDTH_L51: u32 = 4096;
1114        const MAX_TILE_AREA_L51: u32 = 4_230_144;
1115
1116        for (w, h) in RESOLUTIONS {
1117            for (label, (cols, rows)) in [
1118                ("rav1e", tile_grid_rav1e(*w, *h)),
1119                ("nvenc", tile_grid_nvenc(*w, *h)),
1120                // AMF and QSV both share tile_grid_hw, which is today
1121                // an alias of tile_grid_nvenc — covering explicitly
1122                // so that if the alias diverges later, this regression
1123                // test catches the Level 5.1 compliance for HW paths.
1124                ("hw", tile_grid_hw(*w, *h)),
1125            ] {
1126                let cols = cols as u32;
1127                let rows = rows as u32;
1128
1129                assert!(
1130                    cols <= MAX_TILE_COLS_L51,
1131                    "{} {}x{} emits {} tile cols; Level 5.1 max is {}",
1132                    label,
1133                    w,
1134                    h,
1135                    cols,
1136                    MAX_TILE_COLS_L51
1137                );
1138                assert!(
1139                    cols * rows <= MAX_TILES_L51,
1140                    "{} {}x{} emits {} total tiles; Level 5.1 max is {}",
1141                    label,
1142                    w,
1143                    h,
1144                    cols * rows,
1145                    MAX_TILES_L51
1146                );
1147
1148                let tile_w = w.div_ceil(cols);
1149                let tile_h = h.div_ceil(rows);
1150                assert!(
1151                    tile_w <= MAX_TILE_WIDTH_L51,
1152                    "{} {}x{} per-tile width {} > Level 5.1 max {}",
1153                    label,
1154                    w,
1155                    h,
1156                    tile_w,
1157                    MAX_TILE_WIDTH_L51
1158                );
1159                assert!(
1160                    tile_w * tile_h <= MAX_TILE_AREA_L51,
1161                    "{} {}x{} per-tile area {} > Level 5.1 max {}",
1162                    label,
1163                    w,
1164                    h,
1165                    tile_w * tile_h,
1166                    MAX_TILE_AREA_L51
1167                );
1168            }
1169        }
1170    }
1171}