diff --git a/Cargo.lock b/Cargo.lock index f7c20c6a..c1153ab0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1134,7 +1134,7 @@ dependencies = [ [[package]] name = "effectcraft" -version = "0.1.0" +version = "0.1.1" dependencies = [ "cpal", "effectcraft-engine", @@ -1155,7 +1155,7 @@ dependencies = [ [[package]] name = "effectcraft-automation" -version = "0.1.0" +version = "0.1.1" dependencies = [ "effectcraft-engine", "effectcraft-script", @@ -1164,9 +1164,16 @@ dependencies = [ "thiserror 2.0.21", ] +[[package]] +name = "effectcraft-av1enc" +version = "0.1.1" +dependencies = [ + "filmcraft-av1", +] + [[package]] name = "effectcraft-cli" -version = "0.1.0" +version = "0.1.1" dependencies = [ "cpu-time", "effectcraft-automation", @@ -1182,7 +1189,7 @@ dependencies = [ [[package]] name = "effectcraft-color" -version = "0.1.0" +version = "0.1.1" dependencies = [ "proptest", "serde", @@ -1190,7 +1197,7 @@ dependencies = [ [[package]] name = "effectcraft-effects" -version = "0.1.0" +version = "0.1.1" dependencies = [ "effectcraft-color", "effectcraft-geom", @@ -1206,7 +1213,7 @@ dependencies = [ [[package]] name = "effectcraft-engine" -version = "0.1.0" +version = "0.1.1" dependencies = [ "effectcraft-color", "effectcraft-effects", @@ -1237,9 +1244,11 @@ dependencies = [ [[package]] name = "effectcraft-export" -version = "0.1.0" +version = "0.1.1" dependencies = [ + "effectcraft-av1enc", "effectcraft-color", + "effectcraft-hevcenc", "effectcraft-keyframe", "effectcraft-media", "effectcraft-opusenc", @@ -1264,7 +1273,7 @@ dependencies = [ [[package]] name = "effectcraft-expr" -version = "0.1.0" +version = "0.1.1" dependencies = [ "boa_engine", "effectcraft-color", @@ -1281,7 +1290,7 @@ dependencies = [ [[package]] name = "effectcraft-geom" -version = "0.1.0" +version = "0.1.1" dependencies = [ "proptest", "serde", @@ -1289,7 +1298,7 @@ dependencies = [ [[package]] name = "effectcraft-gpu" -version = "0.1.0" +version = "0.1.1" dependencies = [ "bytemuck", "effectcraft-color", @@ -1306,9 +1315,16 @@ dependencies = [ "wgpu", ] +[[package]] +name = "effectcraft-hevcenc" +version = "0.1.1" +dependencies = [ + "filmcraft-hevc", +] + [[package]] name = "effectcraft-host" -version = "0.1.0" +version = "0.1.1" dependencies = [ "effectcraft-engine", "effectcraft-export", @@ -1328,7 +1344,7 @@ dependencies = [ [[package]] name = "effectcraft-keyframe" -version = "0.1.0" +version = "0.1.1" dependencies = [ "effectcraft-color", "effectcraft-geom", @@ -1340,7 +1356,7 @@ dependencies = [ [[package]] name = "effectcraft-lottie" -version = "0.1.0" +version = "0.1.1" dependencies = [ "effectcraft-color", "effectcraft-effects", @@ -1357,7 +1373,7 @@ dependencies = [ [[package]] name = "effectcraft-media" -version = "0.1.0" +version = "0.1.1" dependencies = [ "effectcraft-model", "effectcraft-pdf", @@ -1382,7 +1398,7 @@ dependencies = [ [[package]] name = "effectcraft-model" -version = "0.1.0" +version = "0.1.1" dependencies = [ "effectcraft-geom", "image", @@ -1393,11 +1409,14 @@ dependencies = [ [[package]] name = "effectcraft-opusenc" -version = "0.1.0" +version = "0.1.1" +dependencies = [ + "filmcraft-opus", +] [[package]] name = "effectcraft-path" -version = "0.1.0" +version = "0.1.1" dependencies = [ "effectcraft-geom", "effectcraft-keyframe", @@ -1409,7 +1428,7 @@ dependencies = [ [[package]] name = "effectcraft-pdf" -version = "0.1.0" +version = "0.1.1" dependencies = [ "effectcraft-raster", "effectcraft-svg", @@ -1423,7 +1442,7 @@ dependencies = [ [[package]] name = "effectcraft-plugin" -version = "0.1.0" +version = "0.1.1" dependencies = [ "effectcraft-effects", "effectcraft-keyframe", @@ -1435,7 +1454,7 @@ dependencies = [ [[package]] name = "effectcraft-project" -version = "0.1.0" +version = "0.1.1" dependencies = [ "effectcraft-color", "effectcraft-geom", @@ -1448,14 +1467,14 @@ dependencies = [ [[package]] name = "effectcraft-psd" -version = "0.1.0" +version = "0.1.1" dependencies = [ "miniz_oxide 0.8.9", ] [[package]] name = "effectcraft-raster" -version = "0.1.0" +version = "0.1.1" dependencies = [ "effectcraft-color", "effectcraft-geom", @@ -1464,7 +1483,7 @@ dependencies = [ [[package]] name = "effectcraft-render" -version = "0.1.0" +version = "0.1.1" dependencies = [ "effectcraft-color", "effectcraft-effects", @@ -1486,7 +1505,7 @@ dependencies = [ [[package]] name = "effectcraft-script" -version = "0.1.0" +version = "0.1.1" dependencies = [ "boa_engine", "effectcraft-engine", @@ -1501,7 +1520,7 @@ dependencies = [ [[package]] name = "effectcraft-svg" -version = "0.1.0" +version = "0.1.1" dependencies = [ "effectcraft-geom", "effectcraft-path", @@ -1513,7 +1532,7 @@ dependencies = [ [[package]] name = "effectcraft-text" -version = "0.1.0" +version = "0.1.1" dependencies = [ "effectcraft-keyframe", "harfrust", @@ -1525,7 +1544,7 @@ dependencies = [ [[package]] name = "effectcraft-time" -version = "0.1.0" +version = "0.1.1" dependencies = [ "proptest", "serde", @@ -1533,7 +1552,7 @@ dependencies = [ [[package]] name = "effectcraft-track" -version = "0.1.0" +version = "0.1.1" dependencies = [ "effectcraft-raster", "rayon", @@ -1544,7 +1563,7 @@ dependencies = [ [[package]] name = "effectcraft-ui-egui" -version = "0.1.0" +version = "0.1.1" dependencies = [ "effectcraft-color", "effectcraft-engine", @@ -1572,11 +1591,11 @@ dependencies = [ [[package]] name = "effectcraft-vp9enc" -version = "0.1.0" +version = "0.1.1" [[package]] name = "effectcraft-web" -version = "0.1.0" +version = "0.1.1" dependencies = [ "effectcraft-engine", "effectcraft-export", @@ -6190,7 +6209,7 @@ checksum = "e450f9b2ed1dff33c94c12589a87338689467b9c4f5d8a5710bd09a847d2c8a7" [[package]] name = "xtask" -version = "0.1.0" +version = "0.1.1" dependencies = [ "serde_json", ] diff --git a/Cargo.toml b/Cargo.toml index 6641f2c4..d3751c18 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,7 @@ members = ["crates/*", "apps/*", "xtask"] default-members = ["crates/*", "apps/*"] [workspace.package] -version = "0.1.0" +version = "0.1.1" edition = "2024" license = "MIT OR Apache-2.0" rust-version = "1.95" @@ -39,6 +39,8 @@ effectcraft-ui-egui = { path = "crates/ui-egui" } effectcraft-automation = { path = "crates/automation" } effectcraft-opusenc = { path = "crates/opusenc" } effectcraft-script = { path = "crates/script" } +effectcraft-hevcenc = { path = "crates/hevcenc" } +effectcraft-av1enc = { path = "crates/av1enc" } effectcraft-plugin = { path = "crates/plugin" } # foundation @@ -80,6 +82,9 @@ filmcraft-isobmff = { git = "https://github.com/storytold/filmcraft", rev = "89c # decoder used as the round-trip oracle of effectcraft-opusenc (crates/export dev-dependency) filmcraft-opus = { git = "https://github.com/storytold/filmcraft", rev = "89cfbf24f698b0f0e538eb7795929f19bbfb1dbb" } filmcraft-vp9 = { git = "https://github.com/storytold/filmcraft", rev = "89cfbf24f698b0f0e538eb7795929f19bbfb1dbb" } +# decoders used as the round-trip oracles of effectcraft-hevcenc / effectcraft-av1enc (dev-dependencies only) +filmcraft-hevc = { git = "https://github.com/storytold/filmcraft", rev = "89cfbf24f698b0f0e538eb7795929f19bbfb1dbb" } +filmcraft-av1 = { git = "https://github.com/storytold/filmcraft", rev = "89cfbf24f698b0f0e538eb7795929f19bbfb1dbb" } # gpu (the same wgpu eframe / egui-wgpu use) wgpu = { version = "30.0.1", features = ["fragile-send-sync-non-atomic-wasm"] } @@ -152,6 +157,12 @@ opt-level = 3 [profile.dev.package.effectcraft-vp9enc] opt-level = 3 +[profile.dev.package.effectcraft-hevcenc] +opt-level = 3 + +[profile.dev.package.effectcraft-av1enc] +opt-level = 3 + # FilmCraft codec hot loops (same settings as FilmCraft's own workspace). [profile.dev.package.filmcraft-h264] opt-level = 3 diff --git a/apps/effectcraft-cli/src/main.rs b/apps/effectcraft-cli/src/main.rs index 0bb196eb..b3e0fb1d 100644 --- a/apps/effectcraft-cli/src/main.rs +++ b/apps/effectcraft-cli/src/main.rs @@ -10,11 +10,13 @@ //! effectcraft-cli get [--time S] read a property //! effectcraft-cli set [--time S] [--expression E] //! effectcraft-cli render-frame [--comp C] [--time S|--frame N] [--max-side PX|--scale K] [--out F.png] -//! effectcraft-cli render [--comp C] --out FILE [--format h264|prores|webm|png|jpeg|tiff|exr|gif|wav|aiff] [--start S] [--end S] +//! effectcraft-cli render [--comp C] --out FILE [--format h264|hevc|av1|prores|webm|png|jpeg|tiff|exr|gif|wav|aiff] [--start S] [--end S] //! [--work-area] [--fps N] [--resolution full|half|third|quarter|K] [--quality best|draft] [--channels rgb|rgba] //! [--jpeg-quality N] [--bitrate KBPS] [--prores proxy|lt|standard|hq|4444|4444xq] [--audio auto|on|off] +//! [--profile main|main10] [--level auto|4.1] [--rate-control bitrate|quality] [--video-quality 1-100] +//! [--keyint FRAMES] [--webm-codec vp9|av1] [--audio-bitrate KBPS] [--opus-app audio|voice] //! effectcraft-cli render F.ecproj --queue render the project's Render Queue -//! effectcraft-cli bench [--comp C] [--time S] [--scale K] [--n N] [--play N] [--gpu] render timings +//! effectcraft-cli bench [--comp C] [--time S] [--scale K] [--n N] [--play N] [--gpu [--adv3d]] render timings //! (--gpu: CPU vs GPU ms/frame for every comp at Full and Half) //! effectcraft-cli bench --ops [--small] [--layers N] [--comps N] [--footage N] everyday-operation timings //! on a large generated project (open, save, auto-save, undo/redo, timeline, Project panel) @@ -47,8 +49,12 @@ const USAGE: &str = "usage: effectcraft-cli Result<(), Failure> { if let Some(v) = args.num("--bitrate")? { p["bitrate"] = json!(v); } + for (flag, key) in [ + ("--profile", "profile"), + ("--level", "level"), + ("--rate-control", "rateControl"), + ("--webm-codec", "webmCodec"), + ("--opus-app", "opusApplication"), + ] { + if let Some(v) = args.opt(flag) { + p[key] = json!(v); + } + } + for (flag, key) in [("--video-quality", "quality"), ("--keyint", "keyframeInterval"), ("--audio-bitrate", "audioBitrate")] { + if let Some(v) = args.num(flag)? { + p[key] = json!(v); + } + } // Render exactly this item: unqueue whatever the project's queue already holds. for k in 0..s.project.render_queue.len() { let _ = s.execute("renderQueue.setRender", json!({"index": k + 1, "render": false})); @@ -848,6 +878,109 @@ fn adjustment_bench_comp(p: &mut effectcraft_engine::project::Project) -> Option Some(p.add_item("Adjustment Layers (bench)", effectcraft_engine::color::Label::None, None, ItemKind::Comp(comp.into()))) } +/// `bench --gpu`'s Advanced 3D comp (1920×1080): a ground plane, PBR primitives, extruded +/// bevelled text, a shadow-casting spot light, a point light and an environment light, seen +/// through a depth-of-field camera (hexagonal iris, highlights) with a motion-blurred sphere. +fn adv3d_bench_comp(p: &mut effectcraft_engine::project::Project) -> Option { + use effectcraft_engine::keyframe::{Keyframe, TextDoc, Value}; + use effectcraft_engine::project::build::{self, Ids}; + use effectcraft_engine::project::{Comp, ItemKind, Layer, LayerSource, LightKind, PrimitiveKind, Renderer as R3, Solid}; + let (w, h) = (1920u32, 1080u32); + let mut comp = Comp::new(w, h, effectcraft_time::FrameRate::FPS_30, Tick::from_seconds_f64(10.0)); + comp.renderer = R3::Advanced3D; + comp.enable_motion_blur = true; + comp.motion_blur_samples = 8; + let mut layers: Vec = vec![]; + let mut add = |p: &mut effectcraft_engine::project::Project, comp: &Comp, src: LayerSource, edit: &dyn Fn(&mut Layer)| { + let mut l = build::layer(p, comp, "L", src, (w, h), None); + edit(&mut l); + layers.insert(0, l); + }; + fn set(l: &mut Layer, path: &str, v: Value) { + if let Some(pr) = l.props.prop_mut(path) { + pr.value = v; + } + } + add(p, &comp, LayerSource::Primitive { kind: PrimitiveKind::Plane }, &|l| { + set(l, "geometryOptions/width", Value::Scalar(6000.0)); + set(l, "geometryOptions/height", Value::Scalar(6000.0)); + set(l, "transform/position", Value::Vec3([960.0, 800.0, 0.0])); + set(l, "transform/rotationX", Value::Scalar(90.0)); + }); + for (i, kind) in [PrimitiveKind::Sphere, PrimitiveKind::Torus, PrimitiveKind::Cube, PrimitiveKind::Sphere].into_iter().enumerate() { + add(p, &comp, LayerSource::Primitive { kind }, &|l| { + let x = 400.0 + 380.0 * i as f64; + set(l, "geometryOptions/radius", Value::Scalar(140.0)); + set(l, "geometryOptions/tubeRadius", Value::Scalar(45.0)); + for k in ["width", "height", "depth"] { + set(l, &format!("geometryOptions/{k}"), Value::Scalar(220.0)); + } + set(l, "transform/position", Value::Vec3([x, 640.0, 300.0 * i as f64 - 300.0])); + set(l, "transform/rotationX", Value::Scalar(35.0)); + set(l, "transform/rotationY", Value::Scalar(25.0)); + set(l, "materialOptions/metallic", Value::Scalar(if i % 2 == 1 { 100.0 } else { 0.0 })); + set(l, "materialOptions/roughness", Value::Scalar(30.0)); + set(l, "materialOptions/castsShadows", Value::Enum(1)); + if i == 0 { + l.switches.motion_blur = true; + if let Some(pr) = l.props.prop_mut("transform/position") { + pr.keys = vec![ + Keyframe::new(Tick::ZERO, Value::Vec3([x, 640.0, -300.0])), + Keyframe::new(Tick::from_seconds_f64(10.0), Value::Vec3([x + 300.0 * 60.0, 640.0, -300.0])), + ]; + } + } + }); + } + add(p, &comp, LayerSource::Text, &|l| { + l.switches.three_d = true; + let doc = TextDoc { text: "EffectCraft".into(), size: 180.0, fill: [0.95, 0.75, 0.2, 1.0], apply_fill: true, ..Default::default() }; + set(l, "text/sourceText", Value::Text(Box::new(doc))); + let mut next = 900_000u64; + l.props.children.push(build::extrusion_geometry_options(&mut Ids(&mut next)).into()); + set(l, "geometryOptions/extrusionDepth", Value::Scalar(40.0)); + set(l, "geometryOptions/bevelStyle", Value::Enum(1)); + set(l, "transform/position", Value::Vec3([960.0, 260.0, 200.0])); + set(l, "transform/rotationY", Value::Scalar(-15.0)); + }); + let env = p.add_item( + "Environment (bench)", + effectcraft_engine::color::Label::None, + None, + ItemKind::Solid(Solid { color: [0.55, 0.65, 0.9], width: 64, height: 32, pixel_aspect: 1.0 }), + ); + add(p, &comp, LayerSource::Solid { item: env }, &|l| { + l.environment = true; + l.switches.three_d = true; + }); + add(p, &comp, LayerSource::Light { kind: LightKind::Environment }, &|l| set(l, "lightOptions/intensity", Value::Scalar(40.0))); + add(p, &comp, LayerSource::Light { kind: LightKind::Spot }, &|l| { + set(l, "transform/position", Value::Vec3([500.0, -600.0, -900.0])); + set(l, "transform/poi", Value::Vec3([960.0, 640.0, 0.0])); + set(l, "lightOptions/castsShadows", Value::Bool(true)); + set(l, "lightOptions/coneAngle", Value::Scalar(100.0)); + set(l, "lightOptions/shadowDiffusion", Value::Scalar(8.0)); + }); + add(p, &comp, LayerSource::Light { kind: LightKind::Point }, &|l| { + set(l, "transform/position", Value::Vec3([1500.0, 200.0, -500.0])); + set(l, "lightOptions/intensity", Value::Scalar(50.0)); + }); + add(p, &comp, LayerSource::Camera, &|l| { + let zoom = match l.props.prop_mut("cameraOptions/zoom").map(|pr| pr.value.clone()) { + Some(Value::Scalar(z)) => z, + _ => 2666.7, + }; + set(l, "cameraOptions/dof", Value::Bool(true)); + set(l, "cameraOptions/focusDistance", Value::Scalar(zoom)); + set(l, "cameraOptions/aperture", Value::Scalar(120.0)); + set(l, "cameraOptions/irisShape", Value::Enum(4)); + set(l, "cameraOptions/highlightGain", Value::Scalar(20.0)); + set(l, "cameraOptions/highlightThreshold", Value::Scalar(200.0)); + }); + comp.layers = layers; + Some(p.add_item("Advanced 3D (bench)", effectcraft_engine::color::Label::None, None, ItemKind::Comp(comp.into()))) +} + /// `bench --gpu`: CPU vs GPU ms/frame for every comp (or `--comp`) at Full and Half. "cold" /// renders everything (no layer cache); "warm" reuses the layer cache (playback / scrubbing of /// unchanged layers: compositing cost); "viewer" is the GPU display path without readback. @@ -856,13 +989,16 @@ fn bench_gpu(s: &Session, args: &Args) -> Result<(), Failure> { let gpu = effectcraft_gpu::Gpu::headless().ok_or_else(|| Failure::Error("--gpu: no usable GPU adapter".into()))?; let n = args.num("--n")?.unwrap_or(10.0).max(1.0) as usize; // Every comp, plus an adjustment-layer comp built for the benchmark (the main comp under a - // full-frame adjustment layer with a GPU effect stack). + // full-frame adjustment layer with a GPU effect stack) and an Advanced 3D comp (`--adv3d`: + // that one only). let mut project = (*s.project).clone(); let comps: Vec = match args.opt("--comp") { + _ if args.flag("--adv3d") => adv3d_bench_comp(&mut project).into_iter().collect(), Some(_) => s.active_comp_id().into_iter().collect(), None => { let mut v: Vec = s.project.comps().map(|(id, _)| *id).collect(); v.extend(adjustment_bench_comp(&mut project)); + v.extend(adv3d_bench_comp(&mut project)); v } }; @@ -910,7 +1046,17 @@ fn bench_gpu(s: &Session, args: &Args) -> Result<(), Failure> { // Agreement: share of pixels where the GPU frame differs from the CPU reference by // more than 1/255 on any channel. let (a, b) = (mk(Backend::Cpu, Some(&cache)), mk(Backend::Gpu, Some(&gcache))); - let off = a.data.iter().zip(&b.data).filter(|(p, q)| (0..4).any(|c| (p[c] - q[c]).abs() > 1.0 / 255.0 + 1e-6)).count(); + let off = a + .data + .iter() + .zip(&b.data) + .filter(|(p, q)| { + (0..4).any(|c| { + let d = (p[c] - q[c]).abs(); + d.is_nan() || d > 1.0 / 255.0 + 1e-6 + }) + }) + .count(); let pct = 100.0 * off as f64 / a.data.len().max(1) as f64; let speedup = cpu_warm / gpu_warm.max(1e-9); eprintln!( diff --git a/crates/av1enc/Cargo.toml b/crates/av1enc/Cargo.toml new file mode 100644 index 00000000..2a4c7ddf --- /dev/null +++ b/crates/av1enc/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "effectcraft-av1enc" +description = "Clean-room pure-Rust AV1 encoder (Main profile, key and inter frames) for MP4/WebM export, from the AV1 Bitstream & Decoding Process Specification" +version.workspace = true +edition.workspace = true +license.workspace = true +rust-version.workspace = true + +[lints] +workspace = true + +[dependencies] + +[dev-dependencies] +filmcraft-av1 = { workspace = true } diff --git a/crates/av1enc/README.md b/crates/av1enc/README.md new file mode 100644 index 00000000..6ced20ba --- /dev/null +++ b/crates/av1enc/README.md @@ -0,0 +1,74 @@ +# effectcraft-av1enc + +Clean-room, pure-Rust AV1 encoder for EffectCraft's MP4 / WebM export (layer L0, `std` only, +no `unsafe`, builds for `wasm32-unknown-unknown`). + +Output is decoded **bit-exactly** by libdav1d (`ffmpeg -c:v libdav1d`, checked in +`tests/ffmpeg_oracle.rs` when ffmpeg is installed) and by FilmCraft's spec-derived +`filmcraft-av1` decoder (`tests/roundtrip.rs`): every decoded frame equals the encoder's own +reconstruction, so prediction never drifts. + +## What is implemented + +- **Stream**: Main profile (`seq_profile` 0), 4:2:0, 8 or 10 bit, BT.709 colour description, + limited or full range. Sequence header with one operating point; `seq_level_idx` given or the + lowest Annex A level whose MaxPicSize / MaxH/VSize / MaxDisplayRate (and, in bitrate mode, Main + tier MaxBitrate) fit. Order hints, film grain, superres, CDEF, loop restoration, intra edge + filter, filter intra, inter-intra, masked / distance compound, warped motion and dual filter + are disabled in the sequence header. +- **Packets**: one temporal unit per frame as stored in MP4 / Matroska samples: OBUs with size + fields, no temporal delimiter; key frames start with the sequence header OBU, followed by one + `OBU_FRAME` (frame header + tile group). Every frame is shown, in input order. + `Encoder::av1c()` returns the `AV1CodecConfigurationRecord` (also the WebM `CodecPrivate`). +- **Frames**: a `KEY_FRAME` every `keyint` frames, `INTER_FRAME`s otherwise. Inter frames use + slot 0 (the previous reconstruction) for all seven references and refresh slot 0 only, so + `LAST_FRAME` is always the previous frame. CDFs adapt per symbol and are saved at the end of + every frame; inter frames load them from `LAST_FRAME` (`primary_ref_frame` = 0). One tile, + `TX_MODE_LARGEST`, `reduced_tx_set`, no segmentation, no delta q / lf. +- **Deblocking**: the normative loop filter (7.14) runs on the reconstruction; the frame level + (the same for both luma directions and both chroma planes, no deltas, sharpness 0) is searched + per frame for the lowest luma error. +- **Tiles**: the multi-symbol arithmetic encoder is the exact inverse of the spec's symbol + decoder (8.2), including the exit process padding; CDF adaptation as in 8.2.6. +- **Partitions**: 64x64 superblocks, `PARTITION_NONE` / `PARTITION_SPLIT` down to 8x8 chosen by + a rate-distortion cost (SSE + λ·estimated bits); blocks crossing the frame edge are split as + the syntax requires. 64x64 blocks are only used as inter skip blocks. +- **Intra**: DC, V, H, Paeth, smooth, smooth-V, smooth-H for luma and chroma, + ranked by SATD with a rate-distortion choice among the best three; the spec's edge preparation + and above-right / below-left availability. +- **Inter**: single reference (`LAST_FRAME`), `NEWMV` / `NEARESTMV` / `NEARMV` / `GLOBALMV` + chosen from the spec's motion vector prediction stack (7.10.2, mirrored exactly); motion search + from neighbour / co-located / parent candidates, a whole-sample diamond search and half- then + quarter-sample refinement (`allow_high_precision_mv` = 0); the normative 8-tap `EIGHTTAP` + prediction for luma and chroma; skip blocks; per-block intra / inter choice. +- **Residual**: one transform per plane and block (4x4 ... 32x32): for intra luma `DCT_DCT` or + the mode's preferred `ADST` combination (rate-distortion choice over the two best modes), the + mode-dependent type for intra chroma, `DCT_DCT` for inter blocks; dead-zone quantisation with the + spec's `Dc_Qlookup` / `Ac_Qlookup`; the normative dequantisation and inverse transforms in the + reconstruction loop; full coefficient coding (all_zero, tx type, eob_pt, eob_extra, + coeff_base_eob, coeff_base, coeff_br, dc_sign, Golomb) with the exact contexts. +- **Rate control**: `ConstantQ(base_q_idx)`; `Bitrate { kbps }` adapts `base_q_idx` per frame + towards the average target (key frames get a lower quantizer and a larger share; the first + key frame is searched in a few passes). + +## Limits + +- One tile: width and height up to 4096 and at most 2304 superblocks (4096x2304 samples). +- No CDEF or loop restoration, no compound prediction, no rectangular or 4xN blocks, no + directional intra modes other than V / H, no CfL or palette, no transform-size splits, so + compression is below mature encoders. +- 4:2:0 only (Main profile), no 12 bit. + +## Specification and provenance + +Implemented from the **AV1 Bitstream & Decoding Process Specification, version 1.0.0 with +Errata 1** (AOMediaCodec, 2019; the edition FilmCraft's decoder follows) and the **AV1 Codec +ISO Media File Format Binding** (v1.2.0) for `av1C`. + +The constant tables (default CDFs, scan orders, quantizer lookup tables, interpolation filter +taps, smooth weights, block / transform size tables, ...) and the decoder-side processes the +encoder must reproduce exactly (inverse transforms, intra and inter prediction rounding, context +derivations, motion vector prediction) are mirrored from FilmCraft's first-party, spec-derived +`filmcraft-av1` decoder (MIT OR Apache-2.0), which is also the round-trip oracle. No source of +libaom, dav1d, rav1e, SVT-AV1, ffmpeg or any other third-party encoder or decoder was consulted; +ffmpeg / libdav1d is used only as an external test oracle. diff --git a/crates/av1enc/src/bitw.rs b/crates/av1enc/src/bitw.rs new file mode 100644 index 00000000..e4c8a1e6 --- /dev/null +++ b/crates/av1enc/src/bitw.rs @@ -0,0 +1,87 @@ +//! MSB-first bit writer for the OBU headers (spec 4.10 `f(n)` descriptors) and `leb128()`. + +#[derive(Default)] +pub(crate) struct BitWriter { + pub buf: Vec, + nbits: usize, +} + +impl BitWriter { + pub fn new() -> Self { + BitWriter::default() + } + + /// `f(n)`: writes the low `n` bits of `v`, most significant first. + pub fn f(&mut self, n: u32, v: u32) { + for i in (0..n).rev() { + self.bit((v >> i) & 1 != 0); + } + } + + pub fn bit(&mut self, b: bool) { + if self.nbits.is_multiple_of(8) { + self.buf.push(0); + } + if b { + let last = self.buf.len() - 1; + self.buf[last] |= 0x80 >> (self.nbits % 8); + } + self.nbits += 1; + } + + /// `byte_alignment()`: zero bits up to the next byte boundary. + pub fn byte_align(&mut self) { + while !self.nbits.is_multiple_of(8) { + self.bit(false); + } + } + + /// `trailing_bits()`: a one bit followed by zero bits up to the byte boundary. + pub fn trailing_bits(&mut self) { + self.bit(true); + self.byte_align(); + } + + pub fn into_bytes(self) -> Vec { + self.buf + } +} + +/// `leb128()` encoding of an OBU size. +pub(crate) fn leb128(mut v: usize, out: &mut Vec) { + loop { + let mut b = (v & 0x7f) as u8; + v >>= 7; + if v != 0 { + b |= 0x80; + } + out.push(b); + if v == 0 { + break; + } + } +} + +/// An OBU with `obu_has_size_field` = 1 and no extension header. +pub(crate) fn obu(obu_type: u8, payload: &[u8], out: &mut Vec) { + out.push((obu_type << 3) | 0x02); + leb128(payload.len(), out); + out.extend_from_slice(payload); +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn bits_and_leb128() { + let mut w = BitWriter::new(); + w.f(3, 0b101); + w.f(6, 0b110011); + w.trailing_bits(); + assert_eq!(w.into_bytes(), vec![0b1011_1001, 0b1100_0000]); + let mut v = Vec::new(); + leb128(300, &mut v); + assert_eq!(v, vec![0xAC, 0x02]); + } +} diff --git a/crates/av1enc/src/cdf.rs b/crates/av1enc/src/cdf.rs new file mode 100644 index 00000000..0466a159 --- /dev/null +++ b/crates/av1enc/src/cdf.rs @@ -0,0 +1,164 @@ +//! The adaptive CDFs this encoder codes with (the subset of spec 6.8.2 init_non_coeff_cdfs / +//! init_coeff_cdfs that its syntax uses). Layout as in filmcraft-av1: each innermost array +//! holds N cumulative values (the last is 32768) plus the adaptation counter. + +use crate::tables::*; + +#[derive(Clone)] +pub(crate) struct Cdfs { + pub y_mode: [[u16; 14]; 4], + pub uv_mode_cfl_not_allowed: [[u16; 14]; 13], + pub uv_mode_cfl_allowed: [[u16; 15]; 13], + pub angle_delta: [[u16; 8]; 8], + pub partition_w8: [[u16; 5]; 4], + pub partition_w16: [[u16; 11]; 4], + pub partition_w32: [[u16; 11]; 4], + pub partition_w64: [[u16; 11]; 4], + pub skip: [[u16; 3]; 3], + pub is_inter: [[u16; 3]; 4], + pub single_ref: [[[u16; 3]; 6]; 3], + pub new_mv: [[u16; 3]; 6], + pub zero_mv: [[u16; 3]; 2], + pub ref_mv: [[u16; 3]; 6], + pub drl_mode: [[u16; 3]; 3], + pub mv_joint: [u16; 5], + pub mv_class: [[u16; 12]; 2], + pub mv_class0_bit: [[u16; 3]; 2], + pub mv_fr: [[u16; 5]; 2], + pub mv_class0_fr: [[[u16; 5]; 2]; 2], + pub mv_sign: [[u16; 3]; 2], + pub mv_bit: [[[u16; 3]; 10]; 2], + pub intra_tx_type_set2: [[[u16; 6]; 13]; 3], + pub inter_tx_type_set3: [[u16; 3]; 4], + pub txb_skip: [[[u16; 3]; 13]; 5], + pub eob_pt_16: [[[u16; 6]; 2]; 2], + pub eob_pt_32: [[[u16; 7]; 2]; 2], + pub eob_pt_64: [[[u16; 8]; 2]; 2], + pub eob_pt_128: [[[u16; 9]; 2]; 2], + pub eob_pt_256: [[[u16; 10]; 2]; 2], + pub eob_pt_512: [[u16; 11]; 2], + pub eob_pt_1024: [[u16; 12]; 2], + pub eob_extra: [[[[u16; 3]; 9]; 2]; 5], + pub dc_sign: [[[u16; 3]; 3]; 2], + pub coeff_base_eob: [[[[u16; 4]; 4]; 2]; 5], + pub coeff_base: [[[[u16; 5]; 42]; 2]; 5], + pub coeff_br: [[[[u16; 5]; 21]; 2]; 5], +} + +impl Cdfs { + /// init_non_coeff_cdfs( ) followed by init_coeff_cdfs( ) for `base_q_idx`. + pub fn new(base_q_idx: u32) -> Box { + let idx = match base_q_idx { + 0..=20 => 0, + 21..=60 => 1, + 61..=120 => 2, + _ => 3, + }; + Box::new(Cdfs { + y_mode: DEFAULT_Y_MODE_CDF, + uv_mode_cfl_not_allowed: DEFAULT_UV_MODE_CFL_NOT_ALLOWED_CDF, + uv_mode_cfl_allowed: DEFAULT_UV_MODE_CFL_ALLOWED_CDF, + angle_delta: DEFAULT_ANGLE_DELTA_CDF, + partition_w8: DEFAULT_PARTITION_W8_CDF, + partition_w16: DEFAULT_PARTITION_W16_CDF, + partition_w32: DEFAULT_PARTITION_W32_CDF, + partition_w64: DEFAULT_PARTITION_W64_CDF, + skip: DEFAULT_SKIP_CDF, + is_inter: DEFAULT_IS_INTER_CDF, + single_ref: DEFAULT_SINGLE_REF_CDF, + new_mv: DEFAULT_NEW_MV_CDF, + zero_mv: DEFAULT_ZERO_MV_CDF, + ref_mv: DEFAULT_REF_MV_CDF, + drl_mode: DEFAULT_DRL_MODE_CDF, + mv_joint: DEFAULT_MV_JOINT_CDF, + mv_class: DEFAULT_MV_CLASS_CDF, + mv_class0_bit: [DEFAULT_MV_CLASS0_BIT_CDF; 2], + mv_fr: DEFAULT_MV_FR_CDF, + mv_class0_fr: DEFAULT_MV_CLASS0_FR_CDF, + mv_sign: [DEFAULT_MV_SIGN_CDF; 2], + mv_bit: [DEFAULT_MV_BIT_CDF; 2], + intra_tx_type_set2: DEFAULT_INTRA_TX_TYPE_SET2_CDF, + inter_tx_type_set3: DEFAULT_INTER_TX_TYPE_SET3_CDF, + txb_skip: DEFAULT_TXB_SKIP_CDF[idx], + eob_pt_16: DEFAULT_EOB_PT_16_CDF[idx], + eob_pt_32: DEFAULT_EOB_PT_32_CDF[idx], + eob_pt_64: DEFAULT_EOB_PT_64_CDF[idx], + eob_pt_128: DEFAULT_EOB_PT_128_CDF[idx], + eob_pt_256: DEFAULT_EOB_PT_256_CDF[idx], + eob_pt_512: DEFAULT_EOB_PT_512_CDF[idx], + eob_pt_1024: DEFAULT_EOB_PT_1024_CDF[idx], + eob_extra: DEFAULT_EOB_EXTRA_CDF[idx], + dc_sign: DEFAULT_DC_SIGN_CDF[idx], + coeff_base_eob: DEFAULT_COEFF_BASE_EOB_CDF[idx], + coeff_base: DEFAULT_COEFF_BASE_CDF[idx], + coeff_br: DEFAULT_COEFF_BR_CDF[idx], + }) + } +} + +impl Cdfs { + /// The counter reset of load_cdfs( ): the last entry of every CDF array is set to 0. + pub fn clear_counts(&mut self) { + macro_rules! clr { + ($($f:ident),*) => { $( self.$f.clear_count(); )* }; + } + clr!( + y_mode, + uv_mode_cfl_not_allowed, + uv_mode_cfl_allowed, + angle_delta, + partition_w8, + partition_w16, + partition_w32, + partition_w64, + skip, + is_inter, + single_ref, + new_mv, + zero_mv, + ref_mv, + drl_mode, + mv_joint, + mv_class, + mv_class0_bit, + mv_fr, + mv_class0_fr, + mv_sign, + mv_bit, + intra_tx_type_set2, + inter_tx_type_set3, + txb_skip, + eob_pt_16, + eob_pt_32, + eob_pt_64, + eob_pt_128, + eob_pt_256, + eob_pt_512, + eob_pt_1024, + eob_extra, + dc_sign, + coeff_base_eob, + coeff_base, + coeff_br + ); + } +} + +/// Zeroes the adaptation counter (last element) of every innermost CDF array. +trait ClearCount { + fn clear_count(&mut self); +} + +impl ClearCount for [u16; N] { + fn clear_count(&mut self) { + self[N - 1] = 0; + } +} + +impl ClearCount for [T; M] { + fn clear_count(&mut self) { + for e in self.iter_mut() { + e.clear_count(); + } + } +} diff --git a/crates/av1enc/src/decide.rs b/crates/av1enc/src/decide.rs new file mode 100644 index 00000000..e71ad6e9 --- /dev/null +++ b/crates/av1enc/src/decide.rs @@ -0,0 +1,874 @@ +//! Mode decision and reconstruction (not normative): partitions, intra modes, motion vectors, +//! transform coefficients. Every prediction and reconstruction step uses the normative +//! processes, so the reconstruction built here is exactly what a decoder produces; the syntax +//! is written afterwards by `writer.rs`. + +use crate::pred::*; +use crate::tables::*; +use crate::transform::{forward_2d, inverse_2d}; + +/// Decisions for one coded block. +#[derive(Clone)] +pub(crate) struct Leaf { + pub bsize: usize, + pub is_inter: bool, + pub y_mode: usize, + pub uv_mode: usize, + pub tx_type_y: usize, + /// Motion vector [row, col] in 1/8 luma samples (inter blocks). + pub mv: [i32; 2], + pub skip: bool, + /// Quantised coefficients of the single transform block of each plane (raster order). + pub coefs: [Vec; 3], +} + +pub(crate) enum Node { + Out, + Leaf(Box), + Split(Box<[Node; 4]>), +} + +/// A reconstructed frame kept as a reference. +pub(crate) struct RefFrame { + pub planes: [Plane; 3], + /// Motion vectors chosen for the frame (per 4x4), used as search candidates. + pub mvs: Vec<[i32; 2]>, + /// The frame's loop filter level. + pub lf_level: u32, + /// The CDFs saved at the end of the frame (loaded by the next frame). + pub cdfs: Box, +} + +pub(crate) struct FrameGeom { + pub width: usize, + pub height: usize, + pub mi_cols: usize, + pub mi_rows: usize, + pub sb_cols: usize, + pub sb_rows: usize, +} + +impl FrameGeom { + pub fn new(width: usize, height: usize) -> FrameGeom { + let mi_cols = 2 * width.div_ceil(8); + let mi_rows = 2 * height.div_ceil(8); + FrameGeom { width, height, mi_cols, mi_rows, sb_cols: mi_cols.div_ceil(16), sb_rows: mi_rows.div_ceil(16) } + } + /// Padded plane size (whole superblocks). + pub fn padded(&self, plane: usize) -> (usize, usize) { + let s = (plane > 0) as usize; + ((self.sb_cols * 64) >> s, (self.sb_rows * 64) >> s) + } + /// Visible plane size. + pub fn visible(&self, plane: usize) -> (usize, usize) { + if plane == 0 { (self.width, self.height) } else { (self.width.div_ceil(2), self.height.div_ceil(2)) } + } +} + +pub(crate) fn tx_size_for(log2: u32) -> usize { + match log2 { + 2 => TX_4X4, + 3 => TX_8X8, + 4 => TX_16X16, + 5 => TX_32X32, + _ => TX_64X64, + } +} + +/// get_tx_set( ) for the square sizes used here with reduced_tx_set = 1. +pub(crate) fn tx_set(tx_sz: usize, is_inter: bool) -> usize { + let sqr_up = TX_SIZE_SQR_UP[tx_sz] as usize; + if sqr_up > TX_32X32 { + return TX_SET_DCTONLY; + } + if is_inter { + TX_SET_INTER_3 + } else if sqr_up == TX_32X32 { + TX_SET_DCTONLY + } else { + TX_SET_INTRA_2 + } +} + +pub(crate) fn tx_in_set(set: usize, is_inter: bool, tx_type: usize) -> bool { + if is_inter { TX_TYPE_IN_SET_INTER[set][tx_type] == 1 } else { TX_TYPE_IN_SET_INTRA[set][tx_type] == 1 } +} + +/// The chroma transform type of an intra block (compute_tx_type for plane > 0). +pub(crate) fn intra_uv_tx_type(uv_mode: usize, uv_tx: usize) -> usize { + let t = MODE_TO_TXFM[uv_mode] as usize; + if tx_in_set(tx_set(uv_tx, false), false, t) { t } else { DCT_DCT } +} + +struct TxResult { + levels: Vec, + nonzero: bool, + dist: f64, + bits: f64, +} + +/// Saved encoder state of a block area (reconstruction, BlockDecoded flags). +struct Snapshot { + planes: [Vec; 3], + decoded: Box<[[[bool; 35]; 35]; 3]>, +} + +pub(crate) struct Decider<'a> { + pub g: &'a FrameGeom, + pub bd: u32, + pub intra_only: bool, + pub src: &'a [Plane; 3], + pub rec: [Plane; 3], + pub refr: Option<&'a RefFrame>, + decoded: Box<[[[bool; 35]; 35]; 3]>, + /// Decision motion vectors per 4x4 (search candidates). + pub mvs: Vec<[i32; 2]>, + dc_q: f32, + ac_q: f32, + dc_qi: i64, + ac_qi: i64, + lambda: f64, + lambda_sad: f64, + /// Half-sample interpolations of the reference luma (visible area): (8, 0), (0, 8), (8, 8). + half: [Vec; 3], +} + +const MODE_BITS_INTRA: f64 = 3.0; + +impl<'a> Decider<'a> { + pub fn new(g: &'a FrameGeom, bd: u32, qidx: u8, intra_only: bool, src: &'a [Plane; 3], refr: Option<&'a RefFrame>) -> Self { + let bdi = ((bd - 8) >> 1) as usize; + let dc_qi = DC_QLOOKUP[bdi][qidx as usize] as i64; + let ac_qi = AC_QLOOKUP[bdi][qidx as usize] as i64; + // Lagrangian multiplier in squared-sample units of the coded bit depth. + let step = ac_qi as f64 / 8.0; + let lambda = 0.25 * step * step; + let rec = std::array::from_fn(|p| { + let (w, h) = g.padded(p); + Plane::new(w, h) + }); + Decider { + g, + bd, + intra_only, + src, + rec, + refr, + decoded: Box::new([[[false; 35]; 35]; 3]), + mvs: vec![[0, 0]; g.mi_cols * g.mi_rows], + dc_q: dc_qi as f32, + ac_q: ac_qi as f32, + dc_qi, + ac_qi, + lambda, + lambda_sad: lambda.sqrt(), + half: match refr { + Some(rf) => [(8, 0), (0, 8), (8, 8)].map(|(fx, fy)| subpel_plane(&rf.planes[0], g.width, g.height, fx, fy, bd)), + None => Default::default(), + }, + } + } + + /// Decides one 64x64 superblock (reconstruction is written to `rec`). + pub fn superblock(&mut self, r: usize, c: usize) -> Node { + // clear_block_decoded_flags( r, c, sbSize4 ) + for plane in 0..3 { + let s = (plane > 0) as usize; + let sb_w4 = (self.g.mi_cols as isize - c as isize) >> s; + let sb_h4 = (self.g.mi_rows as isize - r as isize) >> s; + let bd = &mut self.decoded[plane]; + for y in -1..=((16 >> s) as isize) { + for x in -1..=((16 >> s) as isize) { + let v = if y < 0 && x < sb_w4 { true } else { x < 0 && y < sb_h4 }; + bd[(y + 1) as usize][(x + 1) as usize] = v; + } + } + bd[(16 >> s) + 1][0] = false; + } + self.partition(r, c, BLOCK_64X64, None).0 + } + + fn decoded_at(&self, plane: usize, y: isize, x: isize) -> bool { + let (yy, xx) = (y + 1, x + 1); + if yy < 0 || xx < 0 || yy > 34 || xx > 34 { + return false; + } + self.decoded[plane][yy as usize][xx as usize] + } + + /// Marks the transform block of `plane` covering the block at (r, c) as decoded. + fn mark_decoded(&mut self, plane: usize, r: usize, c: usize, n4: usize) { + let s = (plane > 0) as usize; + let by = (r & 15) >> s; + let bx = (c & 15) >> s; + let step = (n4 >> s).max(1); + for i in 0..step { + for j in 0..step { + if by + i + 1 < 35 && bx + j + 1 < 35 { + self.decoded[plane][by + i + 1][bx + j + 1] = true; + } + } + } + } + + fn snapshot(&self, r: usize, c: usize, n4: usize) -> Snapshot { + let planes = std::array::from_fn(|p| { + let s = (p > 0) as usize; + let (x0, y0, n) = ((c * 4) >> s, (r * 4) >> s, (n4 * 4) >> s); + let pl = &self.rec[p]; + let mut v = Vec::with_capacity(n * n); + for y in y0..y0 + n { + v.extend_from_slice(&pl.data[y * pl.stride + x0..y * pl.stride + x0 + n]); + } + v + }); + Snapshot { planes, decoded: self.decoded.clone() } + } + + fn restore(&mut self, snap: &Snapshot, r: usize, c: usize, n4: usize) { + for p in 0..3 { + let s = (p > 0) as usize; + let (x0, y0, n) = ((c * 4) >> s, (r * 4) >> s, (n4 * 4) >> s); + let pl = &mut self.rec[p]; + for (i, y) in (y0..y0 + n).enumerate() { + pl.data[y * pl.stride + x0..y * pl.stride + x0 + n].copy_from_slice(&snap.planes[p][i * n..i * n + n]); + } + } + self.decoded.clone_from(&snap.decoded); + } + + fn set_mvs(&mut self, r: usize, c: usize, n4: usize, mv: [i32; 2]) { + for y in r..(r + n4).min(self.g.mi_rows) { + for x in c..(c + n4).min(self.g.mi_cols) { + self.mvs[y * self.g.mi_cols + x] = mv; + } + } + } + + fn partition(&mut self, r: usize, c: usize, bsize: usize, parent_mv: Option<[i32; 2]>) -> (Node, f64) { + let g = self.g; + if r >= g.mi_rows || c >= g.mi_cols { + return (Node::Out, 0.0); + } + let n4 = NUM_4X4_BLOCKS_WIDE[bsize] as usize; + let half = n4 >> 1; + let has_rows = r + half < g.mi_rows; + let has_cols = c + half < g.mi_cols; + let can_none = has_rows && has_cols && (bsize != BLOCK_64X64 || !self.intra_only); + let can_split = bsize > BLOCK_8X8; + let sub = PARTITION_SUBSIZE[PARTITION_SPLIT][bsize] as usize; + if !can_none { + return self.split(r, c, sub, half, parent_mv); + } + let before = self.snapshot(r, c, n4); + let (leaf, cost_none) = self.leaf(r, c, bsize, parent_mv); + let cost_none = cost_none + self.lambda * 1.0; + if !can_split { + return (Node::Leaf(Box::new(leaf)), cost_none); + } + // a cheap skip block rarely gains from splitting + if leaf.skip && cost_none < self.lambda * 8.0 { + return (Node::Leaf(Box::new(leaf)), cost_none); + } + let after_none = self.snapshot(r, c, n4); + self.restore(&before, r, c, n4); + let pm = if leaf.is_inter { Some(leaf.mv) } else { parent_mv }; + let (node, cost_split) = self.split(r, c, sub, half, pm); + let cost_split = cost_split + self.lambda * 2.0; + if cost_split < cost_none { + (node, cost_split) + } else { + self.restore(&after_none, r, c, n4); + self.set_mvs(r, c, n4, if leaf.is_inter { leaf.mv } else { [0, 0] }); + (Node::Leaf(Box::new(leaf)), cost_none) + } + } + + fn split(&mut self, r: usize, c: usize, sub: usize, half: usize, pm: Option<[i32; 2]>) -> (Node, f64) { + let (n0, c0) = self.partition(r, c, sub, pm); + let (n1, c1) = self.partition(r, c + half, sub, pm); + let (n2, c2) = self.partition(r + half, c, sub, pm); + let (n3, c3) = self.partition(r + half, c + half, sub, pm); + (Node::Split(Box::new([n0, n1, n2, n3])), c0 + c1 + c2 + c3) + } + + /// Best coding of the block as a single (NONE partition) block. + fn leaf(&mut self, r: usize, c: usize, bsize: usize, parent_mv: Option<[i32; 2]>) -> (Leaf, f64) { + let n4 = NUM_4X4_BLOCKS_WIDE[bsize] as usize; + if self.intra_only { + let res = self.intra_leaf(r, c, bsize); + self.set_mvs(r, c, n4, [0, 0]); + return res; + } + let before = self.snapshot(r, c, n4); + let (inter, cost_inter) = self.inter_leaf(r, c, bsize, parent_mv); + if bsize == BLOCK_64X64 || inter.skip { + self.set_mvs(r, c, n4, inter.mv); + return (inter, cost_inter); + } + let after_inter = self.snapshot(r, c, n4); + self.restore(&before, r, c, n4); + let (intra, cost_intra) = self.intra_leaf(r, c, bsize); + if cost_intra < cost_inter { + self.set_mvs(r, c, n4, [0, 0]); + (intra, cost_intra) + } else { + self.restore(&after_inter, r, c, n4); + self.set_mvs(r, c, n4, inter.mv); + (inter, cost_inter) + } + } + + fn intra_leaf(&mut self, r: usize, c: usize, bsize: usize) -> (Leaf, f64) { + let n4 = NUM_4X4_BLOCKS_WIDE[bsize] as usize; + let log2 = (n4 * 4).trailing_zeros(); + let (sbr, sbc) = ((r & 15) as isize, (c & 15) as isize); + let bd = self.bd; + let mut pred = vec![0u16; 32 * 32]; + let mut best_pred = vec![0u16; 32 * 32]; + let n = 1usize << log2; + // luma + let e = IntraEdge { + x: c * 4, + y: r * 4, + have_left: c > 0, + have_above: r > 0, + have_above_right: self.decoded_at(0, sbr - 1, sbc + n4 as isize), + have_below_left: self.decoded_at(0, sbr + n4 as isize, sbc - 1), + log2, + max_x: (self.g.mi_cols * 4) as i32 - 1, + max_y: (self.g.mi_rows * 4) as i32 - 1, + bit_depth: bd, + }; + let ed = edges(&self.rec[0], &e); + // modes ranked by SATD, then a rate-distortion choice of mode and transform type among + // the best three + let mut ranked: Vec<(f64, usize)> = INTRA_MODES_USED + .iter() + .map(|&mode| { + predict_intra(&ed, &e, mode, &mut pred); + let sad = self.satd(0, c * 4, r * 4, n, &pred[..n * n]) as f64; + (sad + self.lambda_sad * if mode == DC_PRED { 1.0 } else { MODE_BITS_INTRA }, mode) + }) + .collect(); + ranked.sort_by(|a, b| a.0.total_cmp(&b.0)); + let tx_y = tx_size_for(log2); + let mut best: Option<(f64, usize, usize, TxResult, Option>)> = None; + for &(_, mode) in ranked.iter().take(3) { + predict_intra(&ed, &e, mode, &mut pred); + let mut types = vec![DCT_DCT]; + let alt = MODE_TO_TXFM[mode] as usize; + if alt != DCT_DCT && tx_in_set(tx_set(tx_y, false), false, alt) { + types.push(alt); + } + for &t in &types { + let (res, recon) = self.try_tx(0, c * 4, r * 4, log2, t, &pred[..n * n], true); + let type_bits = if !res.nonzero || tx_set(tx_y, false) == TX_SET_DCTONLY { + 0.0 + } else if t == DCT_DCT { + 1.0 + } else { + 2.0 + }; + let mode_bits = if mode == DC_PRED { 1.0 } else { MODE_BITS_INTRA }; + let cost = res.dist + self.lambda * (res.bits + type_bits + mode_bits); + if best.as_ref().is_none_or(|b| cost < b.0) { + best_pred[..n * n].copy_from_slice(&pred[..n * n]); + best = Some((cost, mode, t, res, recon)); + } + } + } + let (_, y_mode, tx_type_y, ty, recon) = best.expect("an intra mode"); + self.put(0, c * 4, r * 4, n, recon.as_deref().unwrap_or(&best_pred[..n * n])); + let tx_type_y = if ty.nonzero { tx_type_y } else { DCT_DCT }; + self.mark_decoded(0, r, c, n4); + // chroma (one mode for both planes) + let cn = n >> 1; + let clog2 = log2 - 1; + let (csbr, csbc) = (sbr >> 1, sbc >> 1); + let ce: [IntraEdge; 2] = std::array::from_fn(|i| IntraEdge { + x: c * 2, + y: r * 2, + have_left: c > 0, + have_above: r > 0, + have_above_right: self.decoded_at(1 + i, csbr - 1, csbc + (n4 >> 1) as isize), + have_below_left: self.decoded_at(1 + i, csbr + (n4 >> 1) as isize, csbc - 1), + log2: clog2, + max_x: (self.g.mi_cols * 2) as i32 - 1, + max_y: (self.g.mi_rows * 2) as i32 - 1, + bit_depth: bd, + }); + let ced = [edges(&self.rec[1], &ce[0]), edges(&self.rec[2], &ce[1])]; + let mut best_uv = (f64::MAX, DC_PRED); + for &mode in INTRA_MODES_USED.iter() { + let mut sad = 0u64; + for i in 0..2 { + predict_intra(&ced[i], &ce[i], mode, &mut pred); + sad += self.satd(1 + i, c * 2, r * 2, cn, &pred[..cn * cn]); + } + let cost = sad as f64 + self.lambda_sad * if mode == DC_PRED { 1.0 } else { 2.5 }; + if cost < best_uv.0 { + best_uv = (cost, mode); + } + } + let uv_mode = best_uv.1; + let uv_tx = tx_size_for(clog2); + let uv_type = intra_uv_tx_type(uv_mode, uv_tx); + let mut tuv = Vec::with_capacity(2); + for i in 0..2 { + predict_intra(&ced[i], &ce[i], uv_mode, &mut pred); + tuv.push(self.code_tx(1 + i, c * 2, r * 2, clog2, uv_type, &pred[..cn * cn], true)); + self.mark_decoded(1 + i, r, c, n4); + } + let skip = !ty.nonzero && !tuv[0].nonzero && !tuv[1].nonzero; + let dist = ty.dist + tuv[0].dist + tuv[1].dist; + let bits = if skip { 1.0 } else { ty.bits + tuv[0].bits + tuv[1].bits + 1.0 } + MODE_BITS_INTRA + 2.0; + let [u, v]: [TxResult; 2] = tuv.try_into().ok().expect("two chroma planes"); + let leaf = Leaf { bsize, is_inter: false, y_mode, uv_mode, tx_type_y, mv: [0, 0], skip, coefs: [ty.levels, u.levels, v.levels] }; + (leaf, dist + self.lambda * bits) + } + + fn inter_leaf(&mut self, r: usize, c: usize, bsize: usize, parent_mv: Option<[i32; 2]>) -> (Leaf, f64) { + let n4 = NUM_4X4_BLOCKS_WIDE[bsize] as usize; + let n = n4 * 4; + let log2 = n.trailing_zeros(); + let (mv, mv_bits) = self.motion_search(r, c, n, parent_mv); + let refr = self.refr.expect("reference frame"); + let mut pred = vec![0u16; n * n]; + let mut tx = Vec::with_capacity(3); + let mut dist = 0.0; + for p in 0..3 { + let s = (p > 0) as usize; + let (vw, vh) = self.g.visible(p); + let pn = n >> s; + let (x, y) = ((c * 4) >> s, (r * 4) >> s); + predict_inter(&refr.planes[p], vw as i32 - 1, vh as i32 - 1, x, y, pn, pn, mv, s, self.bd, &mut pred[..pn * pn]); + if bsize == BLOCK_64X64 { + // coded as a skip block: the prediction is the reconstruction + dist += self.sse(p, x, y, pn, &pred[..pn * pn]); + self.put(p, x, y, pn, &pred[..pn * pn]); + } else { + let t = self.code_tx(p, x, y, log2 - s as u32, DCT_DCT, &pred[..pn * pn], false); + dist += t.dist; + tx.push(t); + } + self.mark_decoded(p, r, c, n4); + } + let mode_bits = if mv == [0, 0] { 1.5 } else { 2.0 + mv_bits }; + if bsize == BLOCK_64X64 { + let leaf = Leaf { bsize, is_inter: true, y_mode: NEWMV, uv_mode: DC_PRED, tx_type_y: DCT_DCT, mv, skip: true, coefs: Default::default() }; + return (leaf, dist + self.lambda * (mode_bits + 1.0)); + } + let skip = tx.iter().all(|t| !t.nonzero); + let bits = mode_bits + if skip { 1.0 } else { 1.0 + tx.iter().map(|t| t.bits).sum::() }; + let [y, u, v]: [TxResult; 3] = tx.try_into().ok().expect("three planes"); + let leaf = Leaf { bsize, is_inter: true, y_mode: NEWMV, uv_mode: DC_PRED, tx_type_y: DCT_DCT, mv, skip, coefs: [y.levels, u.levels, v.levels] }; + (leaf, dist + self.lambda * bits) + } + + /// Full-sample motion search: candidate vectors then a shrinking diamond search. + /// Returns the vector and an estimate of its coding cost in bits. + fn motion_search(&self, r: usize, c: usize, n: usize, parent_mv: Option<[i32; 2]>) -> ([i32; 2], f64) { + let g = self.g; + let refr = self.refr.expect("reference frame"); + let mi = |rr: usize, cc: usize| self.mvs[rr * g.mi_cols + cc]; + let mut pred_mv = [0, 0]; + let mut cands: Vec<[i32; 2]> = vec![[0, 0]]; + if c > 0 { + pred_mv = mi(r, c - 1); + cands.push(pred_mv); + } + if r > 0 { + cands.push(mi(r - 1, c)); + let cr = c + n / 4; + if cr < g.mi_cols { + cands.push(mi(r - 1, cr)); + } + } + if let Some(p) = parent_mv { + cands.push(p); + } + cands.push(refr.mvs[r * g.mi_cols + c]); + let (x0, y0) = ((c * 4) as i32, (r * 4) as i32); + let (w, h) = (g.width as i32, g.height as i32); + // keep the referenced block within 64 samples of the picture + let lim_x = (-x0 - n as i32 - 64, w - x0 + 64); + let lim_y = (-y0 - n as i32 - 64, h - y0 + 64); + let valid = |m: [i32; 2]| { + let (dy, dx) = (m[0] >> 3, m[1] >> 3); + dx >= lim_x.0 && dx <= lim_x.1 && dy >= lim_y.0 && dy <= lim_y.1 + }; + let mv_bits = |m: [i32; 2]| -> f64 { + let comp = |d: i32| if d == 0 { 0.5 } else { 2.0 + 2.0 * ((d.abs() / 8) as f64 + 1.0).log2() }; + comp(m[0] - pred_mv[0]) + comp(m[1] - pred_mv[1]) + }; + let cost = + |m: [i32; 2]| -> f64 { self.sad_ref(&refr.planes[0], x0 + (m[1] >> 3), y0 + (m[0] >> 3), c * 4, r * 4, n) as f64 + self.lambda_sad * mv_bits(m) }; + let mut best = ([0, 0], f64::MAX); + for &m in &cands { + let m = [m[0] & !7, m[1] & !7]; + if !valid(m) { + continue; + } + let cst = cost(m); + if cst < best.1 { + best = (m, cst); + } + } + let mut step = 8 * if n >= 32 { + 16 + } else if n == 16 { + 8 + } else { + 4 + }; + while step >= 8 { + let mut moved = true; + let mut iters = 0; + while moved && iters < 16 { + moved = false; + iters += 1; + let center = best.0; + for d in [[-step, 0], [step, 0], [0, -step], [0, step]] { + let m = [center[0] + d[0], center[1] + d[1]]; + if !valid(m) { + continue; + } + let cst = cost(m); + if cst < best.1 { + best = (m, cst); + moved = true; + } + } + } + step >>= 1; + } + // half- then quarter-sample refinement (plus the exact neighbour candidates) + let mut buf = vec![0u16; n * n]; + let sub_cost = |m: [i32; 2], buf: &mut Vec| -> f64 { + self.search_block(m, c * 4, r * 4, n, buf); + self.sad(0, c * 4, r * 4, n, buf) as f64 + self.lambda_sad * mv_bits(m) + }; + for m in cands { + if m[0] & 7 != 0 || m[1] & 7 != 0 { + let m = [m[0] & !1, m[1] & !1]; + if valid(m) { + let cst = sub_cost(m, &mut buf); + if cst < best.1 { + best = (m, cst); + } + } + } + } + for step in [4, 2] { + let center = best.0; + for d in [[-1, -1], [-1, 0], [-1, 1], [0, -1], [0, 1], [1, -1], [1, 0], [1, 1]] { + let m = [center[0] + d[0] * step, center[1] + d[1] * step]; + if !valid(m) { + continue; + } + let cst = sub_cost(m, &mut buf); + if cst < best.1 { + best = (m, cst); + } + } + } + (best.0, mv_bits(best.0)) + } + + /// Approximate luma prediction for motion search: exact at whole and half-sample + /// positions (precomputed planes), averaged neighbours at quarter-sample positions. + fn search_block(&self, mv: [i32; 2], x: usize, y: usize, n: usize, out: &mut [u16]) { + let refr = self.refr.expect("reference frame"); + let (w, h) = (self.g.width as i32, self.g.height as i32); + // per axis: up to two (integer offset, half) taps + let axis = |v: i32| -> ([(i32, bool); 2], usize) { + let p = 2 * v; // 1/16 units + let (i, f) = (p >> 4, p & 15); + match f { + 0 => ([(i, false), (i, false)], 1), + 8 => ([(i, true), (i, true)], 1), + 4 => ([(i, false), (i, true)], 2), + _ => ([(i, true), (i + 1, false)], 2), + } + }; + let (ax, nx) = axis(mv[1]); + let (ay, ny) = axis(mv[0]); + let taps = nx.max(ny); + let fetch = |k: usize, xx: i32, yy: i32| -> u16 { + let (ox, hx) = ax[k.min(nx - 1)]; + let (oy, hy) = ay[k.min(ny - 1)]; + let sx = (xx + ox).clamp(0, w - 1) as usize; + let sy = (yy + oy).clamp(0, h - 1) as usize; + match (hx, hy) { + (false, false) => refr.planes[0].at(sx, sy), + (true, false) => self.half[0][sy * w as usize + sx], + (false, true) => self.half[1][sy * w as usize + sx], + (true, true) => self.half[2][sy * w as usize + sx], + } + }; + let vw = n.min(self.g.width.saturating_sub(x)); + let vh = n.min(self.g.height.saturating_sub(y)); + // fast path: every tap inside the picture + let (x0, y0) = (x as i32, y as i32); + let inside = (0..taps).all(|k| { + let (ox, _) = ax[k.min(nx - 1)]; + let (oy, _) = ay[k.min(ny - 1)]; + x0 + ox >= 0 && y0 + oy >= 0 && x0 + ox + vw as i32 <= w && y0 + oy + vh as i32 <= h + }); + if inside { + let src = |k: usize| -> (&[u16], usize, usize) { + let (ox, hx) = ax[k.min(nx - 1)]; + let (oy, hy) = ay[k.min(ny - 1)]; + let (sx, sy) = ((x0 + ox) as usize, (y0 + oy) as usize); + match (hx, hy) { + (false, false) => (&refr.planes[0].data, refr.planes[0].stride, sy * refr.planes[0].stride + sx), + (true, false) => (&self.half[0], w as usize, sy * w as usize + sx), + (false, true) => (&self.half[1], w as usize, sy * w as usize + sx), + (true, true) => (&self.half[2], w as usize, sy * w as usize + sx), + } + }; + let (a, sa, oa) = src(0); + if taps == 1 { + for i in 0..vh { + out[i * n..i * n + vw].copy_from_slice(&a[oa + i * sa..oa + i * sa + vw]); + } + } else { + let (b, sb, ob) = src(1); + for i in 0..vh { + let (ra, rb) = (&a[oa + i * sa..oa + i * sa + vw], &b[ob + i * sb..ob + i * sb + vw]); + for (o, (&p, &q)) in out[i * n..i * n + vw].iter_mut().zip(ra.iter().zip(rb)) { + *o = ((p as u32 + q as u32 + 1) >> 1) as u16; + } + } + } + return; + } + for i in 0..vh { + for j in 0..vw { + let (xx, yy) = (x0 + j as i32, y0 + i as i32); + out[i * n + j] = if taps == 1 { fetch(0, xx, yy) } else { ((fetch(0, xx, yy) as u32 + fetch(1, xx, yy) as u32 + 1) >> 1) as u16 }; + } + } + } + + /// SAD of the source block at (x, y) (luma) against the reference block at (rx, ry). + fn sad_ref(&self, refp: &Plane, rx: i32, ry: i32, x: usize, y: usize, n: usize) -> u64 { + let (vw, vh) = (self.g.width, self.g.height); + let w = n.min(vw.saturating_sub(x)); + let h = n.min(vh.saturating_sub(y)); + let src = &self.src[0]; + let (lx, ly) = (vw as i32 - 1, vh as i32 - 1); + let mut sad = 0u64; + let inside = rx >= 0 && ry >= 0 && rx + w as i32 - 1 <= lx && ry + h as i32 - 1 <= ly; + for i in 0..h { + let s = &src.row(y + i)[x..x + w]; + let rr = refp.row((ry + i as i32).clamp(0, ly) as usize); + if inside { + let rrow = &rr[rx as usize..rx as usize + w]; + sad += s.iter().zip(rrow).map(|(&a, &b)| (a as i32 - b as i32).unsigned_abs()).sum::() as u64; + } else { + for j in 0..w { + let b = rr[(rx + j as i32).clamp(0, lx) as usize]; + sad += (s[j] as i32 - b as i32).unsigned_abs() as u64; + } + } + } + sad + } + + /// Sum of absolute Hadamard-transformed differences (4x4 or 8x8 sub-blocks) of a prediction + /// against the source, scaled to about the size of the SAD. + fn satd(&self, p: usize, x: usize, y: usize, n: usize, pred: &[u16]) -> u64 { + let src = &self.src[p]; + let k = n.min(8); + let mut total = 0u64; + let mut d = [0i32; 64]; + for by in (0..n).step_by(k) { + for bx in (0..n).step_by(k) { + for i in 0..k { + let row = &src.row(y + by + i)[x + bx..x + bx + k]; + for j in 0..k { + d[i * k + j] = row[j] as i32 - pred[(by + i) * n + bx + j] as i32; + } + } + for i in 0..k { + hadamard(&mut d[i * k..i * k + k]); + } + let mut col = [0i32; 8]; + let mut s = 0u64; + for j in 0..k { + for i in 0..k { + col[i] = d[i * k + j]; + } + hadamard(&mut col[..k]); + s += col[..k].iter().map(|v| v.unsigned_abs() as u64).sum::(); + } + total += s / (k as u64 / 2); + } + } + total + } + + /// SAD of a prediction against the source over the visible part of the block. + fn sad(&self, p: usize, x: usize, y: usize, n: usize, pred: &[u16]) -> u64 { + let (vw, vh) = self.g.visible(p); + let w = n.min(vw.saturating_sub(x)); + let h = n.min(vh.saturating_sub(y)); + let src = &self.src[p]; + let mut s = 0u64; + for i in 0..h { + let row = &src.row(y + i)[x..x + w]; + s += row.iter().zip(&pred[i * n..i * n + w]).map(|(&a, &b)| (a as i32 - b as i32).unsigned_abs()).sum::() as u64; + } + s + } + + fn sse(&self, p: usize, x: usize, y: usize, n: usize, blk: &[u16]) -> f64 { + let (vw, vh) = self.g.visible(p); + let w = n.min(vw.saturating_sub(x)); + let h = n.min(vh.saturating_sub(y)); + let src = &self.src[p]; + let mut s = 0u64; + for i in 0..h { + let row = &src.row(y + i)[x..x + w]; + s += row + .iter() + .zip(&blk[i * n..i * n + w]) + .map(|(&a, &b)| { + let d = a as i64 - b as i64; + (d * d) as u64 + }) + .sum::(); + } + s as f64 + } + + fn put(&mut self, p: usize, x: usize, y: usize, n: usize, blk: &[u16]) { + let pl = &mut self.rec[p]; + for i in 0..n { + let o = (y + i) * pl.stride + x; + pl.data[o..o + n].copy_from_slice(&blk[i * n..i * n + n]); + } + } + + /// Transform, quantise and reconstruct one square transform block of `plane` at (x, y) + /// over the prediction `pred`; the reconstruction is written to `rec`. + fn code_tx(&mut self, p: usize, x: usize, y: usize, log2: u32, tx_type: usize, pred: &[u16], intra: bool) -> TxResult { + let (res, recon) = self.try_tx(p, x, y, log2, tx_type, pred, intra); + let n = 1usize << log2; + self.put(p, x, y, n, recon.as_deref().unwrap_or(pred)); + res + } + + /// Transform and quantise one block; returns the result and its reconstruction (None: the + /// prediction) without writing it. + fn try_tx(&self, p: usize, x: usize, y: usize, log2: u32, tx_type: usize, pred: &[u16], intra: bool) -> (TxResult, Option>) { + let n = 1usize << log2; + let tx_sz = tx_size_for(log2); + let bd = self.bd; + let src = &self.src[p]; + let mut res = vec![0i32; n * n]; + for i in 0..n { + let row = &src.row(y + i)[x..x + n]; + for j in 0..n { + res[i * n + j] = row[j] as i32 - pred[i * n + j] as i32; + } + } + let mut coef = vec![0f32; n * n]; + forward_2d(&res, &mut coef, n, tx_type); + // dead-zone quantiser: rounding offset below one half + let rnd = if intra { 0.38f32 } else { 0.30f32 }; + let mut levels = vec![0i32; n * n]; + let mut nonzero = false; + for (k, (&cf, l)) in coef.iter().zip(levels.iter_mut()).enumerate() { + let q = if k == 0 { self.dc_q } else { self.ac_q }; + let a = (cf.abs() / q + rnd).floor() as i32; + if a != 0 { + *l = if cf < 0.0 { -a.min(1 << 16) } else { a.min(1 << 16) }; + nonzero = true; + } + } + let zero_dist = self.sse(p, x, y, n, pred); + if !nonzero { + return (TxResult { levels, nonzero, dist: zero_dist, bits: 1.0 }, None); + } + let recon = self.reconstruct(&levels, tx_sz, tx_type, pred, n, bd); + let dist = self.sse(p, x, y, n, &recon); + let bits = estimate_bits(&levels, tx_sz); + if dist + self.lambda * bits >= zero_dist + self.lambda { + levels.iter_mut().for_each(|l| *l = 0); + return (TxResult { levels, nonzero: false, dist: zero_dist, bits: 1.0 }, None); + } + (TxResult { levels, nonzero: true, dist, bits }, Some(recon)) + } + + /// Dequantisation (7.12.3) and the inverse transform, added to the prediction. + fn reconstruct(&self, levels: &[i32], tx_sz: usize, tx_type: usize, pred: &[u16], n: usize, bd: u32) -> Vec { + let denom: i64 = if tx_sz == TX_32X32 { 2 } else { 1 }; + let lim = 1i64 << (7 + bd); + let mut deq = vec![0i32; n * n]; + for (k, (&l, d)) in levels.iter().zip(deq.iter_mut()).enumerate() { + if l == 0 { + continue; + } + let q = if k == 0 { self.dc_qi } else { self.ac_qi }; + let dq = l as i64 * q; + let mag = (dq.abs() & 0xFF_FFFF) / denom; + *d = (if dq < 0 { -mag } else { mag }).clamp(-lim, lim - 1) as i32; + } + let mut resid = vec![0i32; n * n]; + inverse_2d(&deq, &mut resid, tx_sz, tx_type, bd); + let max = (1i32 << bd) - 1; + pred.iter().zip(&resid).map(|(&p, &r)| (p as i32 + r).clamp(0, max) as u16).collect() + } +} + +/// Unnormalised Walsh-Hadamard transform of 4 or 8 values, in place. +fn hadamard(v: &mut [i32]) { + let n = v.len(); + let mut h = 1; + while h < n { + for i in (0..n).step_by(2 * h) { + for j in i..i + h { + let (a, b) = (v[j], v[j + h]); + v[j] = a + b; + v[j + h] = a - b; + } + } + h *= 2; + } +} + +/// Rough coefficient coding cost in bits. +fn estimate_bits(levels: &[i32], tx_sz: usize) -> f64 { + let scan = scan_for(tx_sz); + let mut eob = 0; + for (i, &pos) in scan.iter().enumerate() { + if levels[pos as usize] != 0 { + eob = i + 1; + } + } + if eob == 0 { + return 1.0; + } + let mut bits = 3.0 + (eob as f64).log2(); + for &pos in &scan[..eob] { + let a = levels[pos as usize].unsigned_abs(); + bits += match a { + 0 => 0.7, + 1 => 2.5, + _ => 3.5 + 1.6 * (a as f64).log2(), + }; + } + bits +} + +pub(crate) fn scan_for(tx_sz: usize) -> &'static [u16] { + match tx_sz { + TX_4X4 => &DEFAULT_SCAN_4X4, + TX_8X8 => &DEFAULT_SCAN_8X8, + TX_16X16 => &DEFAULT_SCAN_16X16, + _ => &DEFAULT_SCAN_32X32, + } +} diff --git a/crates/av1enc/src/lib.rs b/crates/av1enc/src/lib.rs new file mode 100644 index 00000000..0eeded9c --- /dev/null +++ b/crates/av1enc/src/lib.rs @@ -0,0 +1,518 @@ +//! Clean-room pure-Rust AV1 encoder (Main profile, 4:2:0, 8 and 10 bit), implemented from the +//! AV1 Bitstream & Decoding Process Specification v1.0.0 with Errata 1. +//! +//! Key frames and single-reference inter frames (LAST_FRAME = the previous frame), 64x64 +//! superblocks split down to 8x8 by rate-distortion decisions, intra modes DC / V / H / Paeth / +//! smooth, full-sample motion search with the spec's motion vector prediction, DCT / ADST +//! transforms up to 32x32, one tile per frame, in-loop filters off. See the crate README. +//! +//! The public API below is the contract `crates/export` codes against; keep it stable. + +mod bitw; +mod cdf; +mod decide; +mod loopfilter; +mod pred; +mod symbol; +mod tables; +mod transform; +mod writer; + +use bitw::{BitWriter, obu}; +use decide::{Decider, FrameGeom, RefFrame}; +use loopfilter::{LfInfo, loop_filter}; +use pred::Plane; +use writer::TileWriter; + +/// Rate control. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum RateControl { + /// Constant quantizer index (`base_q_idx`, 1–255; lower is better). + ConstantQ(u8), + /// Average bitrate target in kbit/s (frame-level quantizer adaptation). + Bitrate { kbps: u32 }, +} + +/// Encoder settings. Main profile (seq_profile 0), 4:2:0. +#[derive(Clone, Debug, PartialEq)] +pub struct EncoderConfig { + pub width: u32, + pub height: u32, + pub fps_num: u32, + pub fps_den: u32, + /// 8 or 10. + pub bit_depth: u8, + /// `seq_level_idx` (e.g. 8 = level 4.0); `None` picks the lowest level that fits. + pub level_idx: Option, + pub rate: RateControl, + /// Frames between key frames (1 = all key frames). + pub keyint: u32, + /// BT.709 limited range is signalled unless this is set. + pub full_range: bool, +} + +impl EncoderConfig { + pub fn new(width: u32, height: u32, fps_num: u32, fps_den: u32) -> Self { + EncoderConfig { width, height, fps_num, fps_den, bit_depth: 8, level_idx: None, rate: RateControl::ConstantQ(100), keyint: 60, full_range: false } + } +} + +/// One input picture, 4:2:0, samples at the configured bit depth. +#[derive(Clone, Copy, Debug)] +pub struct Frame<'a> { + pub y: &'a [u16], + pub u: &'a [u16], + pub v: &'a [u16], + pub y_stride: usize, + pub uv_stride: usize, +} + +/// One temporal unit as stored in MP4 / WebM samples: OBUs with `obu_has_size_field` = 1 and +/// no temporal delimiter (key frames start with the sequence header OBU). +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Packet { + pub data: Vec, + pub keyframe: bool, +} + +#[derive(Debug, PartialEq, Eq)] +pub enum Error { + InvalidConfig(String), +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::InvalidConfig(s) => write!(f, "invalid AV1 encoder config: {s}"), + } + } +} + +impl std::error::Error for Error {} + +const OBU_SEQUENCE_HEADER: u8 = 1; +const OBU_FRAME: u8 = 6; + +/// Annex A level limits: (seq_level_idx, MaxPicSize, MaxHSize, MaxVSize, MaxDisplayRate, +/// MaxBitrate (Main tier, Mbit/s)). +const LEVELS: [(u8, u64, u32, u32, u64, u64); 14] = [ + (0, 147_456, 2048, 1152, 4_423_680, 1_500_000), + (1, 278_784, 2816, 1584, 8_363_520, 3_000_000), + (4, 665_856, 4352, 2448, 19_975_680, 6_000_000), + (5, 1_065_024, 5504, 3096, 31_950_720, 10_000_000), + (8, 2_359_296, 6144, 3456, 70_778_880, 12_000_000), + (9, 2_359_296, 6144, 3456, 141_557_760, 20_000_000), + (12, 8_912_896, 8192, 4352, 267_386_880, 30_000_000), + (13, 8_912_896, 8192, 4352, 534_773_760, 40_000_000), + (14, 8_912_896, 8192, 4352, 1_069_547_520, 60_000_000), + (15, 8_912_896, 8192, 4352, 1_069_547_520, 60_000_000), + (16, 35_651_584, 16384, 8704, 1_069_547_520, 60_000_000), + (17, 35_651_584, 16384, 8704, 2_139_095_040, 100_000_000), + (18, 35_651_584, 16384, 8704, 4_278_190_080, 160_000_000), + (19, 35_651_584, 16384, 8704, 4_278_190_080, 160_000_000), +]; + +/// The lowest level whose limits fit the stream (31 = no level limits when none does). +fn pick_level(cfg: &EncoderConfig) -> u8 { + let pic = cfg.width as u64 * cfg.height as u64; + let rate = pic * cfg.fps_num as u64 / cfg.fps_den.max(1) as u64; + let bitrate = match cfg.rate { + RateControl::Bitrate { kbps } => kbps as u64 * 1000, + RateControl::ConstantQ(_) => 0, + }; + for &(idx, max_pic, max_h, max_v, max_rate, max_br) in LEVELS.iter() { + if pic <= max_pic && cfg.width <= max_h && cfg.height <= max_v && rate <= max_rate && bitrate <= max_br { + return idx; + } + } + 31 +} + +/// Frame-level rate control state for `RateControl::Bitrate`. +struct RateState { + /// Bits per frame at the target rate. + frame_bits: f64, + /// Quantizer index for inter frames (continuous). + q: f64, + /// Accumulated (spent - budget) bits. + debt: f64, +} + +/// AV1 encoder. Frames come out in input order (no hidden/reordered frames). +pub struct Encoder { + cfg: EncoderConfig, + geom: FrameGeom, + level: u8, + seq_obu: Vec, + frame_num: u64, + refr: Option, + rc: Option, +} + +impl Encoder { + pub fn new(cfg: EncoderConfig) -> Result { + if cfg.width == 0 || cfg.height == 0 { + return Err(Error::InvalidConfig("empty frame".into())); + } + if cfg.width > 4096 || cfg.height > 4096 { + return Err(Error::InvalidConfig(format!("{}x{} exceeds the single-tile limit (4096 samples per side)", cfg.width, cfg.height))); + } + if cfg.bit_depth != 8 && cfg.bit_depth != 10 { + return Err(Error::InvalidConfig(format!("bit depth {} (8 or 10 supported)", cfg.bit_depth))); + } + if cfg.fps_num == 0 || cfg.fps_den == 0 { + return Err(Error::InvalidConfig("frame rate".into())); + } + let geom = FrameGeom::new(cfg.width as usize, cfg.height as usize); + if geom.sb_cols * geom.sb_rows > 2304 { + return Err(Error::InvalidConfig(format!("{}x{} exceeds the single-tile area limit (MAX_TILE_AREA)", cfg.width, cfg.height))); + } + if let RateControl::Bitrate { kbps } = cfg.rate + && kbps == 0 + { + return Err(Error::InvalidConfig("zero bitrate".into())); + } + let level = cfg.level_idx.unwrap_or_else(|| pick_level(&cfg)); + if level > 31 { + return Err(Error::InvalidConfig(format!("seq_level_idx {level}"))); + } + let rc = match cfg.rate { + RateControl::ConstantQ(_) => None, + RateControl::Bitrate { kbps } => { + let frame_bits = kbps as f64 * 1000.0 * cfg.fps_den as f64 / cfg.fps_num as f64; + // initial guess from the bits per sample + let bpp = frame_bits / (cfg.width as f64 * cfg.height as f64); + let q = (110.0 - 40.0 * (bpp / 0.1).log2()).clamp(20.0, 230.0); + Some(RateState { frame_bits, q, debt: 0.0 }) + } + }; + let mut enc = Encoder { cfg, geom, level, seq_obu: Vec::new(), frame_num: 0, refr: None, rc }; + let payload = enc.sequence_header_payload(); + obu(OBU_SEQUENCE_HEADER, &payload, &mut enc.seq_obu); + Ok(enc) + } + + pub fn config(&self) -> &EncoderConfig { + &self.cfg + } + + /// The sequence header OBU (with size field). + pub fn sequence_header_obu(&self) -> Vec { + self.seq_obu.clone() + } + + /// AV1CodecConfigurationRecord (`av1C` payload, also the WebM CodecPrivate). + pub fn av1c(&self) -> Vec { + let high = (self.cfg.bit_depth > 8) as u8; + let mut v = vec![ + 0x81, // marker (1), version (1) + self.level & 0x1f, // seq_profile 0, seq_level_idx_0 + (high << 6) | (1 << 3) | (1 << 2), // tier 0, high_bitdepth, twelve_bit 0, mono 0, ss_x 1, ss_y 1, csp 0 + 0, // no initial_presentation_delay + ]; + v.extend_from_slice(&self.seq_obu); + v + } + + /// sequence_header_obu( ) payload (5.5) including trailing bits. + fn sequence_header_payload(&self) -> Vec { + let c = &self.cfg; + let mut w = BitWriter::new(); + w.f(3, 0); // seq_profile + w.f(1, 0); // still_picture + w.f(1, 0); // reduced_still_picture_header + w.f(1, 0); // timing_info_present_flag + w.f(1, 0); // initial_display_delay_present_flag + w.f(5, 0); // operating_points_cnt_minus_1 + w.f(12, 0); // operating_point_idc[ 0 ] + w.f(5, self.level as u32); // seq_level_idx[ 0 ] + if self.level > 7 { + w.f(1, 0); // seq_tier[ 0 ] + } + let wbits = 32 - (c.width - 1).max(1).leading_zeros(); + let hbits = 32 - (c.height - 1).max(1).leading_zeros(); + w.f(4, wbits - 1); + w.f(4, hbits - 1); + w.f(wbits, c.width - 1); + w.f(hbits, c.height - 1); + w.f(1, 0); // frame_id_numbers_present_flag + w.f(1, 0); // use_128x128_superblock + w.f(1, 0); // enable_filter_intra + w.f(1, 0); // enable_intra_edge_filter + w.f(1, 0); // enable_interintra_compound + w.f(1, 0); // enable_masked_compound + w.f(1, 0); // enable_warped_motion + w.f(1, 0); // enable_dual_filter + w.f(1, 0); // enable_order_hint + w.f(1, 0); // seq_choose_screen_content_tools + w.f(1, 0); // seq_force_screen_content_tools + w.f(1, 0); // enable_superres + w.f(1, 0); // enable_cdef + w.f(1, 0); // enable_restoration + // color_config( ) + w.f(1, (c.bit_depth > 8) as u32); // high_bitdepth + w.f(1, 0); // mono_chrome + w.f(1, 1); // color_description_present_flag + w.f(8, 1); // color_primaries: BT.709 + w.f(8, 1); // transfer_characteristics: BT.709 + w.f(8, 1); // matrix_coefficients: BT.709 + w.f(1, c.full_range as u32); // color_range + w.f(2, 0); // chroma_sample_position: unknown + w.f(1, 0); // separate_uv_delta_q + w.f(1, 0); // film_grain_params_present + w.trailing_bits(); + w.into_bytes() + } + + /// uncompressed_header( ) (5.9) for a shown KEY_FRAME or an INTER_FRAME referencing slot 0. + fn frame_header(&self, w: &mut BitWriter, key: bool, qidx: u8, lf_level: u32) { + w.f(1, 0); // show_existing_frame + w.f(2, if key { 0 } else { 1 }); // frame_type + w.f(1, 1); // show_frame + if !key { + w.f(1, 0); // error_resilient_mode + } + w.f(1, 0); // disable_cdf_update + w.f(1, 0); // frame_size_override_flag + if !key { + w.f(3, 0); // primary_ref_frame: LAST_FRAME's saved CDFs + w.f(8, 1); // refresh_frame_flags: slot 0 becomes LAST for the next frame + for _ in 0..7 { + w.f(3, 0); // ref_frame_idx[ i ] + } + w.f(1, 0); // render_and_frame_size_different + w.f(1, 0); // allow_high_precision_mv + w.f(1, 0); // is_filter_switchable + w.f(2, 0); // interpolation_filter = EIGHTTAP + w.f(1, 0); // is_motion_mode_switchable + } else { + w.f(1, 0); // render_and_frame_size_different + } + w.f(1, 0); // disable_frame_end_update_cdf + // tile_info( ): uniform spacing, one tile + let g = &self.geom; + let tile_log2 = |blk: usize, target: usize| { + let mut k = 0; + while (blk << k) < target { + k += 1; + } + k + }; + w.f(1, 1); // uniform_tile_spacing_flag + if tile_log2(1, g.sb_cols.min(64)) > 0 { + w.f(1, 0); // increment_tile_cols_log2 + } + if tile_log2(1, g.sb_rows.min(64)) > 0 { + w.f(1, 0); // increment_tile_rows_log2 + } + // quantization_params( ) + w.f(8, qidx as u32); + w.f(1, 0); // DeltaQYDc + w.f(1, 0); // DeltaQUDc + w.f(1, 0); // DeltaQUAc + w.f(1, 0); // using_qmatrix + w.f(1, 0); // segmentation_enabled + w.f(1, 0); // delta_q_present (base_q_idx > 0) + // loop_filter_params( ): the same level for every plane and direction + w.f(6, lf_level); + w.f(6, lf_level); + if lf_level != 0 { + w.f(6, lf_level); + w.f(6, lf_level); + } + w.f(3, 0); // loop_filter_sharpness + w.f(1, 0); // loop_filter_delta_enabled + w.f(1, 0); // tx_mode_select (TX_MODE_LARGEST) + if !key { + w.f(1, 0); // reference_select + } + w.f(1, 1); // reduced_tx_set + if !key { + for _ in 0..7 { + w.f(1, 0); // is_global + } + } + } + + /// Copies a picture into superblock-padded planes (edge replication). + fn load_source(&self, frame: &Frame) -> [Plane; 3] { + let maxv = (1u16 << self.cfg.bit_depth) - 1; + std::array::from_fn(|p| { + let (vw, vh) = self.geom.visible(p); + let (pw, ph) = self.geom.padded(p); + let (data, stride) = match p { + 0 => (frame.y, frame.y_stride), + 1 => (frame.u, frame.uv_stride), + _ => (frame.v, frame.uv_stride), + }; + assert!(stride >= vw && data.len() >= stride * (vh - 1) + vw, "plane {p} is too small for {vw}x{vh}"); + let mut pl = Plane::new(pw, ph); + for y in 0..ph { + let sy = y.min(vh - 1); + let src = &data[sy * stride..sy * stride + vw]; + let row = &mut pl.data[y * pw..y * pw + pw]; + for (x, d) in row.iter_mut().enumerate() { + *d = src[x.min(vw - 1)].min(maxv); + } + } + pl + }) + } + + /// Codes one frame at `qidx`; returns the frame OBU and the reconstruction. + fn encode_frame(&self, src: &[Plane; 3], key: bool, qidx: u8) -> (Vec, RefFrame) { + let g = &self.geom; + let refr = if key { None } else { self.refr.as_ref() }; + let mut dec = Decider::new(g, self.cfg.bit_depth as u32, qidx, key, src, refr); + let mut tw = TileWriter::new(g, key, qidx, refr.map(|r| &*r.cdfs)); + for sbr in 0..g.sb_rows { + tw.start_sb_row(); + for sbc in 0..g.sb_cols { + let node = dec.superblock(sbr * 16, sbc * 16); + tw.superblock(sbr * 16, sbc * 16, &node); + } + } + let lf_level = self.pick_filter_level(&dec.rec, &tw.lf_info(), src, qidx); + loop_filter(&mut dec.rec, &tw.lf_info(), [lf_level; 4], self.cfg.bit_depth as u32, false, 1); + let (tile, cdfs) = tw.finish(); + let mut w = BitWriter::new(); + self.frame_header(&mut w, key, qidx, lf_level); + w.byte_align(); + // tile_group_obu( ): a single tile, no tile_start_and_end_present_flag + let mut payload = w.into_bytes(); + payload.extend_from_slice(&tile); + let mut out = Vec::with_capacity(payload.len() + 8); + obu(OBU_FRAME, &payload, &mut out); + let rf = RefFrame { planes: dec.rec, mvs: dec.mvs, lf_level, cdfs }; + (out, rf) + } + + /// The loop filter level minimising the luma error (searched around a guess from the + /// quantizer, on the luma of a subset of the superblock rows). + fn pick_filter_level(&self, rec: &[Plane; 3], info: &LfInfo, src: &[Plane; 3], qidx: u8) -> u32 { + let (w, h) = (self.geom.width, self.geom.height); + let sb_row_step = if self.geom.sb_rows >= 9 { 3 } else { 1 }; + let sse = |pl: &Plane| -> u64 { + let mut s = 0u64; + for y in 0..h { + if !(y >> 6).is_multiple_of(sb_row_step) { + continue; + } + for (a, b) in pl.row(y)[..w].iter().zip(&src[0].row(y)[..w]) { + let d = *a as i64 - *b as i64; + s += (d * d) as u64; + } + } + s + }; + let bd = self.cfg.bit_depth as u32; + let mut cache: Vec<(u32, u64)> = vec![(0, sse(&rec[0]))]; + let mut eval = |lvl: u32| -> u64 { + if let Some(&(_, e)) = cache.iter().find(|(l, _)| *l == lvl) { + return e; + } + let mut planes = [rec[0].clone(), Plane::new(0, 0), Plane::new(0, 0)]; + loop_filter(&mut planes, info, [lvl; 4], bd, true, sb_row_step); + let e = sse(&planes[0]); + cache.push((lvl, e)); + e + }; + let guess = ((qidx as u32 * 3) / 16).clamp(2, 50); + let mut best = (0u32, eval(0)); + let mut step = (guess / 2).max(1); + let mut center = guess; + let e = eval(center); + if e < best.1 { + best = (center, e); + } + while step >= 2 { + for lvl in [center.saturating_sub(step), (center + step).min(63)] { + let e = eval(lvl); + if e < best.1 { + best = (lvl, e); + } + } + center = best.0.max(1); + step /= 2; + } + best.0 + } + + /// Encodes one frame. + pub fn encode(&mut self, frame: &Frame) -> Packet { + let key = self.frame_num.is_multiple_of(self.cfg.keyint.max(1) as u64); + let src = self.load_source(frame); + let (frame_obu, rf) = match self.cfg.rate { + RateControl::ConstantQ(q) => self.encode_frame(&src, key, q.max(1)), + RateControl::Bitrate { .. } => self.encode_rate_controlled(&src, key), + }; + self.refr = Some(rf); + self.frame_num += 1; + let mut data = Vec::with_capacity(frame_obu.len() + self.seq_obu.len()); + if key { + data.extend_from_slice(&self.seq_obu); + } + data.extend_from_slice(&frame_obu); + Packet { data, keyframe: key } + } + + fn encode_rate_controlled(&mut self, src: &[Plane; 3], key: bool) -> (Vec, RefFrame) { + let first = self.frame_num == 0; + let rc = self.rc.as_ref().expect("rate control state"); + let (frame_bits, debt, q_inter) = (rc.frame_bits, rc.debt, rc.q); + // key frames get a lower quantizer and a larger share of the budget + let key_boost = 1.0 + (self.cfg.keyint.clamp(1, 60) as f64 - 1.0).sqrt() * 0.75; + let budget = if key { frame_bits * key_boost } else { frame_bits }; + // pay back a fraction of the accumulated deviation + let target = (budget - debt * 0.25).max(budget * 0.25); + let mut q = if key { q_inter - 24.0 } else { q_inter }; + let mut best: Option<(Vec, RefFrame, f64)> = None; + let passes = if first { 4 } else { 1 }; + for pass in 0..passes { + let qi = q.round().clamp(1.0, 255.0) as u8; + let (obu, rf) = self.encode_frame(src, key, qi); + let bits = obu.len() as f64 * 8.0; + let err = (bits / target).log2(); + let better = best.as_ref().is_none_or(|b| err.abs() < (b.2 / target).log2().abs()); + if better { + best = Some((obu, rf, bits)); + } + if pass + 1 < passes { + q = (q + 30.0 * err).clamp(1.0, 255.0); + } + } + let (obu, rf, bits) = best.expect("encoded"); + let rc = self.rc.as_mut().expect("rate control state"); + rc.debt += bits - budget; + // adapt the inter quantizer: about 30 index steps per doubling of the rate + let err = (bits / target).log2().clamp(-1.0, 1.0); + if first && key { + rc.q = (q + 24.0).clamp(1.0, 255.0); + } else { + rc.q = (rc.q + 12.0 * err).clamp(1.0, 255.0); + } + (obu, rf) + } + + /// The loop filter level of the last encoded frame. For tests. + #[doc(hidden)] + pub fn last_loop_filter_level(&self) -> Option { + self.refr.as_ref().map(|r| r.lf_level) + } + + /// The reconstruction of the last encoded frame (visible area, planes Y, U, V), equal to + /// what a decoder outputs. For tests. + #[doc(hidden)] + pub fn last_reconstruction(&self) -> Option<[Vec; 3]> { + let rf = self.refr.as_ref()?; + Some(std::array::from_fn(|p| { + let (vw, vh) = self.geom.visible(p); + let pl = &rf.planes[p]; + let mut v = Vec::with_capacity(vw * vh); + for y in 0..vh { + v.extend_from_slice(&pl.row(y)[..vw]); + } + v + })) + } +} diff --git a/crates/av1enc/src/loopfilter.rs b/crates/av1enc/src/loopfilter.rs new file mode 100644 index 00000000..2add41ed --- /dev/null +++ b/crates/av1enc/src/loopfilter.rs @@ -0,0 +1,197 @@ +//! Deblocking loop filter (spec 7.14), mirrored from the decoder process so that the encoder's +//! references equal a decoder's. Segmentation and loop filter deltas are off and the sharpness +//! is 0 in the frames this encoder writes. + +use crate::pred::Plane; +use crate::tables::*; + +/// Mode info the loop filter reads (per 4x4 luma unit, row stride `mi_cols`), and the +/// transform size per 4x4 unit of each plane (`LoopfilterTxSizes`, row stride `mi_cols + 32`). +pub(crate) struct LfInfo<'a> { + pub mi_cols: usize, + pub mi_rows: usize, + pub width: usize, + pub height: usize, + pub mi_size: &'a [u8], + pub skip: &'a [bool], + pub is_inter: &'a [bool], + pub tx: [&'a [u8]; 3], +} + +/// Applies the loop filter with `loop_filter_level[ 0..4 ]` to `planes`. For level search +/// (`luma_only`), only plane 0 and only every `sb_row_step`-th superblock row are filtered. +pub(crate) fn loop_filter(planes: &mut [Plane; 3], info: &LfInfo, level: [u32; 4], bit_depth: u32, luma_only: bool, sb_row_step: usize) { + if level[0] == 0 && level[1] == 0 { + return; + } + let nplanes = if luma_only { 1 } else { 3 }; + for (plane, pl) in planes.iter_mut().enumerate().take(nplanes) { + if plane > 0 && level[1 + plane] == 0 { + continue; + } + for pass in 0..2 { + let step = if plane == 0 { 1 } else { 2 }; + let mut row = 0; + while row < info.mi_rows { + if !(row >> 4).is_multiple_of(sb_row_step) { + row += step; + continue; + } + let mut col = 0; + while col < info.mi_cols { + edge(pl, info, level, bit_depth, plane, pass, row, col); + col += step; + } + row += step; + } + } + } +} + +fn edge(pl: &mut Plane, info: &LfInfo, level: [u32; 4], bd: u32, plane: usize, pass: usize, row: usize, col: usize) { + let s = (plane > 0) as usize; + let (dx, dy) = if pass == 0 { (1usize, 0usize) } else { (0, 1) }; + let x = col * 4; + let y = row * 4; + let row = row | s; + let col = col | s; + let on_screen = !(x >= info.width || y >= info.height || (pass == 0 && x == 0) || (pass == 1 && y == 0)); + if !on_screen { + return; + } + let xp = x >> s; + let yp = y >> s; + let prev_row = row - (dy << s); + let prev_col = col - (dx << s); + let i = row * info.mi_cols + col; + let mi_size = info.mi_size[i] as usize; + let tx_stride = info.mi_cols + 32; + let tx_sz = info.tx[plane][(row >> s) * tx_stride + (col >> s)] as usize; + let plane_size = SUBSAMPLED_SIZE[mi_size][s][s] as usize; + let skip = info.skip[i]; + let is_intra = !info.is_inter[i]; + let prev_tx_sz = info.tx[plane][(prev_row >> s) * tx_stride + (prev_col >> s)] as usize; + let is_block_edge = if pass == 0 { + xp.is_multiple_of(4 * NUM_4X4_BLOCKS_WIDE[plane_size] as usize) + } else { + yp.is_multiple_of(4 * NUM_4X4_BLOCKS_HIGH[plane_size] as usize) + }; + let is_tx_edge = if pass == 0 { xp.is_multiple_of(TX_WIDTH[tx_sz] as usize) } else { yp.is_multiple_of(TX_HEIGHT[tx_sz] as usize) }; + let apply_filter = is_tx_edge && (is_block_edge || !skip || is_intra); + let base_size = if pass == 0 { TX_WIDTH[prev_tx_sz].min(TX_WIDTH[tx_sz]) } else { TX_HEIGHT[prev_tx_sz].min(TX_HEIGHT[tx_sz]) } as usize; + let filter_size = if plane == 0 { 16.min(base_size) } else { 8.min(base_size) }; + // 7.14.4 with no deltas, segmentation or sharpness: the same strength for every block + let lvl = level[if plane == 0 { pass } else { plane + 1 }].min(MAX_LOOP_FILTER as u32) as i32; + if !apply_filter || lvl == 0 { + return; + } + let limit = lvl.max(1); + let blimit = 2 * (lvl + 2) + limit; + let thresh = lvl >> 4; + for k in 0..4 { + sample_filter(pl, xp + dy * k, yp + dx * k, plane, limit, blimit, thresh, dx as isize, dy as isize, filter_size, bd); + } +} + +#[inline] +fn sample_filter(pl: &mut Plane, x: usize, y: usize, plane: usize, limit: i32, blimit: i32, thresh: i32, dx: isize, dy: isize, filter_size: usize, bd: u32) { + let stride = pl.stride as isize; + let step = dy * stride + dx; + let base = y as isize * stride + x as isize; + let reach: isize = if filter_size >= 16 { 7 } else { 4 }; + let mut v = [0i32; 16]; + for k in -reach..reach { + v[(k + 8) as usize] = pl.data[(base + k * step) as usize] as i32; + } + let at = |k: isize| -> i32 { v[(k + 8) as usize] }; + let (q0, q1, q2, q3) = (at(0), at(1), at(2), at(3)); + let (p0, p1, p2, p3) = (at(-1), at(-2), at(-3), at(-4)); + let d = &mut pl.data; + let sh = bd - 8; + let thresh_bd = thresh << sh; + let hev = (p1 - p0).abs() > thresh_bd || (q1 - q0).abs() > thresh_bd; + let filter_len = if filter_size == 4 { + 4 + } else if plane != 0 { + 6 + } else if filter_size == 8 { + 8 + } else { + 16 + }; + let limit_bd = limit << sh; + let blimit_bd = blimit << sh; + let mut mask = (p1 - p0).abs() > limit_bd || (q1 - q0).abs() > limit_bd || (p0 - q0).abs() * 2 + (p1 - q1).abs() / 2 > blimit_bd; + if filter_len >= 6 { + mask |= (p2 - p1).abs() > limit_bd || (q2 - q1).abs() > limit_bd; + } + if filter_len >= 8 { + mask |= (p3 - p2).abs() > limit_bd || (q3 - q2).abs() > limit_bd; + } + if mask { + return; + } + let t_bd = 1 << sh; + let flat = if filter_size >= 8 { + let mut m = (p1 - p0).abs() > t_bd || (q1 - q0).abs() > t_bd || (p2 - p0).abs() > t_bd || (q2 - q0).abs() > t_bd; + if filter_len >= 8 { + m |= (p3 - p0).abs() > t_bd || (q3 - q0).abs() > t_bd; + } + !m + } else { + false + }; + let flat2 = if filter_size >= 16 { + let (q4, q5, q6, p4, p5, p6) = (at(4), at(5), at(6), at(-5), at(-6), at(-7)); + !((p6 - p0).abs() > t_bd + || (q6 - q0).abs() > t_bd + || (p5 - p0).abs() > t_bd + || (q5 - q0).abs() > t_bd + || (p4 - p0).abs() > t_bd + || (q4 - q0).abs() > t_bd) + } else { + false + }; + if filter_size == 4 || !flat { + // narrow filter (7.14.6.3) + let half = 1i32 << (bd - 1); + let c = |v: i32| v.clamp(-half, half - 1); + let off = 0x80 << sh; + let (ps1, ps0, qs0, qs1) = (p1 - off, p0 - off, q0 - off, q1 - off); + let mut filter = if hev { c(ps1 - qs1) } else { 0 }; + filter = c(filter + 3 * (qs0 - ps0)); + let filter1 = c(filter + 4) >> 3; + let filter2 = c(filter + 3) >> 3; + d[base as usize] = (c(qs0 - filter1) + off) as u16; + d[(base - step) as usize] = (c(ps0 + filter2) + off) as u16; + if !hev { + let f = (filter1 + 1) >> 1; + d[(base + step) as usize] = (c(qs1 - f) + off) as u16; + d[(base - 2 * step) as usize] = (c(ps1 + f) + off) as u16; + } + } else { + // wide filter (7.14.6.4) + let log2 = if filter_size == 8 || !flat2 { 3 } else { 4 }; + let n: isize = if log2 == 4 { + 6 + } else if plane == 0 { + 3 + } else { + 2 + }; + let n2: isize = if log2 == 3 && plane == 0 { 0 } else { 1 }; + let mut f = [0i32; 12]; + for i in -n..n { + let mut t = 0; + for j in -n..=n { + let p = (i + j).clamp(-(n + 1), n); + let tap = if j.abs() <= n2 { 2 } else { 1 }; + t += at(p) * tap; + } + f[(i + n) as usize] = (t + (1 << (log2 - 1))) >> log2; + } + for i in -n..n { + d[(base + i * step) as usize] = f[(i + n) as usize] as u16; + } + } +} diff --git a/crates/av1enc/src/pred.rs b/crates/av1enc/src/pred.rs new file mode 100644 index 00000000..d61a6fa9 --- /dev/null +++ b/crates/av1enc/src/pred.rs @@ -0,0 +1,362 @@ +//! Prediction processes, mirrored from the spec (7.11.2 intra prediction for the non-directional +//! modes plus V_PRED / H_PRED without edge filtering, 7.11.3 block inter prediction without +//! reference scaling) so that the encoder predicts exactly like a decoder. + +use crate::tables::*; +use crate::transform::round2; + +/// A plane buffer (padded to whole superblocks). +#[derive(Clone)] +pub(crate) struct Plane { + pub data: Vec, + pub stride: usize, +} + +impl Plane { + pub fn new(w: usize, h: usize) -> Plane { + Plane { data: vec![0; w * h], stride: w } + } + #[inline(always)] + pub fn at(&self, x: usize, y: usize) -> u16 { + self.data[y * self.stride + x] + } + #[inline(always)] + pub fn row(&self, y: usize) -> &[u16] { + &self.data[y * self.stride..(y + 1) * self.stride] + } +} + +/// The intra modes this encoder uses (values are the spec's y / uv mode numbers). +pub(crate) const INTRA_MODES_USED: [usize; 7] = [DC_PRED, V_PRED, H_PRED, PAETH_PRED, SMOOTH_PRED, SMOOTH_V_PRED, SMOOTH_H_PRED]; + +/// Inputs of the intra prediction process. +pub(crate) struct IntraEdge { + pub x: usize, + pub y: usize, + pub have_left: bool, + pub have_above: bool, + pub have_above_right: bool, + pub have_below_left: bool, + pub log2: u32, + /// ((MiCols * MI_SIZE) >> subX) - 1 and the same for rows. + pub max_x: i32, + pub max_y: i32, + pub bit_depth: u32, +} + +/// Edge samples: `above[0..2n]`, `left[0..2n]` and the top-left corner. +pub(crate) struct Edges { + above: [i32; 64], + left: [i32; 64], + corner: i32, +} + +pub(crate) fn edges(pl: &Plane, e: &IntraEdge) -> Edges { + let w = 1usize << e.log2; + let h = w; + let bd = e.bit_depth; + let (x, y) = (e.x, e.y); + let n = w + h; + let mut above = [0i32; 64]; + let mut left = [0i32; 64]; + if !e.have_above && e.have_left { + let v = pl.at(x - 1, y) as i32; + above[..n].iter_mut().for_each(|a| *a = v); + } else if !e.have_above && !e.have_left { + let v = (1 << (bd - 1)) - 1; + above[..n].iter_mut().for_each(|a| *a = v); + } else { + let above_limit = e.max_x.min(x as i32 + if e.have_above_right { 2 * w as i32 } else { w as i32 } - 1); + let row = pl.row(y - 1); + for (i, a) in above[..n].iter_mut().enumerate() { + *a = row[above_limit.min(x as i32 + i as i32) as usize] as i32; + } + } + if !e.have_left && e.have_above { + let v = pl.at(x, y - 1) as i32; + left[..n].iter_mut().for_each(|a| *a = v); + } else if !e.have_left && !e.have_above { + let v = (1 << (bd - 1)) + 1; + left[..n].iter_mut().for_each(|a| *a = v); + } else { + let left_limit = e.max_y.min(y as i32 + if e.have_below_left { 2 * h as i32 } else { h as i32 } - 1); + for (i, l) in left[..n].iter_mut().enumerate() { + *l = pl.at(x - 1, left_limit.min(y as i32 + i as i32) as usize) as i32; + } + } + let corner = if e.have_above && e.have_left { + pl.at(x - 1, y - 1) as i32 + } else if e.have_above { + pl.at(x, y - 1) as i32 + } else if e.have_left { + pl.at(x - 1, y) as i32 + } else { + 1 << (bd - 1) + }; + Edges { above, left, corner } +} + +fn sm_weights(log2: u32) -> &'static [u8] { + match log2 { + 2 => &SM_WEIGHTS_TX_4X4, + 3 => &SM_WEIGHTS_TX_8X8, + 4 => &SM_WEIGHTS_TX_16X16, + _ => &SM_WEIGHTS_TX_32X32, + } +} + +/// Predicts a square `2^log2` block with `mode` into `pred` (row stride = block width). +pub(crate) fn predict_intra(ed: &Edges, e: &IntraEdge, mode: usize, pred: &mut [u16]) { + let log2 = e.log2; + let w = 1usize << log2; + let h = w; + let bd = e.bit_depth; + let (above, left) = (&ed.above, &ed.left); + match mode { + DC_PRED => { + let v = if e.have_left && e.have_above { + let sum: i32 = left[..h].iter().sum::() + above[..w].iter().sum::() + ((w + h) >> 1) as i32; + sum / (w + h) as i32 + } else if e.have_left { + (left[..h].iter().sum::() + (h >> 1) as i32) >> log2 + } else if e.have_above { + (above[..w].iter().sum::() + (w >> 1) as i32) >> log2 + } else { + 1 << (bd - 1) + }; + let v = v.clamp(0, (1 << bd) - 1) as u16; + pred[..w * h].iter_mut().for_each(|p| *p = v); + } + V_PRED => { + for i in 0..h { + for j in 0..w { + pred[i * w + j] = above[j] as u16; + } + } + } + H_PRED => { + for i in 0..h { + pred[i * w..i * w + w].iter_mut().for_each(|p| *p = left[i] as u16); + } + } + PAETH_PRED => { + let tl = ed.corner; + for i in 0..h { + for j in 0..w { + let a = above[j]; + let l = left[i]; + let base = a + l - tl; + let p_left = (base - l).abs(); + let p_top = (base - a).abs(); + let p_tl = (base - tl).abs(); + pred[i * w + j] = if p_left <= p_top && p_left <= p_tl { + l as u16 + } else if p_top <= p_tl { + a as u16 + } else { + tl as u16 + }; + } + } + } + _ => { + let wx = sm_weights(log2); + let wy = wx; + for i in 0..h { + for j in 0..w { + let v = if mode == SMOOTH_PRED { + let s = wy[i] as i32 * above[j] + (256 - wy[i] as i32) * left[h - 1] + wx[j] as i32 * left[i] + (256 - wx[j] as i32) * above[w - 1]; + round2(s, 9) + } else if mode == SMOOTH_V_PRED { + round2(wy[i] as i32 * above[j] + (256 - wy[i] as i32) * left[h - 1], 8) + } else { + round2(wx[j] as i32 * left[i] + (256 - wx[j] as i32) * above[w - 1], 8) + }; + pred[i * w + j] = v as u16; + } + } + } + } +} + +/// Block inter prediction (7.11.3.3 motion vector scaling without scaling, 7.11.3.4 block +/// inter prediction with the EIGHTTAP filter, single reference so InterRound1 = 11 and the +/// final rounding is the identity) of the `w x h` block at plane position (x, y) displaced by +/// `mv` (1/8 luma sample units, [row, col]). `last_x` / `last_y` are the largest sample +/// coordinates of the reference plane; the result goes to `pred` (row stride `w`). +pub(crate) fn predict_inter( + refp: &Plane, + last_x: i32, + last_y: i32, + x: usize, + y: usize, + w: usize, + h: usize, + mv: [i32; 2], + ss: usize, + bit_depth: u32, + pred: &mut [u16], +) { + // positions in 1/16 sample units ((startX >> 6) & 15 is the filter phase) + let px = ((x as i32) << 4) + ((2 * mv[1]) >> ss); + let py = ((y as i32) << 4) + ((2 * mv[0]) >> ss); + let (fx, fy) = ((px & 15) as usize, (py & 15) as usize); + let (ix, iy) = (px >> 4, py >> 4); + let max = (1i32 << bit_depth) - 1; + if fx == 0 && fy == 0 { + // the filters' phase-0 kernel is a unit impulse: a clamped copy + for r in 0..h { + let ry = (iy + r as i32).clamp(0, last_y) as usize; + let row = refp.row(ry); + let out = &mut pred[r * w..r * w + w]; + let x0 = ix; + if x0 >= 0 && x0 + w as i32 - 1 <= last_x { + out.copy_from_slice(&row[x0 as usize..x0 as usize + w]); + } else { + for (c, o) in out.iter_mut().enumerate() { + *o = row[(x0 + c as i32).clamp(0, last_x) as usize]; + } + } + } + return; + } + let filt_h = if w <= 4 { 4 } else { EIGHTTAP }; + let filt_v = if h <= 4 { 4 } else { EIGHTTAP }; + let kh = &SUBPEL_FILTERS[filt_h][fx]; + let kv = &SUBPEL_FILTERS[filt_v][fy]; + let bx = ix - 3; + let x_inside = bx >= 0 && bx + w as i32 + 7 <= last_x + 1; + // horizontal pass of one reference row into `out` (Round2( sum, InterRound0 = 3 )) + let hfilter = |ry: usize, out: &mut [i32]| { + let row = refp.row(ry); + if x_inside { + let seg = &row[bx as usize..bx as usize + w + 7]; + for (c, o) in out.iter_mut().enumerate() { + let mut s = 0i32; + for t in 0..8 { + s += kh[t] as i32 * seg[c + t] as i32; + } + *o = round2(s, 3); + } + } else { + for (c, o) in out.iter_mut().enumerate() { + let mut s = 0i32; + for t in 0..8 { + s += kh[t] as i32 * row[(bx + c as i32 + t as i32).clamp(0, last_x) as usize] as i32; + } + *o = round2(s, 3); + } + } + }; + let mut inter = [0i32; (64 + 7) * 64]; + if fy == 0 { + // the vertical kernel is 128 at the centre: Round2( 128 * v, 11 ) = Round2( v, 4 ) + for r in 0..h { + let ry = (iy + r as i32).clamp(0, last_y) as usize; + hfilter(ry, &mut inter[..w]); + for (p, &v) in pred[r * w..r * w + w].iter_mut().zip(&inter[..w]) { + *p = round2(v, 4).clamp(0, max) as u16; + } + } + return; + } + if fx == 0 { + // the horizontal pass is exactly 16 * sample: Round2( 16 * sum, 11 ) = Round2( sum, 7 ) + for r in 0..h { + let rows: [&[u16]; 8] = std::array::from_fn(|t| refp.row((iy + r as i32 + t as i32 - 3).clamp(0, last_y) as usize)); + for c in 0..w { + let xx = if x_inside { (ix + c as i32) as usize } else { (ix + c as i32).clamp(0, last_x) as usize }; + let mut s = 0i32; + for t in 0..8 { + s += kv[t] as i32 * rows[t][xx] as i32; + } + pred[r * w + c] = round2(s, 7).clamp(0, max) as u16; + } + } + return; + } + let inter_h = h + 7; + for r in 0..inter_h { + let ry = (iy + r as i32 - 3).clamp(0, last_y) as usize; + hfilter(ry, &mut inter[r * w..r * w + w]); + } + for r in 0..h { + for c in 0..w { + let mut s = 0i32; + for t in 0..8 { + s += kv[t] as i32 * inter[(r + t) * w + c]; + } + pred[r * w + c] = round2(s, 11).clamp(0, max) as u16; + } + } +} + +/// The visible `w x h` area of a reference plane interpolated at the sub-sample phase +/// (`fx`, `fy`) (1/16 units) with the EIGHTTAP filter, as `predict_inter` computes it for +/// blocks wider and taller than 4. Used for motion search. +pub(crate) fn subpel_plane(refp: &Plane, w: usize, h: usize, fx: usize, fy: usize, bit_depth: u32) -> Vec { + let kh = &SUBPEL_FILTERS[EIGHTTAP][fx]; + let kv = &SUBPEL_FILTERS[EIGHTTAP][fy]; + let max = (1i32 << bit_depth) - 1; + let (lx, ly) = (w as i32 - 1, h as i32 - 1); + let mut inter = vec![0i32; w * h]; + for y in 0..h { + let row = refp.row(y); + for x in 0..w { + let mut s = 0i32; + for t in 0..8 { + s += kh[t] as i32 * row[(x as i32 + t as i32 - 3).clamp(0, lx) as usize] as i32; + } + inter[y * w + x] = round2(s, 3); + } + } + let mut out = vec![0u16; w * h]; + for y in 0..h { + for x in 0..w { + let mut s = 0i32; + for t in 0..8 { + s += kv[t] as i32 * inter[(y as i32 + t as i32 - 3).clamp(0, ly) as usize * w + x]; + } + out[y * w + x] = round2(s, 11).clamp(0, max) as u16; + } + } + out +} + +#[cfg(test)] +mod tests { + use super::*; + + /// Whole-plane interpolation equals block inter prediction at the same phase. + #[test] + fn subpel_plane_matches_block_prediction() { + let (w, h) = (37, 29); + let mut p = Plane::new(64, 64); + for (i, v) in p.data.iter_mut().enumerate() { + *v = ((i * 7919) % 256) as u16; + } + for (fx, fy) in [(8, 0), (0, 8), (8, 8), (4, 12)] { + let plane = subpel_plane(&p, w, h, fx, fy, 8); + for (bx, by) in [(0i32, 0i32), (8, 8), (24, 16), (-5, 20), (30, -3)] { + let mut pred = vec![0u16; 64]; + let mv = [by * 8 + fy as i32 / 2, bx * 8 + fx as i32 / 2]; + predict_inter(&p, w as i32 - 1, h as i32 - 1, 0, 0, 8, 8, mv, 0, 8, &mut pred); + for r in 0..8 { + for c in 0..8 { + let (x, y) = ((bx + c).clamp(0, w as i32 - 1), (by + r).clamp(0, h as i32 - 1)); + if bx + c == x && by + r == y { + assert_eq!(pred[(r * 8 + c) as usize], plane[y as usize * w + x as usize], "phase ({fx}, {fy}) at ({}, {})", bx + c, by + r); + } + } + } + } + } + } + + #[test] + fn phase_zero_filters_are_impulses() { + for f in SUBPEL_FILTERS.iter() { + assert_eq!(f[0], [0, 0, 0, 128, 0, 0, 0, 0]); + } + } +} diff --git a/crates/av1enc/src/symbol.rs b/crates/av1enc/src/symbol.rs new file mode 100644 index 00000000..579a51e2 --- /dev/null +++ b/crates/av1enc/src/symbol.rs @@ -0,0 +1,267 @@ +//! Multi-symbol arithmetic encoder: the exact inverse of the symbol decoder of spec 8.2. +//! +//! The decoder keeps `SymbolValue`, the bitwise complement of a 15-bit window into the tile data +//! minus the lower bounds of the symbols decoded so far, and selects symbol `k` when +//! `cur(k) <= SymbolValue < cur(k - 1)` with +//! `cur(k) = ((SymbolRange >> 8) * ((32768 - cdf[k]) >> 6) >> 1) + 4 * (N - k - 1)` and +//! `cur(-1) = SymbolRange`. The encoder therefore runs an ordinary carry-propagating range +//! coder over the complemented bit stream: coding symbol `k` adds `cur(k)` to `low` and sets the +//! range to `cur(k - 1) - cur(k)`; both are renormalised exactly like the decoder's range. +//! +//! At the end (spec 8.2.4 exit process) the decoder has consumed `S` renormalisation bits plus a +//! 15-bit window, and the bits from position `S` on must be a one followed by zeros. In the +//! complemented domain that window is `0b011_1111_1111_1111`, so the encoder picks the `S`-bit +//! prefix `D` with `D * 2^15 + 2^14 - 1` inside `[low, low + range)`, i.e. +//! `D = (low + 2^14) >> 15`, and writes `!D`, a one bit and zero padding. + +pub(crate) struct SymbolWriter { + /// Low end of the interval, holding `15 + cnt` bits not yet moved to `out` (plus carries). + low: u64, + rng: u32, + cnt: u32, + out: Vec, + pub disable_update: bool, +} + +impl SymbolWriter { + pub fn new(disable_update: bool) -> Self { + SymbolWriter { low: 0, rng: 1 << 15, cnt: 0, out: Vec::with_capacity(1 << 16), disable_update } + } + + /// Adds `k` to the bytes already output (carry propagation). + fn carry(&mut self, mut k: u64) { + let mut i = self.out.len(); + while k != 0 { + i -= 1; + let t = self.out[i] as u64 + k; + self.out[i] = t as u8; + k = t >> 8; + } + } + + #[inline(always)] + fn encode(&mut self, cdf: &[u16], n: usize, s: usize) { + let r8 = self.rng >> 8; + let cur = |k: usize| -> u32 { ((r8 * ((32768 - cdf[k] as u32) >> 6)) >> 1) + 4 * (n - k - 1) as u32 }; + let hi = if s == 0 { self.rng } else { cur(s - 1) }; + let lo = cur(s); + debug_assert!(hi > lo, "symbol {s} has an empty interval"); + self.low += lo as u64; + self.rng = hi - lo; + let d = 15 - (31 - self.rng.leading_zeros()); + if d == 0 { + return; + } + self.rng <<= d; + self.low <<= d; + self.cnt += d; + while self.cnt >= 8 { + let sh = 15 + self.cnt - 8; + let c = self.low >> sh; + self.low &= (1u64 << sh) - 1; + self.cnt -= 8; + self.carry(c >> 8); + self.out.push((c & 0xff) as u8); + } + } + + /// Codes `s` with the adaptive CDF `cdf` (N values plus the adaptation counter). + #[inline] + pub fn symbol(&mut self, s: usize, cdf: &mut [u16]) { + let n = cdf.len() - 1; + debug_assert!(s < n); + self.encode(cdf, n, s); + if !self.disable_update { + update_cdf(cdf, n, s); + } + } + + /// Codes `s` with a CDF that is not adapted. + pub fn symbol_fixed(&mut self, s: usize, cdf: &[u16]) { + let n = cdf.len() - 1; + self.encode(cdf, n, s); + } + + /// `read_bool()` counterpart. + pub fn bool(&mut self, b: bool) { + self.encode(&[1 << 14, 1 << 15, 0], 2, b as usize); + } + + /// `L(n)` counterpart (most significant bit first). + #[cfg(test)] + pub fn literal(&mut self, n: u32, v: u32) { + for i in (0..n).rev() { + self.bool((v >> i) & 1 != 0); + } + } + + /// Finishes the tile: returns its bytes (with the trailing one bit and zero padding). + pub fn finish(mut self) -> Vec { + let low = self.low + (1 << 14); + let mut tail = low >> 15; + self.carry(tail >> self.cnt); + tail &= (1u64 << self.cnt) - 1; + let mut bytes: Vec = self.out.iter().map(|b| !b).collect(); + let n = self.cnt + 1; // 1..=8 bits: !tail then the trailing one bit + let v = (((!tail) & ((1u64 << self.cnt) - 1)) << 1) | 1; + bytes.push((v << (8 - n)) as u8); + bytes + } +} + +/// CDF adaptation (spec 8.2.6, as in the decoder). +#[inline(always)] +pub(crate) fn update_cdf(cdf: &mut [u16], n: usize, symbol: usize) { + let count = cdf[n]; + let rate = 3 + (count > 15) as u32 + (count > 31) as u32 + (31 - (n as u32).leading_zeros()).min(2); + let mut tmp = 0u32; + for i in 0..n - 1 { + if i == symbol { + tmp = 1 << 15; + } + let c = cdf[i] as u32; + if tmp < c { + cdf[i] = (c - ((c - tmp) >> rate)) as u16; + } else { + cdf[i] = (c + ((tmp - c) >> rate)) as u16; + } + } + cdf[n] += (count < 32) as u16; +} + +#[cfg(test)] +mod tests { + use super::*; + + /// The spec's symbol decoder (8.2.2 - 8.2.4), for checking the encoder. + struct Dec<'a> { + data: &'a [u8], + pos: usize, + value: u32, + range: u32, + max_bits: i64, + } + + impl<'a> Dec<'a> { + fn new(data: &'a [u8]) -> Self { + let mut d = Dec { data, pos: 0, value: 0, range: 1 << 15, max_bits: 8 * data.len() as i64 - 15 }; + let nb = (data.len() * 8).min(15) as u32; + let buf = d.bits(nb); + d.value = ((1 << 15) - 1) ^ (buf << (15 - nb)); + d + } + fn bits(&mut self, n: u32) -> u32 { + let mut x = 0; + for _ in 0..n { + let b = if self.pos < self.data.len() * 8 { (self.data[self.pos >> 3] >> (7 - (self.pos & 7))) & 1 } else { 0 }; + self.pos += 1; + x = (x << 1) | b as u32; + } + x + } + fn symbol(&mut self, cdf: &mut [u16], adapt: bool) -> usize { + let n = cdf.len() - 1; + let mut cur = self.range; + let mut s = 0; + let mut prev; + loop { + prev = cur; + let f = (1u32 << 15) - cdf[s] as u32; + cur = (((self.range >> 8) * (f >> 6)) >> 1) + 4 * (n - s - 1) as u32; + if self.value >= cur { + break; + } + s += 1; + } + self.range = prev - cur; + self.value -= cur; + let b = 15 - (31 - self.range.leading_zeros()); + if b > 0 { + self.range <<= b; + let nb = (b as i64).min(self.max_bits.max(0)) as u32; + let nd = self.bits(nb) << (b - nb); + self.value = nd ^ (((self.value + 1) << b) - 1); + self.max_bits -= b as i64; + } + if adapt { + update_cdf(cdf, n, s); + } + s + } + /// Position of the trailing one bit (8.2.4). + fn trailing_position(&self) -> i64 { + let consumed = (self.pos as i64).min(self.data.len() as i64 * 8); + consumed - (15i64).min(self.max_bits + 15) + } + } + + #[test] + fn bools_only() { + for len in 1..200 { + let bits: Vec = (0..len).map(|i| (i * 7 + len) % 3 == 0).collect(); + let mut w = SymbolWriter::new(false); + for &b in &bits { + w.bool(b); + } + let data = w.finish(); + let mut d = Dec::new(&data); + for (i, &b) in bits.iter().enumerate() { + assert_eq!(d.symbol(&mut [1 << 14, 1 << 15, 0], false) == 1, b, "len {len} bit {i} data {data:?}"); + } + } + } + + #[test] + fn round_trip_random_symbols() { + let mut seed = 0x1234_5678u32; + let mut rnd = || { + seed ^= seed << 13; + seed ^= seed >> 17; + seed ^= seed << 5; + seed + }; + for trial in 0..300 { + let count = 1 + (rnd() % 3000) as usize; + let mut cdfs_e = vec![[4096u16, 11264, 19328, 32768, 0]; 4]; + let mut cdfs_d = cdfs_e.clone(); + let mut syms = Vec::new(); + let mut w = SymbolWriter::new(false); + for _ in 0..count { + let kind = rnd() % 3; + let ctx = (rnd() % 4) as usize; + // skewed symbols so that the range also gets small + let s = if rnd() % 4 == 0 { (rnd() % 4) as usize } else { 0 }; + match kind { + 0 => w.symbol(s, &mut cdfs_e[ctx]), + 1 => w.bool(s & 1 == 1), + _ => w.literal(5, s as u32 * 7), + } + syms.push((kind, ctx, s)); + } + let data = w.finish(); + let mut d = Dec::new(&data); + for (idx, &(kind, ctx, s)) in syms.iter().enumerate() { + let got = match kind { + 0 => d.symbol(&mut cdfs_d[ctx], true), + 1 => d.symbol(&mut [1 << 14, 1 << 15, 0], false), + _ => { + let mut v = 0; + for _ in 0..5 { + v = (v << 1) | d.symbol(&mut [1 << 14, 1 << 15, 0], false); + } + v / 7 + } + }; + let want = if kind == 1 { s & 1 } else { s }; + assert_eq!(got, want, "trial {trial} idx {idx} kind {kind} of {}", syms.len()); + } + let tp = d.trailing_position(); + assert!(tp >= 0 && (tp as usize) < data.len() * 8, "trial {trial}: trailing bit position {tp}"); + let tp = tp as usize; + assert_eq!((data[tp >> 3] >> (7 - (tp & 7))) & 1, 1, "trial {trial}: trailing one bit"); + for p in tp + 1..data.len() * 8 { + assert_eq!((data[p >> 3] >> (7 - (p & 7))) & 1, 0, "trial {trial}: padding"); + } + } + } +} diff --git a/crates/av1enc/src/tables.rs b/crates/av1enc/src/tables.rs new file mode 100644 index 00000000..3e333a51 --- /dev/null +++ b/crates/av1enc/src/tables.rs @@ -0,0 +1,4805 @@ +// Tables mirrored from filmcraft-av1's spec_tables.rs, which extracts them from the AV1 Bitstream & +// Decoding Process Specification v1.0.0 with Errata 1. Only the tables this encoder uses are kept. +#![allow(dead_code, clippy::unreadable_literal)] + +pub const ADST_ADST: usize = 3; +pub const ADST_DCT: usize = 1; +pub const ADST_FLIPADST: usize = 7; +pub const AFFINE: usize = 3; +pub const ALTREF2_FRAME: usize = 6; +pub const ALTREF_FRAME: usize = 7; +pub const ANGLE_STEP: usize = 3; +pub const BIDIR_COMP_REFERENCE: usize = 1; +pub const BILINEAR: usize = 3; +pub const BLOCK_128X128: usize = 15; +pub const BLOCK_128X64: usize = 14; +pub const BLOCK_16X16: usize = 6; +pub const BLOCK_16X32: usize = 7; +pub const BLOCK_16X4: usize = 17; +pub const BLOCK_16X64: usize = 20; +pub const BLOCK_16X8: usize = 5; +pub const BLOCK_32X16: usize = 8; +pub const BLOCK_32X32: usize = 9; +pub const BLOCK_32X64: usize = 10; +pub const BLOCK_32X8: usize = 19; +pub const BLOCK_4X16: usize = 16; +pub const BLOCK_4X4: usize = 0; +pub const BLOCK_4X8: usize = 1; +pub const BLOCK_64X128: usize = 13; +pub const BLOCK_64X16: usize = 21; +pub const BLOCK_64X32: usize = 11; +pub const BLOCK_64X64: usize = 12; +pub const BLOCK_8X16: usize = 4; +pub const BLOCK_8X32: usize = 18; +pub const BLOCK_8X4: usize = 2; +pub const BLOCK_8X8: usize = 3; +pub const BLOCK_INVALID: usize = 22; +pub const BLOCK_SIZES: usize = 22; +pub const BLOCK_SIZE_GROUPS: usize = 4; +pub const BR_CDF_SIZE: usize = 4; +pub const BUFFER_POOL_MAX_SIZE: usize = 10; +pub const BWDREF_FRAME: usize = 5; +pub const BWD_REFS: usize = 3; +pub const CFL_ALPHABET_SIZE: usize = 16; +pub const CFL_ALPHA_CONTEXTS: usize = 6; +pub const CFL_JOINT_SIGNS: usize = 8; +pub const CFL_SIGN_NEG: usize = 1; +pub const CFL_SIGN_POS: usize = 2; +pub const CFL_SIGN_ZERO: usize = 0; +pub const CLASS0_SIZE: usize = 2; +pub const COEFF_BASE_RANGE: usize = 12; +pub const COEFF_CDF_Q_CTXS: usize = 4; +pub const COMPOUND_AVERAGE: usize = 2; +pub const COMPOUND_DIFFWTD: usize = 1; +pub const COMPOUND_DISTANCE: usize = 4; +pub const COMPOUND_IDX_CONTEXTS: usize = 6; +pub const COMPOUND_INTRA: usize = 3; +pub const COMPOUND_MODES: usize = 8; +pub const COMPOUND_MODE_CONTEXTS: usize = 8; +pub const COMPOUND_REFERENCE: usize = 1; +pub const COMPOUND_TYPES: usize = 2; +pub const COMPOUND_WEDGE: usize = 0; +pub const COMP_GROUP_IDX_CONTEXTS: usize = 6; +pub const COMP_INTER_CONTEXTS: usize = 5; +pub const COMP_NEWMV_CTXS: usize = 5; +pub const COMP_REF_TYPE_CONTEXTS: usize = 5; +pub const CP_BT_2020: usize = 9; +pub const CP_BT_470_B_G: usize = 5; +pub const CP_BT_470_M: usize = 4; +pub const CP_BT_601: usize = 6; +pub const CP_BT_709: usize = 1; +pub const CP_EBU_3213: usize = 22; +pub const CP_GENERIC_FILM: usize = 8; +pub const CP_SMPTE_240: usize = 7; +pub const CP_SMPTE_431: usize = 11; +pub const CP_SMPTE_432: usize = 12; +pub const CP_UNSPECIFIED: usize = 2; +pub const CP_XYZ: usize = 10; +pub const CSP_COLOCATED: usize = 2; +pub const CSP_RESERVED: usize = 3; +pub const CSP_UNKNOWN: usize = 0; +pub const CSP_VERTICAL: usize = 1; +pub const D113_PRED: usize = 5; +pub const D135_PRED: usize = 4; +pub const D157_PRED: usize = 6; +pub const D203_PRED: usize = 7; +pub const D45_PRED: usize = 3; +pub const D67_PRED: usize = 8; +pub const DCT_ADST: usize = 2; +pub const DCT_DCT: usize = 0; +pub const DCT_FLIPADST: usize = 5; +pub const DC_PRED: usize = 0; +pub const DC_SIGN_CONTEXTS: usize = 3; +pub const DELTA_LF_SMALL: usize = 3; +pub const DELTA_Q_SMALL: usize = 3; +pub const DIRECTIONAL_MODES: usize = 8; +pub const DIV_LUT_BITS: usize = 8; +pub const DIV_LUT_NUM: usize = 257; +pub const DIV_LUT_PREC_BITS: usize = 14; +pub const DRL_MODE_CONTEXTS: usize = 3; +pub const EC_MIN_PROB: usize = 4; +pub const EC_PROB_SHIFT: usize = 6; +pub const EIGHTTAP: usize = 0; +pub const EIGHTTAP_SHARP: usize = 2; +pub const EIGHTTAP_SMOOTH: usize = 1; +pub const EOB_COEF_CONTEXTS: usize = 9; +pub const FILTER_BITS: usize = 7; +pub const FILTER_D157_PRED: usize = 3; +pub const FILTER_DC_PRED: usize = 0; +pub const FILTER_H_PRED: usize = 2; +pub const FILTER_PAETH_PRED: usize = 4; +pub const FILTER_V_PRED: usize = 1; +pub const FLIPADST_ADST: usize = 8; +pub const FLIPADST_DCT: usize = 4; +pub const FLIPADST_FLIPADST: usize = 6; +pub const FRAME_LF_COUNT: usize = 4; +pub const FWD_REFS: usize = 4; +pub const GLOBALMV: usize = 16; +pub const GLOBAL_GLOBALMV: usize = 24; +pub const GM_ABS_ALPHA_BITS: usize = 12; +pub const GM_ABS_TRANS_BITS: usize = 12; +pub const GM_ABS_TRANS_ONLY_BITS: usize = 9; +pub const GM_ALPHA_PREC_BITS: usize = 15; +pub const GM_TRANS_ONLY_PREC_BITS: usize = 3; +pub const GM_TRANS_PREC_BITS: usize = 6; +pub const GOLDEN_FRAME: usize = 4; +pub const H_ADST: usize = 13; +pub const H_DCT: usize = 11; +pub const H_FLIPADST: usize = 15; +pub const H_PRED: usize = 2; +pub const IDENTITY: usize = 0; +pub const IDTX: usize = 9; +pub const II_DC_PRED: usize = 0; +pub const II_H_PRED: usize = 2; +pub const II_SMOOTH_PRED: usize = 3; +pub const II_V_PRED: usize = 1; +pub const INTERINTRA_MODES: usize = 4; +pub const INTERP_FILTERS: usize = 3; +pub const INTERP_FILTER_CONTEXTS: usize = 16; +pub const INTER_FRAME: usize = 1; +pub const INTRABC_DELAY_PIXELS: usize = 256; +pub const INTRABC_DELAY_SB64: usize = 4; +pub const INTRA_EDGE_KERNELS: usize = 3; +pub const INTRA_EDGE_TAPS: usize = 5; +pub const INTRA_FILTER_MODES: usize = 5; +pub const INTRA_FILTER_SCALE_BITS: usize = 4; +pub const INTRA_FRAME: usize = 0; +pub const INTRA_MODES: usize = 13; +pub const INTRA_MODE_CONTEXTS: usize = 5; +pub const INTRA_ONLY_FRAME: usize = 2; +pub const IS_INTER_CONTEXTS: usize = 4; +pub const KEY_FRAME: usize = 0; +pub const LAST2_FRAME: usize = 2; +pub const LAST3_FRAME: usize = 3; +pub const LAST_FRAME: usize = 1; +pub const LEAST_SQUARES_SAMPLES_MAX: usize = 8; +pub const LEVEL_CONTEXTS: usize = 21; +pub const LOCALWARP: usize = 2; +pub const LS_MV_MAX: usize = 256; +pub const MASK_MASTER_SIZE: usize = 64; +pub const MAX_ANGLE_DELTA: usize = 3; +pub const MAX_FRAME_DISTANCE: usize = 31; +pub const MAX_LOOP_FILTER: usize = 63; +pub const MAX_OFFSET_HEIGHT: usize = 0; +pub const MAX_OFFSET_WIDTH: usize = 8; +pub const MAX_REF_MV_STACK_SIZE: usize = 8; +pub const MAX_SB_SIZE: usize = 128; +pub const MAX_SEGMENTS: usize = 8; +pub const MAX_TILE_AREA: usize = 9437184; +pub const MAX_TILE_COLS: usize = 64; +pub const MAX_TILE_ROWS: usize = 64; +pub const MAX_TILE_WIDTH: usize = 4096; +pub const MAX_TX_DEPTH: usize = 2; +pub const MAX_VARTX_DEPTH: usize = 2; +pub const MB_MODE_COUNT: usize = 17; +pub const MC_BT_2020_CL: usize = 10; +pub const MC_BT_2020_NCL: usize = 9; +pub const MC_BT_470_B_G: usize = 5; +pub const MC_BT_601: usize = 6; +pub const MC_BT_709: usize = 1; +pub const MC_CHROMAT_CL: usize = 13; +pub const MC_CHROMAT_NCL: usize = 12; +pub const MC_FCC: usize = 4; +pub const MC_ICTCP: usize = 14; +pub const MC_IDENTITY: usize = 0; +pub const MC_RESERVED_3: usize = 3; +pub const MC_SMPTE_2085: usize = 11; +pub const MC_SMPTE_240: usize = 7; +pub const MC_SMPTE_YCGCO: usize = 8; +pub const MC_UNSPECIFIED: usize = 2; +pub const METADATA_TYPE_HDR_CLL: usize = 1; +pub const METADATA_TYPE_HDR_MDCV: usize = 2; +pub const METADATA_TYPE_ITUT_T35: usize = 4; +pub const METADATA_TYPE_SCALABILITY: usize = 3; +pub const METADATA_TYPE_TIMECODE: usize = 5; +pub const MFMV_STACK_SIZE: usize = 3; +pub const MI_SIZE: usize = 4; +pub const MI_SIZE_LOG2: usize = 2; +pub const MOTION_MODES: usize = 3; +pub const MV_BORDER: usize = 128; +pub const MV_CLASSES: usize = 11; +pub const MV_CLASS_0: usize = 0; +pub const MV_CLASS_1: usize = 1; +pub const MV_CLASS_10: usize = 10; +pub const MV_CLASS_2: usize = 2; +pub const MV_CLASS_3: usize = 3; +pub const MV_CLASS_4: usize = 4; +pub const MV_CLASS_5: usize = 5; +pub const MV_CLASS_6: usize = 6; +pub const MV_CLASS_7: usize = 7; +pub const MV_CLASS_8: usize = 8; +pub const MV_CLASS_9: usize = 9; +pub const MV_CONTEXTS: usize = 2; +pub const MV_INTRABC_CONTEXT: usize = 1; +pub const MV_JOINTS: usize = 4; +pub const MV_JOINT_HNZVNZ: usize = 3; +pub const MV_JOINT_HNZVZ: usize = 1; +pub const MV_JOINT_HZVNZ: usize = 2; +pub const MV_JOINT_ZERO: usize = 0; +pub const MV_OFFSET_BITS: usize = 10; +pub const NEARESTMV: usize = 14; +pub const NEAREST_NEARESTMV: usize = 18; +pub const NEAREST_NEWMV: usize = 20; +pub const NEARMV: usize = 15; +pub const NEAR_NEARMV: usize = 19; +pub const NEAR_NEWMV: usize = 22; +pub const NEWMV: usize = 17; +pub const NEW_MV_CONTEXTS: usize = 6; +pub const NEW_NEARESTMV: usize = 21; +pub const NEW_NEARMV: usize = 23; +pub const NEW_NEWMV: usize = 25; +pub const NUM_BASE_LEVELS: usize = 2; +pub const NUM_REF_FRAMES: usize = 8; +pub const OBMC: usize = 1; +pub const OBU_FRAME: usize = 6; +pub const OBU_FRAME_HEADER: usize = 3; +pub const OBU_METADATA: usize = 5; +pub const OBU_PADDING: usize = 15; +pub const OBU_REDUNDANT_FRAME_HEADER: usize = 7; +pub const OBU_SEQUENCE_HEADER: usize = 1; +pub const OBU_TEMPORAL_DELIMITER: usize = 2; +pub const OBU_TILE_GROUP: usize = 4; +pub const OBU_TILE_LIST: usize = 8; +pub const ONLY_4X4: usize = 0; +pub const PAETH_PRED: usize = 12; +pub const PALETTE_BLOCK_SIZE_CONTEXTS: usize = 7; +pub const PALETTE_COLORS: usize = 8; +pub const PALETTE_COLOR_CONTEXTS: usize = 5; +pub const PALETTE_MAX_COLOR_CONTEXT_HASH: usize = 8; +pub const PALETTE_NUM_NEIGHBORS: usize = 3; +pub const PALETTE_SIZES: usize = 7; +pub const PALETTE_UV_MODE_CONTEXTS: usize = 2; +pub const PALETTE_Y_MODE_CONTEXTS: usize = 3; +pub const PARTITION_CONTEXTS: usize = 4; +pub const PARTITION_HORZ: usize = 1; +pub const PARTITION_HORZ_4: usize = 8; +pub const PARTITION_HORZ_A: usize = 4; +pub const PARTITION_HORZ_B: usize = 5; +pub const PARTITION_NONE: usize = 0; +pub const PARTITION_SPLIT: usize = 3; +pub const PARTITION_VERT: usize = 2; +pub const PARTITION_VERT_4: usize = 9; +pub const PARTITION_VERT_A: usize = 6; +pub const PARTITION_VERT_B: usize = 7; +pub const PLANE_TYPES: usize = 2; +pub const PRIMARY_REF_NONE: usize = 7; +pub const QM_TOTAL_SIZE: usize = 3344; +pub const REFMVS_LIMIT: usize = 4095; +pub const REFS_PER_FRAME: usize = 7; +pub const REF_CAT_LEVEL: usize = 640; +pub const REF_CONTEXTS: usize = 3; +pub const REF_MV_CONTEXTS: usize = 6; +pub const REF_SCALE_SHIFT: usize = 14; +pub const RESTORATION_TILESIZE_MAX: usize = 256; +pub const RESTORE_NONE: usize = 0; +pub const RESTORE_SGRPROJ: usize = 2; +pub const RESTORE_SWITCHABLE: usize = 3; +pub const RESTORE_WIENER: usize = 1; +pub const ROTZOOM: usize = 2; +pub const SCALABILITY_L1T2: usize = 0; +pub const SCALABILITY_L1T3: usize = 1; +pub const SCALABILITY_L2T1: usize = 2; +pub const SCALABILITY_L2T2: usize = 3; +pub const SCALABILITY_L2T3: usize = 4; +pub const SCALABILITY_L3T1: usize = 15; +pub const SCALABILITY_L3T2: usize = 16; +pub const SCALABILITY_L3T2_KEY: usize = 21; +pub const SCALABILITY_L3T2_KEY_SHIFT: usize = 25; +pub const SCALABILITY_L3T3: usize = 17; +pub const SCALABILITY_L3T3_KEY: usize = 22; +pub const SCALABILITY_L3T3_KEY_SHIFT: usize = 26; +pub const SCALABILITY_L4T5_KEY: usize = 23; +pub const SCALABILITY_L4T5_KEY_SHIFT: usize = 27; +pub const SCALABILITY_L4T7_KEY: usize = 24; +pub const SCALABILITY_L4T7_KEY_SHIFT: usize = 28; +pub const SCALABILITY_S2T1: usize = 5; +pub const SCALABILITY_S2T2: usize = 6; +pub const SCALABILITY_S2T3: usize = 7; +pub const SCALABILITY_S3T1: usize = 18; +pub const SCALABILITY_S3T2: usize = 19; +pub const SCALABILITY_S3T3: usize = 20; +pub const SCALABILITY_SS: usize = 14; +pub const SCALE_SUBPEL_BITS: usize = 10; +pub const SEGMENT_ID_CONTEXTS: usize = 3; +pub const SEGMENT_ID_PREDICTED_CONTEXTS: usize = 3; +pub const SEG_LVL_ALT_LF_Y_V: usize = 1; +pub const SEG_LVL_ALT_Q: usize = 0; +pub const SEG_LVL_GLOBALMV: usize = 7; +pub const SEG_LVL_MAX: usize = 8; +pub const SEG_LVL_REF_FRAME: usize = 5; +pub const SEG_LVL_SKIP: usize = 6; +pub const SELECT_INTEGER_MV: usize = 2; +pub const SELECT_SCREEN_CONTENT_TOOLS: usize = 2; +pub const SGRPROJ_MTABLE_BITS: usize = 20; +pub const SGRPROJ_PARAMS_BITS: usize = 4; +pub const SGRPROJ_PRJ_BITS: usize = 7; +pub const SGRPROJ_PRJ_SUBEXP_K: usize = 4; +pub const SGRPROJ_RECIP_BITS: usize = 12; +pub const SGRPROJ_RST_BITS: usize = 4; +pub const SGRPROJ_SGR_BITS: usize = 8; +pub const SIG_COEF_CONTEXTS: usize = 42; +pub const SIG_COEF_CONTEXTS_2D: usize = 26; +pub const SIG_COEF_CONTEXTS_EOB: usize = 4; +pub const SIG_REF_DIFF_OFFSET_NUM: usize = 5; +pub const SIMPLE: usize = 0; +pub const SINGLE_REFERENCE: usize = 0; +pub const SINGLE_REFS: usize = 7; +pub const SINPI_1_9: usize = 1321; +pub const SINPI_2_9: usize = 2482; +pub const SINPI_3_9: usize = 3344; +pub const SINPI_4_9: usize = 3803; +pub const SKIP_CONTEXTS: usize = 3; +pub const SKIP_MODE_CONTEXTS: usize = 3; +pub const SMOOTH_H_PRED: usize = 11; +pub const SMOOTH_PRED: usize = 9; +pub const SMOOTH_V_PRED: usize = 10; +pub const SUBPEL_BITS: usize = 4; +pub const SUBPEL_MASK: usize = 15; +pub const SUPERRES_DENOM_BITS: usize = 3; +pub const SUPERRES_DENOM_MIN: usize = 9; +pub const SUPERRES_EXTRA_BITS: usize = 8; +pub const SUPERRES_FILTER_BITS: usize = 6; +pub const SUPERRES_FILTER_OFFSET: usize = 3; +pub const SUPERRES_FILTER_SHIFTS: usize = 64; +pub const SUPERRES_FILTER_TAPS: usize = 8; +pub const SUPERRES_NUM: usize = 8; +pub const SUPERRES_SCALE_BITS: usize = 14; +pub const SUPERRES_SCALE_MASK: usize = 16383; +pub const SWITCHABLE: usize = 4; +pub const SWITCH_FRAME: usize = 3; +pub const TC_BT_1361: usize = 12; +pub const TC_BT_2020_10_BIT: usize = 14; +pub const TC_BT_2020_12_BIT: usize = 15; +pub const TC_BT_470_B_G: usize = 5; +pub const TC_BT_470_M: usize = 4; +pub const TC_BT_601: usize = 6; +pub const TC_BT_709: usize = 1; +pub const TC_HLG: usize = 18; +pub const TC_IEC_61966: usize = 11; +pub const TC_LINEAR: usize = 8; +pub const TC_LOG_100: usize = 9; +pub const TC_LOG_100_SQRT10: usize = 10; +pub const TC_RESERVED_0: usize = 0; +pub const TC_RESERVED_3: usize = 3; +pub const TC_SMPTE_2084: usize = 16; +pub const TC_SMPTE_240: usize = 7; +pub const TC_SMPTE_428: usize = 17; +pub const TC_SRGB: usize = 13; +pub const TC_UNSPECIFIED: usize = 2; +pub const TOTAL_REFS_PER_FRAME: usize = 8; +pub const TRANSLATION: usize = 1; +pub const TXB_SKIP_CONTEXTS: usize = 13; +pub const TXFM_PARTITION_CONTEXTS: usize = 21; +pub const TX_16X16: usize = 2; +pub const TX_16X32: usize = 9; +pub const TX_16X4: usize = 14; +pub const TX_16X64: usize = 17; +pub const TX_16X8: usize = 8; +pub const TX_32X16: usize = 10; +pub const TX_32X32: usize = 3; +pub const TX_32X64: usize = 11; +pub const TX_32X8: usize = 16; +pub const TX_4X16: usize = 13; +pub const TX_4X4: usize = 0; +pub const TX_4X8: usize = 5; +pub const TX_64X16: usize = 18; +pub const TX_64X32: usize = 12; +pub const TX_64X64: usize = 4; +pub const TX_8X16: usize = 7; +pub const TX_8X32: usize = 15; +pub const TX_8X4: usize = 6; +pub const TX_8X8: usize = 1; +pub const TX_CLASS_2D: usize = 0; +pub const TX_CLASS_HORIZ: usize = 1; +pub const TX_CLASS_VERT: usize = 2; +pub const TX_MODES: usize = 3; +pub const TX_MODE_LARGEST: usize = 1; +pub const TX_MODE_SELECT: usize = 2; +pub const TX_SET_DCTONLY: usize = 0; +pub const TX_SET_INTER_1: usize = 1; +pub const TX_SET_INTER_2: usize = 2; +pub const TX_SET_INTER_3: usize = 3; +pub const TX_SET_INTRA_1: usize = 1; +pub const TX_SET_INTRA_2: usize = 2; +pub const TX_SET_TYPES_INTER: usize = 4; +pub const TX_SET_TYPES_INTRA: usize = 3; +pub const TX_SIZES: usize = 5; +pub const TX_SIZES_ALL: usize = 19; +pub const TX_SIZE_CONTEXTS: usize = 3; +pub const TX_TYPES: usize = 16; +pub const UNIDIR_COMP_REFERENCE: usize = 0; +pub const UNIDIR_COMP_REFS: usize = 4; +pub const UNIFORM_45: usize = 0; +pub const UNIFORM_45_INV: usize = 1; +pub const UV_CFL_PRED: usize = 13; +pub const UV_INTRA_MODES_CFL_ALLOWED: usize = 14; +pub const UV_INTRA_MODES_CFL_NOT_ALLOWED: usize = 13; +pub const V_ADST: usize = 12; +pub const V_DCT: usize = 10; +pub const V_FLIPADST: usize = 14; +pub const V_PRED: usize = 1; +pub const WARPEDDIFF_PREC_BITS: usize = 10; +pub const WARPEDMODEL_NONDIAGAFFINE_CLAMP: usize = 8192; +pub const WARPEDMODEL_PREC_BITS: usize = 16; +pub const WARPEDMODEL_TRANS_CLAMP: usize = 8388608; +pub const WARPEDPIXEL_PREC_SHIFTS: usize = 64; +pub const WARP_PARAM_REDUCE_BITS: usize = 6; +pub const WEDGE_HORIZONTAL: usize = 0; +pub const WEDGE_OBLIQUE117: usize = 4; +pub const WEDGE_OBLIQUE153: usize = 5; +pub const WEDGE_OBLIQUE27: usize = 2; +pub const WEDGE_OBLIQUE63: usize = 3; +pub const WEDGE_TYPES: usize = 16; +pub const WEDGE_VERTICAL: usize = 1; +pub const WIENER_COEFFS: usize = 3; +pub const ZERO_MV_CONTEXTS: usize = 2; + +pub static AC_QLOOKUP: [[u16; 256]; 3] = [ + [ + 4, 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, 104, 106, 108, 110, 112, 114, 116, 118, 120, 122, 124, 126, 128, + 130, 132, 134, 136, 138, 140, 142, 144, 146, 148, 150, 152, 155, 158, 161, 164, 167, 170, 173, 176, 179, 182, 185, 188, 191, 194, 197, 200, 203, 207, + 211, 215, 219, 223, 227, 231, 235, 239, 243, 247, 251, 255, 260, 265, 270, 275, 280, 285, 290, 295, 300, 305, 311, 317, 323, 329, 335, 341, 347, 353, + 359, 366, 373, 380, 387, 394, 401, 408, 416, 424, 432, 440, 448, 456, 465, 474, 483, 492, 501, 510, 520, 530, 540, 550, 560, 571, 582, 593, 604, 615, + 627, 639, 651, 663, 676, 689, 702, 715, 729, 743, 757, 771, 786, 801, 816, 832, 848, 864, 881, 898, 915, 933, 951, 969, 988, 1007, 1026, 1046, 1066, + 1087, 1108, 1129, 1151, 1173, 1196, 1219, 1243, 1267, 1292, 1317, 1343, 1369, 1396, 1423, 1451, 1479, 1508, 1537, 1567, 1597, 1628, 1660, 1692, 1725, + 1759, 1793, 1828, + ], + [ + 4, 9, 11, 13, 16, 18, 21, 24, 27, 30, 33, 37, 40, 44, 48, 51, 55, 59, 63, 67, 71, 75, 79, 83, 88, 92, 96, 100, 105, 109, 114, 118, 122, 127, 131, 136, + 140, 145, 149, 154, 158, 163, 168, 172, 177, 181, 186, 190, 195, 199, 204, 208, 213, 217, 222, 226, 231, 235, 240, 244, 249, 253, 258, 262, 267, 271, + 275, 280, 284, 289, 293, 297, 302, 306, 311, 315, 319, 324, 328, 332, 337, 341, 345, 349, 354, 358, 362, 367, 371, 375, 379, 384, 388, 392, 396, 401, + 409, 417, 425, 433, 441, 449, 458, 466, 474, 482, 490, 498, 506, 514, 523, 531, 539, 547, 555, 563, 571, 579, 588, 596, 604, 616, 628, 640, 652, 664, + 676, 688, 700, 713, 725, 737, 749, 761, 773, 785, 797, 809, 825, 841, 857, 873, 889, 905, 922, 938, 954, 970, 986, 1002, 1018, 1038, 1058, 1078, 1098, + 1118, 1138, 1158, 1178, 1198, 1218, 1242, 1266, 1290, 1314, 1338, 1362, 1386, 1411, 1435, 1463, 1491, 1519, 1547, 1575, 1603, 1631, 1663, 1695, 1727, + 1759, 1791, 1823, 1859, 1895, 1931, 1967, 2003, 2039, 2079, 2119, 2159, 2199, 2239, 2283, 2327, 2371, 2415, 2459, 2507, 2555, 2603, 2651, 2703, 2755, + 2807, 2859, 2915, 2971, 3027, 3083, 3143, 3203, 3263, 3327, 3391, 3455, 3523, 3591, 3659, 3731, 3803, 3876, 3952, 4028, 4104, 4184, 4264, 4348, 4432, + 4516, 4604, 4692, 4784, 4876, 4972, 5068, 5168, 5268, 5372, 5476, 5584, 5692, 5804, 5916, 6032, 6148, 6268, 6388, 6512, 6640, 6768, 6900, 7036, 7172, + 7312, + ], + [ + 4, 13, 19, 27, 35, 44, 54, 64, 75, 87, 99, 112, 126, 139, 154, 168, 183, 199, 214, 230, 247, 263, 280, 297, 314, 331, 349, 366, 384, 402, 420, 438, + 456, 475, 493, 511, 530, 548, 567, 586, 604, 623, 642, 660, 679, 698, 716, 735, 753, 772, 791, 809, 828, 846, 865, 884, 902, 920, 939, 957, 976, 994, + 1012, 1030, 1049, 1067, 1085, 1103, 1121, 1139, 1157, 1175, 1193, 1211, 1229, 1246, 1264, 1282, 1299, 1317, 1335, 1352, 1370, 1387, 1405, 1422, 1440, + 1457, 1474, 1491, 1509, 1526, 1543, 1560, 1577, 1595, 1627, 1660, 1693, 1725, 1758, 1791, 1824, 1856, 1889, 1922, 1954, 1987, 2020, 2052, 2085, 2118, + 2150, 2183, 2216, 2248, 2281, 2313, 2346, 2378, 2411, 2459, 2508, 2556, 2605, 2653, 2701, 2750, 2798, 2847, 2895, 2943, 2992, 3040, 3088, 3137, 3185, + 3234, 3298, 3362, 3426, 3491, 3555, 3619, 3684, 3748, 3812, 3876, 3941, 4005, 4069, 4149, 4230, 4310, 4390, 4470, 4550, 4631, 4711, 4791, 4871, 4967, + 5064, 5160, 5256, 5352, 5448, 5544, 5641, 5737, 5849, 5961, 6073, 6185, 6297, 6410, 6522, 6650, 6778, 6906, 7034, 7162, 7290, 7435, 7579, 7723, 7867, + 8011, 8155, 8315, 8475, 8635, 8795, 8956, 9132, 9308, 9484, 9660, 9836, 10028, 10220, 10412, 10604, 10812, 11020, 11228, 11437, 11661, 11885, 12109, + 12333, 12573, 12813, 13053, 13309, 13565, 13821, 14093, 14365, 14637, 14925, 15213, 15502, 15806, 16110, 16414, 16734, 17054, 17390, 17726, 18062, + 18414, 18766, 19134, 19502, 19886, 20270, 20670, 21070, 21486, 21902, 22334, 22766, 23214, 23662, 24126, 24590, 25070, 25551, 26047, 26559, 27071, + 27599, 28143, 28687, 29247, + ], +]; + +pub static ADJUSTED_TX_SIZE: [u8; 19] = [0, 1, 2, 3, 3, 5, 6, 7, 8, 9, 10, 3, 3, 13, 14, 15, 16, 9, 10]; + +pub static COEFF_BASE_CTX_OFFSET: [[[u8; 5]; 5]; 19] = [ + [[0, 1, 6, 6, 0], [1, 6, 6, 21, 0], [6, 6, 21, 21, 0], [6, 21, 21, 21, 0], [0, 0, 0, 0, 0]], + [[0, 1, 6, 6, 21], [1, 6, 6, 21, 21], [6, 6, 21, 21, 21], [6, 21, 21, 21, 21], [21, 21, 21, 21, 21]], + [[0, 1, 6, 6, 21], [1, 6, 6, 21, 21], [6, 6, 21, 21, 21], [6, 21, 21, 21, 21], [21, 21, 21, 21, 21]], + [[0, 1, 6, 6, 21], [1, 6, 6, 21, 21], [6, 6, 21, 21, 21], [6, 21, 21, 21, 21], [21, 21, 21, 21, 21]], + [[0, 1, 6, 6, 21], [1, 6, 6, 21, 21], [6, 6, 21, 21, 21], [6, 21, 21, 21, 21], [21, 21, 21, 21, 21]], + [[0, 11, 11, 11, 0], [11, 11, 11, 11, 0], [6, 6, 21, 21, 0], [6, 21, 21, 21, 0], [21, 21, 21, 21, 0]], + [[0, 16, 6, 6, 21], [16, 16, 6, 21, 21], [16, 16, 21, 21, 21], [16, 16, 21, 21, 21], [0, 0, 0, 0, 0]], + [[0, 11, 11, 11, 11], [11, 11, 11, 11, 11], [6, 6, 21, 21, 21], [6, 21, 21, 21, 21], [21, 21, 21, 21, 21]], + [[0, 16, 6, 6, 21], [16, 16, 6, 21, 21], [16, 16, 21, 21, 21], [16, 16, 21, 21, 21], [16, 16, 21, 21, 21]], + [[0, 11, 11, 11, 11], [11, 11, 11, 11, 11], [6, 6, 21, 21, 21], [6, 21, 21, 21, 21], [21, 21, 21, 21, 21]], + [[0, 16, 6, 6, 21], [16, 16, 6, 21, 21], [16, 16, 21, 21, 21], [16, 16, 21, 21, 21], [16, 16, 21, 21, 21]], + [[0, 11, 11, 11, 11], [11, 11, 11, 11, 11], [6, 6, 21, 21, 21], [6, 21, 21, 21, 21], [21, 21, 21, 21, 21]], + [[0, 16, 6, 6, 21], [16, 16, 6, 21, 21], [16, 16, 21, 21, 21], [16, 16, 21, 21, 21], [16, 16, 21, 21, 21]], + [[0, 11, 11, 11, 0], [11, 11, 11, 11, 0], [6, 6, 21, 21, 0], [6, 21, 21, 21, 0], [21, 21, 21, 21, 0]], + [[0, 16, 6, 6, 21], [16, 16, 6, 21, 21], [16, 16, 21, 21, 21], [16, 16, 21, 21, 21], [0, 0, 0, 0, 0]], + [[0, 11, 11, 11, 11], [11, 11, 11, 11, 11], [6, 6, 21, 21, 21], [6, 21, 21, 21, 21], [21, 21, 21, 21, 21]], + [[0, 16, 6, 6, 21], [16, 16, 6, 21, 21], [16, 16, 21, 21, 21], [16, 16, 21, 21, 21], [16, 16, 21, 21, 21]], + [[0, 11, 11, 11, 11], [11, 11, 11, 11, 11], [6, 6, 21, 21, 21], [6, 21, 21, 21, 21], [21, 21, 21, 21, 21]], + [[0, 16, 6, 6, 21], [16, 16, 6, 21, 21], [16, 16, 21, 21, 21], [16, 16, 21, 21, 21], [16, 16, 21, 21, 21]], +]; + +pub static COEFF_BASE_POS_CTX_OFFSET: [u8; 3] = [26, 31, 36]; + +pub static COS128_LOOKUP: [u16; 65] = [ + 4096, 4095, 4091, 4085, 4076, 4065, 4052, 4036, 4017, 3996, 3973, 3948, 3920, 3889, 3857, 3822, 3784, 3745, 3703, 3659, 3612, 3564, 3513, 3461, 3406, 3349, + 3290, 3229, 3166, 3102, 3035, 2967, 2896, 2824, 2751, 2675, 2598, 2520, 2440, 2359, 2276, 2191, 2106, 2019, 1931, 1842, 1751, 1660, 1567, 1474, 1380, 1285, + 1189, 1092, 995, 897, 799, 700, 601, 501, 401, 301, 201, 101, 0, +]; + +pub static DC_QLOOKUP: [[u16; 256]; 3] = [ + [ + 4, 8, 8, 9, 10, 11, 12, 12, 13, 14, 15, 16, 17, 18, 19, 19, 20, 21, 22, 23, 24, 25, 26, 26, 27, 28, 29, 30, 31, 32, 32, 33, 34, 35, 36, 37, 38, 38, 39, + 40, 41, 42, 43, 43, 44, 45, 46, 47, 48, 48, 49, 50, 51, 52, 53, 53, 54, 55, 56, 57, 57, 58, 59, 60, 61, 62, 62, 63, 64, 65, 66, 66, 67, 68, 69, 70, 70, + 71, 72, 73, 74, 74, 75, 76, 77, 78, 78, 79, 80, 81, 81, 82, 83, 84, 85, 85, 87, 88, 90, 92, 93, 95, 96, 98, 99, 101, 102, 104, 105, 107, 108, 110, 111, + 113, 114, 116, 117, 118, 120, 121, 123, 125, 127, 129, 131, 134, 136, 138, 140, 142, 144, 146, 148, 150, 152, 154, 156, 158, 161, 164, 166, 169, 172, + 174, 177, 180, 182, 185, 187, 190, 192, 195, 199, 202, 205, 208, 211, 214, 217, 220, 223, 226, 230, 233, 237, 240, 243, 247, 250, 253, 257, 261, 265, + 269, 272, 276, 280, 284, 288, 292, 296, 300, 304, 309, 313, 317, 322, 326, 330, 335, 340, 344, 349, 354, 359, 364, 369, 374, 379, 384, 389, 395, 400, + 406, 411, 417, 423, 429, 435, 441, 447, 454, 461, 467, 475, 482, 489, 497, 505, 513, 522, 530, 539, 549, 559, 569, 579, 590, 602, 614, 626, 640, 654, + 668, 684, 700, 717, 736, 755, 775, 796, 819, 843, 869, 896, 925, 955, 988, 1022, 1058, 1098, 1139, 1184, 1232, 1282, 1336, + ], + [ + 4, 9, 10, 13, 15, 17, 20, 22, 25, 28, 31, 34, 37, 40, 43, 47, 50, 53, 57, 60, 64, 68, 71, 75, 78, 82, 86, 90, 93, 97, 101, 105, 109, 113, 116, 120, + 124, 128, 132, 136, 140, 143, 147, 151, 155, 159, 163, 166, 170, 174, 178, 182, 185, 189, 193, 197, 200, 204, 208, 212, 215, 219, 223, 226, 230, 233, + 237, 241, 244, 248, 251, 255, 259, 262, 266, 269, 273, 276, 280, 283, 287, 290, 293, 297, 300, 304, 307, 310, 314, 317, 321, 324, 327, 331, 334, 337, + 343, 350, 356, 362, 369, 375, 381, 387, 394, 400, 406, 412, 418, 424, 430, 436, 442, 448, 454, 460, 466, 472, 478, 484, 490, 499, 507, 516, 525, 533, + 542, 550, 559, 567, 576, 584, 592, 601, 609, 617, 625, 634, 644, 655, 666, 676, 687, 698, 708, 718, 729, 739, 749, 759, 770, 782, 795, 807, 819, 831, + 844, 856, 868, 880, 891, 906, 920, 933, 947, 961, 975, 988, 1001, 1015, 1030, 1045, 1061, 1076, 1090, 1105, 1120, 1137, 1153, 1170, 1186, 1202, 1218, + 1236, 1253, 1271, 1288, 1306, 1323, 1342, 1361, 1379, 1398, 1416, 1436, 1456, 1476, 1496, 1516, 1537, 1559, 1580, 1601, 1624, 1647, 1670, 1692, 1717, + 1741, 1766, 1791, 1817, 1844, 1871, 1900, 1929, 1958, 1990, 2021, 2054, 2088, 2123, 2159, 2197, 2236, 2276, 2319, 2363, 2410, 2458, 2508, 2561, 2616, + 2675, 2737, 2802, 2871, 2944, 3020, 3102, 3188, 3280, 3375, 3478, 3586, 3702, 3823, 3953, 4089, 4236, 4394, 4559, 4737, 4929, 5130, 5347, + ], + [ + 4, 12, 18, 25, 33, 41, 50, 60, 70, 80, 91, 103, 115, 127, 140, 153, 166, 180, 194, 208, 222, 237, 251, 266, 281, 296, 312, 327, 343, 358, 374, 390, + 405, 421, 437, 453, 469, 484, 500, 516, 532, 548, 564, 580, 596, 611, 627, 643, 659, 674, 690, 706, 721, 737, 752, 768, 783, 798, 814, 829, 844, 859, + 874, 889, 904, 919, 934, 949, 964, 978, 993, 1008, 1022, 1037, 1051, 1065, 1080, 1094, 1108, 1122, 1136, 1151, 1165, 1179, 1192, 1206, 1220, 1234, + 1248, 1261, 1275, 1288, 1302, 1315, 1329, 1342, 1368, 1393, 1419, 1444, 1469, 1494, 1519, 1544, 1569, 1594, 1618, 1643, 1668, 1692, 1717, 1741, 1765, + 1789, 1814, 1838, 1862, 1885, 1909, 1933, 1957, 1992, 2027, 2061, 2096, 2130, 2165, 2199, 2233, 2267, 2300, 2334, 2367, 2400, 2434, 2467, 2499, 2532, + 2575, 2618, 2661, 2704, 2746, 2788, 2830, 2872, 2913, 2954, 2995, 3036, 3076, 3127, 3177, 3226, 3275, 3324, 3373, 3421, 3469, 3517, 3565, 3621, 3677, + 3733, 3788, 3843, 3897, 3951, 4005, 4058, 4119, 4181, 4241, 4301, 4361, 4420, 4479, 4546, 4612, 4677, 4742, 4807, 4871, 4942, 5013, 5083, 5153, 5222, + 5291, 5367, 5442, 5517, 5591, 5665, 5745, 5825, 5905, 5984, 6063, 6149, 6234, 6319, 6404, 6495, 6587, 6678, 6769, 6867, 6966, 7064, 7163, 7269, 7376, + 7483, 7599, 7715, 7832, 7958, 8085, 8214, 8352, 8492, 8635, 8788, 8945, 9104, 9275, 9450, 9639, 9832, 10031, 10245, 10465, 10702, 10946, 11210, 11482, + 11776, 12081, 12409, 12750, 13118, 13501, 13913, 14343, 14807, 15290, 15812, 16356, 16943, 17575, 18237, 18949, 19718, 20521, 21387, + ], +]; + +pub static DEFAULT_ANGLE_DELTA_CDF: [[u16; 8]; 8] = [ + [2180, 5032, 7567, 22776, 26989, 30217, 32768, 0], + [2301, 5608, 8801, 23487, 26974, 30330, 32768, 0], + [3780, 11018, 13699, 19354, 23083, 31286, 32768, 0], + [4581, 11226, 15147, 17138, 21834, 28397, 32768, 0], + [1737, 10927, 14509, 19588, 22745, 28823, 32768, 0], + [2664, 10176, 12485, 17650, 21600, 30495, 32768, 0], + [2240, 11096, 15453, 20341, 22561, 28917, 32768, 0], + [3605, 10428, 12459, 17676, 21244, 30655, 32768, 0], +]; + +pub static DEFAULT_COEFF_BASE_CDF: [[[[[u16; 5]; 42]; 2]; 5]; 4] = [ + [ + [ + [ + [4034, 8930, 12727, 32768, 0], + [18082, 29741, 31877, 32768, 0], + [12596, 26124, 30493, 32768, 0], + [9446, 21118, 27005, 32768, 0], + [6308, 15141, 21279, 32768, 0], + [2463, 6357, 9783, 32768, 0], + [20667, 30546, 31929, 32768, 0], + [13043, 26123, 30134, 32768, 0], + [8151, 18757, 24778, 32768, 0], + [5255, 12839, 18632, 32768, 0], + [2820, 7206, 11161, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [15736, 27553, 30604, 32768, 0], + [11210, 23794, 28787, 32768, 0], + [5947, 13874, 19701, 32768, 0], + [4215, 9323, 13891, 32768, 0], + [2833, 6462, 10059, 32768, 0], + [19605, 30393, 31582, 32768, 0], + [13523, 26252, 30248, 32768, 0], + [8446, 18622, 24512, 32768, 0], + [3818, 10343, 15974, 32768, 0], + [1481, 4117, 6796, 32768, 0], + [22649, 31302, 32190, 32768, 0], + [14829, 27127, 30449, 32768, 0], + [8313, 17702, 23304, 32768, 0], + [3022, 8301, 12786, 32768, 0], + [1536, 4412, 7184, 32768, 0], + [22354, 29774, 31372, 32768, 0], + [14723, 25472, 29214, 32768, 0], + [6673, 13745, 18662, 32768, 0], + [2068, 5766, 9322, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + ], + [ + [6302, 16444, 21761, 32768, 0], + [23040, 31538, 32475, 32768, 0], + [15196, 28452, 31496, 32768, 0], + [10020, 22946, 28514, 32768, 0], + [6533, 16862, 23501, 32768, 0], + [3538, 9816, 15076, 32768, 0], + [24444, 31875, 32525, 32768, 0], + [15881, 28924, 31635, 32768, 0], + [9922, 22873, 28466, 32768, 0], + [6527, 16966, 23691, 32768, 0], + [4114, 11303, 17220, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [20201, 30770, 32209, 32768, 0], + [14754, 28071, 31258, 32768, 0], + [8378, 20186, 26517, 32768, 0], + [5916, 15299, 21978, 32768, 0], + [4268, 11583, 17901, 32768, 0], + [24361, 32025, 32581, 32768, 0], + [18673, 30105, 31943, 32768, 0], + [10196, 22244, 27576, 32768, 0], + [5495, 14349, 20417, 32768, 0], + [2676, 7415, 11498, 32768, 0], + [24678, 31958, 32585, 32768, 0], + [18629, 29906, 31831, 32768, 0], + [9364, 20724, 26315, 32768, 0], + [4641, 12318, 18094, 32768, 0], + [2758, 7387, 11579, 32768, 0], + [25433, 31842, 32469, 32768, 0], + [18795, 29289, 31411, 32768, 0], + [7644, 17584, 23592, 32768, 0], + [3408, 9014, 15047, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + ], + ], + [ + [ + [4536, 10072, 14001, 32768, 0], + [25459, 31416, 32206, 32768, 0], + [16605, 28048, 30818, 32768, 0], + [11008, 22857, 27719, 32768, 0], + [6915, 16268, 22315, 32768, 0], + [2625, 6812, 10537, 32768, 0], + [24257, 31788, 32499, 32768, 0], + [16880, 29454, 31879, 32768, 0], + [11958, 25054, 29778, 32768, 0], + [7916, 18718, 25084, 32768, 0], + [3383, 8777, 13446, 32768, 0], + [22720, 31603, 32393, 32768, 0], + [14960, 28125, 31335, 32768, 0], + [9731, 22210, 27928, 32768, 0], + [6304, 15832, 22277, 32768, 0], + [2910, 7818, 12166, 32768, 0], + [20375, 30627, 32131, 32768, 0], + [13904, 27284, 30887, 32768, 0], + [9368, 21558, 27144, 32768, 0], + [5937, 14966, 21119, 32768, 0], + [2667, 7225, 11319, 32768, 0], + [23970, 31470, 32378, 32768, 0], + [17173, 29734, 32018, 32768, 0], + [12795, 25441, 29965, 32768, 0], + [8981, 19680, 25893, 32768, 0], + [4728, 11372, 16902, 32768, 0], + [24287, 31797, 32439, 32768, 0], + [16703, 29145, 31696, 32768, 0], + [10833, 23554, 28725, 32768, 0], + [6468, 16566, 23057, 32768, 0], + [2415, 6562, 10278, 32768, 0], + [26610, 32395, 32659, 32768, 0], + [18590, 30498, 32117, 32768, 0], + [12420, 25756, 29950, 32768, 0], + [7639, 18746, 24710, 32768, 0], + [3001, 8086, 12347, 32768, 0], + [25076, 32064, 32580, 32768, 0], + [17946, 30128, 32028, 32768, 0], + [12024, 24985, 29378, 32768, 0], + [7517, 18390, 24304, 32768, 0], + [3243, 8781, 13331, 32768, 0], + [8192, 16384, 24576, 32768, 0], + ], + [ + [6037, 16771, 21957, 32768, 0], + [24774, 31704, 32426, 32768, 0], + [16830, 28589, 31056, 32768, 0], + [10602, 22828, 27760, 32768, 0], + [6733, 16829, 23071, 32768, 0], + [3250, 8914, 13556, 32768, 0], + [25582, 32220, 32668, 32768, 0], + [18659, 30342, 32223, 32768, 0], + [12546, 26149, 30515, 32768, 0], + [8420, 20451, 26801, 32768, 0], + [4636, 12420, 18344, 32768, 0], + [27581, 32362, 32639, 32768, 0], + [18987, 30083, 31978, 32768, 0], + [11327, 24248, 29084, 32768, 0], + [7264, 17719, 24120, 32768, 0], + [3995, 10768, 16169, 32768, 0], + [25893, 31831, 32487, 32768, 0], + [16577, 28587, 31379, 32768, 0], + [10189, 22748, 28182, 32768, 0], + [6832, 17094, 23556, 32768, 0], + [3708, 10110, 15334, 32768, 0], + [25904, 32282, 32656, 32768, 0], + [19721, 30792, 32276, 32768, 0], + [12819, 26243, 30411, 32768, 0], + [8572, 20614, 26891, 32768, 0], + [5364, 14059, 20467, 32768, 0], + [26580, 32438, 32677, 32768, 0], + [20852, 31225, 32340, 32768, 0], + [12435, 25700, 29967, 32768, 0], + [8691, 20825, 26976, 32768, 0], + [4446, 12209, 17269, 32768, 0], + [27350, 32429, 32696, 32768, 0], + [21372, 30977, 32272, 32768, 0], + [12673, 25270, 29853, 32768, 0], + [9208, 20925, 26640, 32768, 0], + [5018, 13351, 18732, 32768, 0], + [27351, 32479, 32713, 32768, 0], + [21398, 31209, 32387, 32768, 0], + [12162, 25047, 29842, 32768, 0], + [7896, 18691, 25319, 32768, 0], + [4670, 12882, 18881, 32768, 0], + [8192, 16384, 24576, 32768, 0], + ], + ], + [ + [ + [5487, 10460, 13708, 32768, 0], + [21597, 28303, 30674, 32768, 0], + [11037, 21953, 26476, 32768, 0], + [8147, 17962, 22952, 32768, 0], + [5242, 13061, 18532, 32768, 0], + [1889, 5208, 8182, 32768, 0], + [26774, 32133, 32590, 32768, 0], + [17844, 29564, 31767, 32768, 0], + [11690, 24438, 29171, 32768, 0], + [7542, 18215, 24459, 32768, 0], + [2993, 8050, 12319, 32768, 0], + [28023, 32328, 32591, 32768, 0], + [18651, 30126, 31954, 32768, 0], + [12164, 25146, 29589, 32768, 0], + [7762, 18530, 24771, 32768, 0], + [3492, 9183, 13920, 32768, 0], + [27591, 32008, 32491, 32768, 0], + [17149, 28853, 31510, 32768, 0], + [11485, 24003, 28860, 32768, 0], + [7697, 18086, 24210, 32768, 0], + [3075, 7999, 12218, 32768, 0], + [28268, 32482, 32654, 32768, 0], + [19631, 31051, 32404, 32768, 0], + [13860, 27260, 31020, 32768, 0], + [9605, 21613, 27594, 32768, 0], + [4876, 12162, 17908, 32768, 0], + [27248, 32316, 32576, 32768, 0], + [18955, 30457, 32075, 32768, 0], + [11824, 23997, 28795, 32768, 0], + [7346, 18196, 24647, 32768, 0], + [3403, 9247, 14111, 32768, 0], + [29711, 32655, 32735, 32768, 0], + [21169, 31394, 32417, 32768, 0], + [13487, 27198, 30957, 32768, 0], + [8828, 21683, 27614, 32768, 0], + [4270, 11451, 17038, 32768, 0], + [28708, 32578, 32731, 32768, 0], + [20120, 31241, 32482, 32768, 0], + [13692, 27550, 31321, 32768, 0], + [9418, 22514, 28439, 32768, 0], + [4999, 13283, 19462, 32768, 0], + [8192, 16384, 24576, 32768, 0], + ], + [ + [5673, 14302, 19711, 32768, 0], + [26251, 30701, 31834, 32768, 0], + [12782, 23783, 27803, 32768, 0], + [9127, 20657, 25808, 32768, 0], + [6368, 16208, 21462, 32768, 0], + [2465, 7177, 10822, 32768, 0], + [29961, 32563, 32719, 32768, 0], + [18318, 29891, 31949, 32768, 0], + [11361, 24514, 29357, 32768, 0], + [7900, 19603, 25607, 32768, 0], + [4002, 10590, 15546, 32768, 0], + [29637, 32310, 32595, 32768, 0], + [18296, 29913, 31809, 32768, 0], + [10144, 21515, 26871, 32768, 0], + [5358, 14322, 20394, 32768, 0], + [3067, 8362, 13346, 32768, 0], + [28652, 32470, 32676, 32768, 0], + [17538, 30771, 32209, 32768, 0], + [13924, 26882, 30494, 32768, 0], + [10496, 22837, 27869, 32768, 0], + [7236, 16396, 21621, 32768, 0], + [30743, 32687, 32746, 32768, 0], + [23006, 31676, 32489, 32768, 0], + [14494, 27828, 31120, 32768, 0], + [10174, 22801, 28352, 32768, 0], + [6242, 15281, 21043, 32768, 0], + [25817, 32243, 32720, 32768, 0], + [18618, 31367, 32325, 32768, 0], + [13997, 28318, 31878, 32768, 0], + [12255, 26534, 31383, 32768, 0], + [9561, 21588, 28450, 32768, 0], + [28188, 32635, 32724, 32768, 0], + [22060, 32365, 32728, 32768, 0], + [18102, 30690, 32528, 32768, 0], + [14196, 28864, 31999, 32768, 0], + [12262, 25792, 30865, 32768, 0], + [24176, 32109, 32628, 32768, 0], + [18280, 29681, 31963, 32768, 0], + [10205, 23703, 29664, 32768, 0], + [7889, 20025, 27676, 32768, 0], + [6060, 16743, 23970, 32768, 0], + [8192, 16384, 24576, 32768, 0], + ], + ], + [ + [ + [5141, 7096, 8260, 32768, 0], + [27186, 29022, 29789, 32768, 0], + [6668, 12568, 15682, 32768, 0], + [2172, 6181, 8638, 32768, 0], + [1126, 3379, 4531, 32768, 0], + [443, 1361, 2254, 32768, 0], + [26083, 31153, 32436, 32768, 0], + [13486, 24603, 28483, 32768, 0], + [6508, 14840, 19910, 32768, 0], + [3386, 8800, 13286, 32768, 0], + [1530, 4322, 7054, 32768, 0], + [29639, 32080, 32548, 32768, 0], + [15897, 27552, 30290, 32768, 0], + [8588, 20047, 25383, 32768, 0], + [4889, 13339, 19269, 32768, 0], + [2240, 6871, 10498, 32768, 0], + [28165, 32197, 32517, 32768, 0], + [20735, 30427, 31568, 32768, 0], + [14325, 24671, 27692, 32768, 0], + [5119, 12554, 17805, 32768, 0], + [1810, 5441, 8261, 32768, 0], + [31212, 32724, 32748, 32768, 0], + [23352, 31766, 32545, 32768, 0], + [14669, 27570, 31059, 32768, 0], + [8492, 20894, 27272, 32768, 0], + [3644, 10194, 15204, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + ], + [ + [2461, 7013, 9371, 32768, 0], + [24749, 29600, 30986, 32768, 0], + [9466, 19037, 22417, 32768, 0], + [3584, 9280, 14400, 32768, 0], + [1505, 3929, 5433, 32768, 0], + [677, 1500, 2736, 32768, 0], + [23987, 30702, 32117, 32768, 0], + [13554, 24571, 29263, 32768, 0], + [6211, 14556, 21155, 32768, 0], + [3135, 10972, 15625, 32768, 0], + [2435, 7127, 11427, 32768, 0], + [31300, 32532, 32550, 32768, 0], + [14757, 30365, 31954, 32768, 0], + [4405, 11612, 18553, 32768, 0], + [580, 4132, 7322, 32768, 0], + [1695, 10169, 14124, 32768, 0], + [30008, 32282, 32591, 32768, 0], + [19244, 30108, 31748, 32768, 0], + [11180, 24158, 29555, 32768, 0], + [5650, 14972, 19209, 32768, 0], + [2114, 5109, 8456, 32768, 0], + [31856, 32716, 32748, 32768, 0], + [23012, 31664, 32572, 32768, 0], + [13694, 26656, 30636, 32768, 0], + [8142, 19508, 26093, 32768, 0], + [4253, 10955, 16724, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + ], + ], + [ + [ + [601, 983, 1311, 32768, 0], + [18725, 23406, 28087, 32768, 0], + [5461, 8192, 10923, 32768, 0], + [3781, 15124, 21425, 32768, 0], + [2587, 7761, 12072, 32768, 0], + [106, 458, 810, 32768, 0], + [22282, 29710, 31894, 32768, 0], + [8508, 20926, 25984, 32768, 0], + [3726, 12713, 18083, 32768, 0], + [1620, 7112, 10893, 32768, 0], + [729, 2236, 3495, 32768, 0], + [30163, 32474, 32684, 32768, 0], + [18304, 30464, 32000, 32768, 0], + [11443, 26526, 29647, 32768, 0], + [6007, 15292, 21299, 32768, 0], + [2234, 6703, 8937, 32768, 0], + [30954, 32177, 32571, 32768, 0], + [17363, 29562, 31076, 32768, 0], + [9686, 22464, 27410, 32768, 0], + [8192, 16384, 21390, 32768, 0], + [1755, 8046, 11264, 32768, 0], + [31168, 32734, 32748, 32768, 0], + [22486, 31441, 32471, 32768, 0], + [12833, 25627, 29738, 32768, 0], + [6980, 17379, 23122, 32768, 0], + [3111, 8887, 13479, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + ], + [ + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + ], + ], + ], + [ + [ + [ + [6041, 11854, 15927, 32768, 0], + [20326, 30905, 32251, 32768, 0], + [14164, 26831, 30725, 32768, 0], + [9760, 20647, 26585, 32768, 0], + [6416, 14953, 21219, 32768, 0], + [2966, 7151, 10891, 32768, 0], + [23567, 31374, 32254, 32768, 0], + [14978, 27416, 30946, 32768, 0], + [9434, 20225, 26254, 32768, 0], + [6658, 14558, 20535, 32768, 0], + [3916, 8677, 12989, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [18088, 29545, 31587, 32768, 0], + [13062, 25843, 30073, 32768, 0], + [8940, 16827, 22251, 32768, 0], + [7654, 13220, 17973, 32768, 0], + [5733, 10316, 14456, 32768, 0], + [22879, 31388, 32114, 32768, 0], + [15215, 27993, 30955, 32768, 0], + [9397, 19445, 24978, 32768, 0], + [3442, 9813, 15344, 32768, 0], + [1368, 3936, 6532, 32768, 0], + [25494, 32033, 32406, 32768, 0], + [16772, 27963, 30718, 32768, 0], + [9419, 18165, 23260, 32768, 0], + [2677, 7501, 11797, 32768, 0], + [1516, 4344, 7170, 32768, 0], + [26556, 31454, 32101, 32768, 0], + [17128, 27035, 30108, 32768, 0], + [8324, 15344, 20249, 32768, 0], + [1903, 5696, 9469, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + ], + [ + [8455, 19003, 24368, 32768, 0], + [23563, 32021, 32604, 32768, 0], + [16237, 29446, 31935, 32768, 0], + [10724, 23999, 29358, 32768, 0], + [6725, 17528, 24416, 32768, 0], + [3927, 10927, 16825, 32768, 0], + [26313, 32288, 32634, 32768, 0], + [17430, 30095, 32095, 32768, 0], + [11116, 24606, 29679, 32768, 0], + [7195, 18384, 25269, 32768, 0], + [4726, 12852, 19315, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [22822, 31648, 32483, 32768, 0], + [16724, 29633, 31929, 32768, 0], + [10261, 23033, 28725, 32768, 0], + [7029, 17840, 24528, 32768, 0], + [4867, 13886, 21502, 32768, 0], + [25298, 31892, 32491, 32768, 0], + [17809, 29330, 31512, 32768, 0], + [9668, 21329, 26579, 32768, 0], + [4774, 12956, 18976, 32768, 0], + [2322, 7030, 11540, 32768, 0], + [25472, 31920, 32543, 32768, 0], + [17957, 29387, 31632, 32768, 0], + [9196, 20593, 26400, 32768, 0], + [4680, 12705, 19202, 32768, 0], + [2917, 8456, 13436, 32768, 0], + [26471, 32059, 32574, 32768, 0], + [18458, 29783, 31909, 32768, 0], + [8400, 19464, 25956, 32768, 0], + [3812, 10973, 17206, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + ], + ], + [ + [ + [6779, 13743, 17678, 32768, 0], + [24806, 31797, 32457, 32768, 0], + [17616, 29047, 31372, 32768, 0], + [11063, 23175, 28003, 32768, 0], + [6521, 16110, 22324, 32768, 0], + [2764, 7504, 11654, 32768, 0], + [25266, 32367, 32637, 32768, 0], + [19054, 30553, 32175, 32768, 0], + [12139, 25212, 29807, 32768, 0], + [7311, 18162, 24704, 32768, 0], + [3397, 9164, 14074, 32768, 0], + [25988, 32208, 32522, 32768, 0], + [16253, 28912, 31526, 32768, 0], + [9151, 21387, 27372, 32768, 0], + [5688, 14915, 21496, 32768, 0], + [2717, 7627, 12004, 32768, 0], + [23144, 31855, 32443, 32768, 0], + [16070, 28491, 31325, 32768, 0], + [8702, 20467, 26517, 32768, 0], + [5243, 13956, 20367, 32768, 0], + [2621, 7335, 11567, 32768, 0], + [26636, 32340, 32630, 32768, 0], + [19990, 31050, 32341, 32768, 0], + [13243, 26105, 30315, 32768, 0], + [8588, 19521, 25918, 32768, 0], + [4717, 11585, 17304, 32768, 0], + [25844, 32292, 32582, 32768, 0], + [19090, 30635, 32097, 32768, 0], + [11963, 24546, 28939, 32768, 0], + [6218, 16087, 22354, 32768, 0], + [2340, 6608, 10426, 32768, 0], + [28046, 32576, 32694, 32768, 0], + [21178, 31313, 32296, 32768, 0], + [13486, 26184, 29870, 32768, 0], + [7149, 17871, 23723, 32768, 0], + [2833, 7958, 12259, 32768, 0], + [27710, 32528, 32686, 32768, 0], + [20674, 31076, 32268, 32768, 0], + [12413, 24955, 29243, 32768, 0], + [6676, 16927, 23097, 32768, 0], + [2966, 8333, 12919, 32768, 0], + [8192, 16384, 24576, 32768, 0], + ], + [ + [8639, 19339, 24429, 32768, 0], + [24404, 31837, 32525, 32768, 0], + [16997, 29425, 31784, 32768, 0], + [11253, 24234, 29149, 32768, 0], + [6751, 17394, 24028, 32768, 0], + [3490, 9830, 15191, 32768, 0], + [26283, 32471, 32714, 32768, 0], + [19599, 31168, 32442, 32768, 0], + [13146, 26954, 30893, 32768, 0], + [8214, 20588, 26890, 32768, 0], + [4699, 13081, 19300, 32768, 0], + [28212, 32458, 32669, 32768, 0], + [18594, 30316, 32100, 32768, 0], + [11219, 24408, 29234, 32768, 0], + [6865, 17656, 24149, 32768, 0], + [3678, 10362, 16006, 32768, 0], + [25825, 32136, 32616, 32768, 0], + [17313, 29853, 32021, 32768, 0], + [11197, 24471, 29472, 32768, 0], + [6947, 17781, 24405, 32768, 0], + [3768, 10660, 16261, 32768, 0], + [27352, 32500, 32706, 32768, 0], + [20850, 31468, 32469, 32768, 0], + [14021, 27707, 31133, 32768, 0], + [8964, 21748, 27838, 32768, 0], + [5437, 14665, 21187, 32768, 0], + [26304, 32492, 32698, 32768, 0], + [20409, 31380, 32385, 32768, 0], + [13682, 27222, 30632, 32768, 0], + [8974, 21236, 26685, 32768, 0], + [4234, 11665, 16934, 32768, 0], + [26273, 32357, 32711, 32768, 0], + [20672, 31242, 32441, 32768, 0], + [14172, 27254, 30902, 32768, 0], + [9870, 21898, 27275, 32768, 0], + [5164, 13506, 19270, 32768, 0], + [26725, 32459, 32728, 32768, 0], + [20991, 31442, 32527, 32768, 0], + [13071, 26434, 30811, 32768, 0], + [8184, 20090, 26742, 32768, 0], + [4803, 13255, 19895, 32768, 0], + [8192, 16384, 24576, 32768, 0], + ], + ], + [ + [ + [7555, 14942, 18501, 32768, 0], + [24410, 31178, 32287, 32768, 0], + [14394, 26738, 30253, 32768, 0], + [8413, 19554, 25195, 32768, 0], + [4766, 12924, 18785, 32768, 0], + [2029, 5806, 9207, 32768, 0], + [26776, 32364, 32663, 32768, 0], + [18732, 29967, 31931, 32768, 0], + [11005, 23786, 28852, 32768, 0], + [6466, 16909, 23510, 32768, 0], + [3044, 8638, 13419, 32768, 0], + [29208, 32582, 32704, 32768, 0], + [20068, 30857, 32208, 32768, 0], + [12003, 25085, 29595, 32768, 0], + [6947, 17750, 24189, 32768, 0], + [3245, 9103, 14007, 32768, 0], + [27359, 32465, 32669, 32768, 0], + [19421, 30614, 32174, 32768, 0], + [11915, 25010, 29579, 32768, 0], + [6950, 17676, 24074, 32768, 0], + [3007, 8473, 13096, 32768, 0], + [29002, 32676, 32735, 32768, 0], + [22102, 31849, 32576, 32768, 0], + [14408, 28009, 31405, 32768, 0], + [9027, 21679, 27931, 32768, 0], + [4694, 12678, 18748, 32768, 0], + [28216, 32528, 32682, 32768, 0], + [20849, 31264, 32318, 32768, 0], + [12756, 25815, 29751, 32768, 0], + [7565, 18801, 24923, 32768, 0], + [3509, 9533, 14477, 32768, 0], + [30133, 32687, 32739, 32768, 0], + [23063, 31910, 32515, 32768, 0], + [14588, 28051, 31132, 32768, 0], + [9085, 21649, 27457, 32768, 0], + [4261, 11654, 17264, 32768, 0], + [29518, 32691, 32748, 32768, 0], + [22451, 31959, 32613, 32768, 0], + [14864, 28722, 31700, 32768, 0], + [9695, 22964, 28716, 32768, 0], + [4932, 13358, 19502, 32768, 0], + [8192, 16384, 24576, 32768, 0], + ], + [ + [6465, 16958, 21688, 32768, 0], + [25199, 31514, 32360, 32768, 0], + [14774, 27149, 30607, 32768, 0], + [9257, 21438, 26972, 32768, 0], + [5723, 15183, 21882, 32768, 0], + [3150, 8879, 13731, 32768, 0], + [26989, 32262, 32682, 32768, 0], + [17396, 29937, 32085, 32768, 0], + [11387, 24901, 29784, 32768, 0], + [7289, 18821, 25548, 32768, 0], + [3734, 10577, 16086, 32768, 0], + [29728, 32501, 32695, 32768, 0], + [17431, 29701, 31903, 32768, 0], + [9921, 22826, 28300, 32768, 0], + [5896, 15434, 22068, 32768, 0], + [3430, 9646, 14757, 32768, 0], + [28614, 32511, 32705, 32768, 0], + [19364, 30638, 32263, 32768, 0], + [13129, 26254, 30402, 32768, 0], + [8754, 20484, 26440, 32768, 0], + [4378, 11607, 17110, 32768, 0], + [30292, 32671, 32744, 32768, 0], + [21780, 31603, 32501, 32768, 0], + [14314, 27829, 31291, 32768, 0], + [9611, 22327, 28263, 32768, 0], + [4890, 13087, 19065, 32768, 0], + [25862, 32567, 32733, 32768, 0], + [20794, 32050, 32567, 32768, 0], + [17243, 30625, 32254, 32768, 0], + [13283, 27628, 31474, 32768, 0], + [9669, 22532, 28918, 32768, 0], + [27435, 32697, 32748, 32768, 0], + [24922, 32390, 32714, 32768, 0], + [21449, 31504, 32536, 32768, 0], + [16392, 29729, 31832, 32768, 0], + [11692, 24884, 29076, 32768, 0], + [24193, 32290, 32735, 32768, 0], + [18909, 31104, 32563, 32768, 0], + [12236, 26841, 31403, 32768, 0], + [8171, 21840, 29082, 32768, 0], + [7224, 17280, 25275, 32768, 0], + [8192, 16384, 24576, 32768, 0], + ], + ], + [ + [ + [3078, 6839, 9890, 32768, 0], + [13837, 20450, 24479, 32768, 0], + [5914, 14222, 19328, 32768, 0], + [3866, 10267, 14762, 32768, 0], + [2612, 7208, 11042, 32768, 0], + [1067, 2991, 4776, 32768, 0], + [25817, 31646, 32529, 32768, 0], + [13708, 26338, 30385, 32768, 0], + [7328, 18585, 24870, 32768, 0], + [4691, 13080, 19276, 32768, 0], + [1825, 5253, 8352, 32768, 0], + [29386, 32315, 32624, 32768, 0], + [17160, 29001, 31360, 32768, 0], + [9602, 21862, 27396, 32768, 0], + [5915, 15772, 22148, 32768, 0], + [2786, 7779, 12047, 32768, 0], + [29246, 32450, 32663, 32768, 0], + [18696, 29929, 31818, 32768, 0], + [10510, 23369, 28560, 32768, 0], + [6229, 16499, 23125, 32768, 0], + [2608, 7448, 11705, 32768, 0], + [30753, 32710, 32748, 32768, 0], + [21638, 31487, 32503, 32768, 0], + [12937, 26854, 30870, 32768, 0], + [8182, 20596, 26970, 32768, 0], + [3637, 10269, 15497, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + ], + [ + [5244, 12150, 16906, 32768, 0], + [20486, 26858, 29701, 32768, 0], + [7756, 18317, 23735, 32768, 0], + [3452, 9256, 13146, 32768, 0], + [2020, 5206, 8229, 32768, 0], + [1801, 4993, 7903, 32768, 0], + [27051, 31858, 32531, 32768, 0], + [15988, 27531, 30619, 32768, 0], + [9188, 21484, 26719, 32768, 0], + [6273, 17186, 23800, 32768, 0], + [3108, 9355, 14764, 32768, 0], + [31076, 32520, 32680, 32768, 0], + [18119, 30037, 31850, 32768, 0], + [10244, 22969, 27472, 32768, 0], + [4692, 14077, 19273, 32768, 0], + [3694, 11677, 17556, 32768, 0], + [30060, 32581, 32720, 32768, 0], + [21011, 30775, 32120, 32768, 0], + [11931, 24820, 29289, 32768, 0], + [7119, 17662, 24356, 32768, 0], + [3833, 10706, 16304, 32768, 0], + [31954, 32731, 32748, 32768, 0], + [23913, 31724, 32489, 32768, 0], + [15520, 28060, 31286, 32768, 0], + [11517, 23008, 28571, 32768, 0], + [6193, 14508, 20629, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + ], + ], + [ + [ + [1035, 2807, 4156, 32768, 0], + [13162, 18138, 20939, 32768, 0], + [2696, 6633, 8755, 32768, 0], + [1373, 4161, 6853, 32768, 0], + [1099, 2746, 4716, 32768, 0], + [340, 1021, 1599, 32768, 0], + [22826, 30419, 32135, 32768, 0], + [10395, 21762, 26942, 32768, 0], + [4726, 12407, 17361, 32768, 0], + [2447, 7080, 10593, 32768, 0], + [1227, 3717, 6011, 32768, 0], + [28156, 31424, 31934, 32768, 0], + [16915, 27754, 30373, 32768, 0], + [9148, 20990, 26431, 32768, 0], + [5950, 15515, 21148, 32768, 0], + [2492, 7327, 11526, 32768, 0], + [30602, 32477, 32670, 32768, 0], + [20026, 29955, 31568, 32768, 0], + [11220, 23628, 28105, 32768, 0], + [6652, 17019, 22973, 32768, 0], + [3064, 8536, 13043, 32768, 0], + [31769, 32724, 32748, 32768, 0], + [22230, 30887, 32373, 32768, 0], + [12234, 25079, 29731, 32768, 0], + [7326, 18816, 25353, 32768, 0], + [3933, 10907, 16616, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + ], + [ + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + ], + ], + ], + [ + [ + [ + [8896, 16227, 20630, 32768, 0], + [23629, 31782, 32527, 32768, 0], + [15173, 27755, 31321, 32768, 0], + [10158, 21233, 27382, 32768, 0], + [6420, 14857, 21558, 32768, 0], + [3269, 8155, 12646, 32768, 0], + [24835, 32009, 32496, 32768, 0], + [16509, 28421, 31579, 32768, 0], + [10957, 21514, 27418, 32768, 0], + [7881, 15930, 22096, 32768, 0], + [5388, 10960, 15918, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [20745, 30773, 32093, 32768, 0], + [15200, 27221, 30861, 32768, 0], + [13032, 20873, 25667, 32768, 0], + [12285, 18663, 23494, 32768, 0], + [11563, 17481, 21489, 32768, 0], + [26260, 31982, 32320, 32768, 0], + [15397, 28083, 31100, 32768, 0], + [9742, 19217, 24824, 32768, 0], + [3261, 9629, 15362, 32768, 0], + [1480, 4322, 7499, 32768, 0], + [27599, 32256, 32460, 32768, 0], + [16857, 27659, 30774, 32768, 0], + [9551, 18290, 23748, 32768, 0], + [3052, 8933, 14103, 32768, 0], + [2021, 5910, 9787, 32768, 0], + [29005, 32015, 32392, 32768, 0], + [17677, 27694, 30863, 32768, 0], + [9204, 17356, 23219, 32768, 0], + [2403, 7516, 12814, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + ], + [ + [10808, 22056, 26896, 32768, 0], + [25739, 32313, 32676, 32768, 0], + [17288, 30203, 32221, 32768, 0], + [11359, 24878, 29896, 32768, 0], + [6949, 17767, 24893, 32768, 0], + [4287, 11796, 18071, 32768, 0], + [27880, 32521, 32705, 32768, 0], + [19038, 31004, 32414, 32768, 0], + [12564, 26345, 30768, 32768, 0], + [8269, 19947, 26779, 32768, 0], + [5674, 14657, 21674, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [25742, 32319, 32671, 32768, 0], + [19557, 31164, 32454, 32768, 0], + [13381, 26381, 30755, 32768, 0], + [10101, 21466, 26722, 32768, 0], + [9209, 19650, 26825, 32768, 0], + [27107, 31917, 32432, 32768, 0], + [18056, 28893, 31203, 32768, 0], + [10200, 21434, 26764, 32768, 0], + [4660, 12913, 19502, 32768, 0], + [2368, 6930, 12504, 32768, 0], + [26960, 32158, 32613, 32768, 0], + [18628, 30005, 32031, 32768, 0], + [10233, 22442, 28232, 32768, 0], + [5471, 14630, 21516, 32768, 0], + [3235, 10767, 17109, 32768, 0], + [27696, 32440, 32692, 32768, 0], + [20032, 31167, 32438, 32768, 0], + [8700, 21341, 28442, 32768, 0], + [5662, 14831, 21795, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + ], + ], + [ + [ + [9704, 17294, 21132, 32768, 0], + [26762, 32278, 32633, 32768, 0], + [18382, 29620, 31819, 32768, 0], + [10891, 23475, 28723, 32768, 0], + [6358, 16583, 23309, 32768, 0], + [3248, 9118, 14141, 32768, 0], + [27204, 32573, 32699, 32768, 0], + [19818, 30824, 32329, 32768, 0], + [11772, 25120, 30041, 32768, 0], + [6995, 18033, 25039, 32768, 0], + [3752, 10442, 16098, 32768, 0], + [27222, 32256, 32559, 32768, 0], + [15356, 28399, 31475, 32768, 0], + [8821, 20635, 27057, 32768, 0], + [5511, 14404, 21239, 32768, 0], + [2935, 8222, 13051, 32768, 0], + [24875, 32120, 32529, 32768, 0], + [15233, 28265, 31445, 32768, 0], + [8605, 20570, 26932, 32768, 0], + [5431, 14413, 21196, 32768, 0], + [2994, 8341, 13223, 32768, 0], + [28201, 32604, 32700, 32768, 0], + [21041, 31446, 32456, 32768, 0], + [13221, 26213, 30475, 32768, 0], + [8255, 19385, 26037, 32768, 0], + [4930, 12585, 18830, 32768, 0], + [28768, 32448, 32627, 32768, 0], + [19705, 30561, 32021, 32768, 0], + [11572, 23589, 28220, 32768, 0], + [5532, 15034, 21446, 32768, 0], + [2460, 7150, 11456, 32768, 0], + [29874, 32619, 32699, 32768, 0], + [21621, 31071, 32201, 32768, 0], + [12511, 24747, 28992, 32768, 0], + [6281, 16395, 22748, 32768, 0], + [3246, 9278, 14497, 32768, 0], + [29715, 32625, 32712, 32768, 0], + [20958, 31011, 32283, 32768, 0], + [11233, 23671, 28806, 32768, 0], + [6012, 16128, 22868, 32768, 0], + [3427, 9851, 15414, 32768, 0], + [8192, 16384, 24576, 32768, 0], + ], + [ + [11016, 22111, 26794, 32768, 0], + [25946, 32357, 32677, 32768, 0], + [17890, 30452, 32252, 32768, 0], + [11678, 25142, 29816, 32768, 0], + [6720, 17534, 24584, 32768, 0], + [4230, 11665, 17820, 32768, 0], + [28400, 32623, 32747, 32768, 0], + [21164, 31668, 32575, 32768, 0], + [13572, 27388, 31182, 32768, 0], + [8234, 20750, 27358, 32768, 0], + [5065, 14055, 20897, 32768, 0], + [28981, 32547, 32705, 32768, 0], + [18681, 30543, 32239, 32768, 0], + [10919, 24075, 29286, 32768, 0], + [6431, 17199, 24077, 32768, 0], + [3819, 10464, 16618, 32768, 0], + [26870, 32467, 32693, 32768, 0], + [19041, 30831, 32347, 32768, 0], + [11794, 25211, 30016, 32768, 0], + [6888, 18019, 24970, 32768, 0], + [4370, 12363, 18992, 32768, 0], + [29578, 32670, 32744, 32768, 0], + [23159, 32007, 32613, 32768, 0], + [15315, 28669, 31676, 32768, 0], + [9298, 22607, 28782, 32768, 0], + [6144, 15913, 22968, 32768, 0], + [28110, 32499, 32669, 32768, 0], + [21574, 30937, 32015, 32768, 0], + [12759, 24818, 28727, 32768, 0], + [6545, 16761, 23042, 32768, 0], + [3649, 10597, 16833, 32768, 0], + [28163, 32552, 32728, 32768, 0], + [22101, 31469, 32464, 32768, 0], + [13160, 25472, 30143, 32768, 0], + [7303, 18684, 25468, 32768, 0], + [5241, 13975, 20955, 32768, 0], + [28400, 32631, 32744, 32768, 0], + [22104, 31793, 32603, 32768, 0], + [13557, 26571, 30846, 32768, 0], + [7749, 19861, 26675, 32768, 0], + [4873, 14030, 21234, 32768, 0], + [8192, 16384, 24576, 32768, 0], + ], + ], + [ + [ + [9800, 17635, 21073, 32768, 0], + [26153, 31885, 32527, 32768, 0], + [15038, 27852, 31006, 32768, 0], + [8718, 20564, 26486, 32768, 0], + [5128, 14076, 20514, 32768, 0], + [2636, 7566, 11925, 32768, 0], + [27551, 32504, 32701, 32768, 0], + [18310, 30054, 32100, 32768, 0], + [10211, 23420, 29082, 32768, 0], + [6222, 16876, 23916, 32768, 0], + [3462, 9954, 15498, 32768, 0], + [29991, 32633, 32721, 32768, 0], + [19883, 30751, 32201, 32768, 0], + [11141, 24184, 29285, 32768, 0], + [6420, 16940, 23774, 32768, 0], + [3392, 9753, 15118, 32768, 0], + [28465, 32616, 32712, 32768, 0], + [19850, 30702, 32244, 32768, 0], + [10983, 24024, 29223, 32768, 0], + [6294, 16770, 23582, 32768, 0], + [3244, 9283, 14509, 32768, 0], + [30023, 32717, 32748, 32768, 0], + [22940, 32032, 32626, 32768, 0], + [14282, 27928, 31473, 32768, 0], + [8562, 21327, 27914, 32768, 0], + [4846, 13393, 19919, 32768, 0], + [29981, 32590, 32695, 32768, 0], + [20465, 30963, 32166, 32768, 0], + [11479, 23579, 28195, 32768, 0], + [5916, 15648, 22073, 32768, 0], + [3031, 8605, 13398, 32768, 0], + [31146, 32691, 32739, 32768, 0], + [23106, 31724, 32444, 32768, 0], + [13783, 26738, 30439, 32768, 0], + [7852, 19468, 25807, 32768, 0], + [3860, 11124, 16853, 32768, 0], + [31014, 32724, 32748, 32768, 0], + [23629, 32109, 32628, 32768, 0], + [14747, 28115, 31403, 32768, 0], + [8545, 21242, 27478, 32768, 0], + [4574, 12781, 19067, 32768, 0], + [8192, 16384, 24576, 32768, 0], + ], + [ + [9185, 19694, 24688, 32768, 0], + [26081, 31985, 32621, 32768, 0], + [16015, 29000, 31787, 32768, 0], + [10542, 23690, 29206, 32768, 0], + [6732, 17945, 24677, 32768, 0], + [3916, 11039, 16722, 32768, 0], + [28224, 32566, 32744, 32768, 0], + [19100, 31138, 32485, 32768, 0], + [12528, 26620, 30879, 32768, 0], + [7741, 20277, 26885, 32768, 0], + [4566, 12845, 18990, 32768, 0], + [29933, 32593, 32718, 32768, 0], + [17670, 30333, 32155, 32768, 0], + [10385, 23600, 28909, 32768, 0], + [6243, 16236, 22407, 32768, 0], + [3976, 10389, 16017, 32768, 0], + [28377, 32561, 32738, 32768, 0], + [19366, 31175, 32482, 32768, 0], + [13327, 27175, 31094, 32768, 0], + [8258, 20769, 27143, 32768, 0], + [4703, 13198, 19527, 32768, 0], + [31086, 32706, 32748, 32768, 0], + [22853, 31902, 32583, 32768, 0], + [14759, 28186, 31419, 32768, 0], + [9284, 22382, 28348, 32768, 0], + [5585, 15192, 21868, 32768, 0], + [28291, 32652, 32746, 32768, 0], + [19849, 32107, 32571, 32768, 0], + [14834, 26818, 29214, 32768, 0], + [10306, 22594, 28672, 32768, 0], + [6615, 17384, 23384, 32768, 0], + [28947, 32604, 32745, 32768, 0], + [25625, 32289, 32646, 32768, 0], + [18758, 28672, 31403, 32768, 0], + [10017, 23430, 28523, 32768, 0], + [6862, 15269, 22131, 32768, 0], + [23933, 32509, 32739, 32768, 0], + [19927, 31495, 32631, 32768, 0], + [11903, 26023, 30621, 32768, 0], + [7026, 20094, 27252, 32768, 0], + [5998, 18106, 24437, 32768, 0], + [8192, 16384, 24576, 32768, 0], + ], + ], + [ + [ + [4456, 11274, 15533, 32768, 0], + [21219, 29079, 31616, 32768, 0], + [11173, 23774, 28567, 32768, 0], + [7282, 18293, 24263, 32768, 0], + [4890, 13286, 19115, 32768, 0], + [1890, 5508, 8659, 32768, 0], + [26651, 32136, 32647, 32768, 0], + [14630, 28254, 31455, 32768, 0], + [8716, 21287, 27395, 32768, 0], + [5615, 15331, 22008, 32768, 0], + [2675, 7700, 12150, 32768, 0], + [29954, 32526, 32690, 32768, 0], + [16126, 28982, 31633, 32768, 0], + [9030, 21361, 27352, 32768, 0], + [5411, 14793, 21271, 32768, 0], + [2943, 8422, 13163, 32768, 0], + [29539, 32601, 32730, 32768, 0], + [18125, 30385, 32201, 32768, 0], + [10422, 24090, 29468, 32768, 0], + [6468, 17487, 24438, 32768, 0], + [2970, 8653, 13531, 32768, 0], + [30912, 32715, 32748, 32768, 0], + [20666, 31373, 32497, 32768, 0], + [12509, 26640, 30917, 32768, 0], + [8058, 20629, 27290, 32768, 0], + [4231, 12006, 18052, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + ], + [ + [10202, 20633, 25484, 32768, 0], + [27336, 31445, 32352, 32768, 0], + [12420, 24384, 28552, 32768, 0], + [7648, 18115, 23856, 32768, 0], + [5662, 14341, 19902, 32768, 0], + [3611, 10328, 15390, 32768, 0], + [30945, 32616, 32736, 32768, 0], + [18682, 30505, 32253, 32768, 0], + [11513, 25336, 30203, 32768, 0], + [7449, 19452, 26148, 32768, 0], + [4482, 13051, 18886, 32768, 0], + [32022, 32690, 32747, 32768, 0], + [18578, 30501, 32146, 32768, 0], + [11249, 23368, 28631, 32768, 0], + [5645, 16958, 22158, 32768, 0], + [5009, 11444, 16637, 32768, 0], + [31357, 32710, 32748, 32768, 0], + [21552, 31494, 32504, 32768, 0], + [13891, 27677, 31340, 32768, 0], + [9051, 22098, 28172, 32768, 0], + [5190, 13377, 19486, 32768, 0], + [32364, 32740, 32748, 32768, 0], + [24839, 31907, 32551, 32768, 0], + [17160, 28779, 31696, 32768, 0], + [12452, 24137, 29602, 32768, 0], + [6165, 15389, 22477, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + ], + ], + [ + [ + [2575, 7281, 11077, 32768, 0], + [14002, 20866, 25402, 32768, 0], + [6343, 15056, 19658, 32768, 0], + [4474, 11858, 17041, 32768, 0], + [2865, 8299, 12534, 32768, 0], + [1344, 3949, 6391, 32768, 0], + [24720, 31239, 32459, 32768, 0], + [12585, 25356, 29968, 32768, 0], + [7181, 18246, 24444, 32768, 0], + [5025, 13667, 19885, 32768, 0], + [2521, 7304, 11605, 32768, 0], + [29908, 32252, 32584, 32768, 0], + [17421, 29156, 31575, 32768, 0], + [9889, 22188, 27782, 32768, 0], + [5878, 15647, 22123, 32768, 0], + [2814, 8665, 13323, 32768, 0], + [30183, 32568, 32713, 32768, 0], + [18528, 30195, 32049, 32768, 0], + [10982, 24606, 29657, 32768, 0], + [6957, 18165, 25231, 32768, 0], + [3508, 10118, 15468, 32768, 0], + [31761, 32736, 32748, 32768, 0], + [21041, 31328, 32546, 32768, 0], + [12568, 26732, 31166, 32768, 0], + [8052, 20720, 27733, 32768, 0], + [4336, 12192, 18396, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + ], + [ + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + ], + ], + ], + [ + [ + [ + [7062, 16472, 22319, 32768, 0], + [24538, 32261, 32674, 32768, 0], + [13675, 28041, 31779, 32768, 0], + [8590, 20674, 27631, 32768, 0], + [5685, 14675, 22013, 32768, 0], + [3655, 9898, 15731, 32768, 0], + [26493, 32418, 32658, 32768, 0], + [16376, 29342, 32090, 32768, 0], + [10594, 22649, 28970, 32768, 0], + [8176, 17170, 24303, 32768, 0], + [5605, 12694, 19139, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [23888, 31902, 32542, 32768, 0], + [18612, 29687, 31987, 32768, 0], + [16245, 24852, 29249, 32768, 0], + [15765, 22608, 27559, 32768, 0], + [19895, 24699, 27510, 32768, 0], + [28401, 32212, 32457, 32768, 0], + [15274, 27825, 30980, 32768, 0], + [9364, 18128, 24332, 32768, 0], + [2283, 8193, 15082, 32768, 0], + [1228, 3972, 7881, 32768, 0], + [29455, 32469, 32620, 32768, 0], + [17981, 28245, 31388, 32768, 0], + [10921, 20098, 26240, 32768, 0], + [3743, 11829, 18657, 32768, 0], + [2374, 9593, 15715, 32768, 0], + [31068, 32466, 32635, 32768, 0], + [20321, 29572, 31971, 32768, 0], + [10771, 20255, 27119, 32768, 0], + [2795, 10410, 17361, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + ], + [ + [9320, 22102, 27840, 32768, 0], + [27057, 32464, 32724, 32768, 0], + [16331, 30268, 32309, 32768, 0], + [10319, 23935, 29720, 32768, 0], + [6189, 16448, 24106, 32768, 0], + [3589, 10884, 18808, 32768, 0], + [29026, 32624, 32748, 32768, 0], + [19226, 31507, 32587, 32768, 0], + [12692, 26921, 31203, 32768, 0], + [7049, 19532, 27635, 32768, 0], + [7727, 15669, 23252, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [28056, 32625, 32748, 32768, 0], + [22383, 32075, 32669, 32768, 0], + [15417, 27098, 31749, 32768, 0], + [18127, 26493, 27190, 32768, 0], + [5461, 16384, 21845, 32768, 0], + [27982, 32091, 32584, 32768, 0], + [19045, 29868, 31972, 32768, 0], + [10397, 22266, 27932, 32768, 0], + [5990, 13697, 21500, 32768, 0], + [1792, 6912, 15104, 32768, 0], + [28198, 32501, 32718, 32768, 0], + [21534, 31521, 32569, 32768, 0], + [11109, 25217, 30017, 32768, 0], + [5671, 15124, 26151, 32768, 0], + [4681, 14043, 18725, 32768, 0], + [28688, 32580, 32741, 32768, 0], + [22576, 32079, 32661, 32768, 0], + [10627, 22141, 28340, 32768, 0], + [9362, 14043, 28087, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + ], + ], + [ + [ + [7754, 16948, 22142, 32768, 0], + [25670, 32330, 32691, 32768, 0], + [15663, 29225, 31994, 32768, 0], + [9878, 23288, 29158, 32768, 0], + [6419, 17088, 24336, 32768, 0], + [3859, 11003, 17039, 32768, 0], + [27562, 32595, 32725, 32768, 0], + [17575, 30588, 32399, 32768, 0], + [10819, 24838, 30309, 32768, 0], + [7124, 18686, 25916, 32768, 0], + [4479, 12688, 19340, 32768, 0], + [28385, 32476, 32673, 32768, 0], + [15306, 29005, 31938, 32768, 0], + [8937, 21615, 28322, 32768, 0], + [5982, 15603, 22786, 32768, 0], + [3620, 10267, 16136, 32768, 0], + [27280, 32464, 32667, 32768, 0], + [15607, 29160, 32004, 32768, 0], + [9091, 22135, 28740, 32768, 0], + [6232, 16632, 24020, 32768, 0], + [4047, 11377, 17672, 32768, 0], + [29220, 32630, 32718, 32768, 0], + [19650, 31220, 32462, 32768, 0], + [13050, 26312, 30827, 32768, 0], + [9228, 20870, 27468, 32768, 0], + [6146, 15149, 21971, 32768, 0], + [30169, 32481, 32623, 32768, 0], + [17212, 29311, 31554, 32768, 0], + [9911, 21311, 26882, 32768, 0], + [4487, 13314, 20372, 32768, 0], + [2570, 7772, 12889, 32768, 0], + [30924, 32613, 32708, 32768, 0], + [19490, 30206, 32107, 32768, 0], + [11232, 23998, 29276, 32768, 0], + [6769, 17955, 25035, 32768, 0], + [4398, 12623, 19214, 32768, 0], + [30609, 32627, 32722, 32768, 0], + [19370, 30582, 32287, 32768, 0], + [10457, 23619, 29409, 32768, 0], + [6443, 17637, 24834, 32768, 0], + [4645, 13236, 20106, 32768, 0], + [8192, 16384, 24576, 32768, 0], + ], + [ + [8626, 20271, 26216, 32768, 0], + [26707, 32406, 32711, 32768, 0], + [16999, 30329, 32286, 32768, 0], + [11445, 25123, 30286, 32768, 0], + [6411, 18828, 25601, 32768, 0], + [6801, 12458, 20248, 32768, 0], + [29918, 32682, 32748, 32768, 0], + [20649, 31739, 32618, 32768, 0], + [12879, 27773, 31581, 32768, 0], + [7896, 21751, 28244, 32768, 0], + [5260, 14870, 23698, 32768, 0], + [29252, 32593, 32731, 32768, 0], + [17072, 30460, 32294, 32768, 0], + [10653, 24143, 29365, 32768, 0], + [6536, 17490, 23983, 32768, 0], + [4929, 13170, 20085, 32768, 0], + [28137, 32518, 32715, 32768, 0], + [18171, 30784, 32407, 32768, 0], + [11437, 25436, 30459, 32768, 0], + [7252, 18534, 26176, 32768, 0], + [4126, 13353, 20978, 32768, 0], + [31162, 32726, 32748, 32768, 0], + [23017, 32222, 32701, 32768, 0], + [15629, 29233, 32046, 32768, 0], + [9387, 22621, 29480, 32768, 0], + [6922, 17616, 25010, 32768, 0], + [28838, 32265, 32614, 32768, 0], + [19701, 30206, 31920, 32768, 0], + [11214, 22410, 27933, 32768, 0], + [5320, 14177, 23034, 32768, 0], + [5049, 12881, 17827, 32768, 0], + [27484, 32471, 32734, 32768, 0], + [21076, 31526, 32561, 32768, 0], + [12707, 26303, 31211, 32768, 0], + [8169, 21722, 28219, 32768, 0], + [6045, 19406, 27042, 32768, 0], + [27753, 32572, 32745, 32768, 0], + [20832, 31878, 32653, 32768, 0], + [13250, 27356, 31674, 32768, 0], + [7718, 21508, 29858, 32768, 0], + [7209, 18350, 25559, 32768, 0], + [8192, 16384, 24576, 32768, 0], + ], + ], + [ + [ + [7876, 16901, 21741, 32768, 0], + [24001, 31898, 32625, 32768, 0], + [14529, 27959, 31451, 32768, 0], + [8273, 20818, 27258, 32768, 0], + [5278, 14673, 21510, 32768, 0], + [2983, 8843, 14039, 32768, 0], + [28016, 32574, 32732, 32768, 0], + [17471, 30306, 32301, 32768, 0], + [10224, 24063, 29728, 32768, 0], + [6602, 17954, 25052, 32768, 0], + [4002, 11585, 17759, 32768, 0], + [30190, 32634, 32739, 32768, 0], + [17497, 30282, 32270, 32768, 0], + [10229, 23729, 29538, 32768, 0], + [6344, 17211, 24440, 32768, 0], + [3849, 11189, 17108, 32768, 0], + [28570, 32583, 32726, 32768, 0], + [17521, 30161, 32238, 32768, 0], + [10153, 23565, 29378, 32768, 0], + [6455, 17341, 24443, 32768, 0], + [3907, 11042, 17024, 32768, 0], + [30689, 32715, 32748, 32768, 0], + [21546, 31840, 32610, 32768, 0], + [13547, 27581, 31459, 32768, 0], + [8912, 21757, 28309, 32768, 0], + [5548, 15080, 22046, 32768, 0], + [30783, 32540, 32685, 32768, 0], + [17540, 29528, 31668, 32768, 0], + [10160, 21468, 26783, 32768, 0], + [4724, 13393, 20054, 32768, 0], + [2702, 8174, 13102, 32768, 0], + [31648, 32686, 32742, 32768, 0], + [20954, 31094, 32337, 32768, 0], + [12420, 25698, 30179, 32768, 0], + [7304, 19320, 26248, 32768, 0], + [4366, 12261, 18864, 32768, 0], + [31581, 32723, 32748, 32768, 0], + [21373, 31586, 32525, 32768, 0], + [12744, 26625, 30885, 32768, 0], + [7431, 20322, 26950, 32768, 0], + [4692, 13323, 20111, 32768, 0], + [8192, 16384, 24576, 32768, 0], + ], + [ + [7833, 18369, 24095, 32768, 0], + [26650, 32273, 32702, 32768, 0], + [16371, 29961, 32191, 32768, 0], + [11055, 24082, 29629, 32768, 0], + [6892, 18644, 25400, 32768, 0], + [5006, 13057, 19240, 32768, 0], + [29834, 32666, 32748, 32768, 0], + [19577, 31335, 32570, 32768, 0], + [12253, 26509, 31122, 32768, 0], + [7991, 20772, 27711, 32768, 0], + [5677, 15910, 23059, 32768, 0], + [30109, 32532, 32720, 32768, 0], + [16747, 30166, 32252, 32768, 0], + [10134, 23542, 29184, 32768, 0], + [5791, 16176, 23556, 32768, 0], + [4362, 10414, 17284, 32768, 0], + [29492, 32626, 32748, 32768, 0], + [19894, 31402, 32525, 32768, 0], + [12942, 27071, 30869, 32768, 0], + [8346, 21216, 27405, 32768, 0], + [6572, 17087, 23859, 32768, 0], + [32035, 32735, 32748, 32768, 0], + [22957, 31838, 32618, 32768, 0], + [14724, 28572, 31772, 32768, 0], + [10364, 23999, 29553, 32768, 0], + [7004, 18433, 25655, 32768, 0], + [27528, 32277, 32681, 32768, 0], + [16959, 31171, 32096, 32768, 0], + [10486, 23593, 27962, 32768, 0], + [8192, 16384, 23211, 32768, 0], + [8937, 17873, 20852, 32768, 0], + [27715, 32002, 32615, 32768, 0], + [15073, 29491, 31676, 32768, 0], + [11264, 24576, 28672, 32768, 0], + [2341, 18725, 23406, 32768, 0], + [7282, 18204, 25486, 32768, 0], + [28547, 32213, 32657, 32768, 0], + [20788, 29773, 32239, 32768, 0], + [6780, 21469, 30508, 32768, 0], + [5958, 14895, 23831, 32768, 0], + [16384, 21845, 27307, 32768, 0], + [8192, 16384, 24576, 32768, 0], + ], + ], + [ + [ + [5992, 14304, 19765, 32768, 0], + [22612, 31238, 32456, 32768, 0], + [13456, 27162, 31087, 32768, 0], + [8001, 20062, 26504, 32768, 0], + [5168, 14105, 20764, 32768, 0], + [2632, 7771, 12385, 32768, 0], + [27034, 32344, 32709, 32768, 0], + [15850, 29415, 31997, 32768, 0], + [9494, 22776, 28841, 32768, 0], + [6151, 16830, 23969, 32768, 0], + [3461, 10039, 15722, 32768, 0], + [30134, 32569, 32731, 32768, 0], + [15638, 29422, 31945, 32768, 0], + [9150, 21865, 28218, 32768, 0], + [5647, 15719, 22676, 32768, 0], + [3402, 9772, 15477, 32768, 0], + [28530, 32586, 32735, 32768, 0], + [17139, 30298, 32292, 32768, 0], + [10200, 24039, 29685, 32768, 0], + [6419, 17674, 24786, 32768, 0], + [3544, 10225, 15824, 32768, 0], + [31333, 32726, 32748, 32768, 0], + [20618, 31487, 32544, 32768, 0], + [12901, 27217, 31232, 32768, 0], + [8624, 21734, 28171, 32768, 0], + [5104, 14191, 20748, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + ], + [ + [11206, 21090, 26561, 32768, 0], + [28759, 32279, 32671, 32768, 0], + [14171, 27952, 31569, 32768, 0], + [9743, 22907, 29141, 32768, 0], + [6871, 17886, 24868, 32768, 0], + [4960, 13152, 19315, 32768, 0], + [31077, 32661, 32748, 32768, 0], + [19400, 31195, 32515, 32768, 0], + [12752, 26858, 31040, 32768, 0], + [8370, 22098, 28591, 32768, 0], + [5457, 15373, 22298, 32768, 0], + [31697, 32706, 32748, 32768, 0], + [17860, 30657, 32333, 32768, 0], + [12510, 24812, 29261, 32768, 0], + [6180, 19124, 24722, 32768, 0], + [5041, 13548, 17959, 32768, 0], + [31552, 32716, 32748, 32768, 0], + [21908, 31769, 32623, 32768, 0], + [14470, 28201, 31565, 32768, 0], + [9493, 22982, 28608, 32768, 0], + [6858, 17240, 24137, 32768, 0], + [32543, 32752, 32756, 32768, 0], + [24286, 32097, 32666, 32768, 0], + [15958, 29217, 32024, 32768, 0], + [10207, 24234, 29958, 32768, 0], + [6929, 18305, 25652, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + ], + ], + [ + [ + [4137, 10847, 15682, 32768, 0], + [17824, 27001, 30058, 32768, 0], + [10204, 22796, 28291, 32768, 0], + [6076, 15935, 22125, 32768, 0], + [3852, 10937, 16816, 32768, 0], + [2252, 6324, 10131, 32768, 0], + [25840, 32016, 32662, 32768, 0], + [15109, 28268, 31531, 32768, 0], + [9385, 22231, 28340, 32768, 0], + [6082, 16672, 23479, 32768, 0], + [3318, 9427, 14681, 32768, 0], + [30594, 32574, 32718, 32768, 0], + [16836, 29552, 31859, 32768, 0], + [9556, 22542, 28356, 32768, 0], + [6305, 16725, 23540, 32768, 0], + [3376, 9895, 15184, 32768, 0], + [29383, 32617, 32745, 32768, 0], + [18891, 30809, 32401, 32768, 0], + [11688, 25942, 30687, 32768, 0], + [7468, 19469, 26651, 32768, 0], + [3909, 11358, 17012, 32768, 0], + [31564, 32736, 32748, 32768, 0], + [20906, 31611, 32600, 32768, 0], + [13191, 27621, 31537, 32768, 0], + [8768, 22029, 28676, 32768, 0], + [5079, 14109, 20906, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + ], + [ + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + ], + ], + ], +]; + +pub static DEFAULT_COEFF_BASE_EOB_CDF: [[[[[u16; 4]; 4]; 2]; 5]; 4] = [ + [ + [ + [[17837, 29055, 32768, 0], [29600, 31446, 32768, 0], [30844, 31878, 32768, 0], [24926, 28948, 32768, 0]], + [[21365, 30026, 32768, 0], [30512, 32423, 32768, 0], [31658, 32621, 32768, 0], [29630, 31881, 32768, 0]], + ], + [ + [[5717, 26477, 32768, 0], [30491, 31703, 32768, 0], [31550, 32158, 32768, 0], [29648, 31491, 32768, 0]], + [[12608, 27820, 32768, 0], [30680, 32225, 32768, 0], [30809, 32335, 32768, 0], [31299, 32423, 32768, 0]], + ], + [ + [[1786, 12612, 32768, 0], [30663, 31625, 32768, 0], [32339, 32468, 32768, 0], [31148, 31833, 32768, 0]], + [[18857, 23865, 32768, 0], [31428, 32428, 32768, 0], [31744, 32373, 32768, 0], [31775, 32526, 32768, 0]], + ], + [ + [[1787, 2532, 32768, 0], [30832, 31662, 32768, 0], [31824, 32682, 32768, 0], [32133, 32569, 32768, 0]], + [[13751, 22235, 32768, 0], [32089, 32409, 32768, 0], [27084, 27920, 32768, 0], [29291, 32594, 32768, 0]], + ], + [ + [[1725, 3449, 32768, 0], [31102, 31935, 32768, 0], [32457, 32613, 32768, 0], [32412, 32649, 32768, 0]], + [[10923, 21845, 32768, 0], [10923, 21845, 32768, 0], [10923, 21845, 32768, 0], [10923, 21845, 32768, 0]], + ], + ], + [ + [ + [[17560, 29888, 32768, 0], [29671, 31549, 32768, 0], [31007, 32056, 32768, 0], [27286, 30006, 32768, 0]], + [[26594, 31212, 32768, 0], [31208, 32582, 32768, 0], [31835, 32637, 32768, 0], [30595, 32206, 32768, 0]], + ], + [ + [[15239, 29932, 32768, 0], [31315, 32095, 32768, 0], [32130, 32434, 32768, 0], [30864, 31996, 32768, 0]], + [[26279, 30968, 32768, 0], [31142, 32495, 32768, 0], [31713, 32540, 32768, 0], [31929, 32594, 32768, 0]], + ], + [ + [[2644, 25198, 32768, 0], [32038, 32451, 32768, 0], [32639, 32695, 32768, 0], [32166, 32518, 32768, 0]], + [[17187, 27668, 32768, 0], [31714, 32550, 32768, 0], [32283, 32678, 32768, 0], [31930, 32563, 32768, 0]], + ], + [ + [[1044, 2257, 32768, 0], [30755, 31923, 32768, 0], [32208, 32693, 32768, 0], [32244, 32615, 32768, 0]], + [[21317, 26207, 32768, 0], [29133, 30868, 32768, 0], [29311, 31231, 32768, 0], [29657, 31087, 32768, 0]], + ], + [ + [[478, 1834, 32768, 0], [31005, 31987, 32768, 0], [32317, 32724, 32768, 0], [30865, 32648, 32768, 0]], + [[10923, 21845, 32768, 0], [10923, 21845, 32768, 0], [10923, 21845, 32768, 0], [10923, 21845, 32768, 0]], + ], + ], + [ + [ + [[20092, 30774, 32768, 0], [30695, 32020, 32768, 0], [31131, 32103, 32768, 0], [28666, 30870, 32768, 0]], + [[27258, 31095, 32768, 0], [31804, 32623, 32768, 0], [31763, 32528, 32768, 0], [31438, 32506, 32768, 0]], + ], + [ + [[18049, 30489, 32768, 0], [31706, 32286, 32768, 0], [32163, 32473, 32768, 0], [31550, 32184, 32768, 0]], + [[27116, 30842, 32768, 0], [31971, 32598, 32768, 0], [32088, 32576, 32768, 0], [32067, 32664, 32768, 0]], + ], + [ + [[12854, 29093, 32768, 0], [32272, 32558, 32768, 0], [32667, 32729, 32768, 0], [32306, 32585, 32768, 0]], + [[25476, 30366, 32768, 0], [32169, 32687, 32768, 0], [32479, 32689, 32768, 0], [31673, 32634, 32768, 0]], + ], + [ + [[2809, 19301, 32768, 0], [32205, 32622, 32768, 0], [32338, 32730, 32768, 0], [31786, 32616, 32768, 0]], + [[22737, 29105, 32768, 0], [30810, 32362, 32768, 0], [30014, 32627, 32768, 0], [30528, 32574, 32768, 0]], + ], + [ + [[935, 3382, 32768, 0], [30789, 31909, 32768, 0], [32466, 32756, 32768, 0], [30860, 32513, 32768, 0]], + [[10923, 21845, 32768, 0], [10923, 21845, 32768, 0], [10923, 21845, 32768, 0], [10923, 21845, 32768, 0]], + ], + ], + [ + [ + [[22497, 31198, 32768, 0], [31715, 32495, 32768, 0], [31606, 32337, 32768, 0], [30388, 31990, 32768, 0]], + [[27877, 31584, 32768, 0], [32170, 32728, 32768, 0], [32155, 32688, 32768, 0], [32219, 32702, 32768, 0]], + ], + [ + [[21457, 31043, 32768, 0], [31951, 32483, 32768, 0], [32153, 32562, 32768, 0], [31473, 32215, 32768, 0]], + [[27558, 31151, 32768, 0], [32020, 32640, 32768, 0], [32097, 32575, 32768, 0], [32242, 32719, 32768, 0]], + ], + [ + [[19980, 30591, 32768, 0], [32219, 32597, 32768, 0], [32581, 32706, 32768, 0], [31803, 32287, 32768, 0]], + [[26473, 30507, 32768, 0], [32431, 32723, 32768, 0], [32196, 32611, 32768, 0], [31588, 32528, 32768, 0]], + ], + [ + [[24647, 30463, 32768, 0], [32412, 32695, 32768, 0], [32468, 32720, 32768, 0], [31269, 32523, 32768, 0]], + [[28482, 31505, 32768, 0], [32152, 32701, 32768, 0], [31732, 32598, 32768, 0], [31767, 32712, 32768, 0]], + ], + [ + [[12358, 24977, 32768, 0], [31331, 32385, 32768, 0], [32634, 32756, 32768, 0], [30411, 32548, 32768, 0]], + [[10923, 21845, 32768, 0], [10923, 21845, 32768, 0], [10923, 21845, 32768, 0], [10923, 21845, 32768, 0]], + ], + ], +]; + +pub static DEFAULT_COEFF_BR_CDF: [[[[[u16; 5]; 21]; 2]; 5]; 4] = [ + [ + [ + [ + [14298, 20718, 24174, 32768, 0], + [12536, 19601, 23789, 32768, 0], + [8712, 15051, 19503, 32768, 0], + [6170, 11327, 15434, 32768, 0], + [4742, 8926, 12538, 32768, 0], + [3803, 7317, 10546, 32768, 0], + [1696, 3317, 4871, 32768, 0], + [14392, 19951, 22756, 32768, 0], + [15978, 23218, 26818, 32768, 0], + [12187, 19474, 23889, 32768, 0], + [9176, 15640, 20259, 32768, 0], + [7068, 12655, 17028, 32768, 0], + [5656, 10442, 14472, 32768, 0], + [2580, 4992, 7244, 32768, 0], + [12136, 18049, 21426, 32768, 0], + [13784, 20721, 24481, 32768, 0], + [10836, 17621, 21900, 32768, 0], + [8372, 14444, 18847, 32768, 0], + [6523, 11779, 16000, 32768, 0], + [5337, 9898, 13760, 32768, 0], + [3034, 5860, 8462, 32768, 0], + ], + [ + [15967, 22905, 26286, 32768, 0], + [13534, 20654, 24579, 32768, 0], + [9504, 16092, 20535, 32768, 0], + [6975, 12568, 16903, 32768, 0], + [5364, 10091, 14020, 32768, 0], + [4357, 8370, 11857, 32768, 0], + [2506, 4934, 7218, 32768, 0], + [23032, 28815, 30936, 32768, 0], + [19540, 26704, 29719, 32768, 0], + [15158, 22969, 27097, 32768, 0], + [11408, 18865, 23650, 32768, 0], + [8885, 15448, 20250, 32768, 0], + [7108, 12853, 17416, 32768, 0], + [4231, 8041, 11480, 32768, 0], + [19823, 26490, 29156, 32768, 0], + [18890, 25929, 28932, 32768, 0], + [15660, 23491, 27433, 32768, 0], + [12147, 19776, 24488, 32768, 0], + [9728, 16774, 21649, 32768, 0], + [7919, 14277, 19066, 32768, 0], + [5440, 10170, 14185, 32768, 0], + ], + ], + [ + [ + [14406, 20862, 24414, 32768, 0], + [11824, 18907, 23109, 32768, 0], + [8257, 14393, 18803, 32768, 0], + [5860, 10747, 14778, 32768, 0], + [4475, 8486, 11984, 32768, 0], + [3606, 6954, 10043, 32768, 0], + [1736, 3410, 5048, 32768, 0], + [14430, 20046, 22882, 32768, 0], + [15593, 22899, 26709, 32768, 0], + [12102, 19368, 23811, 32768, 0], + [9059, 15584, 20262, 32768, 0], + [6999, 12603, 17048, 32768, 0], + [5684, 10497, 14553, 32768, 0], + [2822, 5438, 7862, 32768, 0], + [15785, 21585, 24359, 32768, 0], + [18347, 25229, 28266, 32768, 0], + [14974, 22487, 26389, 32768, 0], + [11423, 18681, 23271, 32768, 0], + [8863, 15350, 20008, 32768, 0], + [7153, 12852, 17278, 32768, 0], + [3707, 7036, 9982, 32768, 0], + ], + [ + [15460, 21696, 25469, 32768, 0], + [12170, 19249, 23191, 32768, 0], + [8723, 15027, 19332, 32768, 0], + [6428, 11704, 15874, 32768, 0], + [4922, 9292, 13052, 32768, 0], + [4139, 7695, 11010, 32768, 0], + [2291, 4508, 6598, 32768, 0], + [19856, 26920, 29828, 32768, 0], + [17923, 25289, 28792, 32768, 0], + [14278, 21968, 26297, 32768, 0], + [10910, 18136, 22950, 32768, 0], + [8423, 14815, 19627, 32768, 0], + [6771, 12283, 16774, 32768, 0], + [4074, 7750, 11081, 32768, 0], + [19852, 26074, 28672, 32768, 0], + [19371, 26110, 28989, 32768, 0], + [16265, 23873, 27663, 32768, 0], + [12758, 20378, 24952, 32768, 0], + [10095, 17098, 21961, 32768, 0], + [8250, 14628, 19451, 32768, 0], + [5205, 9745, 13622, 32768, 0], + ], + ], + [ + [ + [10563, 16233, 19763, 32768, 0], + [9794, 16022, 19804, 32768, 0], + [6750, 11945, 15759, 32768, 0], + [4963, 9186, 12752, 32768, 0], + [3845, 7435, 10627, 32768, 0], + [3051, 6085, 8834, 32768, 0], + [1311, 2596, 3830, 32768, 0], + [11246, 16404, 19689, 32768, 0], + [12315, 18911, 22731, 32768, 0], + [10557, 17095, 21289, 32768, 0], + [8136, 14006, 18249, 32768, 0], + [6348, 11474, 15565, 32768, 0], + [5196, 9655, 13400, 32768, 0], + [2349, 4526, 6587, 32768, 0], + [13337, 18730, 21569, 32768, 0], + [19306, 26071, 28882, 32768, 0], + [15952, 23540, 27254, 32768, 0], + [12409, 19934, 24430, 32768, 0], + [9760, 16706, 21389, 32768, 0], + [8004, 14220, 18818, 32768, 0], + [4138, 7794, 10961, 32768, 0], + ], + [ + [10870, 16684, 20949, 32768, 0], + [9664, 15230, 18680, 32768, 0], + [6886, 12109, 15408, 32768, 0], + [4825, 8900, 12305, 32768, 0], + [3630, 7162, 10314, 32768, 0], + [3036, 6429, 9387, 32768, 0], + [1671, 3296, 4940, 32768, 0], + [13819, 19159, 23026, 32768, 0], + [11984, 19108, 23120, 32768, 0], + [10690, 17210, 21663, 32768, 0], + [7984, 14154, 18333, 32768, 0], + [6868, 12294, 16124, 32768, 0], + [5274, 8994, 12868, 32768, 0], + [2988, 5771, 8424, 32768, 0], + [19736, 26647, 29141, 32768, 0], + [18933, 26070, 28984, 32768, 0], + [15779, 23048, 27200, 32768, 0], + [12638, 20061, 24532, 32768, 0], + [10692, 17545, 22220, 32768, 0], + [9217, 15251, 20054, 32768, 0], + [5078, 9284, 12594, 32768, 0], + ], + ], + [ + [ + [2331, 3662, 5244, 32768, 0], + [2891, 4771, 6145, 32768, 0], + [4598, 7623, 9729, 32768, 0], + [3520, 6845, 9199, 32768, 0], + [3417, 6119, 9324, 32768, 0], + [2601, 5412, 7385, 32768, 0], + [600, 1173, 1744, 32768, 0], + [7672, 13286, 17469, 32768, 0], + [4232, 7792, 10793, 32768, 0], + [2915, 5317, 7397, 32768, 0], + [2318, 4356, 6152, 32768, 0], + [2127, 4000, 5554, 32768, 0], + [1850, 3478, 5275, 32768, 0], + [977, 1933, 2843, 32768, 0], + [18280, 24387, 27989, 32768, 0], + [15852, 22671, 26185, 32768, 0], + [13845, 20951, 24789, 32768, 0], + [11055, 17966, 22129, 32768, 0], + [9138, 15422, 19801, 32768, 0], + [7454, 13145, 17456, 32768, 0], + [3370, 6393, 9013, 32768, 0], + ], + [ + [5842, 9229, 10838, 32768, 0], + [2313, 3491, 4276, 32768, 0], + [2998, 6104, 7496, 32768, 0], + [2420, 7447, 9868, 32768, 0], + [3034, 8495, 10923, 32768, 0], + [4076, 8937, 10975, 32768, 0], + [1086, 2370, 3299, 32768, 0], + [9714, 17254, 20444, 32768, 0], + [8543, 13698, 17123, 32768, 0], + [4918, 9007, 11910, 32768, 0], + [4129, 7532, 10553, 32768, 0], + [2364, 5533, 8058, 32768, 0], + [1834, 3546, 5563, 32768, 0], + [1473, 2908, 4133, 32768, 0], + [15405, 21193, 25619, 32768, 0], + [15691, 21952, 26561, 32768, 0], + [12962, 19194, 24165, 32768, 0], + [10272, 17855, 22129, 32768, 0], + [8588, 15270, 20718, 32768, 0], + [8682, 14669, 19500, 32768, 0], + [4870, 9636, 13205, 32768, 0], + ], + ], + [ + [ + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + ], + [ + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + ], + ], + ], + [ + [ + [ + [14995, 21341, 24749, 32768, 0], + [13158, 20289, 24601, 32768, 0], + [8941, 15326, 19876, 32768, 0], + [6297, 11541, 15807, 32768, 0], + [4817, 9029, 12776, 32768, 0], + [3731, 7273, 10627, 32768, 0], + [1847, 3617, 5354, 32768, 0], + [14472, 19659, 22343, 32768, 0], + [16806, 24162, 27533, 32768, 0], + [12900, 20404, 24713, 32768, 0], + [9411, 16112, 20797, 32768, 0], + [7056, 12697, 17148, 32768, 0], + [5544, 10339, 14460, 32768, 0], + [2954, 5704, 8319, 32768, 0], + [12464, 18071, 21354, 32768, 0], + [15482, 22528, 26034, 32768, 0], + [12070, 19269, 23624, 32768, 0], + [8953, 15406, 20106, 32768, 0], + [7027, 12730, 17220, 32768, 0], + [5887, 10913, 15140, 32768, 0], + [3793, 7278, 10447, 32768, 0], + ], + [ + [15571, 22232, 25749, 32768, 0], + [14506, 21575, 25374, 32768, 0], + [10189, 17089, 21569, 32768, 0], + [7316, 13301, 17915, 32768, 0], + [5783, 10912, 15190, 32768, 0], + [4760, 9155, 13088, 32768, 0], + [2993, 5966, 8774, 32768, 0], + [23424, 28903, 30778, 32768, 0], + [20775, 27666, 30290, 32768, 0], + [16474, 24410, 28299, 32768, 0], + [12471, 20180, 24987, 32768, 0], + [9410, 16487, 21439, 32768, 0], + [7536, 13614, 18529, 32768, 0], + [5048, 9586, 13549, 32768, 0], + [21090, 27290, 29756, 32768, 0], + [20796, 27402, 30026, 32768, 0], + [17819, 25485, 28969, 32768, 0], + [13860, 21909, 26462, 32768, 0], + [11002, 18494, 23529, 32768, 0], + [8953, 15929, 20897, 32768, 0], + [6448, 11918, 16454, 32768, 0], + ], + ], + [ + [ + [15999, 22208, 25449, 32768, 0], + [13050, 19988, 24122, 32768, 0], + [8594, 14864, 19378, 32768, 0], + [6033, 11079, 15238, 32768, 0], + [4554, 8683, 12347, 32768, 0], + [3672, 7139, 10337, 32768, 0], + [1900, 3771, 5576, 32768, 0], + [15788, 21340, 23949, 32768, 0], + [16825, 24235, 27758, 32768, 0], + [12873, 20402, 24810, 32768, 0], + [9590, 16363, 21094, 32768, 0], + [7352, 13209, 17733, 32768, 0], + [5960, 10989, 15184, 32768, 0], + [3232, 6234, 9007, 32768, 0], + [15761, 20716, 23224, 32768, 0], + [19318, 25989, 28759, 32768, 0], + [15529, 23094, 26929, 32768, 0], + [11662, 18989, 23641, 32768, 0], + [8955, 15568, 20366, 32768, 0], + [7281, 13106, 17708, 32768, 0], + [4248, 8059, 11440, 32768, 0], + ], + [ + [14899, 21217, 24503, 32768, 0], + [13519, 20283, 24047, 32768, 0], + [9429, 15966, 20365, 32768, 0], + [6700, 12355, 16652, 32768, 0], + [5088, 9704, 13716, 32768, 0], + [4243, 8154, 11731, 32768, 0], + [2702, 5364, 7861, 32768, 0], + [22745, 28388, 30454, 32768, 0], + [20235, 27146, 29922, 32768, 0], + [15896, 23715, 27637, 32768, 0], + [11840, 19350, 24131, 32768, 0], + [9122, 15932, 20880, 32768, 0], + [7488, 13581, 18362, 32768, 0], + [5114, 9568, 13370, 32768, 0], + [20845, 26553, 28932, 32768, 0], + [20981, 27372, 29884, 32768, 0], + [17781, 25335, 28785, 32768, 0], + [13760, 21708, 26297, 32768, 0], + [10975, 18415, 23365, 32768, 0], + [9045, 15789, 20686, 32768, 0], + [6130, 11199, 15423, 32768, 0], + ], + ], + [ + [ + [13549, 19724, 23158, 32768, 0], + [11844, 18382, 22246, 32768, 0], + [7919, 13619, 17773, 32768, 0], + [5486, 10143, 13946, 32768, 0], + [4166, 7983, 11324, 32768, 0], + [3364, 6506, 9427, 32768, 0], + [1598, 3160, 4674, 32768, 0], + [15281, 20979, 23781, 32768, 0], + [14939, 22119, 25952, 32768, 0], + [11363, 18407, 22812, 32768, 0], + [8609, 14857, 19370, 32768, 0], + [6737, 12184, 16480, 32768, 0], + [5506, 10263, 14262, 32768, 0], + [2990, 5786, 8380, 32768, 0], + [20249, 25253, 27417, 32768, 0], + [21070, 27518, 30001, 32768, 0], + [16854, 24469, 28074, 32768, 0], + [12864, 20486, 25000, 32768, 0], + [9962, 16978, 21778, 32768, 0], + [8074, 14338, 19048, 32768, 0], + [4494, 8479, 11906, 32768, 0], + ], + [ + [13960, 19617, 22829, 32768, 0], + [11150, 17341, 21228, 32768, 0], + [7150, 12964, 17190, 32768, 0], + [5331, 10002, 13867, 32768, 0], + [4167, 7744, 11057, 32768, 0], + [3480, 6629, 9646, 32768, 0], + [1883, 3784, 5686, 32768, 0], + [18752, 25660, 28912, 32768, 0], + [16968, 24586, 28030, 32768, 0], + [13520, 21055, 25313, 32768, 0], + [10453, 17626, 22280, 32768, 0], + [8386, 14505, 19116, 32768, 0], + [6742, 12595, 17008, 32768, 0], + [4273, 8140, 11499, 32768, 0], + [22120, 27827, 30233, 32768, 0], + [20563, 27358, 29895, 32768, 0], + [17076, 24644, 28153, 32768, 0], + [13362, 20942, 25309, 32768, 0], + [10794, 17965, 22695, 32768, 0], + [9014, 15652, 20319, 32768, 0], + [5708, 10512, 14497, 32768, 0], + ], + ], + [ + [ + [5705, 10930, 15725, 32768, 0], + [7946, 12765, 16115, 32768, 0], + [6801, 12123, 16226, 32768, 0], + [5462, 10135, 14200, 32768, 0], + [4189, 8011, 11507, 32768, 0], + [3191, 6229, 9408, 32768, 0], + [1057, 2137, 3212, 32768, 0], + [10018, 17067, 21491, 32768, 0], + [7380, 12582, 16453, 32768, 0], + [6068, 10845, 14339, 32768, 0], + [5098, 9198, 12555, 32768, 0], + [4312, 8010, 11119, 32768, 0], + [3700, 6966, 9781, 32768, 0], + [1693, 3326, 4887, 32768, 0], + [18757, 24930, 27774, 32768, 0], + [17648, 24596, 27817, 32768, 0], + [14707, 22052, 26026, 32768, 0], + [11720, 18852, 23292, 32768, 0], + [9357, 15952, 20525, 32768, 0], + [7810, 13753, 18210, 32768, 0], + [3879, 7333, 10328, 32768, 0], + ], + [ + [8278, 13242, 15922, 32768, 0], + [10547, 15867, 18919, 32768, 0], + [9106, 15842, 20609, 32768, 0], + [6833, 13007, 17218, 32768, 0], + [4811, 9712, 13923, 32768, 0], + [3985, 7352, 11128, 32768, 0], + [1688, 3458, 5262, 32768, 0], + [12951, 21861, 26510, 32768, 0], + [9788, 16044, 20276, 32768, 0], + [6309, 11244, 14870, 32768, 0], + [5183, 9349, 12566, 32768, 0], + [4389, 8229, 11492, 32768, 0], + [3633, 6945, 10620, 32768, 0], + [3600, 6847, 9907, 32768, 0], + [21748, 28137, 30255, 32768, 0], + [19436, 26581, 29560, 32768, 0], + [16359, 24201, 27953, 32768, 0], + [13961, 21693, 25871, 32768, 0], + [11544, 18686, 23322, 32768, 0], + [9372, 16462, 20952, 32768, 0], + [6138, 11210, 15390, 32768, 0], + ], + ], + [ + [ + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + ], + [ + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + ], + ], + ], + [ + [ + [ + [16138, 22223, 25509, 32768, 0], + [15347, 22430, 26332, 32768, 0], + [9614, 16736, 21332, 32768, 0], + [6600, 12275, 16907, 32768, 0], + [4811, 9424, 13547, 32768, 0], + [3748, 7809, 11420, 32768, 0], + [2254, 4587, 6890, 32768, 0], + [15196, 20284, 23177, 32768, 0], + [18317, 25469, 28451, 32768, 0], + [13918, 21651, 25842, 32768, 0], + [10052, 17150, 21995, 32768, 0], + [7499, 13630, 18587, 32768, 0], + [6158, 11417, 16003, 32768, 0], + [4014, 7785, 11252, 32768, 0], + [15048, 21067, 24384, 32768, 0], + [18202, 25346, 28553, 32768, 0], + [14302, 22019, 26356, 32768, 0], + [10839, 18139, 23166, 32768, 0], + [8715, 15744, 20806, 32768, 0], + [7536, 13576, 18544, 32768, 0], + [5413, 10335, 14498, 32768, 0], + ], + [ + [17394, 24501, 27895, 32768, 0], + [15889, 23420, 27185, 32768, 0], + [11561, 19133, 23870, 32768, 0], + [8285, 14812, 19844, 32768, 0], + [6496, 12043, 16550, 32768, 0], + [4771, 9574, 13677, 32768, 0], + [3603, 6830, 10144, 32768, 0], + [21656, 27704, 30200, 32768, 0], + [21324, 27915, 30511, 32768, 0], + [17327, 25336, 28997, 32768, 0], + [13417, 21381, 26033, 32768, 0], + [10132, 17425, 22338, 32768, 0], + [8580, 15016, 19633, 32768, 0], + [5694, 11477, 16411, 32768, 0], + [24116, 29780, 31450, 32768, 0], + [23853, 29695, 31591, 32768, 0], + [20085, 27614, 30428, 32768, 0], + [15326, 24335, 28575, 32768, 0], + [11814, 19472, 24810, 32768, 0], + [10221, 18611, 24767, 32768, 0], + [7689, 14558, 20321, 32768, 0], + ], + ], + [ + [ + [16214, 22380, 25770, 32768, 0], + [14213, 21304, 25295, 32768, 0], + [9213, 15823, 20455, 32768, 0], + [6395, 11758, 16139, 32768, 0], + [4779, 9187, 13066, 32768, 0], + [3821, 7501, 10953, 32768, 0], + [2293, 4567, 6795, 32768, 0], + [15859, 21283, 23820, 32768, 0], + [18404, 25602, 28726, 32768, 0], + [14325, 21980, 26206, 32768, 0], + [10669, 17937, 22720, 32768, 0], + [8297, 14642, 19447, 32768, 0], + [6746, 12389, 16893, 32768, 0], + [4324, 8251, 11770, 32768, 0], + [16532, 21631, 24475, 32768, 0], + [20667, 27150, 29668, 32768, 0], + [16728, 24510, 28175, 32768, 0], + [12861, 20645, 25332, 32768, 0], + [10076, 17361, 22417, 32768, 0], + [8395, 14940, 19963, 32768, 0], + [5731, 10683, 14912, 32768, 0], + ], + [ + [14433, 21155, 24938, 32768, 0], + [14658, 21716, 25545, 32768, 0], + [9923, 16824, 21557, 32768, 0], + [6982, 13052, 17721, 32768, 0], + [5419, 10503, 15050, 32768, 0], + [4852, 9162, 13014, 32768, 0], + [3271, 6395, 9630, 32768, 0], + [22210, 27833, 30109, 32768, 0], + [20750, 27368, 29821, 32768, 0], + [16894, 24828, 28573, 32768, 0], + [13247, 21276, 25757, 32768, 0], + [10038, 17265, 22563, 32768, 0], + [8587, 14947, 20327, 32768, 0], + [5645, 11371, 15252, 32768, 0], + [22027, 27526, 29714, 32768, 0], + [23098, 29146, 31221, 32768, 0], + [19886, 27341, 30272, 32768, 0], + [15609, 23747, 28046, 32768, 0], + [11993, 20065, 24939, 32768, 0], + [9637, 18267, 23671, 32768, 0], + [7625, 13801, 19144, 32768, 0], + ], + ], + [ + [ + [14438, 20798, 24089, 32768, 0], + [12621, 19203, 23097, 32768, 0], + [8177, 14125, 18402, 32768, 0], + [5674, 10501, 14456, 32768, 0], + [4236, 8239, 11733, 32768, 0], + [3447, 6750, 9806, 32768, 0], + [1986, 3950, 5864, 32768, 0], + [16208, 22099, 24930, 32768, 0], + [16537, 24025, 27585, 32768, 0], + [12780, 20381, 24867, 32768, 0], + [9767, 16612, 21416, 32768, 0], + [7686, 13738, 18398, 32768, 0], + [6333, 11614, 15964, 32768, 0], + [3941, 7571, 10836, 32768, 0], + [22819, 27422, 29202, 32768, 0], + [22224, 28514, 30721, 32768, 0], + [17660, 25433, 28913, 32768, 0], + [13574, 21482, 26002, 32768, 0], + [10629, 17977, 22938, 32768, 0], + [8612, 15298, 20265, 32768, 0], + [5607, 10491, 14596, 32768, 0], + ], + [ + [13569, 19800, 23206, 32768, 0], + [13128, 19924, 23869, 32768, 0], + [8329, 14841, 19403, 32768, 0], + [6130, 10976, 15057, 32768, 0], + [4682, 8839, 12518, 32768, 0], + [3656, 7409, 10588, 32768, 0], + [2577, 5099, 7412, 32768, 0], + [22427, 28684, 30585, 32768, 0], + [20913, 27750, 30139, 32768, 0], + [15840, 24109, 27834, 32768, 0], + [12308, 20029, 24569, 32768, 0], + [10216, 16785, 21458, 32768, 0], + [8309, 14203, 19113, 32768, 0], + [6043, 11168, 15307, 32768, 0], + [23166, 28901, 30998, 32768, 0], + [21899, 28405, 30751, 32768, 0], + [18413, 26091, 29443, 32768, 0], + [15233, 23114, 27352, 32768, 0], + [12683, 20472, 25288, 32768, 0], + [10702, 18259, 23409, 32768, 0], + [8125, 14464, 19226, 32768, 0], + ], + ], + [ + [ + [9040, 14786, 18360, 32768, 0], + [9979, 15718, 19415, 32768, 0], + [7913, 13918, 18311, 32768, 0], + [5859, 10889, 15184, 32768, 0], + [4593, 8677, 12510, 32768, 0], + [3820, 7396, 10791, 32768, 0], + [1730, 3471, 5192, 32768, 0], + [11803, 18365, 22709, 32768, 0], + [11419, 18058, 22225, 32768, 0], + [9418, 15774, 20243, 32768, 0], + [7539, 13325, 17657, 32768, 0], + [6233, 11317, 15384, 32768, 0], + [5137, 9656, 13545, 32768, 0], + [2977, 5774, 8349, 32768, 0], + [21207, 27246, 29640, 32768, 0], + [19547, 26578, 29497, 32768, 0], + [16169, 23871, 27690, 32768, 0], + [12820, 20458, 25018, 32768, 0], + [10224, 17332, 22214, 32768, 0], + [8526, 15048, 19884, 32768, 0], + [5037, 9410, 13118, 32768, 0], + ], + [ + [12339, 17329, 20140, 32768, 0], + [13505, 19895, 23225, 32768, 0], + [9847, 16944, 21564, 32768, 0], + [7280, 13256, 18348, 32768, 0], + [4712, 10009, 14454, 32768, 0], + [4361, 7914, 12477, 32768, 0], + [2870, 5628, 7995, 32768, 0], + [20061, 25504, 28526, 32768, 0], + [15235, 22878, 26145, 32768, 0], + [12985, 19958, 24155, 32768, 0], + [9782, 16641, 21403, 32768, 0], + [9456, 16360, 20760, 32768, 0], + [6855, 12940, 18557, 32768, 0], + [5661, 10564, 15002, 32768, 0], + [25656, 30602, 31894, 32768, 0], + [22570, 29107, 31092, 32768, 0], + [18917, 26423, 29541, 32768, 0], + [15940, 23649, 27754, 32768, 0], + [12803, 20581, 25219, 32768, 0], + [11082, 18695, 23376, 32768, 0], + [7939, 14373, 19005, 32768, 0], + ], + ], + [ + [ + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + ], + [ + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + ], + ], + ], + [ + [ + [ + [18315, 24289, 27551, 32768, 0], + [16854, 24068, 27835, 32768, 0], + [10140, 17927, 23173, 32768, 0], + [6722, 12982, 18267, 32768, 0], + [4661, 9826, 14706, 32768, 0], + [3832, 8165, 12294, 32768, 0], + [2795, 6098, 9245, 32768, 0], + [17145, 23326, 26672, 32768, 0], + [20733, 27680, 30308, 32768, 0], + [16032, 24461, 28546, 32768, 0], + [11653, 20093, 25081, 32768, 0], + [9290, 16429, 22086, 32768, 0], + [7796, 14598, 19982, 32768, 0], + [6502, 12378, 17441, 32768, 0], + [21681, 27732, 30320, 32768, 0], + [22389, 29044, 31261, 32768, 0], + [19027, 26731, 30087, 32768, 0], + [14739, 23755, 28624, 32768, 0], + [11358, 20778, 25511, 32768, 0], + [10995, 18073, 24190, 32768, 0], + [9162, 14990, 20617, 32768, 0], + ], + [ + [21425, 27952, 30388, 32768, 0], + [18062, 25838, 29034, 32768, 0], + [11956, 19881, 24808, 32768, 0], + [7718, 15000, 20980, 32768, 0], + [5702, 11254, 16143, 32768, 0], + [4898, 9088, 16864, 32768, 0], + [3679, 6776, 11907, 32768, 0], + [23294, 30160, 31663, 32768, 0], + [24397, 29896, 31836, 32768, 0], + [19245, 27128, 30593, 32768, 0], + [13202, 19825, 26404, 32768, 0], + [11578, 19297, 23957, 32768, 0], + [8073, 13297, 21370, 32768, 0], + [5461, 10923, 19745, 32768, 0], + [27367, 30521, 31934, 32768, 0], + [24904, 30671, 31940, 32768, 0], + [23075, 28460, 31299, 32768, 0], + [14400, 23658, 30417, 32768, 0], + [13885, 23882, 28325, 32768, 0], + [14746, 22938, 27853, 32768, 0], + [5461, 16384, 27307, 32768, 0], + ], + ], + [ + [ + [18274, 24813, 27890, 32768, 0], + [15537, 23149, 27003, 32768, 0], + [9449, 16740, 21827, 32768, 0], + [6700, 12498, 17261, 32768, 0], + [4988, 9866, 14198, 32768, 0], + [4236, 8147, 11902, 32768, 0], + [2867, 5860, 8654, 32768, 0], + [17124, 23171, 26101, 32768, 0], + [20396, 27477, 30148, 32768, 0], + [16573, 24629, 28492, 32768, 0], + [12749, 20846, 25674, 32768, 0], + [10233, 17878, 22818, 32768, 0], + [8525, 15332, 20363, 32768, 0], + [6283, 11632, 16255, 32768, 0], + [20466, 26511, 29286, 32768, 0], + [23059, 29174, 31191, 32768, 0], + [19481, 27263, 30241, 32768, 0], + [15458, 23631, 28137, 32768, 0], + [12416, 20608, 25693, 32768, 0], + [10261, 18011, 23261, 32768, 0], + [8016, 14655, 19666, 32768, 0], + ], + [ + [17616, 24586, 28112, 32768, 0], + [15809, 23299, 27155, 32768, 0], + [10767, 18890, 23793, 32768, 0], + [7727, 14255, 18865, 32768, 0], + [6129, 11926, 16882, 32768, 0], + [4482, 9704, 14861, 32768, 0], + [3277, 7452, 11522, 32768, 0], + [22956, 28551, 30730, 32768, 0], + [22724, 28937, 30961, 32768, 0], + [18467, 26324, 29580, 32768, 0], + [13234, 20713, 25649, 32768, 0], + [11181, 17592, 22481, 32768, 0], + [8291, 18358, 24576, 32768, 0], + [7568, 11881, 14984, 32768, 0], + [24948, 29001, 31147, 32768, 0], + [25674, 30619, 32151, 32768, 0], + [20841, 26793, 29603, 32768, 0], + [14669, 24356, 28666, 32768, 0], + [11334, 23593, 28219, 32768, 0], + [8922, 14762, 22873, 32768, 0], + [8301, 13544, 20535, 32768, 0], + ], + ], + [ + [ + [17113, 23733, 27081, 32768, 0], + [14139, 21406, 25452, 32768, 0], + [8552, 15002, 19776, 32768, 0], + [5871, 11120, 15378, 32768, 0], + [4455, 8616, 12253, 32768, 0], + [3469, 6910, 10386, 32768, 0], + [2255, 4553, 6782, 32768, 0], + [18224, 24376, 27053, 32768, 0], + [19290, 26710, 29614, 32768, 0], + [14936, 22991, 27184, 32768, 0], + [11238, 18951, 23762, 32768, 0], + [8786, 15617, 20588, 32768, 0], + [7317, 13228, 18003, 32768, 0], + [5101, 9512, 13493, 32768, 0], + [22639, 28222, 30210, 32768, 0], + [23216, 29331, 31307, 32768, 0], + [19075, 26762, 29895, 32768, 0], + [15014, 23113, 27457, 32768, 0], + [11938, 19857, 24752, 32768, 0], + [9942, 17280, 22282, 32768, 0], + [7167, 13144, 17752, 32768, 0], + ], + [ + [15820, 22738, 26488, 32768, 0], + [13530, 20885, 25216, 32768, 0], + [8395, 15530, 20452, 32768, 0], + [6574, 12321, 16380, 32768, 0], + [5353, 10419, 14568, 32768, 0], + [4613, 8446, 12381, 32768, 0], + [3440, 7158, 9903, 32768, 0], + [24247, 29051, 31224, 32768, 0], + [22118, 28058, 30369, 32768, 0], + [16498, 24768, 28389, 32768, 0], + [12920, 21175, 26137, 32768, 0], + [10730, 18619, 25352, 32768, 0], + [10187, 16279, 22791, 32768, 0], + [9310, 14631, 22127, 32768, 0], + [24970, 30558, 32057, 32768, 0], + [24801, 29942, 31698, 32768, 0], + [22432, 28453, 30855, 32768, 0], + [19054, 25680, 29580, 32768, 0], + [14392, 23036, 28109, 32768, 0], + [12495, 20947, 26650, 32768, 0], + [12442, 20326, 26214, 32768, 0], + ], + ], + [ + [ + [12162, 18785, 22648, 32768, 0], + [12749, 19697, 23806, 32768, 0], + [8580, 15297, 20346, 32768, 0], + [6169, 11749, 16543, 32768, 0], + [4836, 9391, 13448, 32768, 0], + [3821, 7711, 11613, 32768, 0], + [2228, 4601, 7070, 32768, 0], + [16319, 24725, 28280, 32768, 0], + [15698, 23277, 27168, 32768, 0], + [12726, 20368, 25047, 32768, 0], + [9912, 17015, 21976, 32768, 0], + [7888, 14220, 19179, 32768, 0], + [6777, 12284, 17018, 32768, 0], + [4492, 8590, 12252, 32768, 0], + [23249, 28904, 30947, 32768, 0], + [21050, 27908, 30512, 32768, 0], + [17440, 25340, 28949, 32768, 0], + [14059, 22018, 26541, 32768, 0], + [11288, 18903, 23898, 32768, 0], + [9411, 16342, 21428, 32768, 0], + [6278, 11588, 15944, 32768, 0], + ], + [ + [13981, 20067, 23226, 32768, 0], + [16922, 23580, 26783, 32768, 0], + [11005, 19039, 24487, 32768, 0], + [7389, 14218, 19798, 32768, 0], + [5598, 11505, 17206, 32768, 0], + [6090, 11213, 15659, 32768, 0], + [3820, 7371, 10119, 32768, 0], + [21082, 26925, 29675, 32768, 0], + [21262, 28627, 31128, 32768, 0], + [18392, 26454, 30437, 32768, 0], + [14870, 22910, 27096, 32768, 0], + [12620, 19484, 24908, 32768, 0], + [9290, 16553, 22802, 32768, 0], + [6668, 14288, 20004, 32768, 0], + [27704, 31055, 31949, 32768, 0], + [24709, 29978, 31788, 32768, 0], + [21668, 29264, 31657, 32768, 0], + [18295, 26968, 30074, 32768, 0], + [16399, 24422, 29313, 32768, 0], + [14347, 23026, 28104, 32768, 0], + [12370, 19806, 24477, 32768, 0], + ], + ], + [ + [ + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + ], + [ + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + [8192, 16384, 24576, 32768, 0], + ], + ], + ], +]; + +pub static DEFAULT_DC_SIGN_CDF: [[[[u16; 3]; 3]; 2]; 4] = [ + [[[16000, 32768, 0], [13056, 32768, 0], [18816, 32768, 0]], [[15232, 32768, 0], [12928, 32768, 0], [17280, 32768, 0]]], + [[[16000, 32768, 0], [13056, 32768, 0], [18816, 32768, 0]], [[15232, 32768, 0], [12928, 32768, 0], [17280, 32768, 0]]], + [[[16000, 32768, 0], [13056, 32768, 0], [18816, 32768, 0]], [[15232, 32768, 0], [12928, 32768, 0], [17280, 32768, 0]]], + [[[16000, 32768, 0], [13056, 32768, 0], [18816, 32768, 0]], [[15232, 32768, 0], [12928, 32768, 0], [17280, 32768, 0]]], +]; + +pub static DEFAULT_DRL_MODE_CDF: [[u16; 3]; 3] = [[13104, 32768, 0], [24560, 32768, 0], [18945, 32768, 0]]; + +pub static DEFAULT_EOB_EXTRA_CDF: [[[[[u16; 3]; 9]; 2]; 5]; 4] = [ + [ + [ + [ + [16961, 32768, 0], + [17223, 32768, 0], + [7621, 32768, 0], + [16384, 32768, 0], + [16384, 32768, 0], + [16384, 32768, 0], + [16384, 32768, 0], + [16384, 32768, 0], + [16384, 32768, 0], + ], + [ + [19069, 32768, 0], + [22525, 32768, 0], + [13377, 32768, 0], + [16384, 32768, 0], + [16384, 32768, 0], + [16384, 32768, 0], + [16384, 32768, 0], + [16384, 32768, 0], + [16384, 32768, 0], + ], + ], + [ + [ + [20401, 32768, 0], + [17025, 32768, 0], + [12845, 32768, 0], + [12873, 32768, 0], + [14094, 32768, 0], + [16384, 32768, 0], + [16384, 32768, 0], + [16384, 32768, 0], + [16384, 32768, 0], + ], + [ + [20681, 32768, 0], + [20701, 32768, 0], + [15250, 32768, 0], + [15017, 32768, 0], + [14928, 32768, 0], + [16384, 32768, 0], + [16384, 32768, 0], + [16384, 32768, 0], + [16384, 32768, 0], + ], + ], + [ + [ + [23905, 32768, 0], + [17194, 32768, 0], + [16170, 32768, 0], + [17695, 32768, 0], + [13826, 32768, 0], + [15810, 32768, 0], + [12036, 32768, 0], + [16384, 32768, 0], + [16384, 32768, 0], + ], + [ + [23959, 32768, 0], + [20799, 32768, 0], + [19021, 32768, 0], + [16203, 32768, 0], + [17886, 32768, 0], + [14144, 32768, 0], + [12010, 32768, 0], + [16384, 32768, 0], + [16384, 32768, 0], + ], + ], + [ + [ + [27399, 32768, 0], + [16327, 32768, 0], + [18071, 32768, 0], + [19584, 32768, 0], + [20721, 32768, 0], + [18432, 32768, 0], + [19560, 32768, 0], + [10150, 32768, 0], + [8805, 32768, 0], + ], + [ + [24932, 32768, 0], + [20833, 32768, 0], + [12027, 32768, 0], + [16670, 32768, 0], + [19914, 32768, 0], + [15106, 32768, 0], + [17662, 32768, 0], + [13783, 32768, 0], + [28756, 32768, 0], + ], + ], + [ + [ + [23406, 32768, 0], + [21845, 32768, 0], + [18432, 32768, 0], + [16384, 32768, 0], + [17096, 32768, 0], + [12561, 32768, 0], + [17320, 32768, 0], + [22395, 32768, 0], + [21370, 32768, 0], + ], + [ + [16384, 32768, 0], + [16384, 32768, 0], + [16384, 32768, 0], + [16384, 32768, 0], + [16384, 32768, 0], + [16384, 32768, 0], + [16384, 32768, 0], + [16384, 32768, 0], + [16384, 32768, 0], + ], + ], + ], + [ + [ + [ + [17471, 32768, 0], + [20223, 32768, 0], + [11357, 32768, 0], + [16384, 32768, 0], + [16384, 32768, 0], + [16384, 32768, 0], + [16384, 32768, 0], + [16384, 32768, 0], + [16384, 32768, 0], + ], + [ + [20335, 32768, 0], + [21667, 32768, 0], + [14818, 32768, 0], + [16384, 32768, 0], + [16384, 32768, 0], + [16384, 32768, 0], + [16384, 32768, 0], + [16384, 32768, 0], + [16384, 32768, 0], + ], + ], + [ + [ + [20430, 32768, 0], + [20662, 32768, 0], + [15367, 32768, 0], + [16970, 32768, 0], + [14657, 32768, 0], + [16384, 32768, 0], + [16384, 32768, 0], + [16384, 32768, 0], + [16384, 32768, 0], + ], + [ + [22117, 32768, 0], + [22028, 32768, 0], + [18650, 32768, 0], + [16042, 32768, 0], + [15885, 32768, 0], + [16384, 32768, 0], + [16384, 32768, 0], + [16384, 32768, 0], + [16384, 32768, 0], + ], + ], + [ + [ + [22409, 32768, 0], + [21012, 32768, 0], + [15650, 32768, 0], + [17395, 32768, 0], + [15469, 32768, 0], + [20205, 32768, 0], + [19511, 32768, 0], + [16384, 32768, 0], + [16384, 32768, 0], + ], + [ + [24220, 32768, 0], + [22480, 32768, 0], + [17737, 32768, 0], + [18916, 32768, 0], + [19268, 32768, 0], + [18412, 32768, 0], + [18844, 32768, 0], + [16384, 32768, 0], + [16384, 32768, 0], + ], + ], + [ + [ + [25991, 32768, 0], + [20314, 32768, 0], + [17731, 32768, 0], + [19678, 32768, 0], + [18649, 32768, 0], + [17307, 32768, 0], + [21798, 32768, 0], + [17549, 32768, 0], + [15630, 32768, 0], + ], + [ + [26585, 32768, 0], + [21469, 32768, 0], + [20432, 32768, 0], + [17735, 32768, 0], + [19280, 32768, 0], + [15235, 32768, 0], + [20297, 32768, 0], + [22471, 32768, 0], + [28997, 32768, 0], + ], + ], + [ + [ + [26605, 32768, 0], + [11304, 32768, 0], + [16726, 32768, 0], + [16560, 32768, 0], + [20866, 32768, 0], + [23524, 32768, 0], + [19878, 32768, 0], + [13469, 32768, 0], + [23084, 32768, 0], + ], + [ + [16384, 32768, 0], + [16384, 32768, 0], + [16384, 32768, 0], + [16384, 32768, 0], + [16384, 32768, 0], + [16384, 32768, 0], + [16384, 32768, 0], + [16384, 32768, 0], + [16384, 32768, 0], + ], + ], + ], + [ + [ + [ + [18983, 32768, 0], + [20512, 32768, 0], + [14885, 32768, 0], + [16384, 32768, 0], + [16384, 32768, 0], + [16384, 32768, 0], + [16384, 32768, 0], + [16384, 32768, 0], + [16384, 32768, 0], + ], + [ + [20090, 32768, 0], + [19444, 32768, 0], + [17286, 32768, 0], + [16384, 32768, 0], + [16384, 32768, 0], + [16384, 32768, 0], + [16384, 32768, 0], + [16384, 32768, 0], + [16384, 32768, 0], + ], + ], + [ + [ + [19139, 32768, 0], + [21487, 32768, 0], + [18959, 32768, 0], + [20910, 32768, 0], + [19089, 32768, 0], + [16384, 32768, 0], + [16384, 32768, 0], + [16384, 32768, 0], + [16384, 32768, 0], + ], + [ + [20536, 32768, 0], + [20664, 32768, 0], + [20625, 32768, 0], + [19123, 32768, 0], + [14862, 32768, 0], + [16384, 32768, 0], + [16384, 32768, 0], + [16384, 32768, 0], + [16384, 32768, 0], + ], + ], + [ + [ + [19833, 32768, 0], + [21502, 32768, 0], + [17485, 32768, 0], + [20267, 32768, 0], + [18353, 32768, 0], + [23329, 32768, 0], + [21478, 32768, 0], + [16384, 32768, 0], + [16384, 32768, 0], + ], + [ + [22041, 32768, 0], + [23434, 32768, 0], + [20001, 32768, 0], + [20554, 32768, 0], + [20951, 32768, 0], + [20145, 32768, 0], + [15562, 32768, 0], + [16384, 32768, 0], + [16384, 32768, 0], + ], + ], + [ + [ + [23312, 32768, 0], + [21607, 32768, 0], + [16526, 32768, 0], + [18957, 32768, 0], + [18034, 32768, 0], + [18934, 32768, 0], + [24247, 32768, 0], + [16921, 32768, 0], + [17080, 32768, 0], + ], + [ + [26579, 32768, 0], + [24910, 32768, 0], + [18637, 32768, 0], + [19800, 32768, 0], + [20388, 32768, 0], + [9887, 32768, 0], + [15642, 32768, 0], + [30198, 32768, 0], + [24721, 32768, 0], + ], + ], + [ + [ + [26998, 32768, 0], + [16737, 32768, 0], + [17838, 32768, 0], + [18922, 32768, 0], + [19515, 32768, 0], + [18636, 32768, 0], + [17333, 32768, 0], + [15776, 32768, 0], + [22658, 32768, 0], + ], + [ + [16384, 32768, 0], + [16384, 32768, 0], + [16384, 32768, 0], + [16384, 32768, 0], + [16384, 32768, 0], + [16384, 32768, 0], + [16384, 32768, 0], + [16384, 32768, 0], + [16384, 32768, 0], + ], + ], + ], + [ + [ + [ + [20177, 32768, 0], + [20789, 32768, 0], + [20262, 32768, 0], + [16384, 32768, 0], + [16384, 32768, 0], + [16384, 32768, 0], + [16384, 32768, 0], + [16384, 32768, 0], + [16384, 32768, 0], + ], + [ + [21416, 32768, 0], + [20855, 32768, 0], + [23410, 32768, 0], + [16384, 32768, 0], + [16384, 32768, 0], + [16384, 32768, 0], + [16384, 32768, 0], + [16384, 32768, 0], + [16384, 32768, 0], + ], + ], + [ + [ + [20238, 32768, 0], + [21057, 32768, 0], + [19159, 32768, 0], + [22337, 32768, 0], + [20159, 32768, 0], + [16384, 32768, 0], + [16384, 32768, 0], + [16384, 32768, 0], + [16384, 32768, 0], + ], + [ + [20125, 32768, 0], + [20559, 32768, 0], + [21707, 32768, 0], + [22296, 32768, 0], + [17333, 32768, 0], + [16384, 32768, 0], + [16384, 32768, 0], + [16384, 32768, 0], + [16384, 32768, 0], + ], + ], + [ + [ + [19941, 32768, 0], + [20527, 32768, 0], + [21470, 32768, 0], + [22487, 32768, 0], + [19558, 32768, 0], + [22354, 32768, 0], + [20331, 32768, 0], + [16384, 32768, 0], + [16384, 32768, 0], + ], + [ + [22752, 32768, 0], + [25006, 32768, 0], + [22075, 32768, 0], + [21576, 32768, 0], + [17740, 32768, 0], + [21690, 32768, 0], + [19211, 32768, 0], + [16384, 32768, 0], + [16384, 32768, 0], + ], + ], + [ + [ + [21442, 32768, 0], + [22358, 32768, 0], + [18503, 32768, 0], + [20291, 32768, 0], + [19945, 32768, 0], + [21294, 32768, 0], + [21178, 32768, 0], + [19400, 32768, 0], + [10556, 32768, 0], + ], + [ + [24648, 32768, 0], + [24949, 32768, 0], + [20708, 32768, 0], + [23905, 32768, 0], + [20501, 32768, 0], + [9558, 32768, 0], + [9423, 32768, 0], + [30365, 32768, 0], + [19253, 32768, 0], + ], + ], + [ + [ + [26064, 32768, 0], + [22098, 32768, 0], + [19613, 32768, 0], + [20525, 32768, 0], + [17595, 32768, 0], + [16618, 32768, 0], + [20497, 32768, 0], + [18989, 32768, 0], + [15513, 32768, 0], + ], + [ + [16384, 32768, 0], + [16384, 32768, 0], + [16384, 32768, 0], + [16384, 32768, 0], + [16384, 32768, 0], + [16384, 32768, 0], + [16384, 32768, 0], + [16384, 32768, 0], + [16384, 32768, 0], + ], + ], + ], +]; + +pub static DEFAULT_EOB_PT_1024_CDF: [[[u16; 12]; 2]; 4] = [ + [[393, 421, 751, 1623, 3160, 6352, 13345, 18047, 22571, 25830, 32768, 0], [1865, 1988, 2930, 4242, 10533, 16538, 21354, 27255, 28546, 31784, 32768, 0]], + [[696, 948, 3145, 5702, 9706, 13217, 17851, 21856, 25692, 28034, 32768, 0], [2672, 3591, 9330, 17084, 22725, 24284, 26527, 28027, 28377, 30876, 32768, 0]], + [ + [2784, 3831, 7041, 10521, 14847, 18844, 23155, 26682, 29229, 31045, 32768, 0], + [9577, 12466, 17739, 20750, 22061, 23215, 24601, 25483, 25843, 32056, 32768, 0], + ], + [ + [6698, 8334, 11961, 15762, 20186, 23862, 27434, 29326, 31082, 32050, 32768, 0], + [20569, 22426, 25569, 26859, 28053, 28913, 29486, 29724, 29807, 32570, 32768, 0], + ], +]; + +pub static DEFAULT_EOB_PT_128_CDF: [[[[u16; 9]; 2]; 2]; 4] = [ + [ + [[219, 482, 1140, 2091, 3680, 6028, 12586, 32768, 0], [371, 699, 1254, 4830, 9479, 12562, 17497, 32768, 0]], + [[5245, 7456, 12880, 15852, 20033, 23932, 27608, 32768, 0], [2054, 3472, 5869, 14232, 18242, 20590, 26752, 32768, 0]], + ], + [ + [[685, 933, 1488, 2714, 4766, 8562, 19254, 32768, 0], [217, 352, 618, 2303, 5261, 9969, 17472, 32768, 0]], + [[8045, 11200, 15497, 19595, 23948, 27408, 30938, 32768, 0], [2310, 4160, 7471, 14997, 17931, 20768, 30240, 32768, 0]], + ], + [ + [[1366, 1738, 2527, 5016, 9355, 15797, 24643, 32768, 0], [354, 558, 944, 2760, 7287, 14037, 21779, 32768, 0]], + [[13627, 16246, 20173, 24429, 27948, 30415, 31863, 32768, 0], [6275, 9889, 14769, 23164, 27988, 30493, 32272, 32768, 0]], + ], + [ + [[3472, 4885, 7489, 12481, 18517, 24536, 29635, 32768, 0], [886, 1731, 3271, 8469, 15569, 22126, 28383, 32768, 0]], + [[24313, 26062, 28385, 30107, 31217, 31898, 32345, 32768, 0], [9165, 13282, 21150, 30286, 31894, 32571, 32712, 32768, 0]], + ], +]; + +pub static DEFAULT_EOB_PT_16_CDF: [[[[u16; 6]; 2]; 2]; 4] = [ + [[[840, 1039, 1980, 4895, 32768, 0], [370, 671, 1883, 4471, 32768, 0]], [[3247, 4950, 9688, 14563, 32768, 0], [1904, 3354, 7763, 14647, 32768, 0]]], + [[[2125, 2551, 5165, 8946, 32768, 0], [513, 765, 1859, 6339, 32768, 0]], [[7637, 9498, 14259, 19108, 32768, 0], [2497, 4096, 8866, 16993, 32768, 0]]], + [[[4016, 4897, 8881, 14968, 32768, 0], [716, 1105, 2646, 10056, 32768, 0]], [[11139, 13270, 18241, 23566, 32768, 0], [3192, 5032, 10297, 19755, 32768, 0]]], + [ + [[6708, 8958, 14746, 22133, 32768, 0], [1222, 2074, 4783, 15410, 32768, 0]], + [[19575, 21766, 26044, 29709, 32768, 0], [7297, 10767, 19273, 28194, 32768, 0]], + ], +]; + +pub static DEFAULT_EOB_PT_256_CDF: [[[[u16; 10]; 2]; 2]; 4] = [ + [ + [[310, 584, 1887, 3589, 6168, 8611, 11352, 15652, 32768, 0], [998, 1850, 2998, 5604, 17341, 19888, 22899, 25583, 32768, 0]], + [[2520, 3240, 5952, 8870, 12577, 17558, 19954, 24168, 32768, 0], [2203, 4130, 7435, 10739, 20652, 23681, 25609, 27261, 32768, 0]], + ], + [ + [[1448, 2109, 4151, 6263, 9329, 13260, 17944, 23300, 32768, 0], [399, 1019, 1749, 3038, 10444, 15546, 22739, 27294, 32768, 0]], + [[6402, 8148, 12623, 15072, 18728, 22847, 26447, 29377, 32768, 0], [1674, 3252, 5734, 10159, 22397, 23802, 24821, 30940, 32768, 0]], + ], + [ + [[3089, 3920, 6038, 9460, 14266, 19881, 25766, 29176, 32768, 0], [1084, 2358, 3488, 5122, 11483, 18103, 26023, 29799, 32768, 0]], + [[11514, 13794, 17480, 20754, 24361, 27378, 29492, 31277, 32768, 0], [6571, 9610, 15516, 21826, 29092, 30829, 31842, 32708, 32768, 0]], + ], + [ + [[5348, 7113, 11820, 15924, 22106, 26777, 30334, 31757, 32768, 0], [2453, 4474, 6307, 8777, 16474, 22975, 29000, 31547, 32768, 0]], + [[23110, 24597, 27140, 28894, 30167, 30927, 31392, 32094, 32768, 0], [9998, 17661, 25178, 28097, 31308, 32038, 32403, 32695, 32768, 0]], + ], +]; + +pub static DEFAULT_EOB_PT_32_CDF: [[[[u16; 7]; 2]; 2]; 4] = [ + [ + [[400, 520, 977, 2102, 6542, 32768, 0], [210, 405, 1315, 3326, 7537, 32768, 0]], + [[2636, 4273, 7588, 11794, 20401, 32768, 0], [1786, 3179, 6902, 11357, 19054, 32768, 0]], + ], + [ + [[989, 1249, 2019, 4151, 10785, 32768, 0], [313, 441, 1099, 2917, 8562, 32768, 0]], + [[8394, 10352, 13932, 18855, 26014, 32768, 0], [2578, 4124, 8181, 13670, 24234, 32768, 0]], + ], + [ + [[2515, 3003, 4452, 8162, 16041, 32768, 0], [574, 821, 1836, 5089, 13128, 32768, 0]], + [[13468, 16303, 20361, 25105, 29281, 32768, 0], [3542, 5502, 10415, 16760, 25644, 32768, 0]], + ], + [ + [[4617, 5709, 8446, 13584, 23135, 32768, 0], [1156, 1702, 3675, 9274, 20539, 32768, 0]], + [[22086, 24282, 27010, 29770, 31743, 32768, 0], [7699, 10897, 20891, 26926, 31628, 32768, 0]], + ], +]; + +pub static DEFAULT_EOB_PT_512_CDF: [[[u16; 11]; 2]; 4] = [ + [[641, 983, 3707, 5430, 10234, 14958, 18788, 23412, 26061, 32768, 0], [5095, 6446, 9996, 13354, 16017, 17986, 20919, 26129, 29140, 32768, 0]], + [[1230, 2278, 5035, 7776, 11871, 15346, 19590, 24584, 28749, 32768, 0], [7265, 9979, 15819, 19250, 21780, 23846, 26478, 28396, 31811, 32768, 0]], + [[2624, 3936, 6480, 9686, 13979, 17726, 23267, 28410, 31078, 32768, 0], [12015, 14769, 19588, 22052, 24222, 25812, 27300, 29219, 32114, 32768, 0]], + [[5927, 7809, 10923, 14597, 19439, 24135, 28456, 31142, 32060, 32768, 0], [21093, 23043, 25742, 27658, 29097, 29716, 30073, 30820, 31956, 32768, 0]], +]; + +pub static DEFAULT_EOB_PT_64_CDF: [[[[u16; 8]; 2]; 2]; 4] = [ + [ + [[329, 498, 1101, 1784, 3265, 7758, 32768, 0], [335, 730, 1459, 5494, 8755, 12997, 32768, 0]], + [[3505, 5304, 10086, 13814, 17684, 23370, 32768, 0], [1563, 2700, 4876, 10911, 14706, 22480, 32768, 0]], + ], + [ + [[1260, 1446, 2253, 3712, 6652, 13369, 32768, 0], [401, 605, 1029, 2563, 5845, 12626, 32768, 0]], + [[8609, 10612, 14624, 18714, 22614, 29024, 32768, 0], [1923, 3127, 5867, 9703, 14277, 27100, 32768, 0]], + ], + [ + [[2374, 2772, 4583, 7276, 12288, 19706, 32768, 0], [497, 810, 1315, 3000, 7004, 15641, 32768, 0]], + [[15050, 17126, 21410, 24886, 28156, 30726, 32768, 0], [4034, 6290, 10235, 14982, 21214, 28491, 32768, 0]], + ], + [ + [[6307, 7541, 12060, 16358, 22553, 27865, 32768, 0], [1289, 2320, 3971, 7926, 14153, 24291, 32768, 0]], + [[24212, 25708, 28268, 30035, 31307, 32049, 32768, 0], [8726, 12378, 19409, 26450, 30038, 32462, 32768, 0]], + ], +]; + +pub static DEFAULT_INTER_TX_TYPE_SET3_CDF: [[u16; 3]; 4] = [[16384, 32768, 0], [4167, 32768, 0], [1998, 32768, 0], [748, 32768, 0]]; + +pub static DEFAULT_INTRA_FRAME_Y_MODE_CDF: [[[u16; 14]; 5]; 5] = [ + [ + [15588, 17027, 19338, 20218, 20682, 21110, 21825, 23244, 24189, 28165, 29093, 30466, 32768, 0], + [12016, 18066, 19516, 20303, 20719, 21444, 21888, 23032, 24434, 28658, 30172, 31409, 32768, 0], + [10052, 10771, 22296, 22788, 23055, 23239, 24133, 25620, 26160, 29336, 29929, 31567, 32768, 0], + [14091, 15406, 16442, 18808, 19136, 19546, 19998, 22096, 24746, 29585, 30958, 32462, 32768, 0], + [12122, 13265, 15603, 16501, 18609, 20033, 22391, 25583, 26437, 30261, 31073, 32475, 32768, 0], + ], + [ + [10023, 19585, 20848, 21440, 21832, 22760, 23089, 24023, 25381, 29014, 30482, 31436, 32768, 0], + [5983, 24099, 24560, 24886, 25066, 25795, 25913, 26423, 27610, 29905, 31276, 31794, 32768, 0], + [7444, 12781, 20177, 20728, 21077, 21607, 22170, 23405, 24469, 27915, 29090, 30492, 32768, 0], + [8537, 14689, 15432, 17087, 17408, 18172, 18408, 19825, 24649, 29153, 31096, 32210, 32768, 0], + [7543, 14231, 15496, 16195, 17905, 20717, 21984, 24516, 26001, 29675, 30981, 31994, 32768, 0], + ], + [ + [12613, 13591, 21383, 22004, 22312, 22577, 23401, 25055, 25729, 29538, 30305, 32077, 32768, 0], + [9687, 13470, 18506, 19230, 19604, 20147, 20695, 22062, 23219, 27743, 29211, 30907, 32768, 0], + [6183, 6505, 26024, 26252, 26366, 26434, 27082, 28354, 28555, 30467, 30794, 32086, 32768, 0], + [10718, 11734, 14954, 17224, 17565, 17924, 18561, 21523, 23878, 28975, 30287, 32252, 32768, 0], + [9194, 9858, 16501, 17263, 18424, 19171, 21563, 25961, 26561, 30072, 30737, 32463, 32768, 0], + ], + [ + [12602, 14399, 15488, 18381, 18778, 19315, 19724, 21419, 25060, 29696, 30917, 32409, 32768, 0], + [8203, 13821, 14524, 17105, 17439, 18131, 18404, 19468, 25225, 29485, 31158, 32342, 32768, 0], + [8451, 9731, 15004, 17643, 18012, 18425, 19070, 21538, 24605, 29118, 30078, 32018, 32768, 0], + [7714, 9048, 9516, 16667, 16817, 16994, 17153, 18767, 26743, 30389, 31536, 32528, 32768, 0], + [8843, 10280, 11496, 15317, 16652, 17943, 19108, 22718, 25769, 29953, 30983, 32485, 32768, 0], + ], + [ + [12578, 13671, 15979, 16834, 19075, 20913, 22989, 25449, 26219, 30214, 31150, 32477, 32768, 0], + [9563, 13626, 15080, 15892, 17756, 20863, 22207, 24236, 25380, 29653, 31143, 32277, 32768, 0], + [8356, 8901, 17616, 18256, 19350, 20106, 22598, 25947, 26466, 29900, 30523, 32261, 32768, 0], + [10835, 11815, 13124, 16042, 17018, 18039, 18947, 22753, 24615, 29489, 30883, 32482, 32768, 0], + [7618, 8288, 9859, 10509, 15386, 18657, 22903, 28776, 29180, 31355, 31802, 32593, 32768, 0], + ], +]; + +pub static DEFAULT_INTRA_TX_TYPE_SET2_CDF: [[[u16; 6]; 13]; 3] = [ + [ + [6554, 13107, 19661, 26214, 32768, 0], + [6554, 13107, 19661, 26214, 32768, 0], + [6554, 13107, 19661, 26214, 32768, 0], + [6554, 13107, 19661, 26214, 32768, 0], + [6554, 13107, 19661, 26214, 32768, 0], + [6554, 13107, 19661, 26214, 32768, 0], + [6554, 13107, 19661, 26214, 32768, 0], + [6554, 13107, 19661, 26214, 32768, 0], + [6554, 13107, 19661, 26214, 32768, 0], + [6554, 13107, 19661, 26214, 32768, 0], + [6554, 13107, 19661, 26214, 32768, 0], + [6554, 13107, 19661, 26214, 32768, 0], + [6554, 13107, 19661, 26214, 32768, 0], + ], + [ + [6554, 13107, 19661, 26214, 32768, 0], + [6554, 13107, 19661, 26214, 32768, 0], + [6554, 13107, 19661, 26214, 32768, 0], + [6554, 13107, 19661, 26214, 32768, 0], + [6554, 13107, 19661, 26214, 32768, 0], + [6554, 13107, 19661, 26214, 32768, 0], + [6554, 13107, 19661, 26214, 32768, 0], + [6554, 13107, 19661, 26214, 32768, 0], + [6554, 13107, 19661, 26214, 32768, 0], + [6554, 13107, 19661, 26214, 32768, 0], + [6554, 13107, 19661, 26214, 32768, 0], + [6554, 13107, 19661, 26214, 32768, 0], + [6554, 13107, 19661, 26214, 32768, 0], + ], + [ + [1127, 12814, 22772, 27483, 32768, 0], + [145, 6761, 11980, 26667, 32768, 0], + [362, 5887, 11678, 16725, 32768, 0], + [385, 15213, 18587, 30693, 32768, 0], + [25, 2914, 23134, 27903, 32768, 0], + [60, 4470, 11749, 23991, 32768, 0], + [37, 3332, 14511, 21448, 32768, 0], + [157, 6320, 13036, 17439, 32768, 0], + [119, 6719, 12906, 29396, 32768, 0], + [47, 5537, 12576, 21499, 32768, 0], + [269, 6076, 11258, 23115, 32768, 0], + [83, 5615, 12001, 17228, 32768, 0], + [1968, 5556, 12023, 18547, 32768, 0], + ], +]; + +pub static DEFAULT_IS_INTER_CDF: [[u16; 3]; 4] = [[806, 32768, 0], [16662, 32768, 0], [20186, 32768, 0], [26538, 32768, 0]]; + +pub static DEFAULT_MV_BIT_CDF: [[u16; 3]; 10] = [ + [17408, 32768, 0], + [17920, 32768, 0], + [18944, 32768, 0], + [20480, 32768, 0], + [22528, 32768, 0], + [24576, 32768, 0], + [28672, 32768, 0], + [29952, 32768, 0], + [29952, 32768, 0], + [30720, 32768, 0], +]; + +pub static DEFAULT_MV_CLASS0_BIT_CDF: [u16; 3] = [27648, 32768, 0]; + +pub static DEFAULT_MV_CLASS0_FR_CDF: [[[u16; 5]; 2]; 2] = + [[[16384, 24576, 26624, 32768, 0], [12288, 21248, 24128, 32768, 0]], [[16384, 24576, 26624, 32768, 0], [12288, 21248, 24128, 32768, 0]]]; + +pub static DEFAULT_MV_CLASS0_HP_CDF: [u16; 3] = [20480, 32768, 0]; + +pub static DEFAULT_MV_CLASS_CDF: [[u16; 12]; 2] = [ + [28672, 30976, 31858, 32320, 32551, 32656, 32740, 32757, 32762, 32767, 32768, 0], + [28672, 30976, 31858, 32320, 32551, 32656, 32740, 32757, 32762, 32767, 32768, 0], +]; + +pub static DEFAULT_MV_FR_CDF: [[u16; 5]; 2] = [[8192, 17408, 21248, 32768, 0], [8192, 17408, 21248, 32768, 0]]; + +pub static DEFAULT_MV_HP_CDF: [u16; 3] = [16384, 32768, 0]; + +pub static DEFAULT_MV_JOINT_CDF: [u16; 5] = [4096, 11264, 19328, 32768, 0]; + +pub static DEFAULT_MV_SIGN_CDF: [u16; 3] = [16384, 32768, 0]; + +pub static DEFAULT_NEW_MV_CDF: [[u16; 3]; 6] = [[24035, 32768, 0], [16630, 32768, 0], [15339, 32768, 0], [8386, 32768, 0], [12222, 32768, 0], [4676, 32768, 0]]; + +pub static DEFAULT_PARTITION_W16_CDF: [[u16; 11]; 4] = [ + [15597, 20929, 24571, 26706, 27664, 28821, 29601, 30571, 31902, 32768, 0], + [7925, 11043, 16785, 22470, 23971, 25043, 26651, 28701, 29834, 32768, 0], + [5414, 13269, 15111, 20488, 22360, 24500, 25537, 26336, 32117, 32768, 0], + [2662, 6362, 8614, 20860, 23053, 24778, 26436, 27829, 31171, 32768, 0], +]; + +pub static DEFAULT_PARTITION_W32_CDF: [[u16; 11]; 4] = [ + [18462, 20920, 23124, 27647, 28227, 29049, 29519, 30178, 31544, 32768, 0], + [7689, 9060, 12056, 24992, 25660, 26182, 26951, 28041, 29052, 32768, 0], + [6015, 9009, 10062, 24544, 25409, 26545, 27071, 27526, 32047, 32768, 0], + [1394, 2208, 2796, 28614, 29061, 29466, 29840, 30185, 31899, 32768, 0], +]; + +pub static DEFAULT_PARTITION_W64_CDF: [[u16; 11]; 4] = [ + [20137, 21547, 23078, 29566, 29837, 30261, 30524, 30892, 31724, 32768, 0], + [6732, 7490, 9497, 27944, 28250, 28515, 28969, 29630, 30104, 32768, 0], + [5945, 7663, 8348, 28683, 29117, 29749, 30064, 30298, 32238, 32768, 0], + [870, 1212, 1487, 31198, 31394, 31574, 31743, 31881, 32332, 32768, 0], +]; + +pub static DEFAULT_PARTITION_W8_CDF: [[u16; 5]; 4] = + [[19132, 25510, 30392, 32768, 0], [13928, 19855, 28540, 32768, 0], [12522, 23679, 28629, 32768, 0], [9896, 18783, 25853, 32768, 0]]; + +pub static DEFAULT_REF_MV_CDF: [[u16; 3]; 6] = + [[23974, 32768, 0], [24188, 32768, 0], [17848, 32768, 0], [28622, 32768, 0], [24312, 32768, 0], [19923, 32768, 0]]; + +pub static DEFAULT_SCAN_16X16: [u16; 256] = [ + 0, 1, 16, 32, 17, 2, 3, 18, 33, 48, 64, 49, 34, 19, 4, 5, 20, 35, 50, 65, 80, 96, 81, 66, 51, 36, 21, 6, 7, 22, 37, 52, 67, 82, 97, 112, 128, 113, 98, 83, + 68, 53, 38, 23, 8, 9, 24, 39, 54, 69, 84, 99, 114, 129, 144, 160, 145, 130, 115, 100, 85, 70, 55, 40, 25, 10, 11, 26, 41, 56, 71, 86, 101, 116, 131, 146, + 161, 176, 192, 177, 162, 147, 132, 117, 102, 87, 72, 57, 42, 27, 12, 13, 28, 43, 58, 73, 88, 103, 118, 133, 148, 163, 178, 193, 208, 224, 209, 194, 179, + 164, 149, 134, 119, 104, 89, 74, 59, 44, 29, 14, 15, 30, 45, 60, 75, 90, 105, 120, 135, 150, 165, 180, 195, 210, 225, 240, 241, 226, 211, 196, 181, 166, + 151, 136, 121, 106, 91, 76, 61, 46, 31, 47, 62, 77, 92, 107, 122, 137, 152, 167, 182, 197, 212, 227, 242, 243, 228, 213, 198, 183, 168, 153, 138, 123, 108, + 93, 78, 63, 79, 94, 109, 124, 139, 154, 169, 184, 199, 214, 229, 244, 245, 230, 215, 200, 185, 170, 155, 140, 125, 110, 95, 111, 126, 141, 156, 171, 186, + 201, 216, 231, 246, 247, 232, 217, 202, 187, 172, 157, 142, 127, 143, 158, 173, 188, 203, 218, 233, 248, 249, 234, 219, 204, 189, 174, 159, 175, 190, 205, + 220, 235, 250, 251, 236, 221, 206, 191, 207, 222, 237, 252, 253, 238, 223, 239, 254, 255, +]; + +pub static DEFAULT_SCAN_32X32: [u16; 1024] = [ + 0, 1, 32, 64, 33, 2, 3, 34, 65, 96, 128, 97, 66, 35, 4, 5, 36, 67, 98, 129, 160, 192, 161, 130, 99, 68, 37, 6, 7, 38, 69, 100, 131, 162, 193, 224, 256, + 225, 194, 163, 132, 101, 70, 39, 8, 9, 40, 71, 102, 133, 164, 195, 226, 257, 288, 320, 289, 258, 227, 196, 165, 134, 103, 72, 41, 10, 11, 42, 73, 104, 135, + 166, 197, 228, 259, 290, 321, 352, 384, 353, 322, 291, 260, 229, 198, 167, 136, 105, 74, 43, 12, 13, 44, 75, 106, 137, 168, 199, 230, 261, 292, 323, 354, + 385, 416, 448, 417, 386, 355, 324, 293, 262, 231, 200, 169, 138, 107, 76, 45, 14, 15, 46, 77, 108, 139, 170, 201, 232, 263, 294, 325, 356, 387, 418, 449, + 480, 512, 481, 450, 419, 388, 357, 326, 295, 264, 233, 202, 171, 140, 109, 78, 47, 16, 17, 48, 79, 110, 141, 172, 203, 234, 265, 296, 327, 358, 389, 420, + 451, 482, 513, 544, 576, 545, 514, 483, 452, 421, 390, 359, 328, 297, 266, 235, 204, 173, 142, 111, 80, 49, 18, 19, 50, 81, 112, 143, 174, 205, 236, 267, + 298, 329, 360, 391, 422, 453, 484, 515, 546, 577, 608, 640, 609, 578, 547, 516, 485, 454, 423, 392, 361, 330, 299, 268, 237, 206, 175, 144, 113, 82, 51, + 20, 21, 52, 83, 114, 145, 176, 207, 238, 269, 300, 331, 362, 393, 424, 455, 486, 517, 548, 579, 610, 641, 672, 704, 673, 642, 611, 580, 549, 518, 487, 456, + 425, 394, 363, 332, 301, 270, 239, 208, 177, 146, 115, 84, 53, 22, 23, 54, 85, 116, 147, 178, 209, 240, 271, 302, 333, 364, 395, 426, 457, 488, 519, 550, + 581, 612, 643, 674, 705, 736, 768, 737, 706, 675, 644, 613, 582, 551, 520, 489, 458, 427, 396, 365, 334, 303, 272, 241, 210, 179, 148, 117, 86, 55, 24, 25, + 56, 87, 118, 149, 180, 211, 242, 273, 304, 335, 366, 397, 428, 459, 490, 521, 552, 583, 614, 645, 676, 707, 738, 769, 800, 832, 801, 770, 739, 708, 677, + 646, 615, 584, 553, 522, 491, 460, 429, 398, 367, 336, 305, 274, 243, 212, 181, 150, 119, 88, 57, 26, 27, 58, 89, 120, 151, 182, 213, 244, 275, 306, 337, + 368, 399, 430, 461, 492, 523, 554, 585, 616, 647, 678, 709, 740, 771, 802, 833, 864, 896, 865, 834, 803, 772, 741, 710, 679, 648, 617, 586, 555, 524, 493, + 462, 431, 400, 369, 338, 307, 276, 245, 214, 183, 152, 121, 90, 59, 28, 29, 60, 91, 122, 153, 184, 215, 246, 277, 308, 339, 370, 401, 432, 463, 494, 525, + 556, 587, 618, 649, 680, 711, 742, 773, 804, 835, 866, 897, 928, 960, 929, 898, 867, 836, 805, 774, 743, 712, 681, 650, 619, 588, 557, 526, 495, 464, 433, + 402, 371, 340, 309, 278, 247, 216, 185, 154, 123, 92, 61, 30, 31, 62, 93, 124, 155, 186, 217, 248, 279, 310, 341, 372, 403, 434, 465, 496, 527, 558, 589, + 620, 651, 682, 713, 744, 775, 806, 837, 868, 899, 930, 961, 992, 993, 962, 931, 900, 869, 838, 807, 776, 745, 714, 683, 652, 621, 590, 559, 528, 497, 466, + 435, 404, 373, 342, 311, 280, 249, 218, 187, 156, 125, 94, 63, 95, 126, 157, 188, 219, 250, 281, 312, 343, 374, 405, 436, 467, 498, 529, 560, 591, 622, + 653, 684, 715, 746, 777, 808, 839, 870, 901, 932, 963, 994, 995, 964, 933, 902, 871, 840, 809, 778, 747, 716, 685, 654, 623, 592, 561, 530, 499, 468, 437, + 406, 375, 344, 313, 282, 251, 220, 189, 158, 127, 159, 190, 221, 252, 283, 314, 345, 376, 407, 438, 469, 500, 531, 562, 593, 624, 655, 686, 717, 748, 779, + 810, 841, 872, 903, 934, 965, 996, 997, 966, 935, 904, 873, 842, 811, 780, 749, 718, 687, 656, 625, 594, 563, 532, 501, 470, 439, 408, 377, 346, 315, 284, + 253, 222, 191, 223, 254, 285, 316, 347, 378, 409, 440, 471, 502, 533, 564, 595, 626, 657, 688, 719, 750, 781, 812, 843, 874, 905, 936, 967, 998, 999, 968, + 937, 906, 875, 844, 813, 782, 751, 720, 689, 658, 627, 596, 565, 534, 503, 472, 441, 410, 379, 348, 317, 286, 255, 287, 318, 349, 380, 411, 442, 473, 504, + 535, 566, 597, 628, 659, 690, 721, 752, 783, 814, 845, 876, 907, 938, 969, 1000, 1001, 970, 939, 908, 877, 846, 815, 784, 753, 722, 691, 660, 629, 598, + 567, 536, 505, 474, 443, 412, 381, 350, 319, 351, 382, 413, 444, 475, 506, 537, 568, 599, 630, 661, 692, 723, 754, 785, 816, 847, 878, 909, 940, 971, 1002, + 1003, 972, 941, 910, 879, 848, 817, 786, 755, 724, 693, 662, 631, 600, 569, 538, 507, 476, 445, 414, 383, 415, 446, 477, 508, 539, 570, 601, 632, 663, 694, + 725, 756, 787, 818, 849, 880, 911, 942, 973, 1004, 1005, 974, 943, 912, 881, 850, 819, 788, 757, 726, 695, 664, 633, 602, 571, 540, 509, 478, 447, 479, + 510, 541, 572, 603, 634, 665, 696, 727, 758, 789, 820, 851, 882, 913, 944, 975, 1006, 1007, 976, 945, 914, 883, 852, 821, 790, 759, 728, 697, 666, 635, + 604, 573, 542, 511, 543, 574, 605, 636, 667, 698, 729, 760, 791, 822, 853, 884, 915, 946, 977, 1008, 1009, 978, 947, 916, 885, 854, 823, 792, 761, 730, + 699, 668, 637, 606, 575, 607, 638, 669, 700, 731, 762, 793, 824, 855, 886, 917, 948, 979, 1010, 1011, 980, 949, 918, 887, 856, 825, 794, 763, 732, 701, + 670, 639, 671, 702, 733, 764, 795, 826, 857, 888, 919, 950, 981, 1012, 1013, 982, 951, 920, 889, 858, 827, 796, 765, 734, 703, 735, 766, 797, 828, 859, + 890, 921, 952, 983, 1014, 1015, 984, 953, 922, 891, 860, 829, 798, 767, 799, 830, 861, 892, 923, 954, 985, 1016, 1017, 986, 955, 924, 893, 862, 831, 863, + 894, 925, 956, 987, 1018, 1019, 988, 957, 926, 895, 927, 958, 989, 1020, 1021, 990, 959, 991, 1022, 1023, +]; + +pub static DEFAULT_SCAN_4X4: [u16; 16] = [0, 1, 4, 8, 5, 2, 3, 6, 9, 12, 13, 10, 7, 11, 14, 15]; + +pub static DEFAULT_SCAN_8X8: [u16; 64] = [ + 0, 1, 8, 16, 9, 2, 3, 10, 17, 24, 32, 25, 18, 11, 4, 5, 12, 19, 26, 33, 40, 48, 41, 34, 27, 20, 13, 6, 7, 14, 21, 28, 35, 42, 49, 56, 57, 50, 43, 36, 29, + 22, 15, 23, 30, 37, 44, 51, 58, 59, 52, 45, 38, 31, 39, 46, 53, 60, 61, 54, 47, 55, 62, 63, +]; + +pub static DEFAULT_SINGLE_REF_CDF: [[[u16; 3]; 6]; 3] = [ + [[4897, 32768, 0], [1555, 32768, 0], [4236, 32768, 0], [8650, 32768, 0], [904, 32768, 0], [1444, 32768, 0]], + [[16973, 32768, 0], [16751, 32768, 0], [19647, 32768, 0], [24773, 32768, 0], [11014, 32768, 0], [15087, 32768, 0]], + [[29744, 32768, 0], [30279, 32768, 0], [31194, 32768, 0], [31895, 32768, 0], [26875, 32768, 0], [30304, 32768, 0]], +]; + +pub static DEFAULT_SKIP_CDF: [[u16; 3]; 3] = [[31671, 32768, 0], [16515, 32768, 0], [4576, 32768, 0]]; + +pub static DEFAULT_TXB_SKIP_CDF: [[[[u16; 3]; 13]; 5]; 4] = [ + [ + [ + [31849, 32768, 0], + [5892, 32768, 0], + [12112, 32768, 0], + [21935, 32768, 0], + [20289, 32768, 0], + [27473, 32768, 0], + [32487, 32768, 0], + [7654, 32768, 0], + [19473, 32768, 0], + [29984, 32768, 0], + [9961, 32768, 0], + [30242, 32768, 0], + [32117, 32768, 0], + ], + [ + [31548, 32768, 0], + [1549, 32768, 0], + [10130, 32768, 0], + [16656, 32768, 0], + [18591, 32768, 0], + [26308, 32768, 0], + [32537, 32768, 0], + [5403, 32768, 0], + [18096, 32768, 0], + [30003, 32768, 0], + [16384, 32768, 0], + [16384, 32768, 0], + [16384, 32768, 0], + ], + [ + [29957, 32768, 0], + [5391, 32768, 0], + [18039, 32768, 0], + [23566, 32768, 0], + [22431, 32768, 0], + [25822, 32768, 0], + [32197, 32768, 0], + [3778, 32768, 0], + [15336, 32768, 0], + [28981, 32768, 0], + [16384, 32768, 0], + [16384, 32768, 0], + [16384, 32768, 0], + ], + [ + [17920, 32768, 0], + [1818, 32768, 0], + [7282, 32768, 0], + [25273, 32768, 0], + [10923, 32768, 0], + [31554, 32768, 0], + [32624, 32768, 0], + [1366, 32768, 0], + [15628, 32768, 0], + [30462, 32768, 0], + [146, 32768, 0], + [5132, 32768, 0], + [31657, 32768, 0], + ], + [ + [6308, 32768, 0], + [117, 32768, 0], + [1638, 32768, 0], + [2161, 32768, 0], + [16384, 32768, 0], + [10923, 32768, 0], + [30247, 32768, 0], + [16384, 32768, 0], + [16384, 32768, 0], + [16384, 32768, 0], + [16384, 32768, 0], + [16384, 32768, 0], + [16384, 32768, 0], + ], + ], + [ + [ + [30371, 32768, 0], + [7570, 32768, 0], + [13155, 32768, 0], + [20751, 32768, 0], + [20969, 32768, 0], + [27067, 32768, 0], + [32013, 32768, 0], + [5495, 32768, 0], + [17942, 32768, 0], + [28280, 32768, 0], + [16384, 32768, 0], + [16384, 32768, 0], + [16384, 32768, 0], + ], + [ + [31782, 32768, 0], + [1836, 32768, 0], + [10689, 32768, 0], + [17604, 32768, 0], + [21622, 32768, 0], + [27518, 32768, 0], + [32399, 32768, 0], + [4419, 32768, 0], + [16294, 32768, 0], + [28345, 32768, 0], + [16384, 32768, 0], + [16384, 32768, 0], + [16384, 32768, 0], + ], + [ + [31901, 32768, 0], + [10311, 32768, 0], + [18047, 32768, 0], + [24806, 32768, 0], + [23288, 32768, 0], + [27914, 32768, 0], + [32296, 32768, 0], + [4215, 32768, 0], + [15756, 32768, 0], + [28341, 32768, 0], + [16384, 32768, 0], + [16384, 32768, 0], + [16384, 32768, 0], + ], + [ + [26726, 32768, 0], + [1045, 32768, 0], + [11703, 32768, 0], + [20590, 32768, 0], + [18554, 32768, 0], + [25970, 32768, 0], + [31938, 32768, 0], + [5583, 32768, 0], + [21313, 32768, 0], + [29390, 32768, 0], + [641, 32768, 0], + [22265, 32768, 0], + [31452, 32768, 0], + ], + [ + [26584, 32768, 0], + [188, 32768, 0], + [8847, 32768, 0], + [24519, 32768, 0], + [22938, 32768, 0], + [30583, 32768, 0], + [32608, 32768, 0], + [16384, 32768, 0], + [16384, 32768, 0], + [16384, 32768, 0], + [16384, 32768, 0], + [16384, 32768, 0], + [16384, 32768, 0], + ], + ], + [ + [ + [29614, 32768, 0], + [9068, 32768, 0], + [12924, 32768, 0], + [19538, 32768, 0], + [17737, 32768, 0], + [24619, 32768, 0], + [30642, 32768, 0], + [4119, 32768, 0], + [16026, 32768, 0], + [25657, 32768, 0], + [16384, 32768, 0], + [16384, 32768, 0], + [16384, 32768, 0], + ], + [ + [31957, 32768, 0], + [3230, 32768, 0], + [11153, 32768, 0], + [18123, 32768, 0], + [20143, 32768, 0], + [26536, 32768, 0], + [31986, 32768, 0], + [3050, 32768, 0], + [14603, 32768, 0], + [25155, 32768, 0], + [16384, 32768, 0], + [16384, 32768, 0], + [16384, 32768, 0], + ], + [ + [32363, 32768, 0], + [10692, 32768, 0], + [19090, 32768, 0], + [24357, 32768, 0], + [24442, 32768, 0], + [28312, 32768, 0], + [32169, 32768, 0], + [3648, 32768, 0], + [15690, 32768, 0], + [26815, 32768, 0], + [16384, 32768, 0], + [16384, 32768, 0], + [16384, 32768, 0], + ], + [ + [30669, 32768, 0], + [3832, 32768, 0], + [11663, 32768, 0], + [18889, 32768, 0], + [19782, 32768, 0], + [23313, 32768, 0], + [31330, 32768, 0], + [5124, 32768, 0], + [18719, 32768, 0], + [28468, 32768, 0], + [3082, 32768, 0], + [20982, 32768, 0], + [29443, 32768, 0], + ], + [ + [28573, 32768, 0], + [3183, 32768, 0], + [17802, 32768, 0], + [25977, 32768, 0], + [26677, 32768, 0], + [27832, 32768, 0], + [32387, 32768, 0], + [16384, 32768, 0], + [16384, 32768, 0], + [16384, 32768, 0], + [16384, 32768, 0], + [16384, 32768, 0], + [16384, 32768, 0], + ], + ], + [ + [ + [26887, 32768, 0], + [6729, 32768, 0], + [10361, 32768, 0], + [17442, 32768, 0], + [15045, 32768, 0], + [22478, 32768, 0], + [29072, 32768, 0], + [2713, 32768, 0], + [11861, 32768, 0], + [20773, 32768, 0], + [16384, 32768, 0], + [16384, 32768, 0], + [16384, 32768, 0], + ], + [ + [31903, 32768, 0], + [2044, 32768, 0], + [7528, 32768, 0], + [14618, 32768, 0], + [16182, 32768, 0], + [24168, 32768, 0], + [31037, 32768, 0], + [2786, 32768, 0], + [11194, 32768, 0], + [20155, 32768, 0], + [16384, 32768, 0], + [16384, 32768, 0], + [16384, 32768, 0], + ], + [ + [32510, 32768, 0], + [8430, 32768, 0], + [17318, 32768, 0], + [24154, 32768, 0], + [23674, 32768, 0], + [28789, 32768, 0], + [32139, 32768, 0], + [3440, 32768, 0], + [13117, 32768, 0], + [22702, 32768, 0], + [16384, 32768, 0], + [16384, 32768, 0], + [16384, 32768, 0], + ], + [ + [31671, 32768, 0], + [2056, 32768, 0], + [11746, 32768, 0], + [16852, 32768, 0], + [18635, 32768, 0], + [24715, 32768, 0], + [31484, 32768, 0], + [4656, 32768, 0], + [16074, 32768, 0], + [24704, 32768, 0], + [1806, 32768, 0], + [14645, 32768, 0], + [25336, 32768, 0], + ], + [ + [31539, 32768, 0], + [8433, 32768, 0], + [20576, 32768, 0], + [27904, 32768, 0], + [27852, 32768, 0], + [30026, 32768, 0], + [32441, 32768, 0], + [16384, 32768, 0], + [16384, 32768, 0], + [16384, 32768, 0], + [16384, 32768, 0], + [16384, 32768, 0], + [16384, 32768, 0], + ], + ], +]; + +pub static DEFAULT_UV_MODE_CFL_ALLOWED_CDF: [[u16; 15]; 13] = [ + [10407, 11208, 12900, 13181, 13823, 14175, 14899, 15656, 15986, 20086, 20995, 22455, 24212, 32768, 0], + [4532, 19780, 20057, 20215, 20428, 21071, 21199, 21451, 22099, 24228, 24693, 27032, 29472, 32768, 0], + [5273, 5379, 20177, 20270, 20385, 20439, 20949, 21695, 21774, 23138, 24256, 24703, 26679, 32768, 0], + [6740, 7167, 7662, 14152, 14536, 14785, 15034, 16741, 18371, 21520, 22206, 23389, 24182, 32768, 0], + [4987, 5368, 5928, 6068, 19114, 20315, 21857, 22253, 22411, 24911, 25380, 26027, 26376, 32768, 0], + [5370, 6889, 7247, 7393, 9498, 21114, 21402, 21753, 21981, 24780, 25386, 26517, 27176, 32768, 0], + [4816, 4961, 7204, 7326, 8765, 8930, 20169, 20682, 20803, 23188, 23763, 24455, 24940, 32768, 0], + [6608, 6740, 8529, 9049, 9257, 9356, 9735, 18827, 19059, 22336, 23204, 23964, 24793, 32768, 0], + [5998, 7419, 7781, 8933, 9255, 9549, 9753, 10417, 18898, 22494, 23139, 24764, 25989, 32768, 0], + [10660, 11298, 12550, 12957, 13322, 13624, 14040, 15004, 15534, 20714, 21789, 23443, 24861, 32768, 0], + [10522, 11530, 12552, 12963, 13378, 13779, 14245, 15235, 15902, 20102, 22696, 23774, 25838, 32768, 0], + [10099, 10691, 12639, 13049, 13386, 13665, 14125, 15163, 15636, 19676, 20474, 23519, 25208, 32768, 0], + [3144, 5087, 7382, 7504, 7593, 7690, 7801, 8064, 8232, 9248, 9875, 10521, 29048, 32768, 0], +]; + +pub static DEFAULT_UV_MODE_CFL_NOT_ALLOWED_CDF: [[u16; 14]; 13] = [ + [22631, 24152, 25378, 25661, 25986, 26520, 27055, 27923, 28244, 30059, 30941, 31961, 32768, 0], + [9513, 26881, 26973, 27046, 27118, 27664, 27739, 27824, 28359, 29505, 29800, 31796, 32768, 0], + [9845, 9915, 28663, 28704, 28757, 28780, 29198, 29822, 29854, 30764, 31777, 32029, 32768, 0], + [13639, 13897, 14171, 25331, 25606, 25727, 25953, 27148, 28577, 30612, 31355, 32493, 32768, 0], + [9764, 9835, 9930, 9954, 25386, 27053, 27958, 28148, 28243, 31101, 31744, 32363, 32768, 0], + [11825, 13589, 13677, 13720, 15048, 29213, 29301, 29458, 29711, 31161, 31441, 32550, 32768, 0], + [14175, 14399, 16608, 16821, 17718, 17775, 28551, 30200, 30245, 31837, 32342, 32667, 32768, 0], + [12885, 13038, 14978, 15590, 15673, 15748, 16176, 29128, 29267, 30643, 31961, 32461, 32768, 0], + [12026, 13661, 13874, 15305, 15490, 15726, 15995, 16273, 28443, 30388, 30767, 32416, 32768, 0], + [19052, 19840, 20579, 20916, 21150, 21467, 21885, 22719, 23174, 28861, 30379, 32175, 32768, 0], + [18627, 19649, 20974, 21219, 21492, 21816, 22199, 23119, 23527, 27053, 31397, 32148, 32768, 0], + [17026, 19004, 19997, 20339, 20586, 21103, 21349, 21907, 22482, 25896, 26541, 31819, 32768, 0], + [12124, 13759, 14959, 14992, 15007, 15051, 15078, 15166, 15255, 15753, 16039, 16606, 32768, 0], +]; + +pub static DEFAULT_Y_MODE_CDF: [[u16; 14]; 4] = [ + [22801, 23489, 24293, 24756, 25601, 26123, 26606, 27418, 27945, 29228, 29685, 30349, 32768, 0], + [18673, 19845, 22631, 23318, 23950, 24649, 25527, 27364, 28152, 29701, 29984, 30852, 32768, 0], + [19770, 20979, 23396, 23939, 24241, 24654, 25136, 27073, 27830, 29360, 29730, 30659, 32768, 0], + [20155, 21301, 22838, 23178, 23261, 23533, 23703, 24804, 25352, 26575, 27016, 28049, 32768, 0], +]; + +pub static DEFAULT_ZERO_MV_CDF: [[u16; 3]; 2] = [[2175, 32768, 0], [1054, 32768, 0]]; + +pub static INTRA_MODE_CONTEXT: [u8; 13] = [0, 1, 2, 3, 4, 4, 4, 4, 3, 0, 1, 2, 0]; + +pub static MAG_REF_OFFSET_WITH_TX_CLASS: [[[u8; 2]; 3]; 3] = [[[0, 1], [1, 0], [1, 1]], [[0, 1], [1, 0], [0, 2]], [[0, 1], [1, 0], [2, 0]]]; + +pub static MAX_TX_SIZE_RECT: [u8; 22] = [0, 5, 6, 1, 7, 8, 2, 9, 10, 3, 11, 12, 4, 4, 4, 4, 13, 14, 15, 16, 17, 18]; + +pub static MI_HEIGHT_LOG2: [u8; 22] = [0, 1, 0, 1, 2, 1, 2, 3, 2, 3, 4, 3, 4, 5, 4, 5, 2, 0, 3, 1, 4, 2]; + +pub static MI_WIDTH_LOG2: [u8; 22] = [0, 0, 1, 1, 1, 2, 2, 2, 3, 3, 3, 4, 4, 4, 5, 5, 0, 2, 1, 3, 2, 4]; + +pub static MODE_TO_TXFM: [u8; 14] = [0, 1, 2, 0, 3, 1, 2, 2, 1, 3, 1, 2, 3, 0]; + +pub static NUM_4X4_BLOCKS_HIGH: [u8; 22] = [1, 2, 1, 2, 4, 2, 4, 8, 4, 8, 16, 8, 16, 32, 16, 32, 4, 1, 8, 2, 16, 4]; + +pub static NUM_4X4_BLOCKS_WIDE: [u8; 22] = [1, 1, 2, 2, 2, 4, 4, 4, 8, 8, 8, 16, 16, 16, 32, 32, 1, 4, 2, 8, 4, 16]; + +pub static PARTITION_SUBSIZE: [[u8; 22]; 10] = [ + [0, 22, 22, 3, 22, 22, 6, 22, 22, 9, 22, 22, 12, 22, 22, 15, 22, 22, 22, 22, 22, 22], + [22, 22, 22, 2, 22, 22, 5, 22, 22, 8, 22, 22, 11, 22, 22, 14, 22, 22, 22, 22, 22, 22], + [22, 22, 22, 1, 22, 22, 4, 22, 22, 7, 22, 22, 10, 22, 22, 13, 22, 22, 22, 22, 22, 22], + [22, 22, 22, 0, 22, 22, 3, 22, 22, 6, 22, 22, 9, 22, 22, 12, 22, 22, 22, 22, 22, 22], + [22, 22, 22, 2, 22, 22, 5, 22, 22, 8, 22, 22, 11, 22, 22, 14, 22, 22, 22, 22, 22, 22], + [22, 22, 22, 2, 22, 22, 5, 22, 22, 8, 22, 22, 11, 22, 22, 14, 22, 22, 22, 22, 22, 22], + [22, 22, 22, 1, 22, 22, 4, 22, 22, 7, 22, 22, 10, 22, 22, 13, 22, 22, 22, 22, 22, 22], + [22, 22, 22, 1, 22, 22, 4, 22, 22, 7, 22, 22, 10, 22, 22, 13, 22, 22, 22, 22, 22, 22], + [22, 22, 22, 22, 22, 22, 17, 22, 22, 19, 22, 22, 21, 22, 22, 22, 22, 22, 22, 22, 22, 22], + [22, 22, 22, 22, 22, 22, 16, 22, 22, 18, 22, 22, 20, 22, 22, 22, 22, 22, 22, 22, 22, 22], +]; + +pub static SIG_REF_DIFF_OFFSET: [[[u8; 2]; 5]; 3] = + [[[0, 1], [1, 0], [1, 1], [0, 2], [2, 0]], [[0, 1], [1, 0], [0, 2], [0, 3], [0, 4]], [[0, 1], [1, 0], [2, 0], [3, 0], [4, 0]]]; + +pub static SIZE_GROUP: [u8; 22] = [0, 0, 0, 1, 1, 1, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 0, 0, 1, 1, 2, 2]; + +pub static SM_WEIGHTS_TX_16X16: [u8; 16] = [255, 225, 196, 170, 145, 123, 102, 84, 68, 54, 43, 33, 26, 20, 17, 16]; + +pub static SM_WEIGHTS_TX_32X32: [u8; 32] = + [255, 240, 225, 210, 196, 182, 169, 157, 145, 133, 122, 111, 101, 92, 83, 74, 66, 59, 52, 45, 39, 34, 29, 25, 21, 17, 14, 12, 10, 9, 8, 8]; + +pub static SM_WEIGHTS_TX_4X4: [u8; 4] = [255, 149, 85, 64]; + +pub static SM_WEIGHTS_TX_8X8: [u8; 8] = [255, 197, 146, 105, 73, 50, 37, 32]; + +pub static SUBPEL_FILTERS: [[[i16; 8]; 16]; 6] = [ + [ + [0, 0, 0, 128, 0, 0, 0, 0], + [0, 2, -6, 126, 8, -2, 0, 0], + [0, 2, -10, 122, 18, -4, 0, 0], + [0, 2, -12, 116, 28, -8, 2, 0], + [0, 2, -14, 110, 38, -10, 2, 0], + [0, 2, -14, 102, 48, -12, 2, 0], + [0, 2, -16, 94, 58, -12, 2, 0], + [0, 2, -14, 84, 66, -12, 2, 0], + [0, 2, -14, 76, 76, -14, 2, 0], + [0, 2, -12, 66, 84, -14, 2, 0], + [0, 2, -12, 58, 94, -16, 2, 0], + [0, 2, -12, 48, 102, -14, 2, 0], + [0, 2, -10, 38, 110, -14, 2, 0], + [0, 2, -8, 28, 116, -12, 2, 0], + [0, 0, -4, 18, 122, -10, 2, 0], + [0, 0, -2, 8, 126, -6, 2, 0], + ], + [ + [0, 0, 0, 128, 0, 0, 0, 0], + [0, 2, 28, 62, 34, 2, 0, 0], + [0, 0, 26, 62, 36, 4, 0, 0], + [0, 0, 22, 62, 40, 4, 0, 0], + [0, 0, 20, 60, 42, 6, 0, 0], + [0, 0, 18, 58, 44, 8, 0, 0], + [0, 0, 16, 56, 46, 10, 0, 0], + [0, -2, 16, 54, 48, 12, 0, 0], + [0, -2, 14, 52, 52, 14, -2, 0], + [0, 0, 12, 48, 54, 16, -2, 0], + [0, 0, 10, 46, 56, 16, 0, 0], + [0, 0, 8, 44, 58, 18, 0, 0], + [0, 0, 6, 42, 60, 20, 0, 0], + [0, 0, 4, 40, 62, 22, 0, 0], + [0, 0, 4, 36, 62, 26, 0, 0], + [0, 0, 2, 34, 62, 28, 2, 0], + ], + [ + [0, 0, 0, 128, 0, 0, 0, 0], + [-2, 2, -6, 126, 8, -2, 2, 0], + [-2, 6, -12, 124, 16, -6, 4, -2], + [-2, 8, -18, 120, 26, -10, 6, -2], + [-4, 10, -22, 116, 38, -14, 6, -2], + [-4, 10, -22, 108, 48, -18, 8, -2], + [-4, 10, -24, 100, 60, -20, 8, -2], + [-4, 10, -24, 90, 70, -22, 10, -2], + [-4, 12, -24, 80, 80, -24, 12, -4], + [-2, 10, -22, 70, 90, -24, 10, -4], + [-2, 8, -20, 60, 100, -24, 10, -4], + [-2, 8, -18, 48, 108, -22, 10, -4], + [-2, 6, -14, 38, 116, -22, 10, -4], + [-2, 6, -10, 26, 120, -18, 8, -2], + [-2, 4, -6, 16, 124, -12, 6, -2], + [0, 2, -2, 8, 126, -6, 2, -2], + ], + [ + [0, 0, 0, 128, 0, 0, 0, 0], + [0, 0, 0, 120, 8, 0, 0, 0], + [0, 0, 0, 112, 16, 0, 0, 0], + [0, 0, 0, 104, 24, 0, 0, 0], + [0, 0, 0, 96, 32, 0, 0, 0], + [0, 0, 0, 88, 40, 0, 0, 0], + [0, 0, 0, 80, 48, 0, 0, 0], + [0, 0, 0, 72, 56, 0, 0, 0], + [0, 0, 0, 64, 64, 0, 0, 0], + [0, 0, 0, 56, 72, 0, 0, 0], + [0, 0, 0, 48, 80, 0, 0, 0], + [0, 0, 0, 40, 88, 0, 0, 0], + [0, 0, 0, 32, 96, 0, 0, 0], + [0, 0, 0, 24, 104, 0, 0, 0], + [0, 0, 0, 16, 112, 0, 0, 0], + [0, 0, 0, 8, 120, 0, 0, 0], + ], + [ + [0, 0, 0, 128, 0, 0, 0, 0], + [0, 0, -4, 126, 8, -2, 0, 0], + [0, 0, -8, 122, 18, -4, 0, 0], + [0, 0, -10, 116, 28, -6, 0, 0], + [0, 0, -12, 110, 38, -8, 0, 0], + [0, 0, -12, 102, 48, -10, 0, 0], + [0, 0, -14, 94, 58, -10, 0, 0], + [0, 0, -12, 84, 66, -10, 0, 0], + [0, 0, -12, 76, 76, -12, 0, 0], + [0, 0, -10, 66, 84, -12, 0, 0], + [0, 0, -10, 58, 94, -14, 0, 0], + [0, 0, -10, 48, 102, -12, 0, 0], + [0, 0, -8, 38, 110, -12, 0, 0], + [0, 0, -6, 28, 116, -10, 0, 0], + [0, 0, -4, 18, 122, -8, 0, 0], + [0, 0, -2, 8, 126, -4, 0, 0], + ], + [ + [0, 0, 0, 128, 0, 0, 0, 0], + [0, 0, 30, 62, 34, 2, 0, 0], + [0, 0, 26, 62, 36, 4, 0, 0], + [0, 0, 22, 62, 40, 4, 0, 0], + [0, 0, 20, 60, 42, 6, 0, 0], + [0, 0, 18, 58, 44, 8, 0, 0], + [0, 0, 16, 56, 46, 10, 0, 0], + [0, 0, 14, 54, 48, 12, 0, 0], + [0, 0, 12, 52, 52, 12, 0, 0], + [0, 0, 12, 48, 54, 14, 0, 0], + [0, 0, 10, 46, 56, 16, 0, 0], + [0, 0, 8, 44, 58, 18, 0, 0], + [0, 0, 6, 42, 60, 20, 0, 0], + [0, 0, 4, 40, 62, 22, 0, 0], + [0, 0, 4, 36, 62, 26, 0, 0], + [0, 0, 2, 34, 62, 30, 0, 0], + ], +]; + +pub static SUBSAMPLED_SIZE: [[[u8; 2]; 2]; 22] = [ + [[0, 0], [0, 0]], + [[1, 0], [22, 0]], + [[2, 22], [0, 0]], + [[3, 2], [1, 0]], + [[4, 3], [22, 1]], + [[5, 22], [3, 2]], + [[6, 5], [4, 3]], + [[7, 6], [22, 4]], + [[8, 22], [6, 5]], + [[9, 8], [7, 6]], + [[10, 9], [22, 7]], + [[11, 22], [9, 8]], + [[12, 11], [10, 9]], + [[13, 12], [22, 10]], + [[14, 22], [12, 11]], + [[15, 14], [13, 12]], + [[16, 1], [22, 1]], + [[17, 22], [2, 2]], + [[18, 4], [22, 16]], + [[19, 22], [5, 17]], + [[20, 7], [22, 18]], + [[21, 22], [8, 19]], +]; + +pub static TRANSFORM_ROW_SHIFT: [u8; 19] = [0, 1, 2, 2, 2, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2]; + +pub static TX_HEIGHT: [u8; 19] = [4, 8, 16, 32, 64, 8, 4, 16, 8, 32, 16, 64, 32, 16, 4, 32, 8, 64, 16]; + +pub static TX_HEIGHT_LOG2: [u8; 19] = [2, 3, 4, 5, 6, 3, 2, 4, 3, 5, 4, 6, 5, 4, 2, 5, 3, 6, 4]; + +pub static TX_SIZE_SQR: [u8; 19] = [0, 1, 2, 3, 4, 0, 0, 1, 1, 2, 2, 3, 3, 0, 0, 1, 1, 2, 2]; + +pub static TX_SIZE_SQR_UP: [u8; 19] = [0, 1, 2, 3, 4, 1, 1, 2, 2, 3, 3, 4, 4, 2, 2, 3, 3, 4, 4]; + +pub static TX_TYPE_IN_SET_INTER: [[u8; 16]; 4] = [ + [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], + [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0], + [1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0], +]; + +pub static TX_TYPE_IN_SET_INTRA: [[u8; 16]; 3] = + [[1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [1, 1, 1, 1, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0], [1, 1, 1, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0]]; + +pub static TX_TYPE_INTER_INV_SET3: [u8; 2] = [9, 0]; + +pub static TX_TYPE_INTRA_INV_SET2: [u8; 5] = [9, 0, 3, 1, 2]; + +pub static TX_WIDTH: [u8; 19] = [4, 8, 16, 32, 64, 4, 8, 8, 16, 16, 32, 32, 64, 4, 16, 8, 32, 16, 64]; + +pub static TX_WIDTH_LOG2: [u8; 19] = [2, 3, 4, 5, 6, 2, 3, 3, 4, 4, 5, 5, 6, 2, 4, 3, 5, 4, 6]; diff --git a/crates/av1enc/src/transform.rs b/crates/av1enc/src/transform.rs new file mode 100644 index 00000000..386133b5 --- /dev/null +++ b/crates/av1enc/src/transform.rs @@ -0,0 +1,481 @@ +//! Transforms. The inverse transforms are the normative ones of spec 7.13 (mirrored from +//! filmcraft-av1, so that the encoder reconstructs exactly like a decoder); the forward +//! transforms are floating-point matrices scaled to match them (not normative). + +use std::sync::OnceLock; + +use crate::tables::*; + +#[inline(always)] +fn round2_64(x: i64, n: u32) -> i32 { + if n == 0 { x as i32 } else { ((x + (1i64 << (n - 1))) >> n) as i32 } +} + +#[inline(always)] +pub(crate) fn round2(x: i32, n: u32) -> i32 { + if n == 0 { x } else { (x + (1 << (n - 1))) >> n } +} + +#[inline(always)] +fn cos128(angle: i32) -> i64 { + let a = (angle & 255) as usize; + (match a { + 0..=64 => COS128_LOOKUP[a] as i32, + 65..=128 => -(COS128_LOOKUP[128 - a] as i32), + 129..=192 => -(COS128_LOOKUP[a - 128] as i32), + _ => COS128_LOOKUP[256 - a] as i32, + }) as i64 +} + +#[inline(always)] +fn sin128(angle: i32) -> i64 { + cos128(angle - 64) +} + +fn brev(num_bits: u32, x: usize) -> usize { + let mut t = 0; + for i in 0..num_bits { + let bit = (x >> i) & 1; + t += bit << (num_bits - 1 - i); + } + t +} + +#[inline(always)] +fn bf(t: &mut [i32], a: usize, b: usize, angle: i32, flip: bool) { + let x = t[a] as i64 * cos128(angle) - t[b] as i64 * sin128(angle); + let y = t[a] as i64 * sin128(angle) + t[b] as i64 * cos128(angle); + t[a] = round2_64(x, 12); + t[b] = round2_64(y, 12); + if flip { + t.swap(a, b); + } +} + +#[inline(always)] +fn hd(t: &mut [i32], a: usize, b: usize, flip: bool, r: u32) { + let (a, b) = if flip { (b, a) } else { (a, b) }; + let x = t[a]; + let y = t[b]; + let lo = -(1i32 << (r - 1)); + let hi = (1i32 << (r - 1)) - 1; + t[a] = (x + y).clamp(lo, hi); + t[b] = (x - y).clamp(lo, hi); +} + +/// Inverse DCT of length 2^n, n = 2..=5 (7.13.2.3), in place. +fn inverse_dct(t: &mut [i32], n: u32, r: u32) { + let len = 1usize << n; + let mut copy = [0i32; 32]; + copy[..len].copy_from_slice(&t[..len]); + for i in 0..len { + t[i] = copy[brev(n, i)]; + } + if n >= 5 { + for i in 0..8 { + bf(t, 16 + i, 31 - i, 6 + ((brev(3, 7 - i) as i32) << 3), false); + } + } + if n >= 4 { + for i in 0..4 { + bf(t, 8 + i, 15 - i, 12 + ((brev(2, 3 - i) as i32) << 4), false); + } + } + if n >= 5 { + for i in 0..8 { + hd(t, 16 + 2 * i, 17 + 2 * i, i & 1 == 1, r); + } + } + if n >= 3 { + for i in 0..2 { + bf(t, 4 + i, 7 - i, 56 - 32 * i as i32, false); + } + } + if n >= 4 { + for i in 0..4 { + hd(t, 8 + 2 * i, 9 + 2 * i, i & 1 == 1, r); + } + } + if n >= 5 { + for i in 0..2 { + for j in 0..2 { + bf(t, 30 - 4 * i - j, 17 + 4 * i + j, 24 + ((j as i32) << 6) + (((1 - i) as i32) << 5), true); + } + } + } + for i in 0..2 { + bf(t, 2 * i, 2 * i + 1, 32 + 16 * i as i32, i == 0); + } + if n >= 3 { + for i in 0..2 { + hd(t, 4 + 2 * i, 5 + 2 * i, i == 1, r); + } + } + if n >= 4 { + for i in 0..2 { + bf(t, 14 - i, 9 + i, 48 + 64 * i as i32, true); + } + } + if n >= 5 { + for i in 0..4 { + for j in 0..2 { + hd(t, 16 + 4 * i + j, 19 + 4 * i - j, i & 1 == 1, r); + } + } + } + for i in 0..2 { + hd(t, i, 3 - i, false, r); + } + if n >= 3 { + bf(t, 6, 5, 32, true); + } + if n >= 4 { + for i in 0..2 { + for j in 0..2 { + hd(t, 8 + 4 * i + j, 11 + 4 * i - j, i == 1, r); + } + } + } + if n >= 5 { + for i in 0..4 { + bf(t, 29 - i, 18 + i, 48 + (i as i32 >> 1) * 64, true); + } + } + if n >= 3 { + for i in 0..4 { + hd(t, i, 7 - i, false, r); + } + } + if n >= 4 { + for i in 0..2 { + bf(t, 13 - i, 10 + i, 32, true); + } + } + if n >= 5 { + for i in 0..2 { + for j in 0..4 { + hd(t, 16 + i * 8 + j, 23 + i * 8 - j, i == 1, r); + } + } + } + if n >= 4 { + for i in 0..8 { + hd(t, i, 15 - i, false, r); + } + } + if n >= 5 { + for i in 0..4 { + bf(t, 27 - i, 20 + i, 32, true); + } + } + if n >= 5 { + for i in 0..16 { + hd(t, i, 31 - i, false, r); + } + } +} + +const SINPI_1_9: i64 = 1321; +const SINPI_2_9: i64 = 2482; +const SINPI_3_9: i64 = 3344; +const SINPI_4_9: i64 = 3803; + +fn inverse_adst4(t: &mut [i32]) { + let (t0, t1, t2, t3) = (t[0] as i64, t[1] as i64, t[2] as i64, t[3] as i64); + let mut s = [SINPI_1_9 * t0, SINPI_2_9 * t0, SINPI_3_9 * t1, SINPI_4_9 * t2, SINPI_1_9 * t2, SINPI_2_9 * t3, SINPI_4_9 * t3]; + let a7 = t0 - t2; + let b7 = a7 + t3; + s[0] += s[3]; + s[1] -= s[4]; + s[3] = s[2]; + s[2] = SINPI_3_9 * b7; + s[0] += s[5]; + s[1] -= s[6]; + let x = [s[0] + s[3], s[1] + s[3], s[2], s[0] + s[1] - s[3]]; + for i in 0..4 { + t[i] = round2_64(x[i], 12); + } +} + +fn adst_in_permute(t: &mut [i32], n: u32) { + let n0 = 1usize << n; + let mut copy = [0i32; 16]; + copy[..n0].copy_from_slice(&t[..n0]); + for i in 0..n0 { + let idx = if i & 1 == 1 { i - 1 } else { n0 - i - 1 }; + t[i] = copy[idx]; + } +} + +fn adst_out_permute(t: &mut [i32], n: u32) { + let n0 = 1usize << n; + let mut copy = [0i32; 16]; + copy[..n0].copy_from_slice(&t[..n0]); + for i in 0..n0 { + let a = (i >> 3) & 1; + let b = ((i >> 2) & 1) ^ ((i >> 3) & 1); + let c = ((i >> 1) & 1) ^ ((i >> 2) & 1); + let d = (i & 1) ^ ((i >> 1) & 1); + let idx = ((d << 3) | (c << 2) | (b << 1) | a) >> (4 - n); + t[i] = if i & 1 == 1 { -copy[idx] } else { copy[idx] }; + } +} + +fn inverse_adst8(t: &mut [i32], r: u32) { + adst_in_permute(t, 3); + for i in 0..4 { + bf(t, 2 * i, 2 * i + 1, 60 - 16 * i as i32, true); + } + for i in 0..4 { + hd(t, i, 4 + i, false, r); + } + for i in 0..2 { + bf(t, 4 + 3 * i, 5 + i, 48 - 32 * i as i32, true); + } + for i in 0..2 { + for j in 0..2 { + hd(t, 4 * j + i, 2 + 4 * j + i, false, r); + } + } + for i in 0..2 { + bf(t, 2 + 4 * i, 3 + 4 * i, 32, true); + } + adst_out_permute(t, 3); +} + +fn inverse_adst16(t: &mut [i32], r: u32) { + adst_in_permute(t, 4); + for i in 0..8 { + bf(t, 2 * i, 2 * i + 1, 62 - 8 * i as i32, true); + } + for i in 0..8 { + hd(t, i, 8 + i, false, r); + } + for i in 0..2 { + bf(t, 8 + 2 * i, 9 + 2 * i, 56 - 32 * i as i32, true); + bf(t, 13 + 2 * i, 12 + 2 * i, 8 + 32 * i as i32, true); + } + for i in 0..4 { + for j in 0..2 { + hd(t, 8 * j + i, 4 + 8 * j + i, false, r); + } + } + for i in 0..2 { + for j in 0..2 { + bf(t, 4 + 8 * j + 3 * i, 5 + 8 * j + i, 48 - 32 * i as i32, true); + } + } + for i in 0..2 { + for j in 0..4 { + hd(t, 4 * j + i, 2 + 4 * j + i, false, r); + } + } + for i in 0..4 { + bf(t, 2 + 4 * i, 3 + 4 * i, 32, true); + } + adst_out_permute(t, 4); +} + +/// 1-D transform kind. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub(crate) enum Kind { + Dct, + Adst, +} + +/// (vertical / column kind, horizontal / row kind) of a transform type. +pub(crate) fn kinds(tx_type: usize) -> (Kind, Kind) { + match tx_type { + DCT_DCT => (Kind::Dct, Kind::Dct), + ADST_DCT => (Kind::Adst, Kind::Dct), + DCT_ADST => (Kind::Dct, Kind::Adst), + ADST_ADST => (Kind::Adst, Kind::Adst), + _ => unreachable!("unsupported transform type {tx_type}"), + } +} + +fn inverse_1d(t: &mut [i32], kind: Kind, n: u32, r: u32) { + match kind { + Kind::Dct => inverse_dct(t, n, r), + Kind::Adst => match n { + 2 => inverse_adst4(t), + 3 => inverse_adst8(t, r), + _ => inverse_adst16(t, r), + }, + } +} + +/// 2-D inverse transform (7.13.3) of a square `n x n` block (n = 4..=32) of dequantised +/// coefficients (row stride `n`) into `residual` (row stride `n`). +pub(crate) fn inverse_2d(dequant: &[i32], residual: &mut [i32], tx_sz: usize, tx_type: usize, bit_depth: u32) { + let log2 = TX_WIDTH_LOG2[tx_sz] as u32; + let n = 1usize << log2; + let row_shift = TRANSFORM_ROW_SHIFT[tx_sz] as u32; + let row_clamp = bit_depth + 8; + let col_clamp = (bit_depth + 6).max(16); + let (ck, rk) = kinds(tx_type); + let lo = -(1i32 << (col_clamp - 1)); + let hi = (1i32 << (col_clamp - 1)) - 1; + let mut t = [0i32; 32]; + for i in 0..n { + let row = &dequant[i * n..i * n + n]; + if row.iter().all(|&v| v == 0) { + residual[i * n..i * n + n].iter_mut().for_each(|v| *v = 0); + continue; + } + t[..n].copy_from_slice(row); + inverse_1d(&mut t, rk, log2, row_clamp); + for j in 0..n { + residual[i * n + j] = round2(t[j], row_shift).clamp(lo, hi); + } + } + for j in 0..n { + for i in 0..n { + t[i] = residual[i * n + j]; + } + inverse_1d(&mut t, ck, log2, col_clamp); + for i in 0..n { + residual[i * n + j] = round2(t[i], 4); + } + } +} + +/// Orthonormal forward basis matrices `[k][i]` (row k = basis function k), sizes 4..=32. +struct Bases { + dct: [Vec; 4], + adst: [Vec; 3], + /// The same matrices transposed (`[i][k]`). + dct_t: [Vec; 4], + adst_t: [Vec; 3], +} + +fn transpose(m: &[f32]) -> Vec { + let n = (m.len() as f64).sqrt() as usize; + let mut t = vec![0f32; n * n]; + for k in 0..n { + for i in 0..n { + t[i * n + k] = m[k * n + i]; + } + } + t +} + +fn bases() -> &'static Bases { + static B: OnceLock = OnceLock::new(); + B.get_or_init(|| { + let pi = std::f64::consts::PI; + let dct = std::array::from_fn(|l| { + let n = 4usize << l; + let mut m = vec![0f32; n * n]; + for k in 0..n { + let c = if k == 0 { std::f64::consts::FRAC_1_SQRT_2 } else { 1.0 }; + for i in 0..n { + m[k * n + i] = (c * (2.0 / n as f64).sqrt() * (pi * (2 * i + 1) as f64 * k as f64 / (2 * n) as f64).cos()) as f32; + } + } + m + }); + let adst = std::array::from_fn(|l| { + let n = 4usize << l; + let mut m = vec![0f32; n * n]; + for k in 0..n { + for i in 0..n { + m[k * n + i] = if n == 4 { + // the AV1 4-point ADST (sin(pi (i + 1)(2k + 1) / 9) basis) + (2.0 / 3.0 * (pi * ((i + 1) * (2 * k + 1)) as f64 / 9.0).sin()) as f32 + } else { + ((2.0 / n as f64).sqrt() * (pi * ((2 * i + 1) * (2 * k + 1)) as f64 / (4 * n) as f64).sin()) as f32 + }; + } + } + m + }); + let dct_t = std::array::from_fn(|l| transpose(&dct[l])); + let adst_t = std::array::from_fn(|l| transpose(&adst[l])); + Bases { dct, adst, dct_t, adst_t } + }) +} + +fn basis_t(kind: Kind, n: usize) -> &'static [f32] { + let l = n.trailing_zeros() as usize - 2; + match kind { + Kind::Dct => &bases().dct_t[l], + Kind::Adst => &bases().adst_t[l], + } +} + +fn basis(kind: Kind, n: usize) -> &'static [f32] { + let l = n.trailing_zeros() as usize - 2; + match kind { + Kind::Dct => &bases().dct[l], + Kind::Adst => &bases().adst[l], + } +} + +/// Forward 2-D transform of the `n x n` residual (row stride `n`) into orthonormal-domain +/// coefficients scaled by 8 (so that `level = out / q` with the spec's dequantiser). +pub(crate) fn forward_2d(residual: &[i32], out: &mut [f32], n: usize, tx_type: usize) { + let (ck, rk) = kinds(tx_type); + let bc = basis(ck, n); + let brt = basis_t(rk, n); + // tmp[i][k] = sum_j residual[i][j] * br[k][j] (horizontal; written as row updates so that + // the inner loops vectorise) + let mut tmp = [0f32; 32 * 32]; + for i in 0..n { + let t = &mut tmp[i * n..i * n + n]; + for j in 0..n { + let r = residual[i * n + j]; + if r == 0 { + continue; + } + let r = r as f32; + for (o, &b) in t.iter_mut().zip(&brt[j * n..j * n + n]) { + *o += r * b; + } + } + } + // out[k][j] = 8 * sum_i bc[k][i] * tmp[i][j] (vertical) + out[..n * n].iter_mut().for_each(|v| *v = 0.0); + for k in 0..n { + let o = &mut out[k * n..k * n + n]; + for i in 0..n { + let b = 8.0 * bc[k * n + i]; + for (v, &t) in o.iter_mut().zip(&tmp[i * n..i * n + n]) { + *v += b * t; + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + /// forward (float) then the normative inverse reproduces the residual: the forward basis + /// and scale match the decoder's inverse transforms for every supported size and type. + #[test] + fn forward_matches_inverse() { + let mut seed = 99u32; + let mut rnd = || { + seed = seed.wrapping_mul(1_103_515_245).wrapping_add(12345); + ((seed >> 16) % 201) as i32 - 100 + }; + for (tx, n) in [(TX_4X4, 4), (TX_8X8, 8), (TX_16X16, 16), (TX_32X32, 32)] { + let denom = if tx == TX_32X32 { 2.0 } else { 1.0 }; + for tx_type in [DCT_DCT, ADST_DCT, DCT_ADST, ADST_ADST] { + if n == 32 && tx_type != DCT_DCT { + continue; + } + let res: Vec = (0..n * n).map(|_| rnd()).collect(); + let mut coef = vec![0f32; n * n]; + forward_2d(&res, &mut coef, n, tx_type); + // q = 8 / denom keeps full precision: dequant = level * q / denom = coef + let deq: Vec = coef.iter().map(|&c| (c / denom).round() as i32).collect(); + let mut out = vec![0i32; n * n]; + inverse_2d(&deq, &mut out, tx, tx_type, 8); + let err: i32 = res.iter().zip(&out).map(|(a, b)| (a - b).abs()).max().unwrap(); + assert!(err <= 1, "tx {tx} type {tx_type}: max error {err}"); + } + } + } +} diff --git a/crates/av1enc/src/writer.rs b/crates/av1enc/src/writer.rs new file mode 100644 index 00000000..903492b1 --- /dev/null +++ b/crates/av1enc/src/writer.rs @@ -0,0 +1,1020 @@ +//! Tile syntax writer (spec 5.11 with the 8.3 CDF selection). It keeps the same per-4x4 mode +//! info, coefficient contexts and adaptive CDFs as a decoder and writes the decisions taken by +//! `decide.rs` as symbols. The context derivations and the motion vector prediction process +//! (7.10.2) mirror the decoder exactly, since symbols and coded motion vector differences +//! depend on them. + +use crate::cdf::Cdfs; +use crate::decide::{FrameGeom, Leaf, Node, intra_uv_tx_type, scan_for, tx_in_set, tx_set, tx_size_for}; +use crate::loopfilter::LfInfo; +use crate::symbol::SymbolWriter; +use crate::tables::*; + +type MvP = [i32; 2]; + +/// Mode info per 4x4 (MiSizes, YModes, RefFrames, Mvs, ...). +struct Mi { + cols: usize, + mi_size: Vec, + y_mode: Vec, + ref_frame: Vec<[i8; 2]>, + mv: Vec, + skip: Vec, + is_inter: Vec, + tx_type: Vec, + written: Vec, +} + +impl Mi { + #[inline(always)] + fn idx(&self, r: usize, c: usize) -> usize { + r * self.cols + c + } +} + +/// Output of find_mv_stack. +#[derive(Default)] +struct MvStack { + ref_stack_mv: [MvP; MAX_REF_MV_STACK_SIZE + 1], + weight_stack: [u32; MAX_REF_MV_STACK_SIZE + 1], + num_mv_found: usize, + new_mv_count: usize, + found_match: bool, + close_matches: usize, + total_matches: usize, + new_mv_context: usize, + ref_mv_context: usize, + zero_mv_context: usize, + drl_ctx_stack: [usize; MAX_REF_MV_STACK_SIZE + 1], +} + +pub(crate) struct TileWriter<'a> { + g: &'a FrameGeom, + intra_only: bool, + qidx: u8, + sw: SymbolWriter, + cdf: Box, + kf_y_mode: [[[u16; 14]; 5]; 5], + mi: Mi, + above_level: [Vec; 3], + above_dc: [Vec; 3], + left_level: [Vec; 3], + left_dc: [Vec; 3], + /// LoopfilterTxSizes per plane (row stride mi_cols + 32). + lf_tx: [Vec; 3], + // current block + r: usize, + c: usize, + bsize: usize, + avail_u: bool, + avail_l: bool, + mvs: MvStack, +} + +fn block_size_px(bs: usize) -> usize { + 4 * NUM_4X4_BLOCKS_WIDE[bs] as usize +} + +fn is_directional(mode: usize) -> bool { + (V_PRED..=D67_PRED).contains(&mode) +} + +impl<'a> TileWriter<'a> { + /// `init`: the CDFs saved by the primary reference frame (load_cdfs), or None for the + /// defaults (init_non_coeff_cdfs / init_coeff_cdfs). + pub fn new(g: &'a FrameGeom, intra_only: bool, qidx: u8, init: Option<&Cdfs>) -> Self { + let n = g.mi_cols * g.mi_rows; + TileWriter { + g, + intra_only, + qidx, + sw: SymbolWriter::new(false), + cdf: match init { + Some(c) => { + let mut c = Box::new(c.clone()); + c.clear_counts(); + c + } + None => Cdfs::new(qidx as u32), + }, + kf_y_mode: DEFAULT_INTRA_FRAME_Y_MODE_CDF, + mi: Mi { + cols: g.mi_cols, + mi_size: vec![0; n], + y_mode: vec![0; n], + ref_frame: vec![[0, -1]; n], + mv: vec![[0, 0]; n], + skip: vec![false; n], + is_inter: vec![false; n], + tx_type: vec![0; n], + written: vec![false; n], + }, + above_level: std::array::from_fn(|_| vec![0; g.mi_cols + 64]), + above_dc: std::array::from_fn(|_| vec![0; g.mi_cols + 64]), + left_level: std::array::from_fn(|_| vec![0; g.mi_rows + 64]), + left_dc: std::array::from_fn(|_| vec![0; g.mi_rows + 64]), + lf_tx: std::array::from_fn(|_| vec![0; (g.mi_cols + 32) * (g.mi_rows + 32)]), + r: 0, + c: 0, + bsize: 0, + avail_u: false, + avail_l: false, + mvs: MvStack::default(), + } + } + + /// The tile data and the final CDFs (saved for the frame-end CDF update). + pub fn finish(self) -> (Vec, Box) { + (self.sw.finish(), self.cdf) + } + + /// The mode info the loop filter needs. + pub fn lf_info(&self) -> LfInfo<'_> { + LfInfo { + mi_cols: self.g.mi_cols, + mi_rows: self.g.mi_rows, + width: self.g.width, + height: self.g.height, + mi_size: &self.mi.mi_size, + skip: &self.mi.skip, + is_inter: &self.mi.is_inter, + tx: [&self.lf_tx[0], &self.lf_tx[1], &self.lf_tx[2]], + } + } + + /// Start of a superblock row (clear_left_context). + pub fn start_sb_row(&mut self) { + for p in 0..3 { + self.left_level[p].iter_mut().for_each(|v| *v = 0); + self.left_dc[p].iter_mut().for_each(|v| *v = 0); + } + } + + pub fn superblock(&mut self, r: usize, c: usize, node: &Node) { + self.partition(r, c, BLOCK_64X64, node); + } + + fn partition(&mut self, r: usize, c: usize, bsize: usize, node: &Node) { + let (mi_rows, mi_cols) = (self.g.mi_rows, self.g.mi_cols); + if r >= mi_rows || c >= mi_cols { + return; + } + let avail_u = r > 0; + let avail_l = c > 0; + let n4 = NUM_4X4_BLOCKS_WIDE[bsize] as usize; + let half = n4 >> 1; + let has_rows = r + half < mi_rows; + let has_cols = c + half < mi_cols; + let split = matches!(node, Node::Split(_)); + let bsl = MI_WIDTH_LOG2[bsize] as usize; + let above = avail_u && (MI_WIDTH_LOG2[self.mi.mi_size[self.mi.idx(r - 1, c)] as usize] as usize) < bsl; + let left = avail_l && (MI_HEIGHT_LOG2[self.mi.mi_size[self.mi.idx(r, c - 1)] as usize] as usize) < bsl; + let ctx = left as usize * 2 + above as usize; + let partition = if split { PARTITION_SPLIT } else { PARTITION_NONE }; + if has_rows && has_cols { + let cdf: &mut [u16] = match bsl { + 1 => &mut self.cdf.partition_w8[ctx], + 2 => &mut self.cdf.partition_w16[ctx], + 3 => &mut self.cdf.partition_w32[ctx], + _ => &mut self.cdf.partition_w64[ctx], + }; + self.sw.symbol(partition, cdf); + } else if has_cols || has_rows { + assert!(split, "blocks crossing the frame edge are split"); + let pcdf: Vec = match bsl { + 2 => self.cdf.partition_w16[ctx].to_vec(), + 3 => self.cdf.partition_w32[ctx].to_vec(), + _ => self.cdf.partition_w64[ctx].to_vec(), + }; + let pr = |k: usize| pcdf[k] as i32 - if k == 0 { 0 } else { pcdf[k - 1] as i32 }; + let psum = if has_cols { + // split_or_horz + pr(PARTITION_VERT) + pr(PARTITION_SPLIT) + pr(PARTITION_HORZ_A) + pr(PARTITION_VERT_A) + pr(PARTITION_VERT_B) + pr(PARTITION_VERT_4) + } else { + // split_or_vert + pr(PARTITION_HORZ) + pr(PARTITION_SPLIT) + pr(PARTITION_HORZ_A) + pr(PARTITION_HORZ_B) + pr(PARTITION_VERT_A) + pr(PARTITION_HORZ_4) + }; + let cdf = [((1i32 << 15) - psum) as u16, 1 << 15, 0]; + self.sw.symbol_fixed(1, &cdf); + } else { + assert!(split); + } + match node { + Node::Split(ch) => { + let sub = PARTITION_SUBSIZE[PARTITION_SPLIT][bsize] as usize; + self.partition(r, c, sub, &ch[0]); + self.partition(r, c + half, sub, &ch[1]); + self.partition(r + half, c, sub, &ch[2]); + self.partition(r + half, c + half, sub, &ch[3]); + } + Node::Leaf(leaf) => { + debug_assert_eq!(leaf.bsize, bsize); + self.block(r, c, leaf); + } + Node::Out => unreachable!("decided block inside the frame"), + } + } + + fn block(&mut self, r: usize, c: usize, b: &Leaf) { + self.r = r; + self.c = c; + self.bsize = b.bsize; + self.avail_u = r > 0; + self.avail_l = c > 0; + let bsize = b.bsize; + let n4 = NUM_4X4_BLOCKS_WIDE[bsize] as usize; + // skip + let mut ctx = 0; + if self.avail_u { + ctx += self.mi.skip[self.mi.idx(r - 1, c)] as usize; + } + if self.avail_l { + ctx += self.mi.skip[self.mi.idx(r, c - 1)] as usize; + } + let mut y_mode = b.y_mode; + if self.intra_only { + self.sw.symbol(b.skip as usize, &mut self.cdf.skip[ctx]); + let above = if self.avail_u { self.mi.y_mode[self.mi.idx(r - 1, c)] as usize } else { DC_PRED }; + let left = if self.avail_l { self.mi.y_mode[self.mi.idx(r, c - 1)] as usize } else { DC_PRED }; + let actx = INTRA_MODE_CONTEXT[above] as usize; + let lctx = INTRA_MODE_CONTEXT[left] as usize; + self.sw.symbol(b.y_mode, &mut self.kf_y_mode[actx][lctx]); + self.intra_angle_and_uv(b); + } else { + self.sw.symbol(b.skip as usize, &mut self.cdf.skip[ctx]); + // is_inter + let left = if self.avail_l { self.mi.ref_frame[self.mi.idx(r, c - 1)] } else { [INTRA_FRAME as i8, -1] }; + let above = if self.avail_u { self.mi.ref_frame[self.mi.idx(r - 1, c)] } else { [INTRA_FRAME as i8, -1] }; + let left_intra = left[0] <= INTRA_FRAME as i8; + let above_intra = above[0] <= INTRA_FRAME as i8; + let ctx = if self.avail_u && self.avail_l { + if left_intra && above_intra { 3 } else { (left_intra || above_intra) as usize } + } else if self.avail_u || self.avail_l { + 2 * (if self.avail_u { above_intra } else { left_intra }) as usize + } else { + 0 + }; + self.sw.symbol(b.is_inter as usize, &mut self.cdf.is_inter[ctx]); + if b.is_inter { + y_mode = self.inter_block_mode_info(b, left, above); + } else { + let ctx = SIZE_GROUP[bsize] as usize; + self.sw.symbol(b.y_mode, &mut self.cdf.y_mode[ctx]); + self.intra_angle_and_uv(b); + } + } + // mode info stored before the residual + let (rows, cols) = (n4.min(self.g.mi_rows - r), n4.min(self.g.mi_cols - c)); + for y in 0..rows { + for x in 0..cols { + let i = self.mi.idx(r + y, c + x); + self.mi.y_mode[i] = y_mode as u8; + self.mi.ref_frame[i] = if b.is_inter { [LAST_FRAME as i8, -1] } else { [INTRA_FRAME as i8, -1] }; + self.mi.written[i] = true; + if b.is_inter { + self.mi.mv[i] = b.mv; + } + } + } + if b.skip { + // reset_block_context( ) + for plane in 0..3 { + let s = (plane > 0) as usize; + for i in (c >> s)..((c + n4) >> s) { + self.above_level[plane][i] = 0; + self.above_dc[plane][i] = 0; + } + for i in (r >> s)..((r + n4) >> s) { + self.left_level[plane][i] = 0; + self.left_dc[plane][i] = 0; + } + } + } else { + let log2 = (n4 * 4).trailing_zeros(); + assert!(log2 <= 5, "64x64 blocks are coded as skip blocks"); + for plane in 0..3 { + let s = (plane > 0) as u32; + self.coeffs(plane, (c * 4) >> s, (r * 4) >> s, tx_size_for(log2 - s), b, &b.coefs[plane]); + } + } + for y in 0..rows { + for x in 0..cols { + let i = self.mi.idx(r + y, c + x); + self.mi.is_inter[i] = b.is_inter; + self.mi.skip[i] = b.skip; + self.mi.mi_size[i] = bsize as u8; + } + } + // LoopfilterTxSizes: one transform block per plane + let log2 = (n4 * 4).trailing_zeros(); + let stride = self.g.mi_cols + 32; + for plane in 0..3 { + let s = (plane > 0) as usize; + let tx = tx_size_for(log2 - s as u32) as u8; + let (r0, c0, m) = (r >> s, c >> s, n4 >> s); + for y in r0..r0 + m { + self.lf_tx[plane][y * stride + c0..y * stride + c0 + m].iter_mut().for_each(|v| *v = tx); + } + } + } + + fn intra_angle_and_uv(&mut self, b: &Leaf) { + if self.bsize >= BLOCK_8X8 && is_directional(b.y_mode) { + self.sw.symbol(MAX_ANGLE_DELTA, &mut self.cdf.angle_delta[b.y_mode - V_PRED]); + } + let bs = block_size_px(self.bsize); + if bs <= 32 { + self.sw.symbol(b.uv_mode, &mut self.cdf.uv_mode_cfl_allowed[b.y_mode]); + } else { + self.sw.symbol(b.uv_mode, &mut self.cdf.uv_mode_cfl_not_allowed[b.y_mode]); + } + if self.bsize >= BLOCK_8X8 && is_directional(b.uv_mode) { + self.sw.symbol(MAX_ANGLE_DELTA, &mut self.cdf.angle_delta[b.uv_mode - V_PRED]); + } + } + + /// read_ref_frames (LAST_FRAME), find_mv_stack, the inter mode, DRL and motion vector. + /// Returns the inter mode (YMode). + fn inter_block_mode_info(&mut self, b: &Leaf, left: [i8; 2], above: [i8; 2]) -> usize { + let count = |ft: i8| -> usize { + let mut n = 0; + if self.avail_u { + n += (above[0] == ft) as usize + (above[1] == ft) as usize; + } + if self.avail_l { + n += (left[0] == ft) as usize + (left[1] == ft) as usize; + } + n + }; + let cmp = |a: usize, b: usize| match a.cmp(&b) { + std::cmp::Ordering::Less => 0, + std::cmp::Ordering::Equal => 1, + std::cmp::Ordering::Greater => 2, + }; + let (last, last2, last3, gold) = (count(LAST_FRAME as i8), count(LAST2_FRAME as i8), count(LAST3_FRAME as i8), count(GOLDEN_FRAME as i8)); + let (bwd, alt2, alt) = (count(BWDREF_FRAME as i8), count(ALTREF2_FRAME as i8), count(ALTREF_FRAME as i8)); + let ctx_p1 = cmp(last + last2 + last3 + gold, bwd + alt2 + alt); + let ctx_comp_ref = cmp(last + last2, last3 + gold); + let ctx_comp_ref_p1 = cmp(last, last2); + self.sw.symbol(0, &mut self.cdf.single_ref[ctx_p1][0]); + self.sw.symbol(0, &mut self.cdf.single_ref[ctx_comp_ref][2]); + self.sw.symbol(0, &mut self.cdf.single_ref[ctx_comp_ref_p1][3]); + self.find_mv_stack(); + let mv = b.mv; + let s = &self.mvs; + let mode = if mv == [0, 0] { + GLOBALMV + } else if mv == s.ref_stack_mv[0] { + NEARESTMV + } else if mv == s.ref_stack_mv[1] { + NEARMV + } else { + NEWMV + }; + let (nctx, zctx, rctx) = (s.new_mv_context, s.zero_mv_context, s.ref_mv_context); + self.sw.symbol((mode != NEWMV) as usize, &mut self.cdf.new_mv[nctx]); + if mode != NEWMV { + self.sw.symbol((mode != GLOBALMV) as usize, &mut self.cdf.zero_mv[zctx]); + if mode != GLOBALMV { + self.sw.symbol((mode == NEARMV) as usize, &mut self.cdf.ref_mv[rctx]); + } + } + // DRL: ref_mv_idx 0 for NEWMV, 1 for NEARMV + if mode == NEWMV { + if self.mvs.num_mv_found > 1 { + let ctx = self.mvs.drl_ctx_stack[0]; + self.sw.symbol(0, &mut self.cdf.drl_mode[ctx]); + } + } else if mode == NEARMV && self.mvs.num_mv_found > 2 { + let ctx = self.mvs.drl_ctx_stack[1]; + self.sw.symbol(0, &mut self.cdf.drl_mode[ctx]); + } + if mode == NEWMV { + let pred = self.mvs.ref_stack_mv[0]; + self.write_mv([mv[0] - pred[0], mv[1] - pred[1]]); + } + mode + } + + fn write_mv(&mut self, diff: MvP) { + let joint = ((diff[0] != 0) as usize) << 1 | (diff[1] != 0) as usize; + // MV_JOINT_ZERO, MV_JOINT_HNZVZ (col only), MV_JOINT_HZVNZ (row only), MV_JOINT_HNZVNZ + self.sw.symbol(joint, &mut self.cdf.mv_joint); + for (comp, &d) in diff.iter().enumerate() { + if d != 0 { + self.write_mv_component(comp, d); + } + } + } + + fn write_mv_component(&mut self, comp: usize, v: i32) { + debug_assert!(v % 2 == 0, "quarter-sample precision"); + self.sw.symbol((v < 0) as usize, &mut self.cdf.mv_sign[comp]); + let m = v.unsigned_abs() - 1; // odd: the implied hp bit is 1 + let fr = ((m >> 1) & 3) as usize; + let int = m >> 3; + if int < CLASS0_SIZE as u32 { + self.sw.symbol(MV_CLASS_0, &mut self.cdf.mv_class[comp]); + self.sw.symbol(int as usize, &mut self.cdf.mv_class0_bit[comp]); + self.sw.symbol(fr, &mut self.cdf.mv_class0_fr[comp][int as usize]); + } else { + let class = 31 - int.leading_zeros(); + let d = int - (1 << class); + self.sw.symbol(class as usize, &mut self.cdf.mv_class[comp]); + for i in 0..class as usize { + self.sw.symbol(((d >> i) & 1) as usize, &mut self.cdf.mv_bit[comp][i]); + } + self.sw.symbol(fr, &mut self.cdf.mv_fr[comp]); + } + } + + // ---- coefficients ---------------------------------------------------------------------- + + fn coeffs(&mut self, plane: usize, start_x: usize, start_y: usize, tx_sz: usize, b: &Leaf, levels: &[i32]) { + let x4 = start_x >> 2; + let y4 = start_y >> 2; + let w4 = TX_WIDTH[tx_sz] as usize >> 2; + let h4 = TX_HEIGHT[tx_sz] as usize >> 2; + let tx_sz_ctx = (TX_SIZE_SQR[tx_sz] as usize + TX_SIZE_SQR_UP[tx_sz] as usize + 1) >> 1; + let ptype = (plane > 0) as usize; + let scan = scan_for(tx_sz); + let mut eob = 0; + for (i, &pos) in scan.iter().enumerate() { + if levels[pos as usize] != 0 { + eob = i + 1; + } + } + let ctx = self.all_zero_ctx(plane, tx_sz, x4, y4, w4, h4); + self.sw.symbol((eob == 0) as usize, &mut self.cdf.txb_skip[tx_sz_ctx][ctx]); + let mut cul_level = 0u32; + let mut dc_category = 0u8; + if eob == 0 { + if plane == 0 { + self.set_tx_types(x4, y4, tx_sz, DCT_DCT); + } + } else { + if plane == 0 { + // transform_type( ) + let set = tx_set(tx_sz, b.is_inter); + let t = b.tx_type_y; + if set > 0 && self.qidx > 0 { + let sqr = TX_SIZE_SQR[tx_sz] as usize; + if b.is_inter { + let s = TX_TYPE_INTER_INV_SET3.iter().position(|&v| v as usize == t).expect("inter tx type in set"); + self.sw.symbol(s, &mut self.cdf.inter_tx_type_set3[sqr]); + } else { + let s = TX_TYPE_INTRA_INV_SET2.iter().position(|&v| v as usize == t).expect("intra tx type in set"); + self.sw.symbol(s, &mut self.cdf.intra_tx_type_set2[sqr][b.y_mode]); + } + } else { + assert_eq!(t, DCT_DCT); + } + self.set_tx_types(x4, y4, tx_sz, t); + } + let plane_tx_type = self.compute_tx_type(plane, tx_sz, x4, y4, b); + debug_assert!(plane_tx_type == DCT_DCT || matches!(plane_tx_type, ADST_DCT | DCT_ADST | ADST_ADST)); + let eob_multisize = (TX_WIDTH_LOG2[tx_sz] as usize).min(5) + (TX_HEIGHT_LOG2[tx_sz] as usize).min(5) - 4; + let eob_pt = if eob < 2 { eob } else { (32 - ((eob - 1) as u32).leading_zeros()) as usize + 1 }; + let ectx = 0; // TX_CLASS_2D + let s = eob_pt - 1; + match eob_multisize { + 0 => self.sw.symbol(s, &mut self.cdf.eob_pt_16[ptype][ectx]), + 1 => self.sw.symbol(s, &mut self.cdf.eob_pt_32[ptype][ectx]), + 2 => self.sw.symbol(s, &mut self.cdf.eob_pt_64[ptype][ectx]), + 3 => self.sw.symbol(s, &mut self.cdf.eob_pt_128[ptype][ectx]), + 4 => self.sw.symbol(s, &mut self.cdf.eob_pt_256[ptype][ectx]), + 5 => self.sw.symbol(s, &mut self.cdf.eob_pt_512[ptype]), + _ => self.sw.symbol(s, &mut self.cdf.eob_pt_1024[ptype]), + } + if eob_pt >= 3 { + let eob_shift = eob_pt - 3; + let offset = eob - ((1 << (eob_pt - 2)) + 1); + self.sw.symbol((offset >> eob_shift) & 1, &mut self.cdf.eob_extra[tx_sz_ctx][ptype][eob_pt - 3]); + for i in 1..eob_pt - 2 { + let sh = eob_pt - 2 - 1 - i; + self.sw.bool((offset >> sh) & 1 == 1); + } + } + let bwl = TX_WIDTH_LOG2[tx_sz] as usize; + let height = TX_HEIGHT[tx_sz] as usize; + let width = 1usize << bwl; + // Quant[] as the decoder sees it while parsing: the levels coded so far (capped) + let mut quant = vec![0i32; width * height]; + for c in (0..eob).rev() { + let pos = scan[c] as usize; + let a = levels[pos].unsigned_abs().min(15) as i32; + if c == eob - 1 { + let ctx = coeff_base_eob_ctx(c, bwl, height); + self.sw.symbol((a.min(3) - 1) as usize, &mut self.cdf.coeff_base_eob[tx_sz_ctx][ptype][ctx]); + } else { + let ctx = coeff_base_ctx(&quant, tx_sz, bwl, width, height, pos); + self.sw.symbol(a.min(3) as usize, &mut self.cdf.coeff_base[tx_sz_ctx][ptype][ctx]); + } + if a > NUM_BASE_LEVELS as i32 { + let ctx = coeff_br_ctx(&quant, bwl, height, pos); + let mut rem = a - 3; + for _ in 0..(COEFF_BASE_RANGE / (BR_CDF_SIZE - 1)) { + let br = rem.min(3); + self.sw.symbol(br as usize, &mut self.cdf.coeff_br[tx_sz_ctx.min(TX_32X32)][ptype][ctx]); + rem -= br; + if br < 3 { + break; + } + } + } + quant[pos] = a; + } + for c in 0..eob { + let pos = scan[c] as usize; + let v = levels[pos]; + if v != 0 { + if c == 0 { + let ctx = self.dc_sign_ctx(plane, x4, y4, w4, h4); + self.sw.symbol((v < 0) as usize, &mut self.cdf.dc_sign[ptype][ctx]); + } else { + self.sw.bool(v < 0); + } + } + let a = v.unsigned_abs(); + if a > (NUM_BASE_LEVELS + COEFF_BASE_RANGE) as u32 { + // Golomb code of a - 15 + let x = a - 15 + 1; + let length = 32 - x.leading_zeros(); + for _ in 0..length - 1 { + self.sw.bool(false); + } + self.sw.bool(true); + for i in (0..length - 1).rev() { + self.sw.bool((x >> i) & 1 == 1); + } + } + if pos == 0 && a > 0 { + dc_category = if v < 0 { 1 } else { 2 }; + } + cul_level += a & 0xFFFFF; + } + cul_level = cul_level.min(63); + } + for i in 0..w4 { + if x4 + i < self.above_level[plane].len() { + self.above_level[plane][x4 + i] = cul_level as u8; + self.above_dc[plane][x4 + i] = dc_category; + } + } + for i in 0..h4 { + if y4 + i < self.left_level[plane].len() { + self.left_level[plane][y4 + i] = cul_level as u8; + self.left_dc[plane][y4 + i] = dc_category; + } + } + } + + fn set_tx_types(&mut self, x4: usize, y4: usize, tx_sz: usize, t: usize) { + for i in 0..(TX_WIDTH[tx_sz] as usize >> 2) { + for j in 0..(TX_HEIGHT[tx_sz] as usize >> 2) { + if y4 + j < self.g.mi_rows && x4 + i < self.g.mi_cols { + let k = self.mi.idx(y4 + j, x4 + i); + self.mi.tx_type[k] = t as u8; + } + } + } + } + + fn compute_tx_type(&self, plane: usize, tx_sz: usize, x4: usize, y4: usize, b: &Leaf) -> usize { + if TX_SIZE_SQR_UP[tx_sz] as usize > TX_32X32 { + return DCT_DCT; + } + if plane == 0 { + return self.mi.tx_type[self.mi.idx(y4, x4)] as usize; + } + if b.is_inter { + let set = tx_set(tx_sz, true); + let xx = self.c.max(x4 << 1); + let yy = self.r.max(y4 << 1); + let t = self.mi.tx_type[self.mi.idx(yy, xx)] as usize; + return if tx_in_set(set, true, t) { t } else { DCT_DCT }; + } + intra_uv_tx_type(b.uv_mode, tx_sz) + } + + fn all_zero_ctx(&self, plane: usize, tx_sz: usize, x4: usize, y4: usize, w4: usize, h4: usize) -> usize { + let s = (plane > 0) as usize; + let max_x4 = self.g.mi_cols >> s; + let max_y4 = self.g.mi_rows >> s; + let w = TX_WIDTH[tx_sz] as usize; + let h = TX_HEIGHT[tx_sz] as usize; + let bsize = SUBSAMPLED_SIZE[self.bsize][s][s] as usize; + let bw = block_size_px(bsize); + let bh = 4 * NUM_4X4_BLOCKS_HIGH[bsize] as usize; + let al = &self.above_level[plane]; + let ll = &self.left_level[plane]; + if plane == 0 { + let mut top = 0u32; + let mut left = 0u32; + for k in 0..w4 { + if x4 + k < max_x4 { + top = top.max(al[x4 + k] as u32); + } + } + for k in 0..h4 { + if y4 + k < max_y4 { + left = left.max(ll[y4 + k] as u32); + } + } + top = top.min(255); + left = left.min(255); + if bw == w && bh == h { + 0 + } else if top == 0 && left == 0 { + 1 + } else if top == 0 || left == 0 { + 2 + (top.max(left) > 3) as usize + } else if top.max(left) <= 3 { + 4 + } else if top.min(left) <= 3 { + 5 + } else { + 6 + } + } else { + let ad = &self.above_dc[plane]; + let ld = &self.left_dc[plane]; + let mut above = 0u8; + let mut left = 0u8; + for i in 0..w4 { + if x4 + i < max_x4 { + above |= al[x4 + i] | ad[x4 + i]; + } + } + for i in 0..h4 { + if y4 + i < max_y4 { + left |= ll[y4 + i] | ld[y4 + i]; + } + } + let mut ctx = (above != 0) as usize + (left != 0) as usize + 7; + if bw * bh > w * h { + ctx += 3; + } + ctx + } + } + + fn dc_sign_ctx(&self, plane: usize, x4: usize, y4: usize, w4: usize, h4: usize) -> usize { + let s = (plane > 0) as usize; + let max_x4 = self.g.mi_cols >> s; + let max_y4 = self.g.mi_rows >> s; + let mut dc_sign = 0i32; + for k in 0..w4 { + if x4 + k < max_x4 { + match self.above_dc[plane][x4 + k] { + 1 => dc_sign -= 1, + 2 => dc_sign += 1, + _ => {} + } + } + } + for k in 0..h4 { + if y4 + k < max_y4 { + match self.left_dc[plane][y4 + k] { + 1 => dc_sign -= 1, + 2 => dc_sign += 1, + _ => {} + } + } + } + match dc_sign.signum() { + -1 => 1, + 1 => 2, + _ => 0, + } + } + + // ---- motion vector prediction (7.10.2), single reference LAST_FRAME -------------------- + + fn inside(&self, r: isize, c: isize) -> bool { + c >= 0 && c < self.g.mi_cols as isize && r >= 0 && r < self.g.mi_rows as isize + } + + fn find_mv_stack(&mut self) { + let n4 = NUM_4X4_BLOCKS_WIDE[self.bsize] as isize; + let (bw4, bh4) = (n4, n4); + self.mvs = MvStack::default(); + self.scan_row(-1); + let mut found_above = self.mvs.found_match; + self.mvs.found_match = false; + self.scan_col(-1); + let mut found_left = self.mvs.found_match; + self.mvs.found_match = false; + if bw4.max(bh4) <= 16 { + self.scan_point(-1, bw4); + } + if self.mvs.found_match { + found_above = true; + } + self.mvs.close_matches = found_above as usize + found_left as usize; + let num_nearest = self.mvs.num_mv_found; + let num_new = self.mvs.new_mv_count; + for idx in 0..num_nearest { + self.mvs.weight_stack[idx] += REF_CAT_LEVEL as u32; + } + self.mvs.zero_mv_context = 0; + // use_ref_frame_mvs = 0: no temporal candidates + self.scan_point(-1, -1); + if self.mvs.found_match { + found_above = true; + } + self.mvs.found_match = false; + self.scan_row(-3); + if self.mvs.found_match { + found_above = true; + } + self.mvs.found_match = false; + self.scan_col(-3); + if self.mvs.found_match { + found_left = true; + } + self.mvs.found_match = false; + if bh4 > 1 { + self.scan_row(-5); + } + if self.mvs.found_match { + found_above = true; + } + self.mvs.found_match = false; + if bw4 > 1 { + self.scan_col(-5); + } + if self.mvs.found_match { + found_left = true; + } + self.mvs.total_matches = found_above as usize + found_left as usize; + let n = self.mvs.num_mv_found; + self.sort_stack(0, num_nearest); + self.sort_stack(num_nearest, n); + if self.mvs.num_mv_found < 2 { + self.extra_search(); + } + self.context_and_clamping(num_new); + } + + fn scan_row(&mut self, delta_row: isize) { + let bw4 = NUM_4X4_BLOCKS_WIDE[self.bsize] as isize; + let (mi_row, mi_col) = (self.r as isize, self.c as isize); + let end4 = bw4.min(self.g.mi_cols as isize - mi_col).min(16); + let mut delta_col = 0; + let use_step16 = bw4 >= 16; + let mut delta_row = delta_row; + if delta_row.abs() > 1 { + delta_row += mi_row & 1; + delta_col = 1 - (mi_col & 1); + } + let mut i = 0; + while i < end4 { + let mv_row = mi_row + delta_row; + let mv_col = mi_col + delta_col + i; + if !self.inside(mv_row, mv_col) { + break; + } + let k = self.mi.idx(mv_row as usize, mv_col as usize); + let mut len = bw4.min(NUM_4X4_BLOCKS_WIDE[self.mi.mi_size[k] as usize] as isize); + if delta_row.abs() > 1 { + len = len.max(2); + } + if use_step16 { + len = len.max(4); + } + self.add_ref_mv_candidate(mv_row as usize, mv_col as usize, len as u32 * 2); + i += len; + } + } + + fn scan_col(&mut self, delta_col: isize) { + let bh4 = NUM_4X4_BLOCKS_HIGH[self.bsize] as isize; + let (mi_row, mi_col) = (self.r as isize, self.c as isize); + let end4 = bh4.min(self.g.mi_rows as isize - mi_row).min(16); + let mut delta_row = 0; + let use_step16 = bh4 >= 16; + let mut delta_col = delta_col; + if delta_col.abs() > 1 { + delta_row = 1 - (mi_row & 1); + delta_col += mi_col & 1; + } + let mut i = 0; + while i < end4 { + let mv_row = mi_row + delta_row + i; + let mv_col = mi_col + delta_col; + if !self.inside(mv_row, mv_col) { + break; + } + let k = self.mi.idx(mv_row as usize, mv_col as usize); + let mut len = bh4.min(NUM_4X4_BLOCKS_HIGH[self.mi.mi_size[k] as usize] as isize); + if delta_col.abs() > 1 { + len = len.max(2); + } + if use_step16 { + len = len.max(4); + } + self.add_ref_mv_candidate(mv_row as usize, mv_col as usize, len as u32 * 2); + i += len; + } + } + + fn scan_point(&mut self, delta_row: isize, delta_col: isize) { + let mv_row = self.r as isize + delta_row; + let mv_col = self.c as isize + delta_col; + if self.inside(mv_row, mv_col) && self.mi.written[self.mi.idx(mv_row as usize, mv_col as usize)] { + self.add_ref_mv_candidate(mv_row as usize, mv_col as usize, 4); + } + } + + fn add_ref_mv_candidate(&mut self, mv_row: usize, mv_col: usize, weight: u32) { + let i = self.mi.idx(mv_row, mv_col); + if !self.mi.is_inter[i] { + return; + } + let rf = self.mi.ref_frame[i]; + for cand_list in 0..2 { + if rf[cand_list] == LAST_FRAME as i8 { + self.search_stack(i, cand_list, weight); + } + } + } + + fn search_stack(&mut self, i: usize, cand_list: usize, weight: u32) { + debug_assert_eq!(cand_list, 0); + let cand_mode = self.mi.y_mode[i] as usize; + // gm_type is IDENTITY: the stored motion vector is used + let mut cand = self.mi.mv[i]; + lower_mv_precision(&mut cand); + if cand_mode == NEWMV { + self.mvs.new_mv_count += 1; + } + self.mvs.found_match = true; + let n = self.mvs.num_mv_found; + if let Some(idx) = (0..n).find(|&k| self.mvs.ref_stack_mv[k] == cand) { + self.mvs.weight_stack[idx] += weight; + } else if n < MAX_REF_MV_STACK_SIZE { + self.mvs.ref_stack_mv[n] = cand; + self.mvs.weight_stack[n] = weight; + self.mvs.num_mv_found += 1; + } + } + + fn sort_stack(&mut self, start: usize, end: usize) { + let mut end = end; + let s = &mut self.mvs; + while end > start { + let mut new_end = start; + for idx in start + 1..end { + if s.weight_stack[idx - 1] < s.weight_stack[idx] { + s.weight_stack.swap(idx - 1, idx); + s.ref_stack_mv.swap(idx - 1, idx); + new_end = idx; + } + } + end = new_end; + } + } + + fn extra_search(&mut self) { + let (mi_row, mi_col) = (self.r as isize, self.c as isize); + let n4 = NUM_4X4_BLOCKS_WIDE[self.bsize] as isize; + let w4 = n4.min(16).min(self.g.mi_cols as isize - mi_col); + let h4 = n4.min(16).min(self.g.mi_rows as isize - mi_row); + let num4x4 = w4.min(h4); + for pass in 0..2 { + let mut idx = 0; + while idx < num4x4 && self.mvs.num_mv_found < 2 { + let (mv_row, mv_col) = if pass == 0 { (mi_row - 1, mi_col + idx) } else { (mi_row + idx, mi_col - 1) }; + if !self.inside(mv_row, mv_col) { + break; + } + let k = self.mi.idx(mv_row as usize, mv_col as usize); + // add_extra_mv_candidate( ) (no sign bias: a single forward reference) + let rfs = self.mi.ref_frame[k]; + for &cand_ref in rfs.iter().take(2) { + if cand_ref > INTRA_FRAME as i8 { + let cand = self.mi.mv[k]; + let n = self.mvs.num_mv_found; + if !(0..n).any(|j| self.mvs.ref_stack_mv[j] == cand) { + self.mvs.ref_stack_mv[n] = cand; + self.mvs.weight_stack[n] = 2; + self.mvs.num_mv_found += 1; + } + } + } + let sz = self.mi.mi_size[k] as usize; + idx += if pass == 0 { NUM_4X4_BLOCKS_WIDE[sz] } else { NUM_4X4_BLOCKS_HIGH[sz] } as isize; + } + } + for idx in self.mvs.num_mv_found..2 { + self.mvs.ref_stack_mv[idx] = [0, 0]; // GlobalMvs[ 0 ] + } + } + + fn context_and_clamping(&mut self, num_new: usize) { + let bs = block_size_px(self.bsize) as i32; + let n = self.mvs.num_mv_found; + for idx in 0..n { + let mut z = 0; + if idx + 1 < n { + let w0 = self.mvs.weight_stack[idx]; + let w1 = self.mvs.weight_stack[idx + 1]; + if w0 >= REF_CAT_LEVEL as u32 { + if w1 < REF_CAT_LEVEL as u32 { + z = 1; + } + } else { + z = 2; + } + } + self.mvs.drl_ctx_stack[idx] = z; + } + let n4 = NUM_4X4_BLOCKS_WIDE[self.bsize] as i32; + for idx in 0..n { + let mut mv = self.mvs.ref_stack_mv[idx]; + let border_r = MV_BORDER as i32 + bs * 8; + let to_top = -((self.r as i32 * 4) * 8); + let to_bottom = ((self.g.mi_rows as i32 - n4 - self.r as i32) * 4) * 8; + mv[0] = mv[0].clamp(to_top - border_r, to_bottom + border_r); + let to_left = -((self.c as i32 * 4) * 8); + let to_right = ((self.g.mi_cols as i32 - n4 - self.c as i32) * 4) * 8; + mv[1] = mv[1].clamp(to_left - border_r, to_right + border_r); + self.mvs.ref_stack_mv[idx] = mv; + } + let (close, total) = (self.mvs.close_matches, self.mvs.total_matches); + if close == 0 { + self.mvs.new_mv_context = total.min(1); + self.mvs.ref_mv_context = total; + } else if close == 1 { + self.mvs.new_mv_context = 3 - num_new.min(1); + self.mvs.ref_mv_context = 2 + total; + } else { + self.mvs.new_mv_context = 5 - num_new.min(1); + self.mvs.ref_mv_context = 5; + } + } +} + +/// lower_mv_precision( ) with allow_high_precision_mv = 0 and force_integer_mv = 0. +fn lower_mv_precision(mv: &mut MvP) { + for c in mv.iter_mut() { + if *c & 1 != 0 { + *c += if *c > 0 { -1 } else { 1 }; + } + } +} + +fn coeff_base_eob_ctx(c: usize, bwl: usize, height: usize) -> usize { + if c == 0 { + 0 + } else if c <= (height << bwl) / 8 { + 1 + } else if c <= (height << bwl) / 4 { + 2 + } else { + 3 + } +} + +/// Context of coeff_base for TX_CLASS_2D (8.3.2). +fn coeff_base_ctx(quant: &[i32], tx_sz: usize, bwl: usize, width: usize, height: usize, pos: usize) -> usize { + let row = pos >> bwl; + let col = pos - (row << bwl); + let mut mag = 0i32; + for off in SIG_REF_DIFF_OFFSET[TX_CLASS_2D].iter() { + let ref_row = row + off[0] as usize; + let ref_col = col + off[1] as usize; + if ref_row < height && ref_col < width { + mag += quant[(ref_row << bwl) + ref_col].abs().min(3); + } + } + let ctx = ((mag + 1) >> 1).min(4) as usize; + if row == 0 && col == 0 { + return 0; + } + ctx + COEFF_BASE_CTX_OFFSET[tx_sz][row.min(4)][col.min(4)] as usize +} + +/// Context of coeff_br for TX_CLASS_2D. +fn coeff_br_ctx(quant: &[i32], bwl: usize, txh: usize, pos: usize) -> usize { + let txw = 1usize << bwl; + let row = pos >> bwl; + let col = pos - (row << bwl); + let mut mag = 0i32; + for off in MAG_REF_OFFSET_WITH_TX_CLASS[TX_CLASS_2D].iter() { + let ref_row = row + off[0] as usize; + let ref_col = col + off[1] as usize; + if ref_row < txh && ref_col < txw { + mag += quant[ref_row * txw + ref_col].min((COEFF_BASE_RANGE + NUM_BASE_LEVELS + 1) as i32); + } + } + let mag = ((mag + 1) >> 1).min(6) as usize; + if pos == 0 { + mag + } else if row < 2 && col < 2 { + mag + 7 + } else { + mag + 14 + } +} diff --git a/crates/av1enc/tests/bench.rs b/crates/av1enc/tests/bench.rs new file mode 100644 index 00000000..6577f8f9 --- /dev/null +++ b/crates/av1enc/tests/bench.rs @@ -0,0 +1,136 @@ +//! Speed / efficiency measurements on ffmpeg-generated test sources (ignored by default): +//! `cargo test -p effectcraft-av1enc --test bench -- --ignored --nocapture`. + +mod common; + +use std::process::Command; +use std::time::Instant; + +use common::*; +use effectcraft_av1enc::{Encoder, EncoderConfig, RateControl}; + +/// Raw 8-bit 4:2:0 frames of an ffmpeg lavfi source, or None without ffmpeg. +fn lavfi(src: &str, w: usize, h: usize, frames: usize) -> Option> { + let out = std::env::temp_dir().join(format!("effectcraft-av1enc-bench-{}-{src}-{w}x{h}.yuv", std::process::id())); + let st = Command::new("ffmpeg") + .args(["-v", "error", "-y", "-f", "lavfi", "-i"]) + .arg(format!("{src}=size={w}x{h}:rate=30")) + .args(["-frames:v", &frames.to_string(), "-pix_fmt", "yuv420p", "-f", "rawvideo"]) + .arg(&out) + .status() + .ok()?; + if !st.success() { + return None; + } + let raw = std::fs::read(&out).ok()?; + let _ = std::fs::remove_file(&out); + let (cw, ch) = (w.div_ceil(2), h.div_ceil(2)); + let fs = w * h + 2 * cw * ch; + Some( + raw.chunks_exact(fs) + .map(|f| Pic { + w, + h, + y: f[..w * h].iter().map(|&b| b as u16).collect(), + u: f[w * h..w * h + cw * ch].iter().map(|&b| b as u16).collect(), + v: f[w * h + cw * ch..].iter().map(|&b| b as u16).collect(), + }) + .collect(), + ) +} + +fn run(name: &str, pics: &[Pic], rate: RateControl, keyint: u32) { + let (w, h) = (pics[0].w, pics[0].h); + let mut cfg = EncoderConfig::new(w as u32, h as u32, 30, 1); + cfg.rate = rate; + cfg.keyint = keyint; + let mut enc = Encoder::new(cfg).unwrap(); + let mut bytes = 0; + let mut psnr_sum = 0.0; + let mut t_key = 0.0; + let mut t_inter = 0.0; + let mut sizes = Vec::new(); + for p in pics { + let t = Instant::now(); + let pkt = enc.encode(&p.frame()); + let dt = t.elapsed().as_secs_f64(); + if pkt.keyframe { + t_key += dt; + } else { + t_inter += dt; + } + bytes += pkt.data.len(); + sizes.push(pkt.data.len()); + let rec = enc.last_reconstruction().unwrap(); + psnr_sum += psnr(&p.y, &rec[0], 8); + } + let n = pics.len() as f64; + let n_key = pics.len().div_ceil(keyint as usize) as f64; + eprintln!( + "{name} {w}x{h} {rate:?}: {:.0} kbit/s, PSNR-Y {:.2} dB, key {:.2} s/frame, inter {:.2} s/frame, sizes {:?}", + bytes as f64 * 8.0 * 30.0 / n / 1000.0, + psnr_sum / n, + t_key / n_key, + if n > n_key { t_inter / (n - n_key) } else { 0.0 }, + &sizes[..sizes.len().min(6)] + ); +} + +#[test] +#[ignore] +fn bench_lavfi() { + if std::env::var("AV1ENC_BENCH_QUICK").is_err() { + let Some(pics) = lavfi("testsrc2", 1920, 1080, 6) else { + eprintln!("ffmpeg not available"); + return; + }; + run("testsrc2", &pics, RateControl::ConstantQ(100), 30); + } + if let Some(pics) = lavfi("mandelbrot", 640, 360, 20) { + for q in [40, 80, 120, 160] { + run("mandelbrot", &pics, RateControl::ConstantQ(q), 30); + } + run("mandelbrot", &pics, RateControl::Bitrate { kbps: 1000 }, 30); + } +} + +#[test] +#[ignore] +fn bench_intra() { + for src in ["mandelbrot", "sierpinski", "gradients"] { + let Some(pics) = lavfi(src, 640, 360, 4) else { + return; + }; + for q in [40, 80, 120, 160] { + run(&format!("{src}-intra"), &pics, RateControl::ConstantQ(q), 1); + } + } +} + +#[test] +#[ignore] +fn bench_1080p_loop() { + let Some(pics) = lavfi("mandelbrot", 1920, 1080, 4) else { + return; + }; + run("mandelbrot", &pics, RateControl::ConstantQ(120), 30); + run("mandelbrot", &pics, RateControl::ConstantQ(120), 30); +} + +#[test] +#[ignore] +fn bench_rate_control() { + let Some(pics) = lavfi("mandelbrot", 480, 272, 60) else { + eprintln!("ffmpeg not available"); + return; + }; + for kbps in [200, 600, 2000] { + run("mandelbrot", &pics, RateControl::Bitrate { kbps }, 30); + } + let Some(pics) = lavfi("testsrc2", 480, 272, 60) else { + return; + }; + for kbps in [300, 3000] { + run("testsrc2", &pics, RateControl::Bitrate { kbps }, 60); + } +} diff --git a/crates/av1enc/tests/common/mod.rs b/crates/av1enc/tests/common/mod.rs new file mode 100644 index 00000000..cf5dba73 --- /dev/null +++ b/crates/av1enc/tests/common/mod.rs @@ -0,0 +1,116 @@ +#![allow(dead_code)] + +use effectcraft_av1enc::{Encoder, EncoderConfig, Frame, Packet}; + +/// A synthetic 4:2:0 picture. +pub struct Pic { + pub w: usize, + pub h: usize, + pub y: Vec, + pub u: Vec, + pub v: Vec, +} + +impl Pic { + pub fn frame(&self) -> Frame<'_> { + Frame { y: &self.y, u: &self.u, v: &self.v, y_stride: self.w, uv_stride: self.w.div_ceil(2) } + } +} + +/// Smooth gradients with a soft blob, moved by (dx, dy) samples per frame index `t`, plus a +/// textured rectangle that moves differently. +pub fn synth(w: usize, h: usize, bd: u32, t: usize) -> Pic { + let max = ((1u32 << bd) - 1) as f64; + let (cw, ch) = (w.div_ceil(2), h.div_ceil(2)); + let sample = |x: f64, y: f64| -> f64 { + let fx = x - 2.0 * t as f64; + let fy = y - 1.0 * t as f64; + let base = 0.25 + 0.5 * (fx / 97.0).sin() * (fy / 61.0).cos() * 0.5 + 0.2 * ((fx + fy) / (w + h) as f64); + let d = ((fx - w as f64 / 2.0).powi(2) + (fy - h as f64 / 2.0).powi(2)).sqrt(); + let blob = 0.3 * (-d * d / (2.0 * (w.min(h) as f64 / 5.0).powi(2))).exp(); + // a checkered patch moving right by 4 per frame + let px = x - 4.0 * t as f64 - w as f64 / 4.0; + let py = y - h as f64 / 3.0; + let patch = + if (0.0..24.0).contains(&px) && (0.0..16.0).contains(&py) { if ((px as i32 / 4) + (py as i32 / 4)) % 2 == 0 { 0.25 } else { -0.25 } } else { 0.0 }; + (base + blob + patch).clamp(0.0, 1.0) + }; + let mut y = vec![0u16; w * h]; + for j in 0..h { + for i in 0..w { + y[j * w + i] = (sample(i as f64, j as f64) * max).round() as u16; + } + } + let mut u = vec![0u16; cw * ch]; + let mut v = vec![0u16; cw * ch]; + for j in 0..ch { + for i in 0..cw { + let s = sample(2.0 * i as f64, 2.0 * j as f64); + u[j * cw + i] = ((0.5 + 0.3 * (s - 0.5) + 0.1 * (i as f64 / 13.0).sin()) * max).round() as u16; + v[j * cw + i] = ((0.5 - 0.25 * (s - 0.5) + 0.1 * (j as f64 / 7.0).cos()) * max).round() as u16; + } + } + Pic { w, h, y, u, v } +} + +/// Deterministic noise picture (hard case for prediction). +pub fn noise(w: usize, h: usize, bd: u32, seed: u32) -> Pic { + let mut s = seed.wrapping_mul(2_654_435_761).max(1); + let mut rnd = || { + s ^= s << 13; + s ^= s >> 17; + s ^= s << 5; + s + }; + let mask = (1u32 << bd) - 1; + let (cw, ch) = (w.div_ceil(2), h.div_ceil(2)); + Pic { + w, + h, + y: (0..w * h).map(|_| (rnd() & mask) as u16).collect(), + u: (0..cw * ch).map(|_| (rnd() & mask) as u16).collect(), + v: (0..cw * ch).map(|_| (rnd() & mask) as u16).collect(), + } +} + +pub fn psnr(a: &[u16], b: &[u16], bd: u32) -> f64 { + let max = ((1u32 << bd) - 1) as f64; + let mse = a.iter().zip(b).map(|(&x, &y)| (x as f64 - y as f64).powi(2)).sum::() / a.len() as f64; + if mse == 0.0 { 99.0 } else { 10.0 * (max * max / mse).log10() } +} + +pub struct Run { + pub packets: Vec, + pub recons: Vec<[Vec; 3]>, + pub psnr_y: Vec, + pub lf_levels: Vec, +} + +/// Encodes `pics`, checks every packet decodes (filmcraft-av1) to exactly the encoder's +/// reconstruction and returns packets, reconstructions and luma PSNRs. +pub fn encode_and_check(cfg: EncoderConfig, pics: &[Pic]) -> Run { + let bd = cfg.bit_depth as u32; + let mut enc = Encoder::new(cfg).expect("config"); + let mut dec = filmcraft_av1::Decoder::new(); + let mut run = Run { packets: Vec::new(), recons: Vec::new(), psnr_y: Vec::new(), lf_levels: Vec::new() }; + for (i, p) in pics.iter().enumerate() { + let pkt = enc.encode(&p.frame()); + let rec = enc.last_reconstruction().expect("reconstruction"); + let out = dec.decode(&pkt.data).unwrap_or_else(|e| panic!("frame {i}: decode error {e:?}")); + assert_eq!(out.len(), 1, "frame {i}: one picture per packet"); + let d = &out[0]; + assert_eq!((d.width as usize, d.height as usize), (p.w, p.h)); + for pl in 0..3 { + if d.planes[pl] != rec[pl] { + let first = d.planes[pl].iter().zip(&rec[pl]).position(|(a, b)| a != b).unwrap_or(usize::MAX); + let pw = if pl == 0 { p.w } else { p.w.div_ceil(2) }; + panic!("frame {i} plane {pl}: decoder output differs from the reconstruction (first at x {} y {})", first % pw, first / pw); + } + } + run.psnr_y.push(psnr(&p.y, &rec[0], bd)); + run.lf_levels.push(enc.last_loop_filter_level().expect("level")); + run.recons.push(rec); + run.packets.push(pkt); + } + run +} diff --git a/crates/av1enc/tests/ffmpeg_oracle.rs b/crates/av1enc/tests/ffmpeg_oracle.rs new file mode 100644 index 00000000..c1a087d2 --- /dev/null +++ b/crates/av1enc/tests/ffmpeg_oracle.rs @@ -0,0 +1,156 @@ +//! External oracle: streams decoded by ffmpeg's libdav1d must equal the encoder's reconstruction +//! bit for bit. Skipped when `ffmpeg` (with libdav1d) is not installed. ffmpeg's native `av1` +//! decoder only drives hardware decoders, so it is not used. + +mod common; + +use std::process::Command; + +use common::*; +use effectcraft_av1enc::{Encoder, EncoderConfig, Packet, RateControl}; + +fn have_libdav1d() -> bool { + let Ok(out) = Command::new("ffmpeg").args(["-hide_banner", "-decoders"]).output() else { + eprintln!("ffmpeg not found: skipping the external oracle"); + return false; + }; + let ok = out.status.success() && String::from_utf8_lossy(&out.stdout).contains("libdav1d"); + if !ok { + eprintln!("ffmpeg without libdav1d: skipping the external oracle"); + } + ok +} + +/// IVF container with one temporal unit (temporal delimiter + packet) per frame. +fn ivf(w: usize, h: usize, packets: &[Packet]) -> Vec { + let mut v = Vec::new(); + v.extend_from_slice(b"DKIF"); + v.extend_from_slice(&0u16.to_le_bytes()); + v.extend_from_slice(&32u16.to_le_bytes()); + v.extend_from_slice(b"AV01"); + v.extend_from_slice(&(w as u16).to_le_bytes()); + v.extend_from_slice(&(h as u16).to_le_bytes()); + v.extend_from_slice(&30u32.to_le_bytes()); + v.extend_from_slice(&1u32.to_le_bytes()); + v.extend_from_slice(&(packets.len() as u32).to_le_bytes()); + v.extend_from_slice(&0u32.to_le_bytes()); + for (i, p) in packets.iter().enumerate() { + v.extend_from_slice(&((p.data.len() + 2) as u32).to_le_bytes()); + v.extend_from_slice(&(i as u64).to_le_bytes()); + v.extend_from_slice(&[0x12, 0x00]); // OBU_TEMPORAL_DELIMITER + v.extend_from_slice(&p.data); + } + v +} + +/// Encodes `pics`, decodes the IVF stream with `ffmpeg -c:v libdav1d` and compares every +/// decoded frame with the encoder's reconstruction. +/// Returns the loop filter levels used. +fn check(name: &str, cfg: EncoderConfig, pics: &[Pic]) -> Vec { + let (w, h, bd) = (cfg.width as usize, cfg.height as usize, cfg.bit_depth); + let mut enc = Encoder::new(cfg).expect("config"); + let mut packets = Vec::new(); + let mut recons = Vec::new(); + let mut levels = Vec::new(); + for p in pics { + packets.push(enc.encode(&p.frame())); + recons.push(enc.last_reconstruction().expect("reconstruction")); + levels.push(enc.last_loop_filter_level().expect("level")); + } + let dir = std::env::temp_dir().join(format!("effectcraft-av1enc-{}-{name}", std::process::id())); + std::fs::create_dir_all(&dir).expect("temp dir"); + let input = dir.join("in.ivf"); + let output = dir.join("out.yuv"); + std::fs::write(&input, ivf(w, h, &packets)).expect("write ivf"); + let pix_fmt = if bd == 8 { "yuv420p" } else { "yuv420p10le" }; + let res = Command::new("ffmpeg") + .args(["-v", "error", "-y", "-c:v", "libdav1d", "-i"]) + .arg(&input) + .args(["-f", "rawvideo", "-pix_fmt", pix_fmt]) + .arg(&output) + .output() + .expect("run ffmpeg"); + let stderr = String::from_utf8_lossy(&res.stderr); + assert!(res.status.success() && stderr.trim().is_empty(), "{name}: ffmpeg -c:v libdav1d failed: {stderr}"); + let raw = std::fs::read(&output).expect("decoded yuv"); + let _ = std::fs::remove_dir_all(&dir); + let samples: Vec = + if bd == 8 { raw.iter().map(|&b| b as u16).collect() } else { raw.chunks_exact(2).map(|c| u16::from_le_bytes([c[0], c[1]])).collect() }; + let (cw, ch) = (w.div_ceil(2), h.div_ceil(2)); + let fsize = w * h + 2 * cw * ch; + assert_eq!(samples.len(), fsize * pics.len(), "{name}: decoded frame count"); + for (i, rec) in recons.iter().enumerate() { + let f = &samples[i * fsize..(i + 1) * fsize]; + let planes = [&f[..w * h], &f[w * h..w * h + cw * ch], &f[w * h + cw * ch..]]; + for p in 0..3 { + if planes[p] != &rec[p][..] { + let pw = if p == 0 { w } else { cw }; + let k = planes[p].iter().zip(&rec[p]).position(|(a, b)| a != b).unwrap_or(0); + panic!("{name}: frame {i} plane {p} differs from the reconstruction at ({}, {})", k % pw, k / pw); + } + } + } + eprintln!( + "{name}: libdav1d bit-exact over {} frames ({} bytes, loop filter levels {levels:?})", + pics.len(), + packets.iter().map(|p| p.data.len()).sum::() + ); + levels +} + +fn cfg(w: usize, h: usize, bd: u8, rate: RateControl, keyint: u32) -> EncoderConfig { + let mut c = EncoderConfig::new(w as u32, h as u32, 30, 1); + c.bit_depth = bd; + c.rate = rate; + c.keyint = keyint; + c +} + +#[test] +fn libdav1d_decodes_bit_exact() { + if !have_libdav1d() { + return; + } + // a moving pattern with inter frames + let pics: Vec = (0..8).map(|t| synth(176, 144, 8, t)).collect(); + check("inter8", cfg(176, 144, 8, RateControl::ConstantQ(90), 5), &pics); + // all-intra at a fine quantizer (large coefficients, Golomb codes) + let pics: Vec = (0..2).map(|t| noise(64, 48, 8, t as u32 + 3)).collect(); + check("noise-intra", cfg(64, 48, 8, RateControl::ConstantQ(4), 1), &pics); + // the finest quantizer: the largest coefficients, 8 and 10 bit, intra and inter + let pics: Vec = (0..3).map(|t| noise(72, 40, 8, t as u32 + 11)).collect(); + check("noise-q1", cfg(72, 40, 8, RateControl::ConstantQ(1), 2), &pics); + let pics: Vec = (0..3).map(|t| noise(40, 72, 10, t as u32 + 21)).collect(); + check("noise-q1-10", cfg(40, 72, 10, RateControl::ConstantQ(1), 2), &pics); + // odd sizes, 8 and 10 bit + let pics: Vec = (0..4).map(|t| synth(33, 17, 8, t)).collect(); + check("odd8", cfg(33, 17, 8, RateControl::ConstantQ(60), 3), &pics); + let pics: Vec = (0..5).map(|t| synth(101, 75, 10, t)).collect(); + check("odd10", cfg(101, 75, 10, RateControl::ConstantQ(110), 4), &pics); + // a noisy moving picture, bitrate mode + let pics: Vec = (0..6).map(|t| noisy(128, 80, t)).collect(); + let levels = check("bitrate", cfg(128, 80, 8, RateControl::Bitrate { kbps: 300 }, 10), &pics); + assert!(levels.iter().any(|&l| l > 0), "the deblocking filter is exercised"); + // a coarse quantizer: strong deblocking, 10 bit + let pics: Vec = (0..4).map(|t| synth(120, 88, 10, t)).collect(); + let levels = check("deblock10", cfg(120, 88, 10, RateControl::ConstantQ(220), 2), &pics); + assert!(levels.iter().any(|&l| l > 0), "the deblocking filter is exercised"); +} + +#[test] +fn libdav1d_decodes_1080p() { + if !have_libdav1d() { + return; + } + let pics: Vec = (0..3).map(|t| noisy(1920, 1080, t)).collect(); + check("1080p", cfg(1920, 1080, 8, RateControl::ConstantQ(120), 30), &pics); +} + +fn noisy(w: usize, h: usize, t: usize) -> Pic { + let mut p = synth(w, h, 8, t); + let n = noise(w, h, 8, 7 + t as u32); + for (a, b) in p.y.iter_mut().zip(&n.y) { + *a = (*a as i32 + (*b as i32 - 128) / 16).clamp(0, 255) as u16; + } + p +} diff --git a/crates/av1enc/tests/roundtrip.rs b/crates/av1enc/tests/roundtrip.rs new file mode 100644 index 00000000..1e777e50 --- /dev/null +++ b/crates/av1enc/tests/roundtrip.rs @@ -0,0 +1,92 @@ +//! Round trips through FilmCraft's spec-derived AV1 decoder: decoded frames must equal the +//! encoder's own reconstruction bit for bit. + +mod common; + +use common::*; +use effectcraft_av1enc::{EncoderConfig, RateControl}; + +fn cfg(w: u32, h: u32, bd: u8, q: u8, keyint: u32) -> EncoderConfig { + let mut c = EncoderConfig::new(w, h, 30, 1); + c.bit_depth = bd; + c.rate = RateControl::ConstantQ(q); + c.keyint = keyint; + c +} + +#[test] +fn intra_8bit_smooth_high_quality() { + let pics = vec![synth(128, 96, 8, 0)]; + let run = encode_and_check(cfg(128, 96, 8, 40, 1), &pics); + eprintln!("intra 8-bit q40: {} bytes, PSNR-Y {:.2} dB", run.packets[0].data.len(), run.psnr_y[0]); + assert!(run.packets[0].keyframe); + assert!(run.psnr_y[0] > 35.0, "PSNR {:.2}", run.psnr_y[0]); +} + +#[test] +fn odd_sizes_all_intra() { + for (w, h) in [(33, 17), (1, 1), (7, 9), (65, 63), (100, 30)] { + let pics: Vec = (0..2).map(|t| synth(w, h, 8, t)).collect(); + let run = encode_and_check(cfg(w as u32, h as u32, 8, 90, 1), &pics); + assert!(run.packets.iter().all(|p| p.keyframe)); + } +} + +#[test] +fn noise_low_q_exact() { + let pics = vec![noise(72, 40, 8, 1)]; + let run = encode_and_check(cfg(72, 40, 8, 1, 1), &pics); + assert!(run.psnr_y[0] > 40.0, "PSNR {:.2}", run.psnr_y[0]); +} + +#[test] +fn inter_moving_pattern() { + let pics: Vec = (0..6).map(|t| synth(160, 112, 8, t)).collect(); + let run = encode_and_check(cfg(160, 112, 8, 80, 30), &pics); + let sizes: Vec = run.packets.iter().map(|p| p.data.len()).collect(); + eprintln!("inter sizes {sizes:?} psnr {:?}", run.psnr_y); + assert!(run.packets[0].keyframe && run.packets[1..].iter().all(|p| !p.keyframe)); + let inter_avg = sizes[1..].iter().sum::() as f64 / (sizes.len() - 1) as f64; + assert!(inter_avg < sizes[0] as f64 * 0.5, "inter frames {inter_avg} vs key {}", sizes[0]); + assert!(run.psnr_y.iter().all(|&p| p > 33.0)); +} + +#[test] +fn ten_bit_inter() { + let pics: Vec = (0..4).map(|t| synth(96, 72, 10, t)).collect(); + let run = encode_and_check(cfg(96, 72, 10, 60, 3), &pics); + eprintln!("10-bit psnr {:?}", run.psnr_y); + assert!(run.psnr_y.iter().all(|&p| p > 35.0)); +} + +#[test] +fn odd_size_inter() { + let pics: Vec = (0..5).map(|t| synth(57, 43, 8, t)).collect(); + encode_and_check(cfg(57, 43, 8, 120, 10), &pics); + let pics: Vec = (0..3).map(|t| synth(33, 17, 10, t)).collect(); + encode_and_check(cfg(33, 17, 10, 120, 10), &pics); +} + +#[test] +fn bitrate_mode_tracks_target() { + let n = 12; + let pics: Vec = (0..n).map(|t| synth(192, 128, 8, t)).collect(); + let mut c = cfg(192, 128, 8, 0, 12); + c.rate = RateControl::Bitrate { kbps: 400 }; + let run = encode_and_check(c, &pics); + let bits: usize = run.packets.iter().map(|p| p.data.len() * 8).sum(); + let kbps = bits as f64 / (n as f64 / 30.0) / 1000.0; + eprintln!("bitrate mode: {kbps:.1} kbit/s for a 400 kbit/s target, psnr {:?}", run.psnr_y); + assert!(kbps > 400.0 / 2.0 && kbps < 400.0 * 2.0, "{kbps} kbit/s"); +} + +#[test] +fn headers() { + let enc = effectcraft_av1enc::Encoder::new(cfg(1920, 1080, 10, 100, 60)).unwrap(); + let c = enc.av1c(); + assert_eq!(c[0], 0x81); + assert_eq!(c[1] & 0x1f, 8, "1080p30 is level 4.0"); + assert_eq!(c[2], 0b0100_1100); + assert_eq!(&c[4..], &enc.sequence_header_obu()[..]); + assert_eq!(enc.sequence_header_obu()[0], 0x0A); +} diff --git a/crates/effects/src/plugin.rs b/crates/effects/src/plugin.rs index 242f52b9..874050f4 100644 --- a/crates/effects/src/plugin.rs +++ b/crates/effects/src/plugin.rs @@ -137,6 +137,7 @@ pub struct PluginManifest { impl PluginManifest { /// Check the manifest against this API version. pub fn validate(&self) -> Result<(), String> { + const MAX_PARAMS: usize = 256; if self.api != PLUGIN_API_VERSION { return Err(format!("plug-in `{}` targets plug-in API {} but this build implements API {PLUGIN_API_VERSION}", self.id, self.api)); } @@ -147,6 +148,9 @@ impl PluginManifest { if self.name.trim().is_empty() || self.category.trim().is_empty() { return Err(format!("plug-in `{}` needs a name and a category", self.id)); } + if self.params.len() > MAX_PARAMS { + return Err(format!("plug-in `{}` has {} parameters (at most {MAX_PARAMS})", self.id, self.params.len())); + } let mut seen = std::collections::HashSet::new(); for p in &self.params { if p.id.is_empty() || !p.id.chars().all(|c| c.is_ascii_alphanumeric() || c == '_') || !seen.insert(p.id.as_str()) { @@ -157,6 +161,12 @@ impl PluginManifest { { return Err(format!("plug-in `{}`: popup `{}` needs options and a default index in range", self.id, p.id)); } + if let PluginParamKind::Slider { default, min, max, slider_min, slider_max, .. } = &p.kind { + let (lo, hi) = (slider_min.unwrap_or(*min), slider_max.unwrap_or(*max)); + if [*default, *min, *max, lo, hi].iter().any(|v| !v.is_finite()) || min > max || lo > hi || default < min || default > max { + return Err(format!("plug-in `{}`: slider `{}` needs min ≤ default ≤ max (and sliderMin ≤ sliderMax)", self.id, p.id)); + } + } } Ok(()) } diff --git a/crates/engine/src/commands/create.rs b/crates/engine/src/commands/create.rs index 07e6af7d..850d38ad 100644 --- a/crates/engine/src/commands/create.rs +++ b/crates/engine/src/commands/create.rs @@ -36,14 +36,56 @@ fn create(s: &mut Session, p: &Value) -> Result { let doc = crate::vector::vector_doc(&f.path, &bytes, f.layer.as_ref(), f.page) .unwrap_or_else(|| Err(format!("{}: not a vector file", f.path))) .map_err(EngineError::Other)?; - let id = s.edit("Create Shapes from Vector Layer", None, |proj, st| { - let l = crate::vector::shapes_from_vector(proj, &comp, &src, &doc); - let id = l.id; - insert_above(proj, cid, lid, l, true)?; - st.selected_layers = vec![id]; - Ok(id) + // Images become footage layers (PNG files written next to the source and + // imported), parented to the shape layer below it. + let (images, masks) = crate::vector::vector_images(&doc); + if masks > 0 { + skipped.push(format!("{}: {masks} soft mask(s) not converted (shape layers have no soft masks)", src.name)); + } + let mut image_items = vec![]; + for (i, img) in images.iter().enumerate() { + match image_footage(s, &f.path, i + 1, img) { + Ok(item) => image_items.push((item, i)), + Err(e) => skipped.push(format!("{}: image {} not converted ({e})", src.name, i + 1)), + } + if img.clipped { + skipped.push(format!("{}: image {} is clipped or masked; its footage layer is not", src.name, i + 1)); + } + } + let mut notes = vec![]; + let ids = s.edit("Create Shapes from Vector Layer", None, |proj, st| { + // Without images: one shape layer. With images: the shapes between them + // in layers of their own, so the stack keeps the document's paint order. + let segments = if image_items.is_empty() { vec![doc.clone()] } else { crate::vector::split_at_images(&doc) }; + let top = crate::vector::shapes_from_vector(proj, &comp, &src, segments.last().unwrap_or(&doc)); + // Top to bottom: the last segment, image n, segment n − 1, … image 1, segment 0. + let mut stack = vec![]; + for k in (0..segments.len().saturating_sub(1)).rev() { + if let Some((item, i)) = image_items.iter().find(|(_, i)| *i == k) { + let (il, note) = crate::vector::image_layer(proj, &comp, *item, &images[*i], &top); + notes.extend(note); + stack.push(il); + } + let seg = &segments[k]; + if seg.root.children.is_empty() { + continue; + } + let mut sl = crate::vector::shapes_from_vector(proj, &comp, &src, seg); + sl.name = format!("{} {}", top.name, k + 1); + stack.push(sl); + } + let mut ids = vec![top.id]; + ids.extend(stack.iter().filter(|l| matches!(l.source, LayerSource::Shape)).map(|l| l.id)); + // Each directly above the source: the top layer first. + insert_above(proj, cid, lid, top, true)?; + for l in stack { + insert_above(proj, cid, lid, l, true)?; + } + st.selected_layers = vec![ids[0]]; + Ok(ids) })?; - created.push(id.0); + skipped.extend(notes); + created.extend(ids.iter().map(|i| i.0)); } "shapesFromText" => { if !matches!(src.source, LayerSource::Text) { @@ -118,6 +160,25 @@ fn create(s: &mut Session, p: &Value) -> Result { Ok(json!({"layers": created, "skipped": skipped})) } +/// An image of a vector file written as ` Image .png` next to it and imported: the +/// footage item. +fn image_footage(s: &mut Session, source: &str, n: usize, img: &crate::vector::VectorImage) -> Result { + let src = std::path::Path::new(source); + let stem = src.file_stem().map(|x| x.to_string_lossy().into_owned()).unwrap_or_else(|| "Vector".into()); + let name = format!("{stem} Image {n}.png"); + let path = match src.parent().filter(|p| !p.as_os_str().is_empty()) { + Some(dir) => dir.join(&name).to_string_lossy().into_owned(), + None => name, + }; + let png = super::comp_more::encode_png(&img.rgba, img.width, img.height).map_err(EngineError::Other)?; + s.services.write_file(&path, &png).map_err(|e| EngineError::Other(format!("cannot write {path}: {e}")))?; + let r = super::file::import(s, &json!({"paths": [path]}))?; + if let Some(e) = r["errors"].as_array().and_then(|e| e.first()) { + return Err(EngineError::Other(e.as_str().unwrap_or("import failed").to_string())); + } + r["items"].get(0).and_then(Value::as_u64).map(effectcraft_project::ItemId).ok_or_else(|| EngineError::Other(format!("{path} was not imported"))) +} + /// Insert `l` directly above layer `below`; optionally switch the source's video off (as After /// Effects does after converting). fn insert_above( diff --git a/crates/engine/src/commands/essential.rs b/crates/engine/src/commands/essential.rs index d7340f68..cbffdc21 100644 --- a/crates/engine/src/commands/essential.rs +++ b/crates/engine/src/commands/essential.rs @@ -96,6 +96,9 @@ fn add_property(s: &mut Session, p: &Value) -> Result { Some(t) } }; + // A property already in the panel (as the same kind of control) is added again as a mirror + // (`mirror: false`: skipped). + let mirror = b_p(p, "mirror").unwrap_or(true); let mut added = vec![]; for (cid, lid, uid) in &targets { let l = s.project.comp(*cid).and_then(|c| c.layer(*lid)).ok_or(EngineError::NoComp)?; @@ -110,30 +113,122 @@ fn add_property(s: &mut Session, p: &Value) -> Result { { return Err(bad(C, format!("`{}` can't be shown as a {} control", pr.name, t.label()))); } - if s.project.comp(*cid).and_then(|c| c.essential.as_ref()).is_some_and(|e| e.control_for_as(*lid, *uid, as_type).is_some()) { - continue; - } - let name = rename.clone().unwrap_or_else(|| match as_type { + let eg = s.project.comp(*cid).and_then(|c| c.essential.as_ref()); + let existing = eg + .and_then(|e| e.control_for_as(*lid, *uid, as_type).or_else(|| as_type.is_none().then(|| e.driver_of(*lid, *uid)).flatten())) + .map(|c| (c.id, c.name.clone())); + let (kind, as_type) = match existing { + Some(_) if !mirror => continue, + Some((of, _)) => (EgKind::Mirror { of }, None), + None => (EgKind::Property { layer: *lid, prop: *uid, links: vec![] }, as_type), + }; + let name = rename.clone().or(existing.map(|e| e.1)).unwrap_or_else(|| match as_type { Some(ControlType::Font) => format!("{} Font", l.name), _ => pr.name.clone(), }); - added.push((*cid, *lid, *uid, name, as_type)); + added.push((*cid, kind, name, as_type)); } let ids = s.edit("Add Property to Essential Graphics", None, |proj, st| { if let Some((cid, ..)) = added.first() { st.essential_primary.get_or_insert(*cid); } let mut ids = vec![]; - for (k, (cid, lid, uid, name, as_type)) in added.iter().enumerate() { + for (k, (cid, kind, name, as_type)) in added.iter().enumerate() { let id = proj.alloc(); let eg = eg_of(proj, *cid)?; - let ctl = EgControl { id, name: name.clone(), kind: EgKind::Property { layer: *lid, prop: *uid }, as_type: *as_type }; + let ctl = EgControl { id, name: name.clone(), kind: kind.clone(), as_type: *as_type }; insert(eg, ctl, group, index.map(|i| i + k), C)?; ids.push(id); } Ok(ids) })?; - Ok(json!({"controls": ids})) + let mirrors: Vec = ids.iter().zip(&added).filter(|(_, a)| matches!(a.1, EgKind::Mirror { .. })).map(|(i, _)| *i).collect(); + Ok(json!({"controls": ids, "mirrors": mirrors})) +} +/// Add a mirror of a property control (the same property shown again, e.g. in another group). +fn add_mirror(s: &mut Session, p: &Value) -> Result { + const C: &str = "essential.addMirror"; + let cid = eg_comp(s, p)?; + let of = control_p(p, C)?; + let eg = s.project.comp(cid).and_then(|c| c.essential.clone()).unwrap_or_default(); + let master = + eg.resolve(of).filter(|c| matches!(c.kind, EgKind::Property { .. })).ok_or_else(|| bad(C, format!("control {of} is not a property control")))?; + let (of, master_name) = (master.id, master.name.clone()); + let name = str_p(p, "name").map(str::to_string).unwrap_or(master_name); + let group = p.get("group").and_then(Value::as_u64); + let index = p.get("index").and_then(Value::as_u64).map(|i| i as usize); + let id = s.edit("Add Mirror to Essential Graphics", None, |proj, _| { + let id = proj.alloc(); + insert(eg_of(proj, cid)?, EgControl { id, name, kind: EgKind::Mirror { of }, as_type: None }, group, index, C)?; + Ok(id) + })?; + Ok(json!({"control": id, "of": of})) +} + +/// The property a link command names: `{layer, path|prop}` or the first selected property. +fn link_target(s: &Session, p: &Value, cmd: &str) -> Result<(ItemId, LayerId, Uid)> { + if p.get("path").is_some() || p.get("prop").is_some() { + return super::prop::prop_ref(s, p, cmd); + } + let cid = comp_id(s, p)?; + selected_leaf_props(s) + .into_iter() + .next() + .map(|(l, u)| (cid, l, u)) + .ok_or_else(|| bad(cmd, "select a property in the timeline (or give `layer` and `path`)")) +} + +/// Make a property control drive another property too (same kind of value). +fn link_property(s: &mut Session, p: &Value) -> Result { + const C: &str = "essential.linkProperty"; + let cid = eg_comp(s, p)?; + let control = control_p(p, C)?; + let (pc, lid, uid) = link_target(s, p, C)?; + if pc != cid { + return Err(bad(C, "the property must be in the Essential Graphics composition")); + } + let eg = s.project.comp(cid).and_then(|c| c.essential.clone()).unwrap_or_default(); + let ctl = eg.resolve(control).ok_or_else(|| bad(C, format!("no control {control}")))?.clone(); + if ctl.as_type == Some(ControlType::Font) { + return Err(bad(C, "a Font control can't drive other properties")); + } + let (main, _) = essential::source_prop(&s.project, cid, &ctl).ok_or_else(|| bad(C, format!("control {control} is not a property control")))?; + let main = main.props.find(match ctl.kind { + EgKind::Property { prop, .. } => prop, + _ => 0, + }); + let target = s.project.comp(cid).and_then(|c| c.layer(lid)).and_then(|l| l.props.find(uid)).ok_or_else(|| bad(C, "no such property"))?; + if !main.is_some_and(|m| essential::linkable(m, target)) { + return Err(bad(C, format!("`{}` is not the same kind of property as the control", target.name))); + } + if let Some(d) = eg.driver_of(lid, uid) { + return Err(bad(C, format!("`{}` is already driven by control {}", target.name, d.id))); + } + let id = ctl.id; + s.edit("Link Property to Essential Graphics Control", None, |proj, _| { + if let Some(EgControl { kind: EgKind::Property { links, .. }, .. }) = eg_of(proj, cid)?.find_mut(id) { + links.push(essential::EgLink { layer: lid, prop: uid }); + } + Ok(()) + })?; + Ok(json!({"control": id, "layer": lid.0, "prop": uid})) +} + +fn unlink_property(s: &mut Session, p: &Value) -> Result { + const C: &str = "essential.unlinkProperty"; + let cid = eg_comp(s, p)?; + let control = control_p(p, C)?; + let (_, lid, uid) = link_target(s, p, C)?; + let eg = s.project.comp(cid).and_then(|c| c.essential.clone()).unwrap_or_default(); + let id = eg.resolve(control).map(|c| c.id).ok_or_else(|| bad(C, format!("no control {control}")))?; + s.edit("Unlink Property from Essential Graphics Control", None, |proj, _| match eg_of(proj, cid)?.find_mut(id) { + Some(EgControl { kind: EgKind::Property { links, .. }, .. }) if links.iter().any(|k| k.layer == lid && k.prop == uid) => { + links.retain(|k| !(k.layer == lid && k.prop == uid)); + Ok(()) + } + _ => Err(bad(C, "the property is not linked to this control")), + })?; + Ok(json!({"control": id, "layer": lid.0, "prop": uid})) } fn control_type(pr: &effectcraft_project::Property) -> Option { @@ -249,11 +344,17 @@ fn remove(s: &mut Session, p: &Value) -> Result { const C: &str = "essential.remove"; let cid = eg_comp(s, p)?; let id = control_p(p, C)?; + // A property control's mirrors go with it. + let mirrors = s.project.comp(cid).and_then(|c| c.essential.as_ref()).map(|e| e.mirrors_of(id)).unwrap_or_default(); s.edit("Remove Essential Graphics Control", None, |proj, _| { - eg_of(proj, cid)?.remove(id).ok_or_else(|| bad(C, format!("no control {id}")))?; + let eg = eg_of(proj, cid)?; + eg.remove(id).ok_or_else(|| bad(C, format!("no control {id}")))?; + for m in &mirrors { + eg.remove(*m); + } Ok(()) })?; - Ok(json!({"control": id})) + Ok(json!({"control": id, "mirrors": mirrors})) } fn move_control(s: &mut Session, p: &Value) -> Result { @@ -381,8 +482,12 @@ pub fn supported_properties(project: &Project, cid: ItemId) -> Vec { fn control_json(project: &Project, cid: ItemId, c: &EgControl) -> Value { match &c.kind { - EgKind::Property { layer, prop } => { + EgKind::Property { layer, prop, .. } => { let src = essential::source_prop(project, cid, c); + let links: Vec = essential::linked_props(project, cid, c) + .into_iter() + .map(|(l, pr)| json!({"layer": l.id.0, "layerName": l.name, "prop": pr.uid, "path": super::match_path_of(&l.props, pr.uid)})) + .collect(); json!({ "id": c.id, "name": c.name, "kind": "property", "layer": layer.0, "prop": prop, "layerName": src.map(|(l, _)| l.name.clone()), @@ -390,6 +495,24 @@ fn control_json(project: &Project, cid: ItemId, c: &EgControl) -> Value { "type": src.and_then(|(_, pr)| essential::effective_type(c, pr)), "value": src.map(|(_, pr)| value_json(essential::effective_type(c, pr), &pr.value_at(Tick::ZERO))), "missing": src.is_none(), + "links": links, + "mirrors": project.comp(cid).and_then(|c| c.essential.as_ref()).map(|e| e.mirrors_of(c.id)).unwrap_or_default(), + }) + } + EgKind::Mirror { of } => { + // Shown as its master (same kind of control). + let master = project.comp(cid).and_then(|x| x.essential.as_ref()).and_then(|e| e.resolve(c.id)).cloned(); + let src = essential::source_prop(project, cid, c); + let ty = |pr| master.as_ref().and_then(|m| essential::effective_type(m, pr)); + json!({ + "id": c.id, "name": c.name, "kind": "mirror", "of": of, + "layer": src.map(|(l, _)| l.id.0), + "prop": src.map(|(_, pr)| pr.uid), + "layerName": src.map(|(l, _)| l.name.clone()), + "path": src.and_then(|(l, pr)| super::match_path_of(&l.props, pr.uid)), + "type": src.and_then(|(_, pr)| ty(pr)), + "value": src.map(|(_, pr)| value_json(ty(pr), &pr.value_at(Tick::ZERO))), + "missing": src.is_none(), }) } EgKind::Media { layer } => json!({ @@ -464,6 +587,7 @@ fn instance_json(s: &mut Session, p: &Value) -> Result { } let kind = match c.kind { EgKind::Property { .. } => "property", + EgKind::Mirror { .. } => "mirror", EgKind::Media { .. } => "media", EgKind::Comment { .. } => "comment", EgKind::Group { .. } => "group", @@ -520,11 +644,21 @@ fn targets(s: &Session, cid: ItemId, lid: LayerId, p: &Value) -> Vec<(u64, Uid)> let Some(l) = s.project.comp(cid).and_then(|c| c.layer(lid)) else { return vec![] }; let over = essential::overridden(l); let Some(g) = essential::group(l) else { return vec![] }; - let want = p.get("control").and_then(Value::as_u64); + // A control, its master and the master's mirrors share one value: act on all of them. + let want: Option> = p.get("control").and_then(Value::as_u64).map(|w| { + let src = match l.source { + LayerSource::Comp { item } => s.project.comp(item).and_then(|c| c.essential.as_ref()), + _ => None, + }; + match src.and_then(|e| e.resolve(w).map(|m| (e, m.id))) { + Some((e, m)) => std::iter::once(m).chain(e.mirrors_of(m)).chain(std::iter::once(w)).collect(), + None => vec![w], + } + }); let mut out = vec![]; g.walk("", &mut |_, pr| { if let Some(c) = essential::control_of(&pr.match_id) - && want.is_none_or(|w| w == c) + && want.as_ref().is_none_or(|w| w.contains(&c)) && (want.is_some() || over.contains(&pr.uid)) { out.push((c, pr.uid)); @@ -570,7 +704,8 @@ fn push_to_comp(s: &mut Session, p: &Value) -> Result { let l = s.project.comp(cid).and_then(|c| c.layer(lid)).cloned().ok_or(EngineError::NoComp)?; let mut writes = vec![]; for (control, uid) in &ts { - let (Some(ctl), Some(pr)) = (eg.find(*control), l.props.find(*uid)) else { continue }; + // A mirror pushes into its master's properties. + let (Some(ctl), Some(pr)) = (eg.resolve(*control), l.props.find(*uid)) else { continue }; writes.push((ctl.kind.clone(), pr.clone(), ctl.as_type)); } s.edit("Push Override Values to Source", None, |proj, _| { @@ -578,7 +713,7 @@ fn push_to_comp(s: &mut Session, p: &Value) -> Result { for (kind, pr, as_type) in &writes { match kind { // A Font control writes only the font (the source keeps its text and keys). - EgKind::Property { layer, prop } if *as_type == Some(ControlType::Font) => { + EgKind::Property { layer, prop, .. } if *as_type == Some(ControlType::Font) => { if let Some(target) = sc.layer_mut(*layer).and_then(|l| l.props.find_mut(*prop)) { essential::merge_font(&mut target.value, &pr.value); for k in &mut target.keys { @@ -586,24 +721,27 @@ fn push_to_comp(s: &mut Session, p: &Value) -> Result { } } } - EgKind::Property { layer, prop } => { - let Some(inner) = sc.layer_mut(*layer) else { continue }; - // Instance keys are in the precomp layer's time (= the source comp's time); - // the source property's keys are in the inner layer's time. - let keys: Vec<_> = pr - .keys - .iter() - .map(|k| { - let mut k = k.clone(); - k.time = inner.layer_time(k.time); - k - }) - .collect(); - if let Some(target) = inner.props.find_mut(*prop) { - target.value = pr.value.clone(); - target.keys = keys; + EgKind::Property { .. } => { + for (layer, prop) in kind.targets() { + let Some(inner) = sc.layer_mut(layer) else { continue }; + // Instance keys are in the precomp layer's time (= the source comp's + // time); the source property's keys are in the inner layer's time. + let keys: Vec<_> = pr + .keys + .iter() + .map(|k| { + let mut k = k.clone(); + k.time = inner.layer_time(k.time); + k + }) + .collect(); + if let Some(target) = inner.props.find_mut(prop) { + target.value = pr.value.clone(); + target.keys = keys; + } } } + EgKind::Media { layer } => { if let Some(inner) = sc.layer_mut(*layer) { inner.source = LayerSource::Footage { item: ItemId(pr.value.as_f64().max(0.0) as u64) }; @@ -843,10 +981,29 @@ pub fn specs() -> Vec { "Add Property to Essential Graphics", ["Animation"], None, - "{comp?, layer?, path?|prop?: uid (default: the selected properties), name?, group?: control id, index?, as?: font (Source Text font family/style/size) | scale (uniform Scale)}", + "{comp?, layer?, path?|prop?: uid (default: the selected properties), name?, group?: control id, index?, as?: font (Source Text font family/style/size) | scale (uniform Scale), mirror?: bool (a property already present is added as a mirror; default true)}", has_selected_props, add_property ), + cmd!("essential.addMirror", "Add Mirror", [], None, "{comp?, control, name?, group?, index?}", has_eg, add_mirror), + cmd!( + "essential.linkProperty", + "Link Property to Control", + [], + None, + "{comp?, control, layer?, path?|prop? (default: the selected property)}", + has_eg, + link_property + ), + cmd!( + "essential.unlinkProperty", + "Unlink Property", + [], + None, + "{comp?, control, layer?, path?|prop? (default: the selected property)}", + has_eg, + unlink_property + ), cmd!("essential.addMedia", "Add Media Replacement", [], None, "{layer, name?, group?}", has_comp, add_media), cmd!("essential.addGroup", "Add Group", [], None, "{comp?, name?, index?}", has_comp, add_group), cmd!("essential.addComment", "Add Comment", [], None, "{comp?, text?, group?}", has_comp, add_comment), diff --git a/crates/engine/src/commands/render_queue.rs b/crates/engine/src/commands/render_queue.rs index 365d7e55..c0eedaec 100644 --- a/crates/engine/src/commands/render_queue.rs +++ b/crates/engine/src/commands/render_queue.rs @@ -213,7 +213,7 @@ fn apply_output(om: &mut OutputModule, templates: &RenderTemplates, roi: Option< } let before = om.clone(); if let Some(f) = str_p(p, "format") { - let f = OutputFormat::from_name(f).ok_or_else(|| bad(cmd, "format: h264|prores|webm|png|jpeg|tiff|exr|gif|wav|aiff"))?; + let f = OutputFormat::from_name(f).ok_or_else(|| bad(cmd, "format: h264|hevc|av1|prores|webm|png|jpeg|tiff|exr|gif|wav|aiff"))?; om.set_format(f); } if let Some(c) = str_p(p, "channels") { @@ -223,7 +223,7 @@ fn apply_output(om: &mut OutputModule, templates: &RenderTemplates, roi: Option< "alpha" | "alphaonly" => Channels::Alpha, _ => return Err(bad(cmd, "channels: rgb|rgba|alpha")), }; - if om.channels == Channels::Rgba && !om.format.supports_alpha() { + if om.channels == Channels::Rgba && !om.supports_alpha() { return Err(bad(cmd, format!("{} has no alpha channel", om.format.label()))); } } @@ -234,6 +234,7 @@ fn apply_output(om: &mut OutputModule, templates: &RenderTemplates, roi: Option< _ => return Err(bad(cmd, "color: straight|premultiplied")), }; } + any |= apply_codec_options(om, p, cmd)?; if let Some(q) = f_p(p, "quality") { om.quality = q.clamp(1.0, 100.0) as u8; } @@ -377,6 +378,62 @@ fn apply_output(om: &mut OutputModule, templates: &RenderTemplates, roi: Option< Ok(any) } +/// HEVC / AV1 codec options, the WebM video codec and the Opus audio options. +fn apply_codec_options(om: &mut OutputModule, p: &Value, cmd: &str) -> Result { + use effectcraft_project::render_queue::{CodecProfile, OpusApplication, RateControlMode, VideoCodecOptions, WebmVideoCodec}; + let mut any = false; + if let Some(c) = str_p(p, "webmCodec") { + om.webm_codec = match c.to_ascii_lowercase().as_str() { + "vp9" => WebmVideoCodec::Vp9, + "av1" => WebmVideoCodec::Av1, + _ => return Err(bad(cmd, "webmCodec: vp9|av1")), + }; + if !om.supports_alpha() { + om.channels = Channels::Rgb; + } + any = true; + } + if let Some(v) = str_p(p, "profile") { + om.codec.profile = CodecProfile::from_name(v).ok_or_else(|| bad(cmd, "profile: main|main10"))?; + any = true; + } + match p.get("level") { + Some(Value::Null) => { + om.codec.level = None; + any = true; + } + Some(v) => { + let s = match v { + Value::String(s) => s.clone(), + other => other.to_string(), + }; + om.codec.level = VideoCodecOptions::parse_level(&s).ok_or_else(|| bad(cmd, "level: auto|null or a level like 4.1"))?; + any = true; + } + None => {} + } + if let Some(v) = str_p(p, "rateControl") { + om.codec.rate_control = RateControlMode::from_name(v).ok_or_else(|| bad(cmd, "rateControl: bitrate|quality"))?; + any = true; + } + if let Some(q) = f_p(p, "quality") { + om.codec.quality = q.clamp(1.0, 100.0) as u8; + } + if let Some(b) = f_p(p, "audioBitrate") { + om.opus_bitrate_kbps = b.clamp(6.0, 510.0) as u32; + any = true; + } + if let Some(a) = str_p(p, "opusApplication") { + om.opus_application = match a.to_ascii_lowercase().as_str() { + "audio" | "music" => OpusApplication::Audio, + "voip" | "voice" | "speech" => OpusApplication::Voip, + _ => return Err(bad(cmd, "opusApplication: audio|voice")), + }; + any = true; + } + Ok(any) +} + /// `module` (1-based; 1 = the first output module). fn module_p(s: &Session, i: usize, p: &Value, cmd: &str) -> Result { let n = p.get("module").and_then(Value::as_u64).unwrap_or(1) as usize; @@ -618,7 +675,17 @@ fn formats(s: &mut Session, _: &Value) -> Result { }) }) .collect(); - Ok(json!({"formats": v, "proresProfiles": ProResProfile::ALL.iter().map(|p| p.label()).collect::>()})) + use effectcraft_project::render_queue::{CodecProfile, VideoCodecOptions}; + let level = |l: &u8| format!("{}.{}", l / 10, l % 10); + Ok(json!({ + "formats": v, + "proresProfiles": ProResProfile::ALL.iter().map(|p| p.label()).collect::>(), + "codecProfiles": CodecProfile::ALL.iter().map(|p| p.label()).collect::>(), + "hevcLevels": VideoCodecOptions::HEVC_LEVELS.iter().map(level).collect::>(), + "av1Levels": VideoCodecOptions::AV1_LEVELS.iter().map(level).collect::>(), + "webmCodecs": ["vp9", "av1"], + "opusApplications": ["audio", "voice"], + })) } /// `item?|index?`, else the last item (the queue's newest entry). @@ -902,7 +969,7 @@ pub fn specs() -> Vec { "Add to Render Queue", ["Composition"], Some("Cmd+M"), - "{comp?: id|name, template?: Render Settings template, format?: h264|prores|webm|png|jpeg|tiff|exr|gif|wav|aiff, output?: path|template, log?: errorsOnly|plusSettings|plusPerFrameInfo, quality?: best|draft|1-100 (jpeg, webm), resolution?: full|half|third|quarter|scale, proxyUse?, effects?, solo?, guideLayers?, colorDepth?, frameBlending?, fieldRender?, pulldown?, motionBlur?, timeSpan?: workArea|comp|custom, start?: s, end?: s, duration?: s, frameRate?: fps|null, skipExisting?: bool, storageOverflow?: bool, channels?: rgb|rgba|alpha, color?, bitrate?: kbps, webmBitrate?, keyframeInterval?, proresProfile?, crop?, resize?, includeProjectLink?, audio?: auto|on|off, sampleRate?, audioChannels?, audioFormat?, loop?: bool (values as in setRenderSettings / setOutputModule)}", + "{comp?: id|name, template?: Render Settings template, format?: h264|hevc|av1|prores|webm|png|jpeg|tiff|exr|gif|wav|aiff, output?: path|template, log?: errorsOnly|plusSettings|plusPerFrameInfo, quality?: best|draft|1-100 (jpeg, webm), resolution?: full|half|third|quarter|scale, proxyUse?, effects?, solo?, guideLayers?, colorDepth?, frameBlending?, fieldRender?, pulldown?, motionBlur?, timeSpan?: workArea|comp|custom, start?: s, end?: s, duration?: s, frameRate?: fps|null, skipExisting?: bool, storageOverflow?: bool, channels?: rgb|rgba|alpha, color?, bitrate?: kbps, webmBitrate?, keyframeInterval?, proresProfile?, profile?, level?, rateControl?, webmCodec?, audioBitrate?, opusApplication?, crop?, resize?, includeProjectLink?, audio?: auto|on|off, sampleRate?, audioChannels?, audioFormat?, loop?: bool (values as in setRenderSettings / setOutputModule)}", can_add, add ), @@ -922,7 +989,7 @@ pub fn specs() -> Vec { "Output Module Settings...", [], None, - "{item?|index?, module?: n (1 = first), template?: name, format?, channels?: rgb|rgba|alpha, color?: straight|premultiplied, quality?: 1-100, bitrate?: kbps, webmBitrate?: bool, keyframeInterval?: frames, proresProfile?: proxy|lt|standard|hq|4444|4444xq, crop?: bool|{useRoi?, roi?, top?, left?, bottom?, right?}, cropTop?, cropLeft?, cropBottom?, cropRight?, resize?: bool|{preset?, width?, height?, lockAspect?, quality?: low|high}, resizeWidth?, resizeHeight?, postRenderAction?: none|import|importAndReplace|setProxy, includeProjectLink?: bool, audio?: auto|on|off, sampleRate?, audioChannels?: mono|stereo, audioFormat?: 16|24|32, loop?: bool, output?}", + "{item?|index?, module?: n (1 = first), template?: name, format?: h264|hevc|av1|prores|webm|png|jpeg|tiff|exr|gif|wav|aiff, channels?: rgb|rgba|alpha, color?: straight|premultiplied, quality?: 1-100, bitrate?: kbps, webmBitrate?: bool, keyframeInterval?: frames (0 = auto), proresProfile?: proxy|lt|standard|hq|4444|4444xq, profile?: main|main10 (HEVC/AV1), level?: auto|4.1, rateControl?: bitrate|quality, webmCodec?: vp9|av1, audioBitrate?: Opus kbps, opusApplication?: audio|voice, crop?: bool|{useRoi?, roi?, top?, left?, bottom?, right?}, cropTop?, cropLeft?, cropBottom?, cropRight?, resize?: bool|{preset?, width?, height?, lockAspect?, quality?: low|high}, resizeWidth?, resizeHeight?, postRenderAction?: none|import|importAndReplace|setProxy, includeProjectLink?: bool, audio?: auto|on|off, sampleRate?, audioChannels?: mono|stereo, audioFormat?: 16|24|32, loop?: bool, output?}", has_items, set_output_module ), diff --git a/crates/engine/src/lib.rs b/crates/engine/src/lib.rs index 4fc5bc1a..9f0f6372 100644 --- a/crates/engine/src/lib.rs +++ b/crates/engine/src/lib.rs @@ -856,6 +856,8 @@ mod tests_anim_tools; #[cfg(test)] mod tests_camera_track; #[cfg(test)] +mod tests_codec_options; +#[cfg(test)] mod tests_color_view; #[cfg(test)] mod tests_disk_cache; diff --git a/crates/engine/src/render_queue.rs b/crates/engine/src/render_queue.rs index 5e77131c..9756b183 100644 --- a/crates/engine/src/render_queue.rs +++ b/crates/engine/src/render_queue.rs @@ -173,7 +173,7 @@ pub fn bytes_per_frame(om: &OutputModule, w: u32, h: u32, fps: f64) -> f64 { let px = (w as f64 * h as f64) / (1920.0 * 1080.0); let mbps = |m: f64| m * 1_000_000.0 / 8.0; match om.format { - OutputFormat::H264 => om.bitrate_kbps as f64 * 1000.0 / 8.0 / fps, + OutputFormat::H264 | OutputFormat::Hevc | OutputFormat::Av1 => om.bitrate_kbps as f64 * 1000.0 / 8.0 / fps, OutputFormat::ProRes => { let rate = match om.prores_profile { Proxy => 45.0, diff --git a/crates/engine/src/rq_tests.rs b/crates/engine/src/rq_tests.rs index d4c127d0..8ffa8dcb 100644 --- a/crates/engine/src/rq_tests.rs +++ b/crates/engine/src/rq_tests.rs @@ -159,6 +159,6 @@ fn without_exporter() { s.execute("renderQueue.add", json!({})).unwrap(); assert!(!s.is_enabled("renderQueue.render")); let f = s.execute("renderQueue.formats", json!({})).unwrap(); - assert_eq!(f["formats"].as_array().unwrap().len(), 10); + assert_eq!(f["formats"].as_array().unwrap().len(), 12); assert_eq!(f["formats"][0]["available"], false); } diff --git a/crates/engine/src/scriptui.rs b/crates/engine/src/scriptui.rs index 1b88bce7..6ef64920 100644 --- a/crates/engine/src/scriptui.rs +++ b/crates/engine/src/scriptui.rs @@ -116,6 +116,8 @@ pub struct Widget { /// Frontends paint it instead of the control's default look, unless it contains /// [`DrawOp::Os`] (`drawOSControl()`), which draws the default look first. pub draw: Vec, + /// The image of an image control or icon button. + pub image: Option, pub children: Vec, } @@ -156,8 +158,37 @@ pub enum DrawOp { }, /// `drawOSControl()`: the control's default look. Os, - /// `drawImage(image, x, y, w?, h?)` (drawn as a placeholder frame). - Image { x: f64, y: f64, w: f64, h: f64 }, + /// `drawImage(image, x, y, w?, h?)`: `w` / `h` 0 = the image's own size. + Image { + #[serde(default)] + image: Option, + x: f64, + y: f64, + w: f64, + h: f64, + }, +} + +/// A ScriptUIImage (`ScriptUI.newImage`, an image control's or icon button's image, a +/// `drawImage` argument): an image file, or embedded PNG / JPEG bytes. +#[derive(Clone, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[serde(default)] +pub struct ImageRef { + /// File path (or a resource name we can't resolve: drawn as a placeholder). + pub src: Option, + /// Embedded bytes as a binary string (one character per byte, as in resource strings). + pub data: Option, + pub name: String, +} + +impl ImageRef { + /// The encoded image bytes (the embedded data, else the file through `services`). + pub fn bytes(&self, services: &dyn crate::Services) -> Option> { + if let Some(d) = &self.data { + return d.chars().map(|c| u8::try_from(c as u32).ok()).collect(); + } + services.read_file(self.src.as_deref()?).ok() + } } impl PathSeg { @@ -207,6 +238,67 @@ impl PathSeg { flush(&mut cur, &mut out, false); out } + + /// The area `fillPath` paints, as triangles: every subpath closed, overlapping subpaths + /// combined by the non-zero winding rule (`even_odd`: the even-odd rule), so concave, + /// self-intersecting and holed paths fill correctly (frontends' polygon fills are often + /// convex-only). Exact trapezoid decomposition: the plane is cut into horizontal bands at + /// every vertex and edge crossing; inside a band no edges cross, so each inside span is a + /// trapezoid (two triangles). + pub fn fill_triangles(path: &[PathSeg], n: usize, even_odd: bool) -> Vec<[[f64; 2]; 3]> { + // Non-horizontal edges, top to bottom, with their winding direction. + let mut edges: Vec<([f64; 2], [f64; 2], i32)> = vec![]; + for (pts, _) in PathSeg::polylines(path, n) { + for i in 0..pts.len() { + let (a, b) = (pts[i], pts[(i + 1) % pts.len()]); + if !(a[0].is_finite() && a[1].is_finite() && b[0].is_finite() && b[1].is_finite()) || a[1] == b[1] { + continue; + } + edges.push(if a[1] < b[1] { (a, b, 1) } else { (b, a, -1) }); + } + } + if edges.len() < 2 { + return vec![]; + } + let x_at = |e: &([f64; 2], [f64; 2], i32), y: f64| e.0[0] + (e.1[0] - e.0[0]) * (y - e.0[1]) / (e.1[1] - e.0[1]); + let mut ys: Vec = edges.iter().flat_map(|e| [e.0[1], e.1[1]]).collect(); + // Where two edges cross, a band boundary. + for i in 0..edges.len() { + for j in i + 1..edges.len() { + let (a, b) = (&edges[i], &edges[j]); + let (lo, hi) = (a.0[1].max(b.0[1]), a.1[1].min(b.1[1])); + if hi <= lo { + continue; + } + let (d0, d1) = (x_at(a, lo) - x_at(b, lo), x_at(a, hi) - x_at(b, hi)); + if d0 * d1 < 0.0 { + ys.push(lo + (hi - lo) * d0 / (d0 - d1)); + } + } + } + ys.sort_by(f64::total_cmp); + ys.dedup_by(|a, b| (*a - *b).abs() < 1e-9); + let mut out = vec![]; + let mut row: Vec<(f64, f64, f64, i32)> = vec![]; + for w in ys.windows(2) { + let (ya, yb) = (w[0], w[1]); + let ym = (ya + yb) * 0.5; + row.clear(); + row.extend(edges.iter().filter(|e| e.0[1] < ym && ym < e.1[1]).map(|e| (x_at(e, ym), x_at(e, ya), x_at(e, yb), e.2))); + row.sort_by(|a, b| a.0.total_cmp(&b.0)); + let mut wind = 0; + for k in 0..row.len().saturating_sub(1) { + wind += row[k].3; + let inside = if even_odd { wind % 2 != 0 } else { wind != 0 }; + if inside { + let (l, r) = (row[k], row[k + 1]); + out.push([[l.1, ya], [r.1, ya], [r.2, yb]]); + out.push([[l.1, ya], [r.2, yb], [l.2, yb]]); + } + } + } + out + } } impl Widget { @@ -689,4 +781,55 @@ mod tests { let j = serde_json::to_value(&ui).unwrap(); assert_eq!(j["windows"][0]["root"]["type"], "window"); } + + fn area(tris: &[[[f64; 2]; 3]]) -> f64 { + tris.iter().map(|t| ((t[1][0] - t[0][0]) * (t[2][1] - t[0][1]) - (t[2][0] - t[0][0]) * (t[1][1] - t[0][1])).abs() * 0.5).sum() + } + + fn poly(pts: &[[f64; 2]]) -> Vec { + let mut v = vec![PathSeg::M { x: pts[0][0], y: pts[0][1] }]; + v.extend(pts[1..].iter().map(|p| PathSeg::L { x: p[0], y: p[1] })); + v.push(PathSeg::Z); + v + } + + fn covers(tris: &[[[f64; 2]; 3]], p: [f64; 2]) -> bool { + let s = |a: [f64; 2], b: [f64; 2]| (b[0] - a[0]) * (p[1] - a[1]) - (b[1] - a[1]) * (p[0] - a[0]); + tris.iter().any(|t| { + let (d0, d1, d2) = (s(t[0], t[1]), s(t[1], t[2]), s(t[2], t[0])); + (d0 >= 0.0 && d1 >= 0.0 && d2 >= 0.0) || (d0 <= 0.0 && d1 <= 0.0 && d2 <= 0.0) + }) + } + + #[test] + fn concave_self_intersecting_and_holed_fills() { + // An L (concave): area 3, the notch stays empty. + let l = poly(&[[0.0, 0.0], [2.0, 0.0], [2.0, 1.0], [1.0, 1.0], [1.0, 2.0], [0.0, 2.0]]); + let t = PathSeg::fill_triangles(&l, 48, false); + assert!((area(&t) - 3.0).abs() < 1e-9, "{}", area(&t)); + assert!(covers(&t, [0.5, 1.5]) && !covers(&t, [1.5, 1.5])); + // A five-pointed star drawn in one stroke: non-zero fills the centre, even-odd doesn't. + let star: Vec<[f64; 2]> = (0..5) + .map(|i| { + let a = (i * 2 % 5) as f64 * std::f64::consts::TAU / 5.0 - std::f64::consts::FRAC_PI_2; + [50.0 + 40.0 * a.cos(), 50.0 + 40.0 * a.sin()] + }) + .collect(); + let nz = PathSeg::fill_triangles(&poly(&star), 48, false); + let eo = PathSeg::fill_triangles(&poly(&star), 48, true); + assert!(covers(&nz, [50.0, 50.0]) && !covers(&eo, [50.0, 50.0])); + assert!(covers(&eo, [50.0, 15.0]), "a point of the star"); + assert!(area(&nz) > area(&eo)); + // A rectangle with a hole (an inner square the other way round). + let mut holed = poly(&[[0.0, 0.0], [10.0, 0.0], [10.0, 10.0], [0.0, 10.0]]); + holed.extend(poly(&[[3.0, 3.0], [3.0, 7.0], [7.0, 7.0], [7.0, 3.0]])); + let h = PathSeg::fill_triangles(&holed, 48, false); + assert!((area(&h) - 84.0).abs() < 1e-9 && !covers(&h, [5.0, 5.0])); + // rectPath + ellipsePath in one path; an unclosed subpath fills as if closed. + let shapes = vec![PathSeg::R { x: 0.0, y: 0.0, w: 4.0, h: 2.0 }, PathSeg::E { x: 10.0, y: 0.0, w: 20.0, h: 20.0 }]; + let a = area(&PathSeg::fill_triangles(&shapes, 256, false)); + assert!((a - 8.0 - std::f64::consts::PI * 100.0).abs() < 1.0, "{a}"); + let open = vec![PathSeg::M { x: 0.0, y: 0.0 }, PathSeg::L { x: 4.0, y: 0.0 }, PathSeg::L { x: 0.0, y: 4.0 }]; + assert!((area(&PathSeg::fill_triangles(&open, 48, false)) - 8.0).abs() < 1e-9); + } } diff --git a/crates/engine/src/tests_codec_options.rs b/crates/engine/src/tests_codec_options.rs new file mode 100644 index 00000000..21dc7e45 --- /dev/null +++ b/crates/engine/src/tests_codec_options.rs @@ -0,0 +1,53 @@ +//! M13.4: HEVC / AV1 output modules and the WebM codec / Opus options over the command API. + +use effectcraft_project::render_queue::{Channels, CodecProfile, OpusApplication, OutputFormat, RateControlMode, VideoCodecOptions, WebmVideoCodec}; +use serde_json::json; + +use crate::Session; + +fn session() -> Session { + let mut s = Session::default(); + s.execute("file.openDemoProject", json!({})).unwrap(); + s +} + +#[test] +fn hevc_and_av1_output_module_options() { + let mut s = session(); + let a = s + .execute( + "renderQueue.add", + json!({"format": "hevc", "profile": "main10", "level": "4.1", "rateControl": "quality", "quality": 85, "keyframeInterval": 30}), + ) + .unwrap(); + assert!(a["outputPath"].as_str().unwrap().ends_with(".mp4"), "{a}"); + let om = &s.project.render_queue[0].output; + assert_eq!(om.format, OutputFormat::Hevc); + assert_eq!(om.codec, VideoCodecOptions { profile: CodecProfile::Main10, level: Some(41), rate_control: RateControlMode::Quality, quality: 85 }); + assert_eq!(om.keyframe_interval, 30); + assert_eq!(a["output"]["codec"]["profile"], "Main10", "{a}"); + let id = a["item"].clone(); + s.execute("renderQueue.setOutputModule", json!({"item": id, "format": "av1", "level": null, "rateControl": "bitrate", "bitrate": 3000})).unwrap(); + let om = &s.project.render_queue[0].output; + assert_eq!((om.format, om.codec.level, om.codec.rate_control, om.bitrate_kbps), (OutputFormat::Av1, None, RateControlMode::Bitrate, 3000)); + assert!(s.execute("renderQueue.setOutputModule", json!({"item": id, "channels": "rgba"})).is_err(), "AV1 MP4 has no alpha"); + assert!(s.execute("renderQueue.setOutputModule", json!({"item": id, "profile": "high"})).is_err()); + assert!(s.execute("renderQueue.setOutputModule", json!({"item": id, "level": "0.5"})).is_err()); + assert!(s.execute("renderQueue.setOutputModule", json!({"item": id, "rateControl": "cbr2"})).is_err()); + + // WebM: VP9 with alpha, then AV1 (alpha dropped) with voice-tuned Opus. + s.execute("renderQueue.setOutputModule", json!({"item": id, "format": "webm", "channels": "rgba"})).unwrap(); + assert_eq!(s.project.render_queue[0].output.channels, Channels::Rgba); + s.execute("renderQueue.setOutputModule", json!({"item": id, "webmCodec": "av1", "audioBitrate": 24, "opusApplication": "voice"})).unwrap(); + let om = &s.project.render_queue[0].output; + assert_eq!((om.webm_codec, om.channels, om.opus_bitrate_kbps, om.opus_application), (WebmVideoCodec::Av1, Channels::Rgb, 24, OpusApplication::Voip)); + assert!(s.execute("renderQueue.setOutputModule", json!({"item": id, "webmCodec": "theora"})).is_err()); + + // The formats query lists the new formats and option values. + let f = s.execute("renderQueue.formats", json!({})).unwrap(); + let ids: Vec<&str> = f["formats"].as_array().unwrap().iter().filter_map(|v| v["id"].as_str()).collect(); + assert!(ids.contains(&"Hevc") && ids.contains(&"Av1"), "{ids:?}"); + assert_eq!(f["codecProfiles"], json!(["Main", "Main 10"])); + assert!(f["hevcLevels"].as_array().unwrap().contains(&json!("4.1"))); + assert_eq!(f["webmCodecs"], json!(["vp9", "av1"])); +} diff --git a/crates/engine/src/tests_essential.rs b/crates/engine/src/tests_essential.rs index 33473857..0732020a 100644 --- a/crates/engine/src/tests_essential.rs +++ b/crates/engine/src/tests_essential.rs @@ -67,11 +67,15 @@ fn controls_are_listed_typed_and_editable() { assert_eq!(l["controls"][1]["name"], "Opacity"); s.execute("essential.remove", json!({"control": opacity})).unwrap(); assert_eq!(s.execute("essential.list", json!({})).unwrap()["controls"].as_array().unwrap().len(), 3); - // Unsupported properties are refused; adding the same property twice adds nothing. + // Unsupported properties are refused; adding the same property twice adds a mirror (or + // nothing with `mirror: false`). s.execute("layer.addMask", json!({"layer": solid})).unwrap(); assert!(s.execute("essential.addProperty", json!({"layer": solid, "path": "masks/#1/path"})).is_err()); - let again = s.execute("essential.addProperty", json!({"layer": solid, "path": "effects/#1/color"})).unwrap(); + let again = s.execute("essential.addProperty", json!({"layer": solid, "path": "effects/#1/color", "mirror": false})).unwrap(); assert_eq!(again["controls"], json!([])); + let again = s.execute("essential.addProperty", json!({"layer": solid, "path": "effects/#1/color"})).unwrap(); + assert_eq!(again["mirrors"], again["controls"]); + s.execute("edit.undo", json!({})).unwrap(); // Dropdown Menu Control: its items are editable and it becomes a dropdown control. s.execute("effect.apply", json!({"layer": solid, "effect": "Dropdown Menu Control"})).unwrap(); s.execute("effect.editDropdown", json!({"layer": solid, "items": ["Small", "Medium", "Large", "Huge"]})).unwrap(); @@ -141,6 +145,79 @@ fn master_properties_override_per_instance_push_and_revert() { assert!(essential::group(&l).unwrap().children.iter().all(|n| n.name() != "Card Color")); } +#[test] +fn mirrored_and_linked_properties() { + let (mut s, card, solid, ids) = card(); + let opacity = ids["opacity"].as_u64().unwrap(); + // A second solid whose opacity the Opacity control drives too. + let bg2 = s.execute("layer.newSolid", json!({"name": "BG2", "color": "#00ff00"})).unwrap()["layer"].as_u64().unwrap(); + s.execute("essential.linkProperty", json!({"control": opacity, "layer": bg2, "path": "transform/opacity"})).unwrap(); + // Not the same kind of property; already driven. + assert!(s.execute("essential.linkProperty", json!({"control": opacity, "layer": bg2, "path": "transform/position"})).is_err()); + assert!(s.execute("essential.linkProperty", json!({"control": ids["color"], "layer": bg2, "path": "transform/opacity"})).is_err()); + let opa = |s: &Session, l: u64| s.project.comp(card).unwrap().layer(LayerId(l)).unwrap().props.prop("transform/opacity").unwrap().value.as_f64(); + // The link follows the main property, and the main property follows the link. + s.execute("prop.set", json!({"comp": card.0, "layer": solid, "path": "transform/opacity", "value": 40})).unwrap(); + assert_eq!(opa(&s, bg2), 40.0); + s.execute("prop.set", json!({"comp": card.0, "layer": bg2, "path": "transform/opacity", "value": 70})).unwrap(); + assert_eq!(opa(&s, solid), 70.0); + let l = s.execute("essential.list", json!({})).unwrap(); + let c = l["controls"].as_array().unwrap().iter().find(|c| c["id"] == opacity).unwrap().clone(); + assert_eq!(c["links"][0]["layer"], bg2); + assert_eq!(c["links"][0]["path"], "transform/opacity"); + // Adding the Opacity again (into the Words group) mirrors it. + let m = s.execute("essential.addProperty", json!({"layer": solid, "path": "transform/opacity", "group": ids["group"], "name": "Fade"})).unwrap(); + let mirror = m["mirrors"][0].as_u64().unwrap(); + let l = s.execute("essential.list", json!({})).unwrap(); + let mc = &l["controls"][2]["children"][1]; + assert_eq!((mc["id"].as_u64(), mc["kind"].as_str(), mc["of"].as_u64(), mc["name"].as_str()), (Some(mirror), Some("mirror"), Some(opacity), Some("Fade"))); + assert_eq!(mc["value"], json!(70.0)); + // A mirror of the mirror shows the same master; the linked solid also counts as present. + let m2 = s.execute("essential.addMirror", json!({"control": mirror})).unwrap(); + assert_eq!(m2["of"], opacity); + let again = s.execute("essential.addProperty", json!({"layer": bg2, "path": "transform/opacity"})).unwrap(); + assert_eq!(again["mirrors"].as_array().unwrap().len(), 1); + s.execute("edit.undo", json!({})).unwrap(); + s.execute("edit.undo", json!({})).unwrap(); + // Instances: setting the mirror sets the master (and both links) for that instance only. + s.execute("comp.new", json!({"name": "Main", "width": 64, "height": 64, "frameRate": 30, "duration": 2})).unwrap(); + let main = s.active_comp_id().unwrap(); + let a = s.execute("layer.addItem", json!({"item": card.0})).unwrap()["layer"].as_u64().unwrap(); + let inst = s.execute("essential.instance", json!({"layer": a})).unwrap(); + let find = |inst: &Value, id: u64| inst["controls"].as_array().unwrap().iter().find(|c| c["control"] == id).unwrap().clone(); + assert_eq!(find(&inst, mirror)["kind"], "mirror"); + s.execute("essential.set", json!({"layer": a, "control": mirror, "value": 20})).unwrap(); + let inst = s.execute("essential.instance", json!({"layer": a})).unwrap(); + assert_eq!((find(&inst, opacity)["value"].clone(), find(&inst, opacity)["overridden"].clone()), (json!(20.0), json!(true))); + assert_eq!((find(&inst, mirror)["value"].clone(), find(&inst, mirror)["overridden"].clone()), (json!(20.0), json!(true))); + // Rendered: both solids at 20 % (green BG2 on top of the red-filled solid). + let px = center(&s, main); + assert!((px[3] - (1.0 - 0.8 * 0.8)).abs() < 0.02, "{px:?}"); + assert_eq!(opa(&s, solid), 70.0, "the source comp keeps its value"); + // Setting the master updates the mirror. + s.execute("essential.set", json!({"layer": a, "control": opacity, "value": 55})).unwrap(); + assert_eq!(find(&s.execute("essential.instance", json!({"layer": a})).unwrap(), mirror)["value"], json!(55.0)); + // Reverting the mirror reverts the master too. + s.execute("essential.revert", json!({"layer": a, "control": mirror})).unwrap(); + let inst = s.execute("essential.instance", json!({"layer": a})).unwrap(); + assert_eq!((find(&inst, opacity)["overridden"].clone(), find(&inst, mirror)["value"].clone()), (json!(false), json!(70.0))); + // Push to Comp from a mirror writes the master and its links. + s.execute("essential.set", json!({"layer": a, "control": mirror, "value": 35})).unwrap(); + s.execute("essential.pushToComp", json!({"layer": a, "control": mirror})).unwrap(); + assert_eq!((opa(&s, solid), opa(&s, bg2)), (35.0, 35.0)); + // Saved and loaded with the project. + let back = effectcraft_project::Project::from_json(&s.project.to_json()).unwrap(); + assert_eq!(back.comp(card).unwrap().essential, s.project.comp(card).unwrap().essential); + // Unlink; removing the master removes its mirrors. + s.execute("essential.unlinkProperty", json!({"comp": card.0, "control": opacity, "layer": bg2, "path": "transform/opacity"})).unwrap(); + s.execute("prop.set", json!({"comp": card.0, "layer": solid, "path": "transform/opacity", "value": 10})).unwrap(); + assert_eq!(opa(&s, bg2), 35.0); + let r = s.execute("essential.remove", json!({"comp": card.0, "control": opacity})).unwrap(); + assert_eq!(r["mirrors"], json!([mirror])); + let eg = s.project.comp(card).unwrap().essential.clone().unwrap(); + assert!(eg.find(mirror).is_none() && essential::group(s.project.comp(main).unwrap().layer(LayerId(a)).unwrap()).is_some()); +} + #[test] fn template_export_import_round_trip_with_overrides() { let dir = out_dir("ectemplate"); diff --git a/crates/engine/src/tests_essential_more.rs b/crates/engine/src/tests_essential_more.rs index 314f7b6d..a8d8dc80 100644 --- a/crates/engine/src/tests_essential_more.rs +++ b/crates/engine/src/tests_essential_more.rs @@ -16,10 +16,20 @@ fn font_and_scale_controls_override_per_instance() { let t = s.execute("essential.addProperty", json!({"layer": text, "path": "text/sourceText"})).unwrap()["controls"][0].as_u64().unwrap(); let f = s.execute("essential.addProperty", json!({"layer": text, "path": "text/sourceText", "as": "font"})).unwrap()["controls"][0].as_u64().unwrap(); let sc = s.execute("essential.addProperty", json!({"layer": text, "path": "transform/scale", "as": "scale"})).unwrap()["controls"][0].as_u64().unwrap(); - // Adding the same font control again is a no-op; opacity can't be a font. + // Adding the same font control again adds nothing with `mirror: false` (else a mirror of + // it); opacity can't be a font. assert!( - s.execute("essential.addProperty", json!({"layer": text, "path": "text/sourceText", "as": "font"})).unwrap()["controls"].as_array().unwrap().is_empty() + s.execute("essential.addProperty", json!({"layer": text, "path": "text/sourceText", "as": "font", "mirror": false})).unwrap()["controls"] + .as_array() + .unwrap() + .is_empty() ); + let m = s.execute("essential.addProperty", json!({"layer": text, "path": "text/sourceText", "as": "font"})).unwrap(); + assert_eq!(m["mirrors"].as_array().unwrap().len(), 1); + let ml = s.execute("essential.list", json!({})).unwrap(); + let mirror = ml["controls"].as_array().unwrap().iter().find(|c| c["kind"] == "mirror").unwrap().clone(); + assert_eq!((mirror["of"].as_u64(), mirror["type"].as_str()), (Some(f), Some("font"))); + s.execute("edit.undo", json!({})).unwrap(); assert!(s.execute("essential.addProperty", json!({"layer": text, "path": "transform/opacity", "as": "font"})).is_err()); assert_eq!(s.execute("essential.canAdd", json!({"layer": text, "path": "transform/opacity", "as": "font"})).unwrap()["ok"], false); assert_eq!(s.execute("essential.canAdd", json!({"layer": text, "path": "transform/scale", "as": "scale"})).unwrap()["type"], "scale"); diff --git a/crates/engine/src/tests_tail.rs b/crates/engine/src/tests_tail.rs index 705425c0..7cd855c6 100644 --- a/crates/engine/src/tests_tail.rs +++ b/crates/engine/src/tests_tail.rs @@ -314,13 +314,32 @@ fn variable_font_axes_animator() { let l = comp.layer(LayerId(t)).unwrap(); effectcraft_render::text::glyph_paths(&ctx, l).iter().map(|(p, _)| effectcraft_text::kurbo::Shape::area(p).abs()).sum::() }; + // The distance between the two H's left edges: the H advance. + let gap = |s: &Session| { + let cid = s.active_comp_id().unwrap(); + let comp = s.project.comp(cid).unwrap(); + let ctx = effectcraft_render::EvalCtx::new(&s.project, cid, comp, s.time()); + let l = comp.layer(LayerId(t)).unwrap(); + let x0: Vec = effectcraft_render::text::glyph_paths(&ctx, l).iter().map(|(p, _)| effectcraft_text::kurbo::Shape::bounding_box(p).x0).collect(); + x0[1] - x0[0] + }; let before = area(&s); + let gap_before = gap(&s); let l = s.active_comp().unwrap().layer(LayerId(t)).unwrap().clone(); let pg = l.props.find_group(anim).unwrap().sub("properties").unwrap().props().next().unwrap().uid; let range = (axis.max - axis.min) as f64; s.execute("prop.set", json!({"layer": t, "prop": pg, "value": range})).unwrap(); let after = area(&s); assert!((after - before).abs() > 1.0, "{} axis moved the outline: {before} → {after}", axis.tag); + // Advances follow the axis too (the text is re-spaced, not only redrawn). + let a = axes.iter().find(|a| a.tag == tag).unwrap(); + let f = effectcraft_text::fonts::face(face); + let gid = f.glyph('H').unwrap(); + let upem = f.units_per_em() as f64; + let units = |v: f32| effectcraft_text::variable::advance_units_at(face, gid, &[(a.tag.clone(), v)]).unwrap() as f64; + let want = (units((a.default + range as f32).min(a.max)) - units(a.default)) * 60.0 / upem; + let moved = gap(&s) - gap_before; + assert!((moved - want).abs() < 0.5, "{} axis: advance change {moved}, expected {want}", a.tag); s.execute("edit.undo", json!({})).unwrap(); assert!((area(&s) - before).abs() < 1e-6); let _ = info; diff --git a/crates/engine/src/vector.rs b/crates/engine/src/vector.rs index 23fc3f24..34edf5b7 100644 --- a/crates/engine/src/vector.rs +++ b/crates/engine/src/vector.rs @@ -162,7 +162,8 @@ fn clipped_items(ids: &mut Ids, s: &effectcraft_svg::Shape, m: Affine, clips: &[ /// Clipping groups (PDF / Illustrator clipping masks): shapes inside them are intersected with /// the clip paths through Merge Paths ▸ Intersect (see [`clipped_items`]). Clips enclosing the /// whole document are layer masks instead ([`vector_clip_masks`], used by -/// [`shapes_from_vector`]). Images and soft masks are not converted. +/// [`shapes_from_vector`]). Images are skipped (Layer ▸ Create makes them footage layers, see +/// [`split_at_images`]); soft masks are not converted. pub fn svg_contents(ids: &mut Ids, doc: &Doc) -> Vec { fn walk(ids: &mut Ids, nodes: &[Node], m: Affine, clips: &[BezPath]) -> Vec { let mut out = vec![]; @@ -340,6 +341,131 @@ fn like_source(dst: &mut Layer, src: &Layer) { dst.switches.three_d = src.switches.three_d; } +/// A raster image of a vector document (Create Shapes from Vector Layer makes it a footage +/// layer: shape layers have no images). +pub struct VectorImage { + pub name: String, + pub width: u32, + pub height: u32, + /// Straight RGBA8. + pub rgba: std::sync::Arc>, + /// Image pixels → document pixels. + pub transform: Affine, + pub opacity: f64, + /// Inside a clipping group or under a soft mask (not carried over). + pub clipped: bool, +} + +/// The document's images, bottom first, and how many soft-masked groups it has. +pub fn vector_images(doc: &Doc) -> (Vec, usize) { + fn walk(g: &effectcraft_svg::Group, m: Affine, opacity: f64, clipped: bool, out: &mut Vec, masks: &mut usize) { + let m = m * g.transform; + let opacity = opacity * g.opacity; + let clipped = clipped || !g.clip.is_empty() || g.mask.is_some(); + if g.mask.is_some() { + *masks += 1; + } + for c in &g.children { + match c { + Node::Group(sub) => walk(sub, m, opacity, clipped, out, masks), + Node::Image(im) => out.push(VectorImage { + name: im.name.clone(), + width: im.width, + height: im.height, + rgba: im.rgba.clone(), + transform: m * im.transform, + opacity: opacity * im.opacity, + clipped, + }), + Node::Shape(_) => {} + } + } + } + let (_, doc) = vector_clip_masks(doc); + let mut out = vec![]; + let mut masks = 0; + walk(&doc.root, Affine::IDENTITY, 1.0, false, &mut out, &mut masks); + (out, masks) +} + +/// The document split at its images (paint order): segment `k` keeps the shapes drawn between +/// image `k − 1` and image `k` (groups, clips and blend modes kept, images dropped), so shape +/// layers and image footage layers can stack in the document's order. One more segment than +/// images. +pub fn split_at_images(doc: &Doc) -> Vec { + fn count(g: &effectcraft_svg::Group) -> usize { + g.children + .iter() + .map(|c| match c { + Node::Group(s) => count(s), + Node::Image(_) => 1, + Node::Shape(_) => 0, + }) + .sum() + } + /// Keep the shapes between images `seg − 1` and `seg`; `seen` counts images passed. + fn prune(g: &effectcraft_svg::Group, seg: usize, seen: &mut usize) -> effectcraft_svg::Group { + let mut out = g.clone(); + out.children = vec![]; + for c in &g.children { + match c { + Node::Image(_) => *seen += 1, + Node::Shape(_) => { + if *seen == seg { + out.children.push(c.clone()); + } + } + Node::Group(s) => { + let p = prune(s, seg, seen); + if !p.children.is_empty() { + out.children.push(Node::Group(p)); + } + } + } + } + out + } + let n = count(&doc.root); + (0..=n) + .map(|seg| { + let mut d = doc.clone(); + d.root = prune(&doc.root, seg, &mut 0); + d + }) + .collect() +} + +/// A footage layer for an image of a vector document, parented to `parent` (the shape layer, +/// whose layer space is document pixels) and placed by the image's transform (anchor at its +/// top-left corner; position, rotation and scale from the matrix). Returns a note when the +/// placement has a skew, which layer transforms cannot express. +pub fn image_layer(proj: &mut Project, comp: &Comp, item: ItemId, img: &VectorImage, parent: &Layer) -> (Layer, Option) { + let mut l = build::layer(proj, comp, &img.name, LayerSource::Footage { item }, (img.width, img.height), None); + let c = img.transform.as_coeffs(); + let theta = c[1].atan2(c[0]); + let (sn, cs) = theta.sin_cos(); + let sx = (c[0] * c[0] + c[1] * c[1]).sqrt(); + let sy = -c[2] * sn + c[3] * cs; + let skew = c[2] * cs + c[3] * sn; + let set = |l: &mut Layer, path: &str, v: Value| { + if let Some(p) = l.props.prop_mut(path) { + p.value = v; + } + }; + set(&mut l, "transform/anchor", Value::Vec3([0.0, 0.0, 0.0])); + set(&mut l, "transform/position", Value::Vec3([c[4], c[5], 0.0])); + set(&mut l, "transform/scale", Value::Vec3([sx * 100.0, sy * 100.0, 100.0])); + set(&mut l, "transform/rotation", Value::Scalar(theta.to_degrees())); + set(&mut l, "transform/opacity", Value::Scalar((img.opacity * 100.0).clamp(0.0, 100.0))); + l.start_time = parent.start_time; + l.in_point = parent.in_point; + l.out_point = parent.out_point; + l.stretch = parent.stretch; + l.parent = Some(parent.id); + let note = (skew.abs() > 1e-3 * sy.abs().max(1e-9)).then(|| format!("{}: skewed image placed without its skew", img.name)); + (l, note) +} + /// Layer ▸ Create ▸ Create Shapes from Vector Layer: `doc` is the SVG shown by `src`. A clip /// enclosing the whole document becomes the layer's masks (Add, then Intersect); nested clips /// are kept with Merge Paths (see [`svg_contents`]). diff --git a/crates/export/Cargo.toml b/crates/export/Cargo.toml index 6900c204..11f398cf 100644 --- a/crates/export/Cargo.toml +++ b/crates/export/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "effectcraft-export" -description = "Render Queue jobs and output modules: H.264 MP4 (+AAC) and ProRes MOV (+PCM) via FilmCraft's pure-Rust encoders, WebM (VP9 + Opus), PNG/JPEG/TIFF/EXR sequences, animated GIF, WAV/AIFF" +description = "Render Queue jobs and output modules: H.264 MP4 (+AAC) and ProRes MOV (+PCM) via FilmCraft, HEVC / AV1 MP4 (+AAC) and WebM (VP9 or AV1 + Opus) via EffectCraft encoders, PNG/JPEG/TIFF/EXR sequences, animated GIF, WAV/AIFF" version.workspace = true edition.workspace = true license.workspace = true @@ -20,6 +20,8 @@ filmcraft-aac = { workspace = true } filmcraft-isobmff = { workspace = true } effectcraft-opusenc = { workspace = true } effectcraft-vp9enc = { workspace = true } +effectcraft-hevcenc = { workspace = true } +effectcraft-av1enc = { workspace = true } image = { workspace = true, features = ["png", "jpeg", "tiff", "exr"] } gif = { workspace = true } rayon = { workspace = true } diff --git a/crates/export/src/encode.rs b/crates/export/src/encode.rs index b0566d9b..167b5f33 100644 --- a/crates/export/src/encode.rs +++ b/crates/export/src/encode.rs @@ -1,4 +1,5 @@ -//! Movie export: FilmCraft H.264 → MP4 (+ AAC) and ProRes → MOV (+ PCM), muxed by +//! Movie export: FilmCraft H.264, HEVC (`effectcraft-hevcenc`) and AV1 (`effectcraft-av1enc`) +//! → MP4 (+ AAC) and ProRes → MOV (+ PCM), muxed by //! FilmCraft's ISO BMFF / QuickTime writer. use std::io::Write; @@ -18,14 +19,14 @@ fn mux_err(e: impl std::fmt::Display) -> ExportError { ExportError::Io(e.to_string()) } -struct Packet { - data: Vec, - key: bool, +pub(crate) struct Packet { + pub(crate) data: Vec, + pub(crate) key: bool, /// pts − dts in the track timescale. - cto: i32, + pub(crate) cto: i32, } -trait VideoEncoder { +pub(crate) trait VideoEncoder { fn sample_entry(&self) -> SampleEntry; fn encode(&mut self, rgba: &[u8], index: u64) -> Result>; fn flush(&mut self) -> Result>; @@ -257,9 +258,11 @@ pub(crate) fn movie(job: &Cx, comp: &Comp, w: u32, h: u32, st: &mut State) -> Re }; let mut venc: Box = match fmt { OutputFormat::H264 => Box::new(H264::new(w, h, rate, job.output.bitrate_kbps)?), + OutputFormat::Hevc => Box::new(crate::hevc_av1::Hevc::new(w, h, rate, job.output)?), + OutputFormat::Av1 => Box::new(crate::hevc_av1::Av1::new(w, h, rate, job.output)?), _ => Box::new(ProRes::new(w, h, job.output.prores_profile, if channels == Channels::Alpha { Channels::Rgb } else { channels })), }; - let brand = if fmt == OutputFormat::H264 { Brand::Mp4 } else { Brand::Mov }; + let brand = if fmt == OutputFormat::ProRes { Brand::Mov } else { Brand::Mp4 }; let batch = batch_size(); let render = |k: u64| -> Vec { job.pixels(&job.frame(comp, k), comp, channels, w, h) }; diff --git a/crates/export/src/hevc_av1.rs b/crates/export/src/hevc_av1.rs new file mode 100644 index 00000000..f42ccda2 --- /dev/null +++ b/crates/export/src/hevc_av1.rs @@ -0,0 +1,203 @@ +//! HEVC (`effectcraft-hevcenc`) and AV1 (`effectcraft-av1enc`) video encoders for the MP4 +//! movie writer (`hvc1` / `av01` sample entries) and AV1 in WebM, configured from the Output +//! Module's [`VideoCodecOptions`]. + +use effectcraft_project::render_queue::{CodecProfile, OutputModule, RateControlMode, VideoCodecOptions}; +use effectcraft_time::FrameRate; +use filmcraft_isobmff::{Av1Config, CodecConfig, FourCc, HevcConfig, SampleEntry}; +use rayon::prelude::*; + +use crate::encode::{Packet, VideoEncoder}; +use crate::{ExportError, Result}; + +fn enc_err(e: impl std::fmt::Display) -> ExportError { + ExportError::Encode(e.to_string()) +} + +/// BT.709 limited-range 4:2:0 planes at `bits` (8 or 10) from RGBA8 (2×2 chroma average; +/// alpha ignored). Chroma planes are `ceil(w/2)` × `ceil(h/2)`. +pub(crate) fn rgba_to_yuv420_bits(rgba: &[u8], w: usize, h: usize, bits: u8, y: &mut Vec, u: &mut Vec, v: &mut Vec) { + let (cw, ch) = (w.div_ceil(2), h.div_ceil(2)); + let scale = (1u32 << (bits - 8)) as f32; + let (lo, hi) = (scale, 254.0 * scale + (scale - 1.0)); + y.resize(w * h, 0); + u.resize(cw * ch, 0); + v.resize(cw * ch, 0); + y.par_chunks_mut(w * 2).zip(u.par_chunks_mut(cw).zip(v.par_chunks_mut(cw))).enumerate().for_each(|(cy, (yr, (ur, vr)))| { + let rows = yr.len() / w; + let mut acc = vec![(0f32, 0f32, 0f32); cw]; + for dy in 0..rows { + let src = &rgba[(cy * 2 + dy) * w * 4..][..w * 4]; + for x in 0..w { + let (r, g, b) = (src[x * 4] as f32 / 255.0, src[x * 4 + 1] as f32 / 255.0, src[x * 4 + 2] as f32 / 255.0); + let l = 0.2126 * r + 0.7152 * g + 0.0722 * b; + yr[dy * w + x] = ((16.0 + 219.0 * l) * scale).round().clamp(lo, hi) as u16; + let a = &mut acc[x / 2]; + a.0 += (b - l) / 1.8556; + a.1 += (r - l) / 1.5748; + a.2 += 1.0; + } + } + for (cx, a) in acc.iter().enumerate() { + ur[cx] = ((128.0 + 224.0 * a.0 / a.2) * scale).round().clamp(lo, hi) as u16; + vr[cx] = ((128.0 + 224.0 * a.1 / a.2) * scale).round().clamp(lo, hi) as u16; + } + }); +} + +#[derive(Default)] +struct Planes { + y: Vec, + u: Vec, + v: Vec, +} + +// ---------------------------------------------------------------- HEVC + +pub(crate) struct Hevc { + enc: effectcraft_hevcenc::Encoder, + w: u32, + h: u32, + bits: u8, + planes: Planes, +} + +/// The HEVC encoder settings for an output module. +pub(crate) fn hevc_config(w: u32, h: u32, rate: FrameRate, om: &OutputModule) -> effectcraft_hevcenc::EncoderConfig { + use effectcraft_hevcenc as he; + let o: &VideoCodecOptions = &om.codec; + let mut cfg = he::EncoderConfig::new(w, h, rate.num as u32, rate.den as u32); + cfg.profile = match o.profile { + CodecProfile::Main => he::Profile::Main, + CodecProfile::Main10 => he::Profile::Main10, + }; + cfg.level_idc = o.hevc_level_idc(); + cfg.rate = match o.rate_control { + RateControlMode::Bitrate => he::RateControl::Bitrate { kbps: om.bitrate_kbps.max(50) }, + RateControlMode::Quality => he::RateControl::ConstantQp(o.hevc_qp()), + }; + cfg.keyint = om.keyint(rate.as_f64()); + cfg +} + +impl Hevc { + pub(crate) fn new(w: u32, h: u32, rate: FrameRate, om: &OutputModule) -> Result { + let cfg = hevc_config(w, h, rate, om); + let bits = cfg.profile.bit_depth(); + let enc = effectcraft_hevcenc::Encoder::new(cfg).map_err(enc_err)?; + Ok(Hevc { enc, w, h, bits, planes: Planes::default() }) + } +} + +impl VideoEncoder for Hevc { + fn sample_entry(&self) -> SampleEntry { + let cfg = HevcConfig::parse(&self.enc.hvcc()).unwrap_or_default(); + SampleEntry::hevc(cfg, self.w as u16, self.h as u16) + } + fn encode(&mut self, rgba: &[u8], _index: u64) -> Result> { + let (w, h) = (self.w as usize, self.h as usize); + let p = &mut self.planes; + rgba_to_yuv420_bits(rgba, w, h, self.bits, &mut p.y, &mut p.u, &mut p.v); + let frame = effectcraft_hevcenc::Frame { y: &p.y, u: &p.u, v: &p.v, y_stride: w, uv_stride: w.div_ceil(2) }; + let pk = self.enc.encode(&frame); + Ok(vec![Packet { data: pk.data, key: pk.keyframe, cto: 0 }]) + } + fn flush(&mut self) -> Result> { + Ok(vec![]) + } +} + +// ---------------------------------------------------------------- AV1 + +pub(crate) struct Av1 { + enc: effectcraft_av1enc::Encoder, + w: u32, + h: u32, + bits: u8, + planes: Planes, +} + +/// The AV1 encoder settings for an output module. +pub(crate) fn av1_config(w: u32, h: u32, rate: FrameRate, om: &OutputModule) -> effectcraft_av1enc::EncoderConfig { + use effectcraft_av1enc as ae; + let o: &VideoCodecOptions = &om.codec; + let mut cfg = ae::EncoderConfig::new(w, h, rate.num as u32, rate.den as u32); + cfg.bit_depth = o.profile.bit_depth(); + cfg.level_idx = o.av1_level_idx(); + cfg.rate = match o.rate_control { + RateControlMode::Bitrate => ae::RateControl::Bitrate { kbps: om.bitrate_kbps.max(50) }, + RateControlMode::Quality => ae::RateControl::ConstantQ(o.av1_qindex()), + }; + cfg.keyint = om.keyint(rate.as_f64()); + cfg +} + +impl Av1 { + pub(crate) fn new(w: u32, h: u32, rate: FrameRate, om: &OutputModule) -> Result { + let cfg = av1_config(w, h, rate, om); + let bits = cfg.bit_depth; + let enc = effectcraft_av1enc::Encoder::new(cfg).map_err(enc_err)?; + Ok(Av1 { enc, w, h, bits, planes: Planes::default() }) + } + /// The `av1C` record (also the WebM CodecPrivate). + pub(crate) fn av1c(&self) -> Vec { + self.enc.av1c() + } +} + +impl VideoEncoder for Av1 { + fn sample_entry(&self) -> SampleEntry { + let cfg = Av1Config::parse(&self.enc.av1c()).unwrap_or_default(); + SampleEntry::video(FourCc(*b"av01"), CodecConfig::Av1(cfg), self.w as u16, self.h as u16) + } + fn encode(&mut self, rgba: &[u8], _index: u64) -> Result> { + let (w, h) = (self.w as usize, self.h as usize); + let p = &mut self.planes; + rgba_to_yuv420_bits(rgba, w, h, self.bits, &mut p.y, &mut p.u, &mut p.v); + let frame = effectcraft_av1enc::Frame { y: &p.y, u: &p.u, v: &p.v, y_stride: w, uv_stride: w.div_ceil(2) }; + let pk = self.enc.encode(&frame); + Ok(vec![Packet { data: pk.data, key: pk.keyframe, cto: 0 }]) + } + fn flush(&mut self) -> Result> { + Ok(vec![]) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn ten_bit_planes_scale_eight_bit_ones() { + let rgba = [255u8, 255, 255, 255, 0, 0, 0, 255, 255, 0, 0, 255, 0, 255, 0, 255]; + let (mut y, mut u, mut v) = (vec![], vec![], vec![]); + rgba_to_yuv420_bits(&rgba, 2, 2, 8, &mut y, &mut u, &mut v); + assert_eq!((y[0], y[1]), (235, 16)); + rgba_to_yuv420_bits(&rgba, 2, 2, 10, &mut y, &mut u, &mut v); + assert_eq!((y[0], y[1]), (940, 64)); + assert_eq!(u.len(), 1); + } + + #[test] + fn codec_options_map_to_encoder_settings() { + let mut om = OutputModule::for_format(effectcraft_project::render_queue::OutputFormat::Hevc); + om.codec.profile = CodecProfile::Main10; + om.codec.level = Some(41); + om.codec.rate_control = RateControlMode::Quality; + om.codec.quality = 100; + om.keyframe_interval = 12; + let rate = FrameRate::new(30, 1); + let h = hevc_config(64, 32, rate, &om); + assert_eq!(h.profile, effectcraft_hevcenc::Profile::Main10); + assert_eq!(h.level_idc, Some(123)); + assert_eq!(h.rate, effectcraft_hevcenc::RateControl::ConstantQp(4)); + assert_eq!(h.keyint, 12); + let a = av1_config(64, 32, rate, &om); + assert_eq!((a.bit_depth, a.level_idx, a.keyint), (10, Some(9), 12)); + om.codec.rate_control = RateControlMode::Bitrate; + om.keyframe_interval = 0; + om.bitrate_kbps = 3000; + let a = av1_config(64, 32, rate, &om); + assert_eq!((a.rate, a.keyint), (effectcraft_av1enc::RateControl::Bitrate { kbps: 3000 }, 60)); + } +} diff --git a/crates/export/src/lib.rs b/crates/export/src/lib.rs index f1e3269f..ae889742 100644 --- a/crates/export/src/lib.rs +++ b/crates/export/src/lib.rs @@ -7,7 +7,9 @@ //! | PNG / JPEG / TIFF sequence | one file per frame | 8-bit | PNG, TIFF | — | //! | OpenEXR sequence | one file per frame | 32-bit float, linear light, premultiplied | yes | — | //! | Animated GIF | GIF89a | 256-colour palette per frame (NeuQuant) | 1-bit | — | -//! | WebM | WebM (Matroska) | VP9 profile 0 (`effectcraft-vp9enc`: key + inter frames, a key frame every 2 s, loop filter), 8-bit 4:2:0 | VP9 alpha (BlockAdditional) | Opus (`effectcraft-opusenc`), 48 kHz stereo | +//! | WebM | WebM (Matroska) | VP9 profile 0 (`effectcraft-vp9enc`: key + inter frames, a key frame every 2 s, loop filter), 8-bit 4:2:0; or AV1 (`effectcraft-av1enc`) | VP9 alpha (BlockAdditional) | Opus (`effectcraft-opusenc`: SILK / hybrid / CELT by bitrate and application), 48 kHz | +//! | HEVC | MP4 (`hvc1`) | `effectcraft-hevcenc` Main / Main 10 4:2:0, I + P slices, bitrate or constant quality, level, key-frame interval | no | AAC-LC (FilmCraft) | +//! | AV1 | MP4 (`av01`) | `effectcraft-av1enc` Main 8/10-bit 4:2:0, key + inter frames | no | AAC-LC (FilmCraft) | //! | WAV / AIFF | RIFF WAVE / AIFF | — | — | PCM 16/24-bit (WAV also 32-bit float), mono/stereo (audio only) | //! //! Every frame goes through [`pipeline`]: the Render Settings overrides (Effects, Solo Switches, @@ -27,9 +29,11 @@ //! output is a single pixel wide/high). mod encode; +mod hevc_av1; mod out; mod pipeline; mod webm; +mod webm_av1; use web_time::Instant; @@ -173,7 +177,7 @@ fn export_with(cx: &Cx, progress: &mut dyn FnMut(&Progress) -> bool, t0: Instant st.advance(0)?; let (w, h) = output_size(comp, cx.settings, cx.output); let mut report = match cx.output.format { - OutputFormat::H264 | OutputFormat::ProRes => encode::movie(cx, comp, w, h, &mut st)?, + OutputFormat::H264 | OutputFormat::Hevc | OutputFormat::Av1 | OutputFormat::ProRes => encode::movie(cx, comp, w, h, &mut st)?, OutputFormat::Gif => gif_export(cx, comp, w, h, &mut st)?, OutputFormat::WebM => webm::webm(cx, comp, w, h, &mut st)?, OutputFormat::Wav => webm::audio_file(cx, comp, false, &mut st)?, diff --git a/crates/export/src/webm.rs b/crates/export/src/webm.rs index af38eaeb..de7c8684 100644 --- a/crates/export/src/webm.rs +++ b/crates/export/src/webm.rs @@ -22,25 +22,24 @@ use rayon::prelude::*; use crate::encode::{mix, pcm_bytes}; use crate::{Cx, Report, Result, State, batch_size, io, wants_audio}; -const OPUS_RATE: u32 = 48_000; -const OPUS_BITRATE: u32 = 192_000; +pub(crate) const OPUS_RATE: u32 = 48_000; // Element ids (with their length markers). -const EBML: u32 = 0x1A45_DFA3; -const SEGMENT: u32 = 0x1853_8067; -const INFO: u32 = 0x1549_A966; -const TRACKS: u32 = 0x1654_AE6B; -const TRACK_ENTRY: u32 = 0xAE; -const CLUSTER: u32 = 0x1F43_B675; +pub(crate) const EBML: u32 = 0x1A45_DFA3; +pub(crate) const SEGMENT: u32 = 0x1853_8067; +pub(crate) const INFO: u32 = 0x1549_A966; +pub(crate) const TRACKS: u32 = 0x1654_AE6B; +pub(crate) const TRACK_ENTRY: u32 = 0xAE; +pub(crate) const CLUSTER: u32 = 0x1F43_B675; -fn id_bytes(id: u32) -> Vec { +pub(crate) fn id_bytes(id: u32) -> Vec { let b = id.to_be_bytes(); let skip = b.iter().position(|x| *x != 0).unwrap_or(3); b[skip..].to_vec() } /// EBML variable-size integer for an element data size (shortest form). -fn vint(n: u64) -> Vec { +pub(crate) fn vint(n: u64) -> Vec { for len in 1..=8u32 { let max = (1u64 << (7 * len)) - 2; if n <= max { @@ -53,23 +52,23 @@ fn vint(n: u64) -> Vec { v } -fn el(out: &mut Vec, id: u32, data: &[u8]) { +pub(crate) fn el(out: &mut Vec, id: u32, data: &[u8]) { out.extend(id_bytes(id)); out.extend(vint(data.len() as u64)); out.extend_from_slice(data); } -fn el_uint(out: &mut Vec, id: u32, v: u64) { +pub(crate) fn el_uint(out: &mut Vec, id: u32, v: u64) { let b = v.to_be_bytes(); let skip = b.iter().position(|x| *x != 0).unwrap_or(7); el(out, id, &b[skip..]); } -fn el_float(out: &mut Vec, id: u32, v: f64) { +pub(crate) fn el_float(out: &mut Vec, id: u32, v: f64) { el(out, id, &v.to_be_bytes()); } -fn el_str(out: &mut Vec, id: u32, s: &str) { +pub(crate) fn el_str(out: &mut Vec, id: u32, s: &str) { el(out, id, s.as_bytes()); } @@ -125,7 +124,20 @@ fn cluster(frames: &mut [Frame]) -> Vec { out } +/// The Opus encoder of a WebM output: the module's channel count, bitrate and application. +pub(crate) fn opus_encoder(job: &Cx) -> effectcraft_opusenc::OpusEncoder { + let channels = if job.output.audio_channels == 1 { 1 } else { 2 }; + let app = match job.output.opus_application { + effectcraft_project::render_queue::OpusApplication::Audio => effectcraft_opusenc::Application::Audio, + effectcraft_project::render_queue::OpusApplication::Voip => effectcraft_opusenc::Application::Voip, + }; + effectcraft_opusenc::OpusEncoder::with_application(channels, job.output.opus_bitrate_kbps.clamp(6, 510) * 1000, app) +} + pub(crate) fn webm(job: &Cx, comp: &Comp, w: u32, h: u32, st: &mut State) -> Result { + if job.output.webm_codec == effectcraft_project::render_queue::WebmVideoCodec::Av1 { + return crate::webm_av1::webm_av1(job, comp, w, h, st); + } let rate = job.settings.rate(comp); let alpha = job.output.channels == Channels::Rgba; let colour_channels = if job.output.channels == Channels::Alpha { Channels::Alpha } else { Channels::Rgb }; @@ -145,7 +157,7 @@ pub(crate) fn webm(job: &Cx, comp: &Comp, w: u32, h: u32, st: &mut State) -> Res let mut alpha_enc = alpha.then(|| effectcraft_vp9enc::Vp9Encoder::new(cfg.clone())); let with_audio = wants_audio(job); let opus_channels = if job.output.audio_channels == 1 { 1usize } else { 2 }; - let mut opus = with_audio.then(|| effectcraft_opusenc::OpusEncoder::new(opus_channels as _, OPUS_BITRATE)); + let mut opus = with_audio.then(|| opus_encoder(job)); let total = st.total; let frame_ms = |k: u64| -> i64 { ((k as i128 * 1000 * rate.den as i128 + rate.num as i128 / 2) / rate.num as i128) as i64 }; let duration_ms = frame_ms(total) as f64; diff --git a/crates/export/src/webm_av1.rs b/crates/export/src/webm_av1.rs new file mode 100644 index 00000000..69378e4f --- /dev/null +++ b/crates/export/src/webm_av1.rs @@ -0,0 +1,177 @@ +//! WebM with AV1 video (`V_AV1`, CodecPrivate = the `av1C` record, per the AV1-in-Matroska +//! codec mapping) and Opus audio. Frames are rendered in parallel batches and encoded in order +//! (AV1 inter frames reference the previous frame); each batch becomes one Cluster whose +//! SimpleBlocks carry the key-frame flag only on AV1 key frames (the Cues-less layout of the +//! VP9 writer: players seek by scanning clusters). + +use std::io::{Seek, SeekFrom, Write}; + +use effectcraft_project::Comp; +use effectcraft_project::render_queue::Channels; +use effectcraft_time::{TICKS_PER_SECOND, Tick}; +use rayon::prelude::*; + +use crate::encode::VideoEncoder; +use crate::encode::mix; +use crate::webm::{CLUSTER, EBML, INFO, OPUS_RATE, SEGMENT, TRACK_ENTRY, TRACKS, el, el_float, el_str, el_uint, id_bytes, opus_encoder, vint}; +use crate::{Cx, Report, Result, State, batch_size, io, wants_audio}; + +struct Block { + ms: i64, + track: u64, + key: bool, + data: Vec, +} + +fn cluster(blocks: &mut [Block]) -> Vec { + blocks.sort_by_key(|b| (b.ms, b.track)); + let base = blocks.first().map_or(0, |b| b.ms); + let mut body = vec![]; + el_uint(&mut body, 0xE7, base.max(0) as u64); + for b in blocks.iter() { + let rel = (b.ms - base).clamp(i16::MIN as i64, i16::MAX as i64) as i16; + let mut p = vint(b.track); + p.extend_from_slice(&rel.to_be_bytes()); + p.push(if b.key { 0x80 } else { 0x00 }); + p.extend_from_slice(&b.data); + el(&mut body, 0xA3, &p); + } + let mut out = vec![]; + el(&mut out, CLUSTER, &body); + out +} + +pub(crate) fn webm_av1(job: &Cx, comp: &Comp, w: u32, h: u32, st: &mut State) -> Result { + let rate = job.settings.rate(comp); + let mut venc = crate::hevc_av1::Av1::new(w, h, rate, job.output)?; + let with_audio = wants_audio(job); + let opus_channels = if job.output.audio_channels == 1 { 1usize } else { 2 }; + let mut opus = with_audio.then(|| opus_encoder(job)); + let total = st.total; + let frame_ms = |k: u64| -> i64 { ((k as i128 * 1000 * rate.den as i128 + rate.num as i128 / 2) / rate.num as i128) as i64 }; + let duration_ms = frame_ms(total) as f64; + + let path = job.place(job.path, 1); + let mut file = crate::out::create(job.sink, &path)?; + let mut head = vec![]; + let mut eb = vec![]; + el_uint(&mut eb, 0x4286, 1); + el_uint(&mut eb, 0x42F7, 1); + el_uint(&mut eb, 0x42F2, 4); + el_uint(&mut eb, 0x42F3, 8); + el_str(&mut eb, 0x4282, "webm"); + el_uint(&mut eb, 0x4287, 4); + el_uint(&mut eb, 0x4285, 2); + el(&mut head, EBML, &eb); + head.extend(id_bytes(SEGMENT)); + let size_pos = head.len() as u64; + head.extend_from_slice(&[0x01, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF]); + let seg_start = head.len() as u64; + let mut info = vec![]; + el_uint(&mut info, 0x2AD7B1, 1_000_000); + el_float(&mut info, 0x4489, duration_ms); + el_str(&mut info, 0x4D80, "EffectCraft"); + el_str(&mut info, 0x5741, "EffectCraft"); + el(&mut head, INFO, &info); + let mut tracks = vec![]; + let mut v = vec![]; + el_uint(&mut v, 0xD7, 1); + el_uint(&mut v, 0x73C5, 1); + el_uint(&mut v, 0x83, 1); + el_uint(&mut v, 0x9C, 0); + el_str(&mut v, 0x86, "V_AV1"); + el(&mut v, 0x63A2, &venc.av1c()); + el_uint(&mut v, 0x23E383, (1_000_000_000u128 * rate.den as u128 / rate.num.max(1) as u128) as u64); + let mut video = vec![]; + el_uint(&mut video, 0xB0, w as u64); + el_uint(&mut video, 0xBA, h as u64); + el(&mut v, 0xE0, &video); + el(&mut tracks, TRACK_ENTRY, &v); + if let Some(o) = &opus { + let mut a = vec![]; + el_uint(&mut a, 0xD7, 2); + el_uint(&mut a, 0x73C5, 2); + el_uint(&mut a, 0x83, 2); + el_uint(&mut a, 0x9C, 0); + el_str(&mut a, 0x86, "A_OPUS"); + el(&mut a, 0x63A2, &o.opus_head()); + el_uint(&mut a, 0x56AA, o.pre_skip() as u64 * 1_000_000_000 / OPUS_RATE as u64); + el_uint(&mut a, 0x56BB, 80_000_000); + let mut au = vec![]; + el_float(&mut au, 0xB5, OPUS_RATE as f64); + el_uint(&mut au, 0x9F, opus_channels as u64); + el(&mut a, 0xE1, &au); + el(&mut tracks, TRACK_ENTRY, &a); + } + el(&mut head, TRACKS, &tracks); + file.write_all(&head).map_err(io)?; + let mut seg_len = head.len() as u64 - seg_start; + + let (span_start, span_end) = job.settings.span(comp); + let mut cursor = span_start.to_units_floor(OPUS_RATE as i64); + let mut pcm_left: Vec = vec![]; + let mut packets = 0u64; + let mut audio_until = |until: Tick, blocks: &mut Vec, finish: bool| { + let Some(enc) = opus.as_mut() else { return }; + let end = until.to_units_floor(OPUS_RATE as i64); + if end > cursor { + let n = (end - cursor) as usize; + let start = Tick(((cursor as i128 * TICKS_PER_SECOND as i128) / OPUS_RATE as i128) as i64); + pcm_left.extend(mix(job, start, n, OPUS_RATE)); + cursor = end; + } + let fs = effectcraft_opusenc::OpusEncoder::FRAME_SIZE * opus_channels; + if finish && !pcm_left.is_empty() { + let pad = (fs - pcm_left.len() % fs) % fs + fs; + pcm_left.extend(std::iter::repeat_n(0.0, pad)); + } + while pcm_left.len() >= fs { + let pkt = enc.encode_float(&pcm_left[..fs]); + pcm_left.drain(..fs); + blocks.push(Block { ms: packets as i64 * 20, track: 2, key: true, data: pkt }); + packets += 1; + } + }; + + let batch = batch_size(); + let mut i = 0; + let flush_blocks = |blocks: &mut Vec, file: &mut crate::out::Out, seg_len: &mut u64| -> Result<()> { + if !blocks.is_empty() { + let c = cluster(blocks); + file.write_all(&c).map_err(io)?; + *seg_len += c.len() as u64; + blocks.clear(); + } + Ok(()) + }; + let mut blocks = vec![]; + while i < total { + let end = (i + batch).min(total); + // RGB, or the alpha matte alone (Channels: Alpha); AV1 in WebM carries no alpha track. + let channels = if job.output.channels == Channels::Alpha { Channels::Alpha } else { Channels::Rgb }; + let frames: Vec> = (i..end).into_par_iter().map(|k| job.pixels(&job.frame(comp, k), comp, channels, w, h)).collect(); + for (j, px) in frames.iter().enumerate() { + let k = i + j as u64; + for p in venc.encode(px, k)? { + blocks.push(Block { ms: frame_ms(k), track: 1, key: p.key, data: p.data }); + } + } + let t_end = if end >= total { span_end } else { job.settings.frame_time(comp, end) }; + audio_until(t_end, &mut blocks, end >= total); + flush_blocks(&mut blocks, &mut file, &mut seg_len)?; + st.advance(end - i)?; + i = end; + } + if total == 0 { + audio_until(span_end, &mut blocks, true); + flush_blocks(&mut blocks, &mut file, &mut seg_len)?; + } + let mut size = [0u8; 8]; + size[0] = 0x01; + size[1..].copy_from_slice(&seg_len.to_be_bytes()[1..]); + file.seek(SeekFrom::Start(size_pos)).map_err(io)?; + file.write_all(&size).map_err(io)?; + file.seek(SeekFrom::End(0)).map_err(io)?; + let bytes = file.finish()?; + Ok(Report { path, frames: 0, width: w, height: h, seconds: 0.0, bytes, audio: with_audio, log: None, overflow: vec![] }) +} diff --git a/crates/export/tests/hevc_av1_roundtrip.rs b/crates/export/tests/hevc_av1_roundtrip.rs new file mode 100644 index 00000000..93a82719 --- /dev/null +++ b/crates/export/tests/hevc_av1_roundtrip.rs @@ -0,0 +1,313 @@ +//! HEVC (MP4 `hvc1`) and AV1 (MP4 `av01`, WebM `V_AV1`) export, decoded back through +//! EffectCraft's own import path (FilmCraft's demuxers and HEVC / AV1 decoders) and — when +//! installed — ffprobe / ffmpeg as external oracles (skipped otherwise). +//! +//! The comp: 64×48 @ 10 fps, 1.2 s (12 frames), dark-blue background, a red solid of 32×48 +//! centred, starting at frame 5 (inter frames code both static and changing blocks). + +use std::path::{Path, PathBuf}; +use std::process::Command; + +use effectcraft_color::Label; +use effectcraft_export::render_queue::{ + AudioOutput, CodecProfile, OpusApplication, OutputFormat, OutputModule, RateControlMode, RenderSettings, TimeSpan, WebmVideoCodec, +}; +use effectcraft_export::{Job, Progress, export}; +use effectcraft_media::MediaPool; +use effectcraft_project::{Comp, ItemId, ItemKind, LayerSource, Project, Solid, build}; +use effectcraft_raster::Image; +use effectcraft_render::{FootageSource, NoFootage}; +use effectcraft_time::{FrameRate, Tick}; + +const W: u32 = 64; +const H: u32 = 48; +const BG: [f32; 3] = [0.1, 0.1, 0.4]; +const FRAMES: u64 = 12; + +fn out_dir(name: &str) -> PathBuf { + let d = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../target/test-out/export-hevc-av1").join(name); + let _ = std::fs::remove_dir_all(&d); + std::fs::create_dir_all(&d).expect("mkdir"); + d +} + +fn project(audio: Option<&Path>) -> (Project, ItemId) { + let mut p = Project::default(); + let mut comp = Comp::new(W, H, FrameRate::new(10, 1), Tick::from_seconds_f64(1.2)); + comp.background = BG; + let sid = p.add_item("Red", Label::Red, None, ItemKind::Solid(Solid { color: [1.0, 0.0, 0.0], width: 32, height: H, pixel_aspect: 1.0 })); + let mut l = build::layer(&mut p, &comp, "Red", LayerSource::Solid { item: sid }, (32, H), None); + l.in_point = comp.frame_rate.tick_of(5); + comp.layers.push(l); + if let Some(path) = audio { + let f = effectcraft_media::probe(path).expect("probe wav"); + let fid = p.add_item("tone.wav", Label::SeaFoam, None, ItemKind::Footage(f)); + let l = build::layer(&mut p, &comp, "tone", LayerSource::Footage { item: fid }, (0, 0), None); + comp.layers.push(l); + } + let cid = p.add_item("Comp 1", Label::Sandstone, None, ItemKind::Comp(comp.into())); + (p, cid) +} + +fn run(p: &Project, cid: ItemId, footage: &dyn FootageSource, om: &OutputModule, path: &Path) -> effectcraft_export::Report { + let s = RenderSettings { time_span: TimeSpan::LengthOfComp, ..Default::default() }; + let path = path.to_string_lossy().to_string(); + let job = Job { + project: p, + footage, + expr: None, + accel: None, + comp: cid, + settings: &s, + output: om, + path: &path, + sink: None, + nested_switches: true, + options: Default::default(), + }; + let mut last = Progress::default(); + let r = export(&job, &mut |pr| { + last = *pr; + true + }) + .expect("export"); + assert_eq!((last.done, r.frames), (FRAMES, FRAMES)); + r +} + +fn px(img: &Image, x: u32, y: u32) -> [f32; 4] { + img.data[img.idx(x, y)] +} + +fn near(a: [f32; 3], b: [f32; 3], tol: f32) -> bool { + a.iter().zip(b).all(|(x, y)| (x - y).abs() <= tol) +} + +fn check_frame(img: &Image, k: u64, tol: f32) { + let corner = px(img, 2, 2); + assert!(near([corner[0], corner[1], corner[2]], BG, tol), "frame {k} corner {corner:?}"); + let c = px(img, img.width / 2, img.height / 2); + let want = if k >= 5 { [1.0, 0.0, 0.0] } else { BG }; + assert!(near([c[0], c[1], c[2]], want, tol), "frame {k} centre {c:?} want {want:?}"); +} + +/// Import our file through the media layer and check every frame. +fn decode_movie(path: &Path, tol: f32) -> effectcraft_project::Footage { + let f = effectcraft_media::probe(path).expect("probe our output"); + assert_eq!((f.width, f.height), (W, H)); + assert!(f.has_video); + let n = f.frame_rate.frame_at(f.duration - Tick(1)) + 1; + assert_eq!(n as u64, FRAMES, "frame count from duration {:?} @ {:?}", f.duration, f.frame_rate); + let pool = MediaPool::new(); + for k in 0..FRAMES { + let img = pool.frame_at(&f, f.frame_rate.tick_of(k as i64)).expect("decode"); + check_frame(&img, k, tol); + } + f +} + +fn have(tool: &str) -> bool { + Command::new(tool).arg("-version").output().map(|o| o.status.success()).unwrap_or(false) +} + +/// `codec|w|h|pix_fmt|frames` of the video stream, or `None` without ffprobe. +fn ffprobe_video(path: &Path) -> Option { + if !have("ffprobe") { + eprintln!("ffprobe not found: skipping the oracle"); + return None; + } + let out = Command::new("ffprobe") + .args(["-v", "error", "-select_streams", "v:0", "-count_frames", "-show_entries", "stream=codec_name,width,height,pix_fmt,nb_read_frames"]) + .args(["-of", "compact=p=0:nk=1"]) + .arg(path) + .output() + .expect("ffprobe"); + assert!(out.status.success() && out.stderr.is_empty(), "ffprobe rejected {}: {}", path.display(), String::from_utf8_lossy(&out.stderr)); + Some(String::from_utf8_lossy(&out.stdout).trim().to_string()) +} + +/// Decode every frame with ffmpeg (no errors allowed) and check frames 0, 4, 5 and the last. +fn ffmpeg_check(path: &Path, decoder: Option<&str>, tol: f32) { + if !have("ffmpeg") { + eprintln!("ffmpeg not found: skipping the oracle decode"); + return; + } + let dir = path.with_extension("ffmpeg"); + let _ = std::fs::create_dir_all(&dir); + let mut cmd = Command::new("ffmpeg"); + cmd.args(["-hide_banner", "-loglevel", "error", "-xerror", "-y"]); + if let Some(d) = decoder { + cmd.args(["-c:v", d]); + } + let out = cmd.arg("-i").arg(path).args(["-pix_fmt", "rgba"]).arg(dir.join("f%03d.png")).output().expect("ffmpeg"); + assert!(out.status.success() && out.stderr.is_empty(), "ffmpeg ({decoder:?}) failed on {}: {}", path.display(), String::from_utf8_lossy(&out.stderr)); + for k in [0, 4, 5, FRAMES - 1] { + let img = image::open(dir.join(format!("f{:03}.png", k + 1))).expect("ffmpeg frame").to_rgba8(); + check_frame(&Image::from_rgba8(img.width(), img.height(), img.as_raw()), k, tol); + } +} + +fn module(format: OutputFormat, keyint: u32) -> OutputModule { + let mut om = OutputModule::for_format(format); + om.keyframe_interval = keyint; + om.codec.rate_control = RateControlMode::Quality; + om.codec.quality = 80; + om.audio = AudioOutput::Off; + om +} + +/// Key-frame flags of the MP4 video samples (via ffprobe), or `None` without it. +fn ffprobe_keyframes(path: &Path) -> Option> { + if !have("ffprobe") { + return None; + } + let out = Command::new("ffprobe") + .args(["-v", "error", "-select_streams", "v:0", "-show_entries", "packet=flags", "-of", "csv=p=0"]) + .arg(path) + .output() + .expect("ffprobe"); + Some(String::from_utf8_lossy(&out.stdout).lines().map(|l| l.starts_with('K')).collect()) +} + +#[test] +fn hevc_mp4_roundtrip() { + let d = out_dir("hevc"); + let (p, cid) = project(None); + let om = module(OutputFormat::Hevc, 4); + let path = d.join("comp.mp4"); + let r = run(&p, cid, &NoFootage, &om, &path); + assert!(r.bytes > 100 && !r.audio); + let bytes = std::fs::read(&path).unwrap(); + assert!(bytes.windows(4).any(|w| w == b"hvc1") && bytes.windows(4).any(|w| w == b"hvcC")); + decode_movie(&path, 0.08); + if let Some(s) = ffprobe_video(&path) { + assert_eq!(s, "hevc|64|48|yuv420p|12"); + } + if let Some(k) = ffprobe_keyframes(&path) { + assert_eq!(k, (0..FRAMES).map(|i| i % 4 == 0).collect::>(), "IDR every 4 frames"); + } + ffmpeg_check(&path, None, 0.08); +} + +#[test] +fn hevc_main10_bitrate_mode() { + let d = out_dir("hevc10"); + let (p, cid) = project(None); + let mut om = module(OutputFormat::Hevc, 0); + om.codec.profile = CodecProfile::Main10; + om.codec.rate_control = RateControlMode::Bitrate; + om.bitrate_kbps = 2_000; + om.codec.level = Some(31); + let path = d.join("comp10.mp4"); + run(&p, cid, &NoFootage, &om, &path); + decode_movie(&path, 0.08); + if let Some(s) = ffprobe_video(&path) { + assert_eq!(s, "hevc|64|48|yuv420p10le|12"); + } + ffmpeg_check(&path, None, 0.08); +} + +#[test] +fn av1_mp4_roundtrip() { + let d = out_dir("av1"); + let (p, cid) = project(None); + let om = module(OutputFormat::Av1, 6); + let path = d.join("comp.mp4"); + run(&p, cid, &NoFootage, &om, &path); + let bytes = std::fs::read(&path).unwrap(); + assert!(bytes.windows(4).any(|w| w == b"av01") && bytes.windows(4).any(|w| w == b"av1C")); + decode_movie(&path, 0.08); + if let Some(s) = ffprobe_video(&path) { + assert_eq!(s, "av1|64|48|yuv420p|12"); + } + if let Some(k) = ffprobe_keyframes(&path) { + assert_eq!(k, (0..FRAMES).map(|i| i % 6 == 0).collect::>(), "key frame every 6 frames"); + } + ffmpeg_check(&path, Some("libdav1d"), 0.08); +} + +#[test] +fn av1_webm_with_opus() { + let d = out_dir("av1-webm"); + let wav = d.join("tone.wav"); + write_tone(&wav, 2.0); + let (p, cid) = project(Some(&wav)); + let pool = MediaPool::new(); + let mut om = module(OutputFormat::WebM, 0); + om.webm_codec = WebmVideoCodec::Av1; + om.codec.profile = CodecProfile::Main10; + om.audio = AudioOutput::Auto; + om.opus_bitrate_kbps = 96; + let path = d.join("comp.webm"); + let r = run(&p, cid, &pool, &om, &path); + assert!(r.audio); + let f = decode_movie(&path, 0.08); + assert!(f.has_audio); + let s = MediaPool::new().audio_samples(&f, Tick::from_seconds_f64(0.3), 24_000, 48_000); + let l = rms(s.iter().step_by(2).copied()); + assert!((l - 0.3536).abs() < 0.06, "left RMS {l}"); + if let Some(s) = ffprobe_video(&path) { + assert_eq!(s, "av1|64|48|yuv420p10le|12"); + } + ffmpeg_check(&path, Some("libdav1d"), 0.08); +} + +/// Low-bitrate WebM audio goes through Opus SILK (voice) or hybrid (audio) coding and still +/// decodes to the tone (FilmCraft's decoder, and ffmpeg when installed). +#[test] +fn webm_low_bitrate_opus_modes() { + let d = out_dir("opus-modes"); + let wav = d.join("tone.wav"); + write_tone(&wav, 2.0); + let (p, cid) = project(Some(&wav)); + let pool = MediaPool::new(); + for (kbps, app, name) in [(24u32, OpusApplication::Voip, "voice24.webm"), (40, OpusApplication::Audio, "audio40.webm")] { + let mut om = OutputModule::for_format(OutputFormat::WebM); + om.audio = AudioOutput::On; + om.opus_bitrate_kbps = kbps; + om.opus_application = app; + let path = d.join(name); + assert!(run(&p, cid, &pool, &om, &path).audio); + let f = effectcraft_media::probe(&path).expect("probe"); + let s = MediaPool::new().audio_samples(&f, Tick::from_seconds_f64(0.3), 24_000, 48_000); + let l = rms(s.iter().step_by(2).copied()); + assert!((l - 0.3536).abs() < 0.08, "{name}: left RMS {l}"); + if have("ffmpeg") { + let out = + Command::new("ffmpeg").args(["-v", "error", "-xerror", "-i"]).arg(&path).args(["-map", "0:a", "-f", "null", "-"]).output().expect("ffmpeg"); + assert!(out.status.success() && out.stderr.is_empty(), "{name}: {}", String::from_utf8_lossy(&out.stderr)); + } + } +} + +fn rms(v: impl Iterator) -> f32 { + let (s, n) = v.fold((0.0f64, 0usize), |(s, n), x| (s + (x as f64) * (x as f64), n + 1)); + (s / n.max(1) as f64).sqrt() as f32 +} + +/// 16-bit stereo WAV with a 440 Hz sine at amplitude 0.5 (left) and silence (right). +fn write_tone(path: &Path, secs: f64) { + let sr = 48_000u32; + let n = (secs * sr as f64) as usize; + let mut data = Vec::with_capacity(n * 4); + for i in 0..n { + let s = (0.5 * (2.0 * std::f64::consts::PI * 440.0 * i as f64 / sr as f64).sin() * 32767.0).round() as i16; + data.extend_from_slice(&s.to_le_bytes()); + data.extend_from_slice(&0i16.to_le_bytes()); + } + let mut w = Vec::new(); + w.extend_from_slice(b"RIFF"); + w.extend_from_slice(&(36 + data.len() as u32).to_le_bytes()); + w.extend_from_slice(b"WAVEfmt "); + w.extend_from_slice(&16u32.to_le_bytes()); + w.extend_from_slice(&1u16.to_le_bytes()); + w.extend_from_slice(&2u16.to_le_bytes()); + w.extend_from_slice(&sr.to_le_bytes()); + w.extend_from_slice(&(sr * 4).to_le_bytes()); + w.extend_from_slice(&4u16.to_le_bytes()); + w.extend_from_slice(&16u16.to_le_bytes()); + w.extend_from_slice(b"data"); + w.extend_from_slice(&(data.len() as u32).to_le_bytes()); + w.extend_from_slice(&data); + std::fs::write(path, w).expect("write wav"); +} diff --git a/crates/gpu/src/adv3d.rs b/crates/gpu/src/adv3d.rs index 6e428c84..677db3e5 100644 --- a/crates/gpu/src/adv3d.rs +++ b/crates/gpu/src/adv3d.rs @@ -2,19 +2,33 @@ //! (`Depth32Float`, Greater), the scene's materials, textures, lights and shadow maps in //! storage buffers, and `advanced3d.wgsl`'s physically based fragment shader. Opaque triangles //! are drawn with depth writes, then the sorted transparent ones blended over (premultiplied). -//! Colour (`Rgba16Float`) and camera depth (`R32Float`) are read back for the CPU's resolve, -//! depth of field and encoding. +//! Colour (`Rgba16Float`) and camera depth (`R32Float`) stay on the GPU for `adv3d.wgsl`'s +//! compute kernels, which run the rest of `effectcraft_render::three_d::adv::render_prepared`: +//! the 2×2 supersampling resolve, the motion-blur sub-sample average (nearest depth), the +//! depth-based iris depth of field (the Classic 3D bokeh kernel's row spans and prefix-sum +//! gather, highlight boost, progressive blend between blur levels), the sRGB encode and the +//! premultiplied "over" onto the compositor's canvas. Only the depth of field reads anything +//! back (its 8-byte radius range, to choose the blur levels as the CPU does). +//! +//! Wireframe-quality layers draw their outlines here too ([`wireframe`]). -use effectcraft_render::three_d::adv::{Scene, Target}; +use effectcraft_render::three_d::Dof; +use effectcraft_render::three_d::adv::{Prepared, Rendered, Scene, Target}; +use effectcraft_render::three_d::bokeh; use wgpu::util::DeviceExt; -use crate::context::{Enc, GpuContext}; +use crate::context::{Enc, GpuContext, GpuImage}; /// Pipelines (built on first use). pub(crate) struct Pipes { bgl: wgpu::BindGroupLayout, opaque: wgpu::RenderPipeline, transparent: wgpu::RenderPipeline, + post: Post, +} + +fn pipes(g: &GpuContext) -> &Pipes { + g.adv3d.get_or_init(|| Pipes::new(&g.device)) } const COLOR: wgpu::TextureFormat = wgpu::TextureFormat::Rgba16Float; @@ -96,7 +110,7 @@ impl Pipes { cache: None, }) }; - Pipes { opaque: make(false), transparent: make(true), bgl } + Pipes { opaque: make(false), transparent: make(true), bgl, post: Post::new(device) } } } @@ -208,9 +222,12 @@ fn half_to_f32(h: u16) -> f32 { } } -/// Rasterise a scene. `None` when the device can't (no readback, size or buffer limits). -pub(crate) fn render(g: &GpuContext, s: &Scene) -> Option { - if !g.can_readback() || !g.fits(s.width, s.height) || s.indices.is_empty() { +/// Record the render pass of one scene into `e`: (colour `Rgba16Float`, camera depth `R32Float` +/// with −1 where nothing was drawn) at the scene's raster size. `None` when the device can't +/// (size or buffer limits). +fn raster_into(e: &mut Enc, s: &Scene) -> Option<(wgpu::Texture, wgpu::Texture)> { + let g = e.g; + if !g.fits(s.width, s.height) || s.indices.is_empty() { return None; } let limits = g.device.limits(); @@ -218,7 +235,7 @@ pub(crate) fn render(g: &GpuContext, s: &Scene) -> Option { if bufs.iter().any(|b| b.len() as u64 > limits.max_storage_buffer_binding_size) { return None; } - let p = g.adv3d.get_or_init(|| Pipes::new(&g.device)); + let p = pipes(g); let dev = &g.device; let vb: Vec = s .vertices @@ -257,12 +274,12 @@ pub(crate) fn render(g: &GpuContext, s: &Scene) -> Option { view_formats: &[], }) }; - let rt = wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC; + let rt = wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC | wgpu::TextureUsages::TEXTURE_BINDING; let color = tex(COLOR, rt); let zout = tex(DEPTH_OUT, rt); let depth = tex(DEPTH, wgpu::TextureUsages::RENDER_ATTACHMENT); let (cv, zv, dv) = (color.create_view(&Default::default()), zout.create_view(&Default::default()), depth.create_view(&Default::default())); - let mut enc = dev.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: Some("adv3d") }); + let enc = e.encoder(); { let mut pass = enc.begin_render_pass(&wgpu::RenderPassDescriptor { label: Some("adv3d"), @@ -302,8 +319,17 @@ pub(crate) fn render(g: &GpuContext, s: &Scene) -> Option { pass.draw_indexed(s.opaque_count..n, 0, 0..1); } } - g.queue.submit([enc.finish()]); + Some((color, zout)) +} + +/// Rasterise a scene and read it back ([`effectcraft_render::Accelerator::raster_3d`]). `None` +/// when the device can't (no readback, size or buffer limits). +pub(crate) fn render(g: &GpuContext, s: &Scene) -> Option { + if !g.can_readback() { + return None; + } let mut e = Enc::new(g); + let (color, zout) = raster_into(&mut e, s)?; let cb = e.read_texture(&color, s.width, s.height, 8)?; let zb = e.read_texture(&zout, s.width, s.height, 4)?; let color = cb.chunks_exact(8).map(|c| [0, 1, 2, 3].map(|k| half_to_f32(u16::from_le_bytes([c[2 * k], c[2 * k + 1]])))).collect(); @@ -316,3 +342,381 @@ pub(crate) fn render(g: &GpuContext, s: &Scene) -> Option { .collect(); Some(Target { width: s.width, height: s.height, color, depth }) } + +// ---------------------------------------------------------------- after the rasteriser + +/// Compute kernels of `adv3d.wgsl` (resolve, motion-blur accumulation, depth of field, +/// encoding, compositing, wireframes) with their shared bind group layout and the placeholder +/// resources bound to the slots a kernel doesn't use. +pub(crate) struct Post { + bgl: wgpu::BindGroupLayout, + kernels: Vec<(&'static str, wgpu::ComputePipeline)>, + /// Placeholders for bindings 3–9 (distinct buffers: writable bindings must not alias). + dummy_bufs: Vec, + dummy_color: wgpu::TextureView, + dummy_z: wgpu::TextureView, + dummy_out: wgpu::TextureView, +} + +const KERNELS: &[&str] = &["resolve", "radius_of", "boost", "prefix_rows", "gather", "finish", "wire"]; + +impl Post { + fn new(device: &wgpu::Device) -> Post { + let module = device.create_shader_module(wgpu::ShaderModuleDescriptor { + label: Some("effectcraft advanced 3d post"), + source: wgpu::ShaderSource::Wgsl(include_str!("shaders/adv3d.wgsl").into()), + }); + let tex = |binding| wgpu::BindGroupLayoutEntry { + binding, + visibility: wgpu::ShaderStages::COMPUTE, + ty: wgpu::BindingType::Texture { + sample_type: wgpu::TextureSampleType::Float { filterable: false }, + view_dimension: wgpu::TextureViewDimension::D2, + multisampled: false, + }, + count: None, + }; + let buf = |binding, read_only| wgpu::BindGroupLayoutEntry { + binding, + visibility: wgpu::ShaderStages::COMPUTE, + ty: wgpu::BindingType::Buffer { ty: wgpu::BufferBindingType::Storage { read_only }, has_dynamic_offset: false, min_binding_size: None }, + count: None, + }; + let entries = [ + wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::COMPUTE, + ty: wgpu::BindingType::Buffer { ty: wgpu::BufferBindingType::Uniform, has_dynamic_offset: false, min_binding_size: None }, + count: None, + }, + tex(1), + tex(2), + buf(3, false), + buf(4, false), + buf(5, false), + buf(6, true), + buf(7, false), + buf(8, false), + buf(9, false), + wgpu::BindGroupLayoutEntry { + binding: 10, + visibility: wgpu::ShaderStages::COMPUTE, + ty: wgpu::BindingType::StorageTexture { + access: wgpu::StorageTextureAccess::WriteOnly, + format: crate::context::FORMAT, + view_dimension: wgpu::TextureViewDimension::D2, + }, + count: None, + }, + ]; + let bgl = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { label: Some("advanced 3d post"), entries: &entries }); + let layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + label: Some("advanced 3d post"), + bind_group_layouts: &[Some(&bgl)], + immediate_size: 0, + }); + let kernels = KERNELS + .iter() + .map(|k| { + let p = device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor { + label: Some(k), + layout: Some(&layout), + module: &module, + entry_point: Some(k), + compilation_options: Default::default(), + cache: None, + }); + (*k, p) + }) + .collect(); + let dummy_bufs = (3..=9) + .map(|_| { + device.create_buffer(&wgpu::BufferDescriptor { + label: Some("adv3d dummy"), + size: 16, + usage: wgpu::BufferUsages::STORAGE, + mapped_at_creation: false, + }) + }) + .collect(); + let tex1 = |format, usage| { + device + .create_texture(&wgpu::TextureDescriptor { + label: Some("adv3d dummy"), + size: wgpu::Extent3d { width: 1, height: 1, depth_or_array_layers: 1 }, + mip_level_count: 1, + sample_count: 1, + dimension: wgpu::TextureDimension::D2, + format, + usage, + view_formats: &[], + }) + .create_view(&Default::default()) + }; + Post { + bgl, + kernels, + dummy_bufs, + dummy_color: tex1(COLOR, wgpu::TextureUsages::TEXTURE_BINDING), + dummy_z: tex1(DEPTH_OUT, wgpu::TextureUsages::TEXTURE_BINDING), + dummy_out: tex1(crate::context::FORMAT, wgpu::TextureUsages::STORAGE_BINDING), + } + } +} + +/// The resources of one post kernel dispatch (unset slots get placeholders). +#[derive(Default)] +struct Binds<'b> { + color: Option<&'b wgpu::TextureView>, + z: Option<&'b wgpu::TextureView>, + /// Bindings 3–9: acc, depth, src, table, radius, range, prefix. + bufs: [Option<&'b wgpu::Buffer>; 7], + out: Option<&'b wgpu::TextureView>, +} + +/// Uniform block of the post kernels (`Post` in `adv3d.wgsl`). +fn params(u0: [u32; 4], f0: [f32; 4]) -> Vec { + let mut v: Vec = u0.iter().flat_map(|x| x.to_le_bytes()).collect(); + v.extend([0u8; 16]); + v.extend(f0.iter().flat_map(|x| x.to_le_bytes())); + v.extend([0u8; 16]); + v +} + +fn dispatch(e: &mut Enc, kernel: &str, u0: [u32; 4], f0: [f32; 4], b: Binds, groups: (u32, u32)) { + let g = e.g; + let post = &pipes(g).post; + let Some((_, pipe)) = post.kernels.iter().find(|(k, _)| *k == kernel) else { return }; + let ub = g.device.create_buffer_init(&wgpu::util::BufferInitDescriptor { label: None, contents: ¶ms(u0, f0), usage: wgpu::BufferUsages::UNIFORM }); + let mut entries = vec![ + wgpu::BindGroupEntry { binding: 0, resource: ub.as_entire_binding() }, + wgpu::BindGroupEntry { binding: 1, resource: wgpu::BindingResource::TextureView(b.color.unwrap_or(&post.dummy_color)) }, + wgpu::BindGroupEntry { binding: 2, resource: wgpu::BindingResource::TextureView(b.z.unwrap_or(&post.dummy_z)) }, + ]; + for (i, buf) in b.bufs.iter().enumerate() { + entries.push(wgpu::BindGroupEntry { binding: 3 + i as u32, resource: buf.unwrap_or(&post.dummy_bufs[i]).as_entire_binding() }); + } + entries.push(wgpu::BindGroupEntry { binding: 10, resource: wgpu::BindingResource::TextureView(b.out.unwrap_or(&post.dummy_out)) }); + let bg = g.device.create_bind_group(&wgpu::BindGroupDescriptor { label: Some(kernel), layout: &post.bgl, entries: &entries }); + let enc = e.encoder(); + let mut pass = enc.begin_compute_pass(&wgpu::ComputePassDescriptor { label: Some(kernel), timestamp_writes: None }); + pass.set_pipeline(pipe); + pass.set_bind_group(0, &bg, &[]); + pass.dispatch_workgroups(groups.0.max(1), groups.1.max(1), 1); +} + +fn pixel_groups(w: u32, h: u32) -> (u32, u32) { + (w.div_ceil(16), h.div_ceil(16)) +} + +fn storage_buf(g: &GpuContext, size: u64, label: &str) -> wgpu::Buffer { + g.device.create_buffer(&wgpu::BufferDescriptor { + label: Some(label), + size: size.max(16), + usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_SRC | wgpu::BufferUsages::COPY_DST, + mapped_at_creation: false, + }) +} + +/// Read a storage buffer back (submits and waits). +fn read_buffer(e: &mut Enc, buf: &wgpu::Buffer, size: u64) -> Option> { + if !e.g.can_readback() { + return None; + } + let rb = e.g.device.create_buffer(&wgpu::BufferDescriptor { + label: Some("adv3d readback"), + size, + usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST, + mapped_at_creation: false, + }); + e.encoder().copy_buffer_to_buffer(buf, 0, &rb, 0, size); + e.submit(); + let (tx, rx) = std::sync::mpsc::channel(); + rb.map_async(wgpu::MapMode::Read, .., move |r| { + let _ = tx.send(r); + }); + e.g.device.poll(wgpu::PollType::wait_indefinitely()).ok()?; + rx.recv().ok()?.ok()?; + let out = rb.get_mapped_range(..).ok()?.to_vec(); + rb.unmap(); + Some(out) +} + +/// A resolved run on the GPU: premultiplied linear (or working-space) colour and camera depth +/// (`BIG` where nothing was drawn), one entry per output pixel. +struct Resolved { + acc: wgpu::Buffer, + depth: wgpu::Buffer, + width: u32, + height: u32, +} + +/// The rasteriser's "nothing drawn" depth on the GPU (∞ on the CPU). +const BIG: f32 = 3.0e38; + +/// Rasterise every motion-blur sub-sample of a prepared run, resolve and average them, then +/// apply the depth of field (`render_prepared`'s steps). `None` = can't run here (sizes, +/// limits, or a depth of field without readback); `Some(None)` = nothing drawn. +fn resolve_run(e: &mut Enc, prep: &Prepared) -> Option> { + let (w, h) = prep.out; + let g = e.g; + if !g.fits(w, h) || (prep.dof.is_some() && !g.can_readback()) { + return None; + } + let px = w as u64 * h as u64; + let max = g.device.limits().max_storage_buffer_binding_size; + if px * 16 > max { + return None; + } + let acc = storage_buf(g, px * 16, "adv3d acc"); + let depth = storage_buf(g, px * 4, "adv3d depth"); + let n = prep.samples.max(1); + let weight = if n <= 1 { 1.0 } else { 1.0 / n as f32 }; + let mut first = true; + for i in 0..n { + let s = prep.scene(i); + if s.indices.is_empty() { + continue; + } + if s.width != w * s.ssaa.max(1) || s.height != h * s.ssaa.max(1) { + return None; + } + let (color, zout) = raster_into(e, &s)?; + let (cv, zv) = (color.create_view(&Default::default()), zout.create_view(&Default::default())); + let b = Binds { color: Some(&cv), z: Some(&zv), bufs: [Some(&acc), Some(&depth), None, None, None, None, None], out: None }; + dispatch(e, "resolve", [w, h, s.ssaa.max(1), first as u32], [weight, 0.0, 0.0, 0.0], b, pixel_groups(w, h)); + first = false; + // One sub-sample's targets alive at a time. + e.submit(); + } + if first { + return Some(None); + } + if let Some(dof) = &prep.dof { + depth_of_field(e, &acc, &depth, (w, h), dof, prep.scale)?; + } + Some(Some(Resolved { acc, depth, width: w, height: h })) +} + +/// `adv::depth_of_field` on the resolved buffers: radii, highlights, prefix sums, the iris +/// gather at the levels around each pixel's radius. The result replaces `acc`. +fn depth_of_field(e: &mut Enc, acc: &wgpu::Buffer, depth: &wgpu::Buffer, (w, h): (u32, u32), dof: &Dof, scale: f64) -> Option<()> { + let g = e.g; + let px = w as u64 * h as u64; + if (w as u64 + 1) * h as u64 * 16 > g.device.limits().max_storage_buffer_binding_size { + return None; + } + let radius = storage_buf(g, px * 4, "adv3d radius"); + let mut init = Vec::with_capacity(16); + for v in [f32::INFINITY.to_bits(), 0, 0, 0] { + init.extend_from_slice(&v.to_le_bytes()); + } + let range = g.device.create_buffer_init(&wgpu::util::BufferInitDescriptor { + label: Some("adv3d radius range"), + contents: &init, + usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_SRC, + }); + let f0 = [dof.focus as f32, dof.aperture as f32, dof.blur_level as f32, scale as f32]; + let b = Binds { bufs: [None, Some(depth), None, None, Some(&radius), Some(&range), None], ..Default::default() }; + dispatch(e, "radius_of", [w, h, 0, 0], f0, b, pixel_groups(w, h)); + let rb = read_buffer(e, &range, 16)?; + let lo = f32::from_bits(u32::from_le_bytes([rb[0], rb[1], rb[2], rb[3]])); + let hi = f32::from_bits(u32::from_le_bytes([rb[4], rb[5], rb[6], rb[7]])); + if hi < 0.5 { + return Some(()); + } + let Some(levels) = bokeh::blur_levels(&[lo, hi]) else { return Some(()) }; + // Level headers, then every level's weighted spans. + let mut heads: Vec<[f32; 4]> = vec![]; + let mut spans: Vec<[f32; 4]> = vec![]; + for &r in &levels { + match bokeh::kernel_spans(&dof.iris, r as f64) { + Some((kernels, norm)) => { + let first = levels.len() + spans.len(); + for (wt, sp) in &kernels { + spans.extend(sp.iter().map(|&(dy, x0, x1)| [dy as f32, x0 as f32, x1 as f32, *wt])); + } + let count = levels.len() + spans.len() - first; + heads.push([first as f32, count as f32, 1.0 / norm, 0.0]); + } + None => heads.push([0.0, 0.0, 0.0, 1.0]), + } + } + let table: Vec = heads.iter().chain(&spans).flatten().flat_map(|x| x.to_le_bytes()).collect(); + if table.len() as u64 > g.device.limits().max_storage_buffer_binding_size { + return None; + } + let table = e.bytes(table); + let src = storage_buf(g, px * 16, "adv3d dof source"); + let prefix = storage_buf(g, (w as u64 + 1) * h as u64 * 16, "adv3d prefix"); + let hl = dof.highlight; + let b = Binds { bufs: [Some(acc), None, Some(&src), None, Some(&radius), None, None], ..Default::default() }; + dispatch(e, "boost", [w, h, 0, 0], [hl.gain as f32, hl.threshold as f32, hl.saturation as f32, 0.0], b, pixel_groups(w, h)); + let b = Binds { bufs: [None, None, Some(&src), None, None, None, Some(&prefix)], ..Default::default() }; + dispatch(e, "prefix_rows", [w, h, 0, 0], [0.0; 4], b, (h.div_ceil(64), 1)); + let b = Binds { bufs: [Some(acc), None, Some(&src), Some(&table), Some(&radius), None, Some(&prefix)], ..Default::default() }; + dispatch(e, "gather", [w, h, levels.len() as u32, 0], [lo, hi, 0.0, 0.0], b, pixel_groups(w, h)); + e.submit(); + Some(()) +} + +/// The resolved run as an image in the canvas's encoding, composited over `canvas` when given. +fn finish(e: &mut Enc, r: &Resolved, encode: bool, canvas: Option<&GpuImage>) -> GpuImage { + let out = e.image(r.width, r.height); + let ov = out.texture.create_view(&Default::default()); + let cv = canvas.map(|c| c.texture.create_view(&Default::default())); + let b = Binds { color: cv.as_ref(), bufs: [Some(&r.acc), None, None, None, None, None, None], out: Some(&ov), ..Default::default() }; + dispatch(e, "finish", [r.width, r.height, encode as u32, canvas.is_some() as u32], [0.0; 4], b, pixel_groups(r.width, r.height)); + out +} + +/// [`effectcraft_render::Accelerator::render_3d`]: the whole prepared run on the GPU, read +/// back. +pub(crate) fn render_prepared(g: &GpuContext, prep: &Prepared) -> Option { + if !g.can_readback() { + return None; + } + let mut e = Enc::new(g); + let Some(r) = resolve_run(&mut e, prep)? else { return Some(None) }; + let img = finish(&mut e, &r, !prep.linear, None); + let img = e.download(&img)?; + let px = r.width as u64 * r.height as u64; + let db = read_buffer(&mut e, &r.depth, px * 4)?; + let depth = db + .chunks_exact(4) + .map(|c| { + let z = f32::from_le_bytes([c[0], c[1], c[2], c[3]]); + if z >= BIG * 0.5 { f32::INFINITY } else { z } + }) + .collect(); + Some(Some((img, depth))) +} + +/// Draw a prepared run over the GPU canvas (the compositor's walk; no readback unless the +/// depth of field needs its radius range). `None` = render it on the CPU. +pub(crate) fn draw_run(e: &mut Enc, prep: &Prepared, canvas: &GpuImage) -> Option { + if (canvas.width, canvas.height) != prep.out { + return None; + } + match resolve_run(e, prep)? { + None => Some(canvas.clone()), + Some(r) => Some(finish(e, &r, !prep.linear, Some(canvas))), + } +} + +/// Wireframe-quality layer outlines: `canvas` with `pixels` set to opaque white +/// ([`effectcraft_render::Renderer::wireframe_pixels`]). +pub(crate) fn wireframe(e: &mut Enc, canvas: &GpuImage, pixels: &[(u32, u32)]) -> GpuImage { + let out = e.image(canvas.width, canvas.height); + e.copy_into(canvas, &out, 0, 0); + if pixels.is_empty() { + return out; + } + let table: Vec = pixels.iter().flat_map(|&(x, y)| [x as f32, y as f32, 0.0, 0.0]).flat_map(f32::to_le_bytes).collect(); + let table = e.bytes(table); + let ov = out.texture.create_view(&Default::default()); + let b = Binds { bufs: [None, None, None, Some(&table), None, None, None], out: Some(&ov), ..Default::default() }; + let n = pixels.len() as u32; + dispatch(e, "wire", [n, 0, 0, 0], [0.0; 4], b, (n.div_ceil(64), 1)); + out +} diff --git a/crates/gpu/src/context.rs b/crates/gpu/src/context.rs index eac24208..677e76da 100644 --- a/crates/gpu/src/context.rs +++ b/crates/gpu/src/context.rs @@ -405,7 +405,7 @@ impl<'g> Enc<'g> { Enc { g, enc: None, pending: 0 } } - fn encoder(&mut self) -> &mut wgpu::CommandEncoder { + pub(crate) fn encoder(&mut self) -> &mut wgpu::CommandEncoder { let g = self.g; self.enc.get_or_insert_with(|| g.device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: Some("effectcraft") })) } diff --git a/crates/gpu/src/lib.rs b/crates/gpu/src/lib.rs index 559393bc..fd20971b 100644 --- a/crates/gpu/src/lib.rs +++ b/crates/gpu/src/lib.rs @@ -8,11 +8,14 @@ //! mattes, Preserve Transparency, layer styles' passes, all 38 blend modes and the 8/16 bpc //! clamping and quantisation, and converts colour spaces. Classic 3D runs composite here too //! (`classic3d`: per-pixel fragment sort, lights, ray-cast shadows), and adjustment layers run -//! their effect stacks on the GPU-resident comp. Advanced 3D compositing and wireframes run on -//! the CPU between GPU steps (read back, draw, upload). +//! their effect stacks on the GPU-resident comp. Wireframe-quality outlines are drawn here too. //! //! Advanced 3D comps rasterise on a render pipeline (`advanced3d.wgsl`: depth buffer, PBR, -//! image-based light, shadow maps), see [`Accelerator::raster_3d`]. +//! image-based light, shadow maps) and finish in compute kernels (`adv3d.wgsl`: supersampling +//! resolve, motion-blur sub-samples, iris depth of field, encoding, compositing), see +//! [`Accelerator::render_3d`] and `Renderer::prepare_adv_run`. Advanced 3D layers with blend +//! modes, track mattes or Preserve Transparency still composite on the CPU (their scenes +//! render here through [`Accelerator::render_3d`]). //! //! GPU effects ([`effectcraft_effects::GPU_EFFECTS`]) run as compute kernels with the CPU //! effect's exact steps (padding, box-blur radii, parameter conversions); chains of them are @@ -155,6 +158,10 @@ impl Accelerator for Gpu { adv3d::render(&self.ctx, scene) } + fn render_3d(&self, run: &effectcraft_render::three_d::adv::Prepared) -> Option { + adv3d::render_prepared(&self.ctx, run) + } + fn particles(&self) -> Option<&dyn effectcraft_effects::psim::ParticleSim> { self.ctx.can_readback().then_some(self as &dyn effectcraft_effects::psim::ParticleSim) } diff --git a/crates/gpu/src/shaders/adv3d.wgsl b/crates/gpu/src/shaders/adv3d.wgsl new file mode 100644 index 00000000..35e5a1bc --- /dev/null +++ b/crates/gpu/src/shaders/adv3d.wgsl @@ -0,0 +1,257 @@ +// Advanced 3D on the GPU after the rasteriser (`advanced3d.wgsl`): the steps of +// `effectcraft_render::three_d::adv::render_prepared` on GPU-resident buffers, operation for +// operation: +// +// resolve box-filter the 2×2 supersampled colour and camera depth of one motion-blur +// sub-sample and add it to the accumulation (weight 1/n; depth = nearest) +// radius each pixel's depth-of-field blur radius (`Dof::coc` · scale / 2, ≤ 48) and the +// range over the frame (atomics on the float bits; radii are ≥ 0) +// boost Highlight Gain / Threshold / Saturation (`bokeh::boost_highlights`), blended in +// where the pixel is blurred +// prefix per-row prefix sums (`bokeh::row_prefix`) +// gather the iris-shaped gather at the two blur levels around each pixel's radius +// (`bokeh::span_gather` on `bokeh::kernel` spans) blended (`progressive_blur`) +// finish linear → sRGB (`adv::encode`) and premultiplied "over" the canvas (`draw_run`) +// wire Wireframe-quality outlines: set the listed pixels to opaque white +// +// Depth "nothing drawn" is BIG on the GPU (∞ on the CPU). + +struct Post { + u0: vec4, + u1: vec4, + f0: vec4, + f1: vec4, +}; + +@group(0) @binding(0) var P: Post; +@group(0) @binding(1) var color_in: texture_2d; +@group(0) @binding(2) var z_in: texture_2d; +@group(0) @binding(3) var acc: array>; +@group(0) @binding(4) var depth: array; +@group(0) @binding(5) var src: array>; +@group(0) @binding(6) var table: array>; +@group(0) @binding(7) var radius: array; +@group(0) @binding(8) var range: array>; +@group(0) @binding(9) var prefix: array>; +@group(0) @binding(10) var out: texture_storage_2d; + +const BIG: f32 = 3.0e38; +const MAX_RADIUS: f32 = 48.0; +const NEAR: f32 = 1.0; + +// P.u0 = (width, height, ssaa, first), P.f0.x = weight +@compute @workgroup_size(16, 16) +fn resolve(@builtin(global_invocation_id) id: vec3) { + let w = P.u0.x; + let h = P.u0.y; + if (id.x >= w || id.y >= h) { + return; + } + let k = P.u0.z; + var a = vec4(0.0); + var dz = 0.0; + var dn = 0.0; + for (var j = 0u; j < k; j = j + 1u) { + for (var i = 0u; i < k; i = i + 1u) { + let p = vec2(i32(id.x * k + i), i32(id.y * k + j)); + a = a + textureLoad(color_in, p, 0); + let z = textureLoad(z_in, p, 0).x; + if (z >= 0.0) { + dz = dz + z; + dn = dn + 1.0; + } + } + } + let n = f32(k * k); + let res = a / n; + var d = BIG; + if (dn > 0.0) { + d = dz / dn; + } + let ix = id.y * w + id.x; + let wt = P.f0.x; + if (P.u0.w == 1u) { + acc[ix] = res * wt; + depth[ix] = d; + } else { + acc[ix] = acc[ix] + res * wt; + depth[ix] = min(depth[ix], d); + } +} + +// P.u0 = (width, height), P.f0 = (focus, aperture, blur level, scale) +@compute @workgroup_size(16, 16) +fn radius_of(@builtin(global_invocation_id) id: vec3) { + let w = P.u0.x; + if (id.x >= w || id.y >= P.u0.y) { + return; + } + let ix = id.y * w + id.x; + var z = depth[ix]; + if (z >= BIG * 0.5) { + z = 1.0e9; + } + var coc = 0.0; + if (z > NEAR && P.f0.x > 0.0) { + coc = P.f0.y * abs(z - P.f0.x) / z * P.f0.z; + } + let r = min(coc * P.f0.w * 0.5, MAX_RADIUS); + radius[ix] = r; + atomicMin(&range[0], bitcast(r)); + atomicMax(&range[1], bitcast(r)); +} + +fn luminance(c: vec3) -> f32 { + return 0.2126 * c.x + 0.7152 * c.y + 0.0722 * c.z; +} + +// P.u0 = (width, height), P.f0 = (gain, threshold, saturation) +@compute @workgroup_size(16, 16) +fn boost(@builtin(global_invocation_id) id: vec3) { + let w = P.u0.x; + if (id.x >= w || id.y >= P.u0.y) { + return; + } + let ix = id.y * w + id.x; + let p = acc[ix]; + var b = p; + let gain = P.f0.x; + if (gain > 0.0 && p.w > 0.0) { + let a = p.w; + let c = p.xyz / a; + let l = luminance(c); + let thr = clamp(P.f0.y, 0.0, 1.0); + if (!(l < thr || (thr >= 1.0 && l < 1.0))) { + var over = 1.0; + if (thr < 1.0) { + over = clamp((l - thr) / (1.0 - thr), 0.0, 1.0); + } + let k = 1.0 + gain * 4.0 * max(over, 0.25); + let sat = 1.0 + P.f0.z; + let s = max(vec3(l) + (c - vec3(l)) * sat, vec3(0.0)) * k; + b = vec4(s * a, a); + } + let kk = clamp(radius[ix] - 0.5, 0.0, 1.0); + b = p + (b - p) * kk; + } + src[ix] = b; +} + +// One invocation per row. P.u0 = (width, height) +@compute @workgroup_size(64) +fn prefix_rows(@builtin(global_invocation_id) id: vec3) { + let w = P.u0.x; + let y = id.x; + if (y >= P.u0.y) { + return; + } + let base = y * (w + 1u); + var a = vec4(0.0); + prefix[base] = a; + for (var x = 0u; x < w; x = x + 1u) { + a = a + src[y * w + x]; + prefix[base + x + 1u] = a; + } +} + +fn prefix_at(y: u32, t: f32) -> vec4 { + let n = P.u0.x; + let base = y * (n + 1u); + if (t <= 0.0) { + return vec4(0.0); + } + if (t >= f32(n)) { + return prefix[base + n]; + } + let i = u32(floor(t)); + let f = t - f32(i); + let a = prefix[base + i]; + let b = prefix[base + i + 1u]; + return a + (b - a) * f; +} + +// Level `j` of the table: (first span, span count, 1 / norm, identity). +fn level(j: u32, x: u32, y: u32) -> vec4 { + let w = P.u0.x; + let h = i32(P.u0.y); + let l = table[j]; + if (l.w > 0.5) { + return src[y * w + x]; + } + let first = u32(l.x); + let count = u32(l.y); + var a = vec4(0.0); + for (var s = first; s < first + count; s = s + 1u) { + let sp = table[s]; + let sy = i32(y) + i32(sp.x); + if (sy < 0 || sy >= h) { + continue; + } + let hi = prefix_at(u32(sy), f32(x) + sp.z + 0.5); + let lo = prefix_at(u32(sy), f32(x) + sp.y + 0.5); + a = a + (hi - lo) * sp.w; + } + return a * l.z; +} + +// P.u0 = (width, height, levels), P.f0 = (lo, hi) +@compute @workgroup_size(16, 16) +fn gather(@builtin(global_invocation_id) id: vec3) { + let w = P.u0.x; + if (id.x >= w || id.y >= P.u0.y) { + return; + } + let ix = id.y * w + id.x; + let n = P.u0.z; + if (n <= 1u) { + acc[ix] = level(0u, id.x, id.y); + return; + } + let lo = P.f0.x; + let hi = P.f0.y; + let r = radius[ix]; + let f = clamp((r - lo) / (hi - lo) * f32(n - 1u), 0.0, f32(n - 1u)); + let k = min(u32(floor(f)), n - 2u); + let t = f - f32(k); + let a = level(k, id.x, id.y); + let b = level(k + 1u, id.x, id.y); + acc[ix] = a + (b - a) * t; +} + +fn linear_to_srgb(c: f32) -> f32 { + if (c <= 0.0031308) { + return c * 12.92; + } + return 1.055 * pow(c, 1.0 / 2.4) - 0.055; +} + +// P.u0 = (width, height, encode, over the canvas in color_in) +@compute @workgroup_size(16, 16) +fn finish(@builtin(global_invocation_id) id: vec3) { + let w = P.u0.x; + if (id.x >= w || id.y >= P.u0.y) { + return; + } + var p = acc[id.y * w + id.x]; + if (P.u0.z == 1u && p.w > 1e-6) { + let a = p.w; + p = vec4(linear_to_srgb(p.x / a) * a, linear_to_srgb(p.y / a) * a, linear_to_srgb(p.z / a) * a, a); + } + let q = vec2(i32(id.x), i32(id.y)); + if (P.u0.w == 1u) { + let d = textureLoad(color_in, q, 0); + let k = 1.0 - p.w; + p = vec4(p.x + d.x * k, p.y + d.y * k, p.z + d.z * k, p.w + d.w * k); + } + textureStore(out, q, p); +} + +// One invocation per pixel listed in `table` (x, y). P.u0.x = count +@compute @workgroup_size(64) +fn wire(@builtin(global_invocation_id) id: vec3) { + if (id.x >= P.u0.x) { + return; + } + let p = table[id.x]; + textureStore(out, vec2(i32(p.x), i32(p.y)), vec4(1.0)); +} diff --git a/crates/gpu/src/shaders/advanced3d.wgsl b/crates/gpu/src/shaders/advanced3d.wgsl index 12100cc3..f7c94ff5 100644 --- a/crates/gpu/src/shaders/advanced3d.wgsl +++ b/crates/gpu/src/shaders/advanced3d.wgsl @@ -167,7 +167,12 @@ fn env_brdf(n_v: f32, rough: f32) -> vec2 { } fn equirect_uv(d: vec3, rot: f32) -> vec2 { - let u = 0.5 + (atan2(d.x, d.z) + rot) / (2.0 * PI); + // atan2(0, 0) is 0 on the CPU but may be NaN on GPUs (straight up / down normals). + var a = 0.0; + if (d.x != 0.0 || d.z != 0.0) { + a = atan2(d.x, d.z); + } + let u = 0.5 + (a + rot) / (2.0 * PI); let v = acos(clamp(-d.y, -1.0, 1.0)) / PI; return vec2(u - floor(u), v); } diff --git a/crates/gpu/src/tests_adv3d.rs b/crates/gpu/src/tests_adv3d.rs index f0e0f08b..596c41a4 100644 --- a/crates/gpu/src/tests_adv3d.rs +++ b/crates/gpu/src/tests_adv3d.rs @@ -6,7 +6,8 @@ //! 1.5 % of pixels (silhouettes) may differ more. Skipped without an adapter. use effectcraft_keyframe::Value; -use effectcraft_project::{Comp, ItemId, ItemKind, LayerSource, LightKind, PrimitiveKind, Project, Renderer as R3, Solid, build}; +use effectcraft_project::build; +use effectcraft_project::{Comp, ItemId, ItemKind, LayerSource, LightKind, PrimitiveKind, Project, Renderer as R3, Solid}; use effectcraft_render::three_d::adv::{self, Scene}; use effectcraft_render::{Accelerator, EvalCtx, NoFootage, RenderOpts, Renderer}; use effectcraft_time::{FrameRate, Tick}; @@ -165,3 +166,277 @@ fn gpu_raster_perf_smoke() { let gpu = t0.elapsed(); eprintln!("adv3d 480x320 (2x SSAA of 240x160), {} triangles: cpu {:?}, gpu {:?}", s.indices.len() / 3, cpu, gpu); } + +// ---------------------------------------------------------------- whole runs (M13.11) +// +// `Accelerator::render_3d` (raster, resolve, motion-blur average, iris depth of field, encode +// on the GPU) against `adv::render_prepared` on the CPU. Tolerance: ≤ 1/255 on ≥ 99 % of the +// pixels. Against the CPU rasteriser the remaining pixels are silhouette pixels the two +// rasterisers cover differently (and the blur spreads); against the CPU post-processing of +// the *same* GPU raster the agreement is ≥ 99.9 % (f32 order of operations only). + +/// The lit scene seen through a depth-of-field camera (hexagonal iris, diffraction fringe, +/// highlight boost) with the sphere moving under motion blur. +fn dof_mb_project() -> (Project, ItemId) { + let (mut p, cid) = scene_project(); + add(&mut p, cid, LayerSource::Camera, |l| { + let zoom = match l.props.prop_mut("cameraOptions/zoom").unwrap().value { + Value::Scalar(z) => z, + _ => 300.0, + }; + set(l, "cameraOptions/dof", Value::Bool(true)); + set(l, "cameraOptions/focusDistance", Value::Scalar(zoom - 60.0)); + set(l, "cameraOptions/aperture", Value::Scalar(160.0)); + set(l, "cameraOptions/irisShape", Value::Enum(4)); + set(l, "cameraOptions/irisRotation", Value::Scalar(15.0)); + set(l, "cameraOptions/irisDiffractionFringe", Value::Scalar(50.0)); + set(l, "cameraOptions/highlightGain", Value::Scalar(30.0)); + set(l, "cameraOptions/highlightThreshold", Value::Scalar(150.0)); + }); + let c = p.comp_mut(cid).unwrap(); + c.enable_motion_blur = true; + c.motion_blur_samples = 6; + // The sphere moves 40 px per frame. + let sphere = c.layers.iter_mut().find(|l| matches!(l.source, LayerSource::Primitive { kind: PrimitiveKind::Sphere })).unwrap(); + sphere.switches.motion_blur = true; + sphere.props.prop_mut("transform/position").unwrap().keys = vec![ + effectcraft_keyframe::Keyframe::new(Tick::ZERO, Value::Vec3([80.0, 95.0, 0.0])), + effectcraft_keyframe::Keyframe::new(Tick::from_seconds_f64(1.0), Value::Vec3([80.0 + 30.0 * 40.0, 95.0, 0.0])), + ]; + (p, cid) +} + +/// Share of pixels (0–1) differing by more than 1/255 on any channel, and the largest +/// difference. +fn off_share(a: &[[f32; 4]], b: &[[f32; 4]]) -> (f64, f32) { + let mut off = 0; + let mut max = 0.0f32; + for (p, q) in a.iter().zip(b) { + // NaN counts as a difference (f32::max would drop it). + let d = (0..4).map(|k| (p[k] - q[k]).abs()).fold(0.0f32, |a, b| if b.is_nan() || a.is_nan() { f32::NAN } else { a.max(b) }); + max = max.max(d); + if d.is_nan() || d > 1.0 / 255.0 + 1e-6 { + off += 1; + } + } + (off as f64 / a.len().max(1) as f64, max) +} + +fn run_of(p: &Project, cid: ItemId) -> (EvalCtx<'_>, Vec<&effectcraft_project::Layer>) { + let ctx = EvalCtx { project: p, comp_id: cid, comp: p.comp(cid).unwrap(), time: Tick::ZERO, expr: None, footage: None }; + let run: Vec<&effectcraft_project::Layer> = ctx.comp.layers.iter().rev().filter(|l| l.is_3d() && l.has_video()).collect(); + (ctx, run) +} + +fn compare_run(p: &Project, cid: ItemId, g: &Gpu, what: &str) { + let (ctx, run) = run_of(p, cid); + let out = (ctx.comp.width, ctx.comp.height); + // CPU reference: CPU raster and post. + let cpu_r = Renderer::new(p, &NoFootage, RenderOpts::default()); + let prep = adv::Prepared::new(&cpu_r, &ctx, &run, out); + let (cimg, cdepth) = adv::render_prepared(&prep).expect("cpu drew"); + // The GPU: everything on the device. + let (gimg, gdepth) = g.render_3d(&prep).expect("gpu handled").expect("gpu drew"); + let (share, max) = off_share(&cimg.data, &gimg.data); + eprintln!("{what}: gpu vs cpu {:.3} % of pixels beyond 1/255 (max {max:.4})", share * 100.0); + assert!(share <= 0.01, "{what}: {:.3} % of pixels differ", share * 100.0); + let dz = cdepth.iter().zip(&gdepth).filter(|(a, b)| a.is_finite() != b.is_finite() || (a.is_finite() && (*a - *b).abs() > 0.01 * a.abs().max(1.0))).count(); + assert!((dz as f64) < 0.015 * cdepth.len() as f64, "{what}: {dz} depth mismatches"); + // CPU post-processing of the GPU raster: the post kernels alone. + let mut gr = Renderer::new(p, &NoFootage, RenderOpts { backend: effectcraft_render::Backend::Gpu, ..Default::default() }); + gr.accel = Some(g); + let prep = adv::Prepared::new(&gr, &ctx, &run, out); + let (himg, _) = adv::render_prepared(&prep).expect("cpu post drew"); + let (share, max) = off_share(&himg.data, &gimg.data); + eprintln!("{what}: gpu post vs cpu post on the gpu raster: {:.4} % beyond 1/255 (max {max:.4})", share * 100.0); + assert!(share <= 0.001, "{what}: post kernels differ on {:.3} % of pixels", share * 100.0); +} + +#[test] +fn gpu_runs_match_cpu_with_dof_and_motion_blur() { + let Some(g) = gpu() else { return }; + let (p, cid) = dof_mb_project(); + let (ctx, run) = run_of(&p, cid); + let r = Renderer::new(&p, &NoFootage, RenderOpts::default()); + let prep = adv::Prepared::new(&r, &ctx, &run, (240, 160)); + assert_eq!(prep.samples, 6); + let dof = prep.dof.expect("depth of field"); + assert_eq!(dof.iris.sides, 6); + compare_run(&p, cid, &g, "dof + motion blur"); + // The depth of field really blurs (against the same frame in focus). + let (blurred, _) = g.render_3d(&prep).unwrap().unwrap(); + let mut sharp_prep = adv::Prepared::new(&r, &ctx, &run, (240, 160)); + sharp_prep.dof = None; + let (sharp, _) = g.render_3d(&sharp_prep).unwrap().unwrap(); + let (changed, _) = off_share(&blurred.data, &sharp.data); + assert!(changed > 0.2, "out-of-focus pixels: {:.1} %", changed * 100.0); + // Fast Rectangle iris, no highlights; and without depth of field. + let (mut p, cid) = dof_mb_project(); + for l in &mut p.comp_mut(cid).unwrap().layers { + if let Some(pr) = l.props.prop_mut("cameraOptions/irisShape") { + pr.value = Value::Enum(0); + } + if let Some(pr) = l.props.prop_mut("cameraOptions/highlightGain") { + pr.value = Value::Scalar(0.0); + } + } + compare_run(&p, cid, &g, "fast rectangle"); + for l in &mut p.comp_mut(cid).unwrap().layers { + if let Some(pr) = l.props.prop_mut("cameraOptions/dof") { + pr.value = Value::Bool(false); + } + } + compare_run(&p, cid, &g, "motion blur only"); +} + +#[test] +fn gpu_runs_match_cpu_for_extrusions_and_collapsed_precomps() { + let Some(g) = gpu() else { return }; + let (mut p, cid) = scene_project(); + // Extruded text with a bevelled stroke. + add(&mut p, cid, LayerSource::Text, |l| { + l.switches.three_d = true; + let doc = effectcraft_keyframe::TextDoc { + text: "Ab".into(), + size: 50.0, + fill: [0.9, 0.8, 0.2, 1.0], + apply_fill: true, + stroke: [0.1, 0.2, 0.9, 1.0], + apply_stroke: true, + stroke_width: 4.0, + ..Default::default() + }; + set(l, "text/sourceText", Value::Text(Box::new(doc))); + let mut next = 20_000u64; + l.props.children.push(build::extrusion_geometry_options(&mut build::Ids(&mut next)).into()); + set(l, "geometryOptions/extrusionDepth", Value::Scalar(15.0)); + set(l, "geometryOptions/bevelStyle", Value::Enum(1)); + set(l, "transform/position", Value::Vec3([120.0, 50.0, -30.0])); + set(l, "transform/rotationY", Value::Scalar(25.0)); + }); + // A collapsed precomp holding a cube. + let mut nc = Comp::new(240, 160, FrameRate::FPS_30, Tick::from_seconds_f64(1.0)); + nc.renderer = R3::Classic3D; + let nid = p.add_item("N", Default::default(), None, ItemKind::Comp(nc.into())); + add(&mut p, nid, LayerSource::Primitive { kind: PrimitiveKind::Cube }, |l| { + for k in ["width", "height", "depth"] { + set(l, &format!("geometryOptions/{k}"), Value::Scalar(30.0)); + } + set(l, "transform/position", Value::Vec3([200.0, 120.0, -20.0])); + set(l, "transform/rotationY", Value::Scalar(35.0)); + set(l, "transform/rotationX", Value::Scalar(-25.0)); + }); + add(&mut p, cid, LayerSource::Comp { item: nid }, |l| { + l.switches.three_d = true; + l.switches.collapse = true; + }); + let (ctx, run) = run_of(&p, cid); + let s = adv::scene_of(&Renderer::new(&p, &NoFootage, RenderOpts::default()), &ctx, &run, (240, 160)); + assert!(s.indices.len() / 3 > 500, "meshes: {} triangles", s.indices.len() / 3); + compare_run(&p, cid, &g, "extrusions + collapsed precomp"); +} + +#[test] +fn gpu_compositor_draws_advanced_3d_runs_on_the_device() { + let Some(g) = gpu() else { return }; + // A 2D background below the run and a 2D layer above it. + let (mut p, cid) = dof_mb_project(); + let bg = p.add_item("B", Default::default(), None, ItemKind::Solid(Solid { color: [0.1, 0.1, 0.15], width: 240, height: 160, pixel_aspect: 1.0 })); + let comp = p.comp(cid).unwrap().clone(); + let l = build::layer(&mut p, &comp, "B", LayerSource::Solid { item: bg }, (240, 160), None); + p.comp_mut(cid).unwrap().layers.push(l); + let top = p.add_item("T", Default::default(), None, ItemKind::Solid(Solid { color: [1.0, 0.5, 0.0], width: 40, height: 30, pixel_aspect: 1.0 })); + add(&mut p, cid, LayerSource::Solid { item: top }, |l| set(l, "transform/opacity", Value::Scalar(50.0))); + let (ctx, run) = run_of(&p, cid); + assert!(Renderer::new(&p, &NoFootage, RenderOpts::default()).prepare_adv_run(&ctx, &run, (240, 160)).is_some()); + let cpu = Renderer::new(&p, &NoFootage, RenderOpts::default()).comp_frame(cid, Tick::ZERO); + let mut r = Renderer::new(&p, &NoFootage, RenderOpts { backend: effectcraft_render::Backend::Gpu, ..Default::default() }); + r.accel = Some(&g); + let gimg = g.render(&r, cid, Tick::ZERO).expect("gpu walk"); + let (share, max) = off_share(&cpu.data, &gimg.data); + eprintln!("walk: {:.3} % beyond 1/255 (max {max:.4})", share * 100.0); + assert!(share <= 0.01, "{:.3} % of pixels differ", share * 100.0); + // A blend mode in the run takes the CPU's 2D path (its scene still renders on the GPU). + p.comp_mut(cid).unwrap().layers.iter_mut().find(|l| l.switches.three_d && !l.is_light() && l.source.is_av()).unwrap().blend_mode = + effectcraft_color::BlendMode::Screen; + let (ctx, run) = run_of(&p, cid); + assert!(Renderer::new(&p, &NoFootage, RenderOpts::default()).prepare_adv_run(&ctx, &run, (240, 160)).is_none()); + let cpu = Renderer::new(&p, &NoFootage, RenderOpts::default()).comp_frame(cid, Tick::ZERO); + let mut r = Renderer::new(&p, &NoFootage, RenderOpts { backend: effectcraft_render::Backend::Gpu, ..Default::default() }); + r.accel = Some(&g); + let gimg = r.comp_frame(cid, Tick::ZERO); + let (share, _) = off_share(&cpu.data, &gimg.data); + assert!(share <= 0.01, "blend-mode run: {:.3} %", share * 100.0); +} + +#[test] +fn gpu_wireframes_match_cpu_exactly() { + let Some(g) = gpu() else { return }; + let mut p = Project::default(); + let c = Comp::new(200, 120, FrameRate::FPS_30, Tick::from_seconds_f64(1.0)); + let cid = p.add_item("C", Default::default(), None, ItemKind::Comp(c.into())); + let sid = p.add_item("S", Default::default(), None, ItemKind::Solid(Solid { color: [0.2, 0.4, 0.6], width: 200, height: 120, pixel_aspect: 1.0 })); + add(&mut p, cid, LayerSource::Solid { item: sid }, |_| {}); + let s2 = p.add_item("W", Default::default(), None, ItemKind::Solid(Solid { color: [1.0, 0.0, 0.0], width: 70, height: 40, pixel_aspect: 1.0 })); + add(&mut p, cid, LayerSource::Solid { item: s2 }, |l| { + l.switches.quality = effectcraft_project::Quality::Wireframe; + set(l, "transform/rotation", Value::Scalar(23.0)); + set(l, "transform/scale", Value::Vec3([130.0, 90.0, 100.0])); + }); + let cpu = Renderer::new(&p, &NoFootage, RenderOpts::default()).comp_frame(cid, Tick::ZERO); + let mut r = Renderer::new(&p, &NoFootage, RenderOpts { backend: effectcraft_render::Backend::Gpu, ..Default::default() }); + r.accel = Some(&g); + let gimg = g.render(&r, cid, Tick::ZERO).expect("gpu walk"); + let white = cpu.data.iter().filter(|q| **q == [1.0; 4]).count(); + assert!(white > 100, "outline drawn: {white}"); + let (share, max) = off_share(&cpu.data, &gimg.data); + assert!(share == 0.0, "{share} differ (max {max})"); +} + +#[test] +fn gpu_dof_with_capped_radii() { + // Radii at the 48 px cap over most of the frame (8 blur levels). + let Some(g) = gpu() else { return }; + let (mut p, cid) = dof_mb_project(); + for l in &mut p.comp_mut(cid).unwrap().layers { + if let Some(pr) = l.props.prop_mut("cameraOptions/aperture") { + pr.value = Value::Scalar(1500.0); + } + l.switches.motion_blur = false; + } + compare_run(&p, cid, &g, "capped radii"); +} + +#[test] +fn gpu_environment_light_on_flat_extrusions_has_no_nan() { + // Bevelled text faces pointing straight up or down: atan2(0, 0) in the environment lookup + // (NaN on some GPUs) must give the CPU's 0. + let Some(g) = gpu() else { return }; + let mut p = Project::default(); + let mut c = Comp::new(480, 270, FrameRate::FPS_30, Tick::from_seconds_f64(1.0)); + c.renderer = R3::Advanced3D; + let cid = p.add_item("C", Default::default(), None, ItemKind::Comp(c.into())); + add(&mut p, cid, LayerSource::Text, |l| { + l.switches.three_d = true; + let doc = + effectcraft_keyframe::TextDoc { text: "EffectCraft".into(), size: 45.0, fill: [0.95, 0.75, 0.2, 1.0], apply_fill: true, ..Default::default() }; + set(l, "text/sourceText", Value::Text(Box::new(doc))); + let mut next = 900_000u64; + l.props.children.push(build::extrusion_geometry_options(&mut build::Ids(&mut next)).into()); + set(l, "geometryOptions/extrusionDepth", Value::Scalar(10.0)); + set(l, "geometryOptions/bevelStyle", Value::Enum(1)); + set(l, "transform/position", Value::Vec3([240.0, 65.0, 50.0])); + set(l, "transform/rotationY", Value::Scalar(-15.0)); + }); + let env = p.add_item("E", Default::default(), None, ItemKind::Solid(Solid { color: [0.6, 0.7, 1.0], width: 32, height: 16, pixel_aspect: 1.0 })); + add(&mut p, cid, LayerSource::Solid { item: env }, |l| { + l.environment = true; + l.switches.three_d = true; + }); + add(&mut p, cid, LayerSource::Light { kind: LightKind::Environment }, |_| {}); + let s = scene(&p, cid); + let t = g.raster_3d(&s).unwrap(); + let nan = t.color.iter().filter(|c| c.iter().any(|x| !x.is_finite())).count(); + assert_eq!(nan, 0, "non-finite pixels"); + compare(&s, &g); +} diff --git a/crates/gpu/src/walk.rs b/crates/gpu/src/walk.rs index edef7ef1..24bc6a28 100644 --- a/crates/gpu/src/walk.rs +++ b/crates/gpu/src/walk.rs @@ -107,6 +107,14 @@ fn draw_3d<'a>(e: &mut Enc, r: &Renderer<'a>, ctx: &EvalCtx<'a>, run: &[&'a Laye if run.is_empty() { return Some(()); } + // Advanced 3D: rendered and composited on the GPU (`adv3d`). + if let Some(prep) = r.prepare_adv_run(ctx, run, (canvas.width, canvas.height)) { + match crate::adv3d::draw_run(e, &prep, canvas) { + Some(img) => *canvas = img, + None => on_cpu(e, canvas, |img| r.draw_3d_run(ctx, run, img))?, + } + return Some(()); + } match r.prepare_3d_run(ctx, run, (canvas.width, canvas.height)).and_then(|prep| crate::classic3d::draw_run(e, &prep, canvas)) { Some(img) => *canvas = img, None => on_cpu(e, canvas, |img| r.draw_3d_run(ctx, run, img))?, @@ -139,9 +147,10 @@ fn draw_layer(e: &mut Enc, r: &Renderer, ctx: &EvalCtx, layer: &Layer, canvas: & } return on_cpu(e, canvas, |img| r.draw_layer_cpu(ctx, layer, img)); } - if layer.switches.quality == Quality::Wireframe { - // Wireframes draw outlines on the CPU. - return on_cpu(e, canvas, |img| r.draw_layer_cpu(ctx, layer, img)); + if r.quality(layer) == Quality::Wireframe { + // The layer's bounds as a one-pixel outline (the CPU's pixels). + *canvas = crate::adv3d::wireframe(e, canvas, &r.wireframe_pixels(ctx, layer, (canvas.width, canvas.height))); + return Some(()); } let opacity = r.layer_opacity(ctx, layer); if opacity <= 0.0 { diff --git a/crates/hevcenc/Cargo.toml b/crates/hevcenc/Cargo.toml new file mode 100644 index 00000000..136611ff --- /dev/null +++ b/crates/hevcenc/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "effectcraft-hevcenc" +description = "Clean-room pure-Rust HEVC (H.265) encoder (Main / Main 10, I and P slices, CABAC) for MP4 export, from ITU-T H.265" +version.workspace = true +edition.workspace = true +license.workspace = true +rust-version.workspace = true + +[lints] +workspace = true + +[dependencies] + +[dev-dependencies] +filmcraft-hevc = { workspace = true } diff --git a/crates/hevcenc/README.md b/crates/hevcenc/README.md new file mode 100644 index 00000000..5c4e5deb --- /dev/null +++ b/crates/hevcenc/README.md @@ -0,0 +1,88 @@ +# effectcraft-hevcenc + +Clean-room, pure-Rust HEVC / H.265 encoder for EffectCraft's MP4 export (layer L0, `std` only, +no `unsafe`, builds for `wasm32-unknown-unknown`). + +## What it produces + +- **Profiles:** Main (8-bit) and Main 10 (10-bit), 4:2:0, Main tier. `general_level_idc` is taken + from the config or chosen as the lowest level whose MaxLumaPs, MaxLumaSr (and, in bitrate mode, + MaxBR) fit (Table A.8). +- **Parameter sets:** VPS, SPS (with VUI: BT.709 colour description, `video_full_range_flag`, + timing info from the frame rate) and PPS, as raw NAL units (`Encoder::parameter_sets`) and as an + `hvcC` record (ISO/IEC 14496-15 §8.3.3.1, `lengthSizeMinusOne` 3, complete VPS / SPS / PPS + arrays, `Encoder::hvcc`). +- **Pictures:** one slice per picture; `IDR_N_LP` every `keyint` pictures (`1` = all intra, `0` = + only the first), `TRAIL_R` P pictures otherwise, each referencing the previous picture (one + short-term RPS in the SPS, `num_ref_idx_l0_active` 1, no reordering, pts = dts). Packets are + length-prefixed (4-byte big-endian) NAL units without parameter sets. +- **Picture size:** any even width / height; the coded size is padded to a multiple of 8 by edge + replication and cropped with the conformance window. Odd sizes are rejected with + `Error::InvalidConfig` (4:2:0 conformance-window offsets are in units of two luma samples). + +## Coding tools + +- 64x64 CTUs, coding units 64x64 .. 8x8 chosen by rate-distortion optimisation: every candidate is + actually predicted, transformed, quantised and reconstructed, and its rate is measured with a + CABAC bit estimator that evolves the same context states as the real coder. Split decisions + compare the best unsplit candidate with the recursively coded children (early exit after a skip). +- **Intra:** all 35 modes (SATD search on a coarse grid plus refinement and the MPMs), 2Nx2N at + 8..32 and NxN (four 4x4 PUs) at 8x8, chroma in DM mode, MPM / remaining-mode coding, reference + substitution, [1 2 1] and strong (32x32) smoothing, DC / angular boundary filters. +- **Inter (P):** skip and merge (spatial candidates + zero candidates, `MaxNumMergeCand` 5) and AMVP + with mvd coding; integer motion search (predictor starts + square pattern, steps 16..1) and + half / quarter-sample refinement on pre-interpolated half-sample planes; the coded prediction uses + the normative 8-tap / 4-tap interpolation filters and reference-picture edge clamping. +- **Transforms:** DST-VII 4x4 (intra luma) and DCT 4..32 (inverse by even/odd decomposition, + bit-identical to the matrix form), dead-zone quantisation (1/3 intra, 1/6 inter), flat scaling, + sign data hiding, inferred transform splits only (`max_transform_hierarchy_depth_*` 0, so 64x64 + CUs use four 32x32 TUs). +- **Entropy coding:** CABAC (context initialisation by slice QP and init type, decision / bypass / + terminate bins, flush), `end_of_slice_segment_flag`, byte alignment, emulation prevention. +- **In-loop filters:** the deblocking filter is on (the encoder deblocks its own reconstruction, so + references match the decoder); SAO is disabled. +- **Rate control:** `ConstantQp(qp)` or `Bitrate { kbps }`, a per-picture QP from a bits-x-Qstep + model per picture type (I pictures get roughly 4x the bits of a P picture) with a buffer that + spreads over- and undershoot over the following pictures. + +Not implemented: B pictures / reordering, multiple references, TMVP, AMP and other inter partition +shapes, residual quadtree splits, transform skip, scaling lists, PCM, SAO, RDOQ, cu_qp_delta / +adaptive quantisation, tiles / WPP / multiple slices, range extensions (4:2:2, 4:4:4, > 10 bits). + +## Measurements + +On a shared, heavily loaded machine, opt-level 3: about 0.45 s of CPU per 1920x1080 picture +(ffmpeg `testsrc2`, QP 22 / 32). On ffmpeg's `mandelbrot` 1080p source (8 pictures, IPPP, QP 32) +the stream is about 12 % larger at about 0.2 dB lower mean PSNR-Y than x265 `--preset medium` at +the same QP without psy tuning (110.5 kB / 42.3 dB vs 98.3 kB / 42.5 dB). The synthetic test image +reaches 45.8 dB PSNR-Y at QP 22. + +## Tests + +`cargo test -p effectcraft-hevcenc`: + +- every stream is decoded with the first-party `filmcraft-hevc` decoder and compared bit-exactly + with the encoder's reconstruction (`Encoder::last_reconstruction`, hidden API); +- when `ffmpeg` is on `PATH`, the Annex-B elementary stream is decoded with + `ffmpeg -v error -xerror` (no stderr output allowed) and compared bit-exactly as well (skipped + otherwise). ffmpeg is used only as an external test oracle; +- covered: 8-bit and 10-bit, all-intra and IP, QP 0 noise (escape codes), QP 0..51, strong + deblocking at high QP, sizes 8x8 .. 200x136 with cropping, POC wrap (300 pictures without IDR), + bitrate mode within 2x of the target, `hvcC` layout; +- `bench_1080p` (ignored) reports speed, size and PSNR on 1080p content generated by ffmpeg. + +## Specifications + +- ITU-T H.265 (V8, 08/2021) | ISO/IEC 23008-2, *High efficiency video coding*: 7 (syntax and + semantics), 8.4 - 8.7 (intra / inter prediction, scaling and transforms, deblocking), 9.3 (CABAC, + including the informative encoder description of 9.3.4), Annex A (profiles, tiers and levels), + Annex E (VUI). +- ISO/IEC 14496-15 (2019), §8.3.3.1 HEVCDecoderConfigurationRecord. + +Implemented from the specification text. Constant tables (CABAC context layout and initValues, +rangeTabLPS / transIdx, scan orders, the DCT / DST matrices, intra angles, interpolation filters, +the chroma QP table, the deblocking beta / tC tables) and the decoder-side reconstruction +arithmetic (intra reference substitution / filtering / prediction, fractional interpolation, +deblocking) are mirrored from FilmCraft's first-party, spec-derived `filmcraft-hevc` decoder (same +organisation, MIT OR Apache-2.0), so that encoder and decoder reconstruct identically. No +third-party encoder or decoder source (x265, libde265, ffmpeg, HM, ...) was consulted. diff --git a/crates/hevcenc/src/bitstream.rs b/crates/hevcenc/src/bitstream.rs new file mode 100644 index 00000000..27ed3063 --- /dev/null +++ b/crates/hevcenc/src/bitstream.rs @@ -0,0 +1,135 @@ +//! RBSP bit writer (7.2, 9.2 Exp-Golomb) and NAL unit encapsulation with emulation prevention (7.3.1). + +/// MSB-first bit writer. +#[derive(Default)] +pub struct BitWriter { + buf: Vec, + acc: u64, + nbits: u32, +} + +impl BitWriter { + pub fn new() -> Self { + Self::default() + } + + /// Write the `n` (<= 32) low bits of `v`. + #[inline] + pub fn put(&mut self, v: u32, n: u32) { + debug_assert!(n <= 32); + if n == 0 { + return; + } + let v = (v as u64) & ((1u64 << n) - 1); + self.acc = (self.acc << n) | v; + self.nbits += n; + while self.nbits >= 8 { + self.nbits -= 8; + self.buf.push((self.acc >> self.nbits) as u8); + } + self.acc &= (1u64 << self.nbits) - 1; + } + + #[inline] + pub fn bit(&mut self, b: u32) { + self.put(b & 1, 1); + } + + pub fn flag(&mut self, b: bool) { + self.put(b as u32, 1); + } + + /// ue(v). + pub fn ue(&mut self, v: u32) { + let x = v as u64 + 1; + let len = 64 - x.leading_zeros(); // bits of x + // len - 1 leading zeros, then x in len bits + self.put(0, len - 1); + if len > 32 { + self.put((x >> 32) as u32, len - 32); + self.put(x as u32, 32); + } else { + self.put(x as u32, len); + } + } + + /// se(v). + pub fn se(&mut self, v: i32) { + let k = if v > 0 { 2 * v as u32 - 1 } else { (-(v as i64) as u32) * 2 }; + self.ue(k); + } + + pub fn is_aligned(&self) -> bool { + self.nbits == 0 + } + + /// rbsp_trailing_bits(): stop bit and zero alignment. + pub fn trailing(&mut self) { + self.bit(1); + self.align_zero(); + } + + pub fn align_zero(&mut self) { + if self.nbits > 0 { + self.put(0, 8 - self.nbits); + } + } + + pub fn into_bytes(mut self) -> Vec { + self.align_zero(); + self.buf + } +} + +/// Build a NAL unit (2-byte header, `nuh_layer_id` 0, `TemporalId` 0) from an RBSP, inserting +/// emulation prevention bytes. +pub fn nal_unit(nal_type: u8, rbsp: &[u8]) -> Vec { + let mut out = Vec::with_capacity(rbsp.len() + rbsp.len() / 64 + 4); + out.push(nal_type << 1); + out.push(1); + let mut zeros = 0; + for &b in rbsp { + if zeros >= 2 && b <= 3 { + out.push(3); + zeros = 0; + } + out.push(b); + if b == 0 { + zeros += 1; + } else { + zeros = 0; + } + } + // cabac_zero_words / trailing zero bytes never occur: every RBSP ends with the stop bit. + out +} + +pub const NAL_TRAIL_R: u8 = 1; +pub const NAL_IDR_N_LP: u8 = 20; +pub const NAL_VPS: u8 = 32; +pub const NAL_SPS: u8 = 33; +pub const NAL_PPS: u8 = 34; + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn exp_golomb() { + let mut w = BitWriter::new(); + w.ue(0); // 1 + w.ue(1); // 010 + w.ue(2); // 011 + w.ue(3); // 00100 + w.se(-1); // ue(2) = 011 + w.trailing(); + // 1 010 011 00100 011 + stop bit: 1010 0110 0100 0111 + assert_eq!(w.into_bytes(), vec![0xa6, 0x47]); + } + + #[test] + fn emulation_prevention() { + let n = nal_unit(1, &[0, 0, 1, 0, 0, 0, 0, 0, 4]); + assert_eq!(&n[2..], &[0, 0, 3, 1, 0, 0, 3, 0, 0, 3, 0, 4]); + } +} diff --git a/crates/hevcenc/src/cabac.rs b/crates/hevcenc/src/cabac.rs new file mode 100644 index 00000000..220b999c --- /dev/null +++ b/crates/hevcenc/src/cabac.rs @@ -0,0 +1,220 @@ +//! CABAC: context initialisation (9.3.2.2), the arithmetic encoder (9.3.4.x, informative encoder +//! description: EncodeDecision, EncodeBypass, EncodeTerminate, EncodeFlush) and a fractional-bit +//! estimator with the same context-state evolution, used for rate-distortion decisions. + +use crate::bitstream::BitWriter; +use crate::tables::{CABAC_INIT, NEXT_STATE, NUM_CTX, RANGE_TAB_LPS}; +use std::sync::OnceLock; + +/// Context states, each `pStateIdx << 1 | valMps`. +pub type Contexts = [u8; NUM_CTX]; + +/// Initialise all context variables for `slice_qp` and `init_type` (9.3.2.2). +pub fn init_contexts(slice_qp: i32, init_type: usize) -> Contexts { + let mut ctx = [0u8; NUM_CTX]; + let qp = slice_qp.clamp(0, 51); + for (c, &v) in ctx.iter_mut().zip(CABAC_INIT[init_type].iter()) { + let slope = (v >> 4) as i32; + let offset = (v & 15) as i32; + let m = slope * 5 - 45; + let n = (offset << 3) - 16; + let pre = (((m * qp) >> 4) + n).clamp(1, 126); + *c = if pre <= 63 { ((63 - pre) << 1) as u8 } else { (((pre - 64) << 1) | 1) as u8 }; + } + ctx +} + +/// Destination of binarised syntax elements: the real arithmetic coder or the bit estimator. +pub trait Sink { + fn decision(&mut self, ctx: usize, bin: u32); + fn bypass(&mut self, bin: u32); + /// `n` bypass bins carrying the low `n` bits of `v`, MSB first. + fn bypass_bits(&mut self, v: u32, n: u32) { + for i in (0..n).rev() { + self.bypass((v >> i) & 1); + } + } + fn terminate(&mut self, bin: u32); +} + +/// Arithmetic encoder writing into a bit writer. +pub struct CabacEncoder { + pub ctx: Contexts, + low: u32, + range: u32, + outstanding: u32, + first: bool, + pub w: BitWriter, +} + +impl CabacEncoder { + /// Start encoding slice data into `w` (which must be byte aligned). + pub fn new(w: BitWriter, ctx: Contexts) -> Self { + debug_assert!(w.is_aligned()); + CabacEncoder { ctx, low: 0, range: 510, outstanding: 0, first: true, w } + } + + #[inline] + fn put_bit(&mut self, b: u32) { + if self.first { + self.first = false; + } else { + self.w.bit(b); + } + while self.outstanding > 0 { + self.w.bit(1 - b); + self.outstanding -= 1; + } + } + + #[inline] + fn renorm(&mut self) { + while self.range < 256 { + if self.low < 256 { + self.put_bit(0); + } else if self.low >= 512 { + self.low -= 512; + self.put_bit(1); + } else { + self.low -= 256; + self.outstanding += 1; + } + self.range <<= 1; + self.low <<= 1; + } + } + + /// EncodeFlush after a terminating bin equal to 1; the final bit written is the + /// rbsp_stop_one_bit. Returns the writer (byte aligned with zero bits). + pub fn finish(mut self) -> BitWriter { + self.range = 2; + self.renorm(); + let b = (self.low >> 9) & 1; + self.put_bit(b); + self.w.put(((self.low >> 7) & 3) | 1, 2); + self.w.align_zero(); + self.w + } +} + +impl Sink for CabacEncoder { + #[inline] + fn decision(&mut self, ctx_idx: usize, bin: u32) { + let s = self.ctx[ctx_idx] as usize; + let mps = (s & 1) as u32; + let q = ((self.range >> 6) & 3) as usize; + let lps = RANGE_TAB_LPS[s >> 1][q] as u32; + self.range -= lps; + if bin != mps { + self.low += self.range; + self.range = lps; + self.ctx[ctx_idx] = NEXT_STATE[s][1]; + } else { + self.ctx[ctx_idx] = NEXT_STATE[s][0]; + } + self.renorm(); + } + + #[inline] + fn bypass(&mut self, bin: u32) { + self.low <<= 1; + if bin != 0 { + self.low += self.range; + } + if self.low >= 1024 { + self.put_bit(1); + self.low -= 1024; + } else if self.low < 512 { + self.put_bit(0); + } else { + self.low -= 512; + self.outstanding += 1; + } + } + + fn terminate(&mut self, bin: u32) { + self.range -= 2; + if bin != 0 { + self.low += self.range; + // the caller flushes with finish() + } else { + self.renorm(); + } + } +} + +/// Fractional bit cost (1/32768 bit units) of coding the MPS / LPS in each state. +fn cost_table() -> &'static [[u32; 2]; 64] { + static T: OnceLock<[[u32; 2]; 64]> = OnceLock::new(); + T.get_or_init(|| { + let mut t = [[0u32; 2]; 64]; + // pLPS(s) = 0.5 * alpha^s with alpha = (0.01875 / 0.5)^(1/63) (9.3.4.3 design) + let alpha = (0.01875f64 / 0.5).powf(1.0 / 63.0); + for (s, e) in t.iter_mut().enumerate() { + let p = 0.5 * alpha.powi(s as i32); + e[0] = (-(1.0 - p).log2() * 32768.0).round() as u32; + e[1] = (-p.log2() * 32768.0).round() as u32; + } + t + }) +} + +pub const BIT: u64 = 32768; + +/// Bit estimator: tracks context states like the encoder and accumulates the ideal code length. +#[derive(Clone)] +pub struct Estimator { + pub ctx: Contexts, + /// Accumulated cost in 1/32768 bits. + pub bits: u64, + table: &'static [[u32; 2]; 64], +} + +impl Estimator { + pub fn new(ctx: Contexts) -> Self { + Estimator { ctx, bits: 0, table: cost_table() } + } +} + +impl Sink for Estimator { + #[inline] + fn decision(&mut self, ctx_idx: usize, bin: u32) { + let s = self.ctx[ctx_idx] as usize; + let lps = (bin != (s & 1) as u32) as usize; + self.bits += self.table[s >> 1][lps] as u64; + self.ctx[ctx_idx] = NEXT_STATE[s][lps]; + } + #[inline] + fn bypass(&mut self, _bin: u32) { + self.bits += BIT; + } + #[inline] + fn bypass_bits(&mut self, _v: u32, n: u32) { + self.bits += BIT * n as u64; + } + fn terminate(&mut self, _bin: u32) { + self.bits += 7; // ~0 bits for a 0 bin + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn context_init_formula() { + let ctx = init_contexts(30, 0); + // initValue 154: m = 0, n = 64 -> pStateIdx 0, valMps 1 + assert_eq!(ctx[crate::tables::CU_TRANSQUANT_BYPASS], 1); + // split_cu_flag ctx 0 (initValue 139) at QP 30: preCtxState 62 + assert_eq!(ctx[crate::tables::SPLIT_CU], (63 - 62) << 1); + } + + #[test] + fn costs_are_sane() { + let t = cost_table(); + assert_eq!(t[0][0], 32768); + assert_eq!(t[0][1], 32768); + assert!(t[62][0] < 1000 && t[62][1] > 5 * 32768); + } +} diff --git a/crates/hevcenc/src/deblock.rs b/crates/hevcenc/src/deblock.rs new file mode 100644 index 00000000..6f5d17b8 --- /dev/null +++ b/crates/hevcenc/src/deblock.rs @@ -0,0 +1,149 @@ +//! Deblocking filter (8.7.2) over a whole reconstructed picture, mirrored from the filmcraft-hevc +//! decoder: all vertical edges on the 8x8 grid first, then all horizontal edges. One slice, one QP, +//! uni-directional prediction from a single reference picture, offsets 0. + +use crate::frame::{Blk, E_EDGE_H, E_EDGE_V, F_CBF, F_INTRA}; +use crate::tables::{BETA_TABLE, TC_TABLE, qpc_420}; + +#[inline] +fn tc_beta(q: i32, bs: i32, bd: u32) -> (i32, i32) { + let qb = q.clamp(0, 51); + let qt = (q + 2 * (bs - 1)).clamp(0, 53); + ((TC_TABLE[qt as usize] as i32) << (bd - 8), (BETA_TABLE[qb as usize] as i32) << (bd - 8)) +} + +/// Boundary strength (8.7.2.4) between 4x4 blocks p and q on a transform / prediction edge. +fn boundary_strength(p: &Blk, q: &Blk) -> i32 { + if (p.flags | q.flags) & F_INTRA != 0 { + return 2; + } + if (p.flags | q.flags) & F_CBF != 0 { + return 1; + } + let far = (p.mv[0] as i32 - q.mv[0] as i32).abs() >= 4 || (p.mv[1] as i32 - q.mv[1] as i32).abs() >= 4; + far as i32 +} + +/// Deblock the planes in place. `qp` is the slice QpY (without QpBdOffset). +pub fn deblock(planes: &mut [Vec; 3], blk: &[Blk], w: usize, h: usize, qp: i32, bd: u32) { + let (w4, h4) = (w / 4, h / 4); + let cw = w / 2; + // chroma tc (bS 2 only; QpC from the averaged luma QP, no offsets) + let qt = (qpc_420(qp) + 2).clamp(0, 53); + let tc_c = (TC_TABLE[qt as usize] as i32) << (bd - 8); + // vertical edges + for by in 0..h4 { + for bx in (2..w4).step_by(2) { + let qi = by * w4 + bx; + if blk[qi].flags & E_EDGE_V == 0 { + continue; + } + let bs = boundary_strength(&blk[qi - 1], &blk[qi]); + if bs == 0 { + continue; + } + let (x, y) = (bx * 4, by * 4); + let (tc, beta) = tc_beta(qp, bs, bd); + filter_luma(&mut planes[0], y * w + x, 1, w, tc, beta, bd); + // chroma: 8x8 chroma grid (16 luma samples), one 4-sample chroma segment per 8 luma rows + if bs == 2 && bx % 4 == 0 && by % 2 == 0 { + for c in 1..3 { + filter_chroma(&mut planes[c], (y / 2) * cw + x / 2, 1, cw, tc_c, bd); + } + } + } + } + // horizontal edges + for by in (2..h4).step_by(2) { + for bx in 0..w4 { + let qi = by * w4 + bx; + if blk[qi].flags & E_EDGE_H == 0 { + continue; + } + let bs = boundary_strength(&blk[qi - w4], &blk[qi]); + if bs == 0 { + continue; + } + let (x, y) = (bx * 4, by * 4); + let (tc, beta) = tc_beta(qp, bs, bd); + filter_luma(&mut planes[0], y * w + x, w, 1, tc, beta, bd); + if bs == 2 && by % 4 == 0 && bx % 2 == 0 { + for c in 1..3 { + filter_chroma(&mut planes[c], (y / 2) * cw + x / 2, cw, 1, tc_c, bd); + } + } + } + } +} + +/// Filter one 4-sample luma edge segment. `o` = offset of q0 of the first line, `step` = distance +/// between samples across the edge, `along` = distance between lines. +#[allow(clippy::too_many_arguments)] +fn filter_luma(s: &mut [u16], o: usize, step: usize, along: usize, tc: i32, beta: i32, bd: u32) { + let px = |s: &[u16], k: usize, i: usize| s[o + k * along - (i + 1) * step] as i32; + let qx = |s: &[u16], k: usize, i: usize| s[o + k * along + i * step] as i32; + let dp0 = (px(s, 0, 2) - 2 * px(s, 0, 1) + px(s, 0, 0)).abs(); + let dp3 = (px(s, 3, 2) - 2 * px(s, 3, 1) + px(s, 3, 0)).abs(); + let dq0 = (qx(s, 0, 2) - 2 * qx(s, 0, 1) + qx(s, 0, 0)).abs(); + let dq3 = (qx(s, 3, 2) - 2 * qx(s, 3, 1) + qx(s, 3, 0)).abs(); + let (dpq0, dpq3) = (dp0 + dq0, dp3 + dq3); + let (dp, dq) = (dp0 + dp3, dq0 + dq3); + let d = dpq0 + dpq3; + if d >= beta { + return; + } + let dsam = |s: &[u16], k: usize, dpq: i32| -> bool { + dpq < (beta >> 2) + && (px(s, k, 3) - px(s, k, 0)).abs() + (qx(s, k, 0) - qx(s, k, 3)).abs() < (beta >> 3) + && (px(s, k, 0) - qx(s, k, 0)).abs() < ((5 * tc + 1) >> 1) + }; + let strong = dsam(s, 0, 2 * dpq0) && dsam(s, 3, 2 * dpq3); + let dep = dp < ((beta + (beta >> 1)) >> 3); + let deq = dq < ((beta + (beta >> 1)) >> 3); + let max = (1i32 << bd) - 1; + for k in 0..4 { + let base = o + k * along; + let p = |i: usize| s[base - (i + 1) * step] as i32; + let q = |i: usize| s[base + i * step] as i32; + let (p0, p1, p2, p3) = (p(0), p(1), p(2), p(3)); + let (q0, q1, q2, q3) = (q(0), q(1), q(2), q(3)); + if strong { + let t2 = 2 * tc; + s[base - step] = ((p2 + 2 * p1 + 2 * p0 + 2 * q0 + q1 + 4) >> 3).clamp(p0 - t2, p0 + t2) as u16; + s[base - 2 * step] = ((p2 + p1 + p0 + q0 + 2) >> 2).clamp(p1 - t2, p1 + t2) as u16; + s[base - 3 * step] = ((2 * p3 + 3 * p2 + p1 + p0 + q0 + 4) >> 3).clamp(p2 - t2, p2 + t2) as u16; + s[base] = ((p1 + 2 * p0 + 2 * q0 + 2 * q1 + q2 + 4) >> 3).clamp(q0 - t2, q0 + t2) as u16; + s[base + step] = ((p0 + q0 + q1 + q2 + 2) >> 2).clamp(q1 - t2, q1 + t2) as u16; + s[base + 2 * step] = ((p0 + q0 + q1 + 3 * q2 + 2 * q3 + 4) >> 3).clamp(q2 - t2, q2 + t2) as u16; + } else { + let mut delta = (9 * (q0 - p0) - 3 * (q1 - p1) + 8) >> 4; + if delta.abs() >= tc * 10 { + continue; + } + delta = delta.clamp(-tc, tc); + s[base - step] = (p0 + delta).clamp(0, max) as u16; + s[base] = (q0 - delta).clamp(0, max) as u16; + if dep { + let dp = ((((p2 + p0 + 1) >> 1) - p1 + delta) >> 1).clamp(-(tc >> 1), tc >> 1); + s[base - 2 * step] = (p1 + dp).clamp(0, max) as u16; + } + if deq { + let dq = ((((q2 + q0 + 1) >> 1) - q1 - delta) >> 1).clamp(-(tc >> 1), tc >> 1); + s[base + step] = (q1 + dq).clamp(0, max) as u16; + } + } + } +} + +/// Filter one 4-sample chroma edge segment. +fn filter_chroma(s: &mut [u16], o: usize, step: usize, along: usize, tc: i32, bd: u32) { + let max = (1i32 << bd) - 1; + for k in 0..4 { + let base = o + k * along; + let (p0, p1) = (s[base - step] as i32, s[base - 2 * step] as i32); + let (q0, q1) = (s[base] as i32, s[base + step] as i32); + let delta = ((((q0 - p0) << 2) + p1 - q1 + 4) >> 3).clamp(-tc, tc); + s[base - step] = (p0 + delta).clamp(0, max) as u16; + s[base] = (q0 - delta).clamp(0, max) as u16; + } +} diff --git a/crates/hevcenc/src/frame.rs b/crates/hevcenc/src/frame.rs new file mode 100644 index 00000000..2cbeb4f3 --- /dev/null +++ b/crates/hevcenc/src/frame.rs @@ -0,0 +1,1438 @@ +//! Picture coding: CTU loop, rate-distortion mode decisions (with a CABAC bit estimator) and the +//! slice_segment_data() syntax (7.3.8): coding quadtree, coding unit, prediction unit, transform tree +//! and residual coding. Reconstruction mirrors the decoding process exactly (no in-loop filters). + +use crate::cabac::{BIT, CabacEncoder, Estimator, Sink}; +use crate::inter::{self, PlaneRef}; +use crate::intra::{self, Refs}; +use crate::me::MeRef; +use crate::params::{LOG2_CTB, LOG2_MAX_TB, LOG2_MIN_CB, MAX_MERGE_CAND}; +use crate::tables::*; +use crate::transform; + +pub const F_INTRA: u8 = 1; +pub const F_SKIP: u8 = 2; +/// The luma transform block covering this 4x4 block has non-zero coefficients. +pub const F_CBF: u8 = 4; +/// The left / top edge of this 4x4 block is a transform (and prediction) block edge. +pub const E_EDGE_V: u8 = 8; +pub const E_EDGE_H: u8 = 16; + +/// Per 4x4 luma block state of the current picture. +#[derive(Clone, Copy, Default)] +pub struct Blk { + pub depth: u8, + pub flags: u8, + /// IntraPredModeY (DC for inter blocks). + pub ipm: u8, + /// L0 motion vector (quarter samples) of inter blocks. + pub mv: [i16; 2], +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum CuMode { + Intra { nxn: bool, modes: [u8; 4] }, + Skip { idx: u8 }, + Merge { idx: u8 }, + Amvp { mv: [i16; 2], mvp: u8 }, +} + +#[derive(Clone, Copy, Debug)] +struct CuDec { + x: u32, + y: u32, + log2: u32, + mode: CuMode, +} + +/// One coded transform block of the current CU. +#[derive(Clone, Copy)] +struct Tb { + c: u8, + /// Luma-coordinate region the block belongs to (for cbf derivation). + lx: u32, + ly: u32, + log2: u32, + nz: bool, + off: usize, + scan: u8, +} + +/// Saved samples and block state of a square region. +struct Snap { + x: usize, + y: usize, + n: usize, + planes: [Vec; 3], + blk: Vec, +} + +#[derive(Clone, Copy, PartialEq, Eq)] +pub enum SliceKind { + I, + P, +} + +pub struct FrameCoder<'a> { + pub bd: u32, + pub w: usize, + pub h: usize, + pub cw: usize, + pub ch: usize, + w4: usize, + wctb: usize, + hctb: usize, + pub kind: SliceKind, + /// QP' (with QpBdOffset) for luma and chroma. + slice_qp: i32, + qp_y: i32, + qp_c: i32, + lambda: f64, + lambda_sad: f64, + src: &'a [Vec; 3], + pub rec: [Vec; 3], + refp: Option<&'a [Vec; 3]>, + me: Option<&'a MeRef>, + blk: Vec, + tbs: Vec, + coefs: Vec, + pred: Vec, + scratch: [Vec; 2], +} + +#[inline] +fn morton(x: u32, y: u32) -> u32 { + let mut r = 0; + for i in 0..5 { + r |= ((x >> i) & 1) << (2 * i); + r |= ((y >> i) & 1) << (2 * i + 1); + } + r +} + +/// Intra modes tried in the coarse pass of the SATD search. +const COARSE_MODES: [u32; 11] = [0, 1, 2, 6, 10, 14, 18, 22, 26, 30, 34]; + +impl<'a> FrameCoder<'a> { + #[allow(clippy::too_many_arguments)] + pub fn new( + bd: u32, + w: usize, + h: usize, + kind: SliceKind, + slice_qp: i32, + src: &'a [Vec; 3], + refp: Option<&'a [Vec; 3]>, + me: Option<&'a MeRef>, + ) -> Self { + let off = 6 * (bd as i32 - 8); + let qpi = slice_qp.clamp(-off, 57); + let qp_c = qpc_420(qpi) + off; + let base = if kind == SliceKind::I { 0.57 } else { 0.62 }; + let lambda = base * 2f64.powf((slice_qp as f64 - 12.0) / 3.0) * (1u64 << (2 * (bd - 8))) as f64; + let ctb = 1usize << LOG2_CTB; + FrameCoder { + bd, + w, + h, + cw: w / 2, + ch: h / 2, + w4: w / 4, + wctb: w.div_ceil(ctb), + hctb: h.div_ceil(ctb), + kind, + slice_qp, + qp_y: slice_qp + off, + qp_c, + lambda, + lambda_sad: lambda.sqrt(), + src, + rec: [vec![0; w * h], vec![0; w * h / 4], vec![0; w * h / 4]], + refp, + me, + blk: vec![Blk::default(); (w / 4) * (h / 4)], + tbs: Vec::with_capacity(16), + coefs: Vec::with_capacity(64 * 64 * 2), + pred: vec![0; 64 * 64], + scratch: [vec![0; 32 * 32], vec![0; 32 * 32]], + } + } + + // --------------------------------------------------------------------------------------- + // neighbourhood + + /// z-scan order availability (6.4.1) of (xn, yn) for the block at (xc, yc): single slice, no tiles. + #[inline] + fn zavail(&self, xc: i32, yc: i32, xn: i32, yn: i32) -> bool { + if xn < 0 || yn < 0 || xn >= self.w as i32 || yn >= self.h as i32 { + return false; + } + let s = LOG2_CTB; + let ctb_n = (yn >> s) as usize * self.wctb + (xn >> s) as usize; + let ctb_c = (yc >> s) as usize * self.wctb + (xc >> s) as usize; + if ctb_n != ctb_c { + ctb_n < ctb_c + } else { + let m = (1 << s) - 1; + morton(((xn & m) >> 2) as u32, ((yn & m) >> 2) as u32) <= morton(((xc & m) >> 2) as u32, ((yc & m) >> 2) as u32) + } + } + + #[inline] + fn blk_at(&self, x: i32, y: i32) -> &Blk { + &self.blk[(y as usize >> 2) * self.w4 + (x as usize >> 2)] + } + + fn fill(&mut self, x: usize, y: usize, n: usize, f: impl Fn(&mut Blk)) { + for by in (y >> 2)..((y + n) >> 2) { + for b in &mut self.blk[by * self.w4 + (x >> 2)..by * self.w4 + ((x + n) >> 2)] { + f(b); + } + } + } + + /// Mark the CU's left / top edges and, for CUs larger than the maximum transform size, the + /// inner transform edges (deblocking). + fn mark_edges(&mut self, x: usize, y: usize, log2: u32) { + let n = 1usize << log2; + let t = 1usize << log2.min(LOG2_MAX_TB); + for ty in (y..y + n).step_by(t) { + for tx in (x..x + n).step_by(t) { + for by in ty / 4..(ty + t) / 4 { + self.blk[by * self.w4 + tx / 4].flags |= E_EDGE_V; + } + for b in &mut self.blk[(ty / 4) * self.w4 + tx / 4..(ty / 4) * self.w4 + (tx + t) / 4] { + b.flags |= E_EDGE_H; + } + } + } + } + + /// In-loop deblocking of the finished picture. + pub fn deblock(&mut self) { + crate::deblock::deblock(&mut self.rec, &self.blk, self.w, self.h, self.slice_qp, self.bd); + } + + fn snap(&self, x: usize, y: usize, n: usize) -> Snap { + let mut planes: [Vec; 3] = Default::default(); + for (c, p) in planes.iter_mut().enumerate() { + let (px, py, pn, stride) = if c == 0 { (x, y, n, self.w) } else { (x / 2, y / 2, n / 2, self.cw) }; + p.reserve(pn * pn); + for r in 0..pn { + p.extend_from_slice(&self.rec[c][(py + r) * stride + px..][..pn]); + } + } + let mut blk = Vec::with_capacity((n / 4) * (n / 4)); + for by in (y >> 2)..((y + n) >> 2) { + blk.extend_from_slice(&self.blk[by * self.w4 + (x >> 2)..by * self.w4 + ((x + n) >> 2)]); + } + Snap { x, y, n, planes, blk } + } + + fn restore(&mut self, s: &Snap) { + let (x, y, n) = (s.x, s.y, s.n); + for c in 0..3 { + let (px, py, pn, stride) = if c == 0 { (x, y, n, self.w) } else { (x / 2, y / 2, n / 2, self.cw) }; + for r in 0..pn { + self.rec[c][(py + r) * stride + px..][..pn].copy_from_slice(&s.planes[c][r * pn..r * pn + pn]); + } + } + let bw = n / 4; + for (i, by) in ((y >> 2)..((y + n) >> 2)).enumerate() { + self.blk[by * self.w4 + (x >> 2)..by * self.w4 + (x >> 2) + bw].copy_from_slice(&s.blk[i * bw..i * bw + bw]); + } + } + + // --------------------------------------------------------------------------------------- + // picture loop + + /// Code all CTUs of the picture into `enc` (one slice); the caller flushes the encoder. + pub fn encode(&mut self, enc: &mut CabacEncoder) { + let ctb = 1usize << LOG2_CTB; + let total = self.wctb * self.hctb; + for i in 0..total { + let (x, y) = ((i % self.wctb) * ctb, (i / self.wctb) * ctb); + let mut est = Estimator::new(enc.ctx); + let mut decs = Vec::with_capacity(64); + self.decide(x, y, LOG2_CTB, 0, &mut est, &mut decs); + let mut idx = 0; + self.write_cq(enc, x, y, LOG2_CTB, 0, &decs, &mut idx); + debug_assert_eq!(idx, decs.len()); + debug_assert_eq!(est.ctx, enc.ctx); + enc.terminate((i + 1 == total) as u32); // end_of_slice_segment_flag + } + } + + fn write_split(&self, s: &mut S, x: usize, y: usize, depth: u32, split: bool) { + let (x, y) = (x as i32, y as i32); + let mut inc = 0; + if self.zavail(x, y, x - 1, y) && self.blk_at(x - 1, y).depth as u32 > depth { + inc += 1; + } + if self.zavail(x, y, x, y - 1) && self.blk_at(x, y - 1).depth as u32 > depth { + inc += 1; + } + s.decision(SPLIT_CU + inc, split as u32); + } + + /// Final pass: re-code the decided quadtree with the real arithmetic coder. + fn write_cq(&mut self, enc: &mut CabacEncoder, x: usize, y: usize, log2: u32, depth: u32, decs: &[CuDec], idx: &mut usize) { + if x >= self.w || y >= self.h { + return; + } + let n = 1usize << log2; + let inside = x + n <= self.w && y + n <= self.h; + let d = decs[*idx]; + let leaf = d.x as usize == x && d.y as usize == y && d.log2 == log2; + if inside && log2 > LOG2_MIN_CB { + self.write_split(enc, x, y, depth, !leaf); + } else { + debug_assert_eq!(leaf, log2 == LOG2_MIN_CB); + } + if leaf { + *idx += 1; + let (eff, _) = self.encode_cu(enc, x, y, log2, depth, d.mode); + debug_assert_eq!(eff, d.mode); + } else { + let h = n / 2; + for (dx, dy) in [(0, 0), (h, 0), (0, h), (h, h)] { + self.write_cq(enc, x + dx, y + dy, log2 - 1, depth + 1, decs, idx); + } + } + } + + #[inline] + fn cost(&self, dist: u64, bits: u64) -> f64 { + dist as f64 + self.lambda * bits as f64 / BIT as f64 + } + + /// Rate-distortion decision for the quadtree node at (x, y); leaves the chosen reconstruction + /// and block state in place, advances `est` and appends the decided CUs. Returns the RD cost. + fn decide(&mut self, x: usize, y: usize, log2: u32, depth: u32, est: &mut Estimator, decs: &mut Vec) -> f64 { + if x >= self.w || y >= self.h { + return 0.0; + } + let n = 1usize << log2; + let inside = x + n <= self.w && y + n <= self.h; + let can_split = log2 > LOG2_MIN_CB; + let base = est.bits; + let leaf_ok = inside && (self.kind == SliceKind::P || log2 <= LOG2_MAX_TB); + let mut best: Option<(f64, Estimator, Snap, CuMode)> = None; + if leaf_ok { + let mut e0 = est.clone(); + if can_split { + self.write_split(&mut e0, x, y, depth, false); + } + let try_mode = |me: &mut Self, mode: CuMode, best: &mut Option<(f64, Estimator, Snap, CuMode)>| { + let mut e = e0.clone(); + let (eff, dist) = me.encode_cu(&mut e, x, y, log2, depth, mode); + let cost = me.cost(dist, e.bits - base); + if best.as_ref().is_none_or(|b| cost < b.0) { + *best = Some((cost, e, me.snap(x, y, n), eff)); + } + }; + let mut inter_satd = u64::MAX; + if self.kind == SliceKind::P { + let merge = self.merge_list(x, y, n); + let (midx, msatd) = self.best_merge(x, y, n, &merge); + try_mode(self, CuMode::Skip { idx: midx as u8 }, &mut best); + try_mode(self, CuMode::Merge { idx: midx as u8 }, &mut best); + let amvp = self.amvp(x, y, n); + let (mv, mvp, asatd) = self.motion_search(x, y, n, &amvp, &merge); + if mv != merge[midx] { + try_mode(self, CuMode::Amvp { mv, mvp }, &mut best); + } + inter_satd = msatd.min(asatd); + } + if log2 <= LOG2_MAX_TB && !matches!(best, Some((_, _, _, CuMode::Skip { .. }))) { + let (mode, satd) = self.search_intra_mode(x, y, log2); + if satd < inter_satd { + try_mode(self, CuMode::Intra { nxn: false, modes: [mode as u8; 4] }, &mut best); + if log2 == LOG2_MIN_CB { + let modes = self.search_nxn_modes(x, y); + try_mode(self, CuMode::Intra { nxn: true, modes }, &mut best); + } + } + } + if let Some(b) = &best { + self.restore(&b.2); + } + } + let early = matches!(best, Some((_, _, _, CuMode::Skip { .. }))); + if can_split && (!leaf_ok || !early) { + let mut e = est.clone(); + if inside { + self.write_split(&mut e, x, y, depth, true); + } + let limit = best.as_ref().map_or(f64::INFINITY, |b| b.0); + let mut cost = self.cost(0, e.bits - base); + let mut d2 = Vec::new(); + let h = n / 2; + for (dx, dy) in [(0, 0), (h, 0), (0, h), (h, h)] { + cost += self.decide(x + dx, y + dy, log2 - 1, depth + 1, &mut e, &mut d2); + if cost >= limit { + break; + } + } + if cost < limit { + *est = e; + decs.extend(d2); + return cost; + } + } + let (cost, e, snap, mode) = best.expect("a coding unit decision"); + self.restore(&snap); + *est = e; + decs.push(CuDec { x: x as u32, y: y as u32, log2, mode }); + cost + } + + // --------------------------------------------------------------------------------------- + // coding unit + + fn skip_ctx(&self, x: usize, y: usize) -> usize { + let (x, y) = (x as i32, y as i32); + let mut inc = 0; + if self.zavail(x, y, x - 1, y) && self.blk_at(x - 1, y).flags & F_SKIP != 0 { + inc += 1; + } + if self.zavail(x, y, x, y - 1) && self.blk_at(x, y - 1).flags & F_SKIP != 0 { + inc += 1; + } + inc + } + + /// Predict, transform, quantise and reconstruct one CU and write its syntax into `s`. + /// Returns the effective mode (a residual-free merge becomes a skip) and the SSE. + fn encode_cu(&mut self, s: &mut S, x: usize, y: usize, log2: u32, depth: u32, mode: CuMode) -> (CuMode, u64) { + let n = 1usize << log2; + self.tbs.clear(); + self.coefs.clear(); + let p_slice = self.kind == SliceKind::P; + let skip_ctx = if p_slice { self.skip_ctx(x, y) } else { 0 }; + let d8 = depth as u8; + match mode { + CuMode::Intra { nxn, modes } => { + self.fill(x, y, n, |b| { + b.depth = d8; + b.flags = F_INTRA; + b.ipm = modes[0]; + }); + self.mark_edges(x, y, log2); + if nxn { + let h = n / 2; + for k in 1..4 { + let m = modes[k]; + self.fill(x + (k & 1) * h, y + (k >> 1) * h, h, |b| b.ipm = m); + } + for k in 0..4 { + let (px, py) = (x + (k & 1) * h, y + (k >> 1) * h); + self.code_tb(0, px, py, 2, Some(modes[k] as u32), (x, y)); + } + for c in 1..3 { + self.code_tb(c, x / 2, y / 2, 2, Some(modes[0] as u32), (x, y)); + } + } else { + self.code_tus(x, y, log2, Some(modes[0] as u32)); + } + if p_slice { + s.decision(CU_SKIP + skip_ctx, 0); + s.decision(PRED_MODE, 1); + } + if log2 == LOG2_MIN_CB { + s.decision(PART_MODE, (!nxn) as u32); + } + self.write_intra_modes(s, x, y, n, nxn, &modes); + s.decision(INTRA_CHROMA, 0); // intra_chroma_pred_mode 4 (DM) + let mut ti = 0; + self.write_tt(s, x, y, x, y, log2, 0, 0, [true, true], true, nxn, &mut ti); + (mode, self.cu_dist(x, y, n)) + } + _ => { + let mv = match mode { + CuMode::Skip { idx } | CuMode::Merge { idx } => self.merge_list(x, y, n)[idx as usize], + CuMode::Amvp { mv, .. } => mv, + CuMode::Intra { .. } => unreachable!(), + }; + let amvp = if let CuMode::Amvp { .. } = mode { self.amvp(x, y, n) } else { [[0; 2]; 2] }; + let skip = matches!(mode, CuMode::Skip { .. }); + self.fill(x, y, n, |b| { + b.depth = d8; + b.flags = if skip { F_SKIP } else { 0 }; + b.ipm = 1; + b.mv = mv; + }); + self.mark_edges(x, y, log2); + self.predict_inter(x, y, n, mv); + let mut eff = mode; + let mut root = false; + if !skip { + self.code_tus(x, y, log2, None); + root = self.tbs.iter().any(|t| t.nz); + if !root && let CuMode::Merge { idx } = mode { + // rqt_root_cbf would be inferred: code a skipped CU instead + eff = CuMode::Skip { idx }; + self.fill(x, y, n, |b| b.flags |= F_SKIP); + } + } + match eff { + CuMode::Skip { idx } => { + s.decision(CU_SKIP + skip_ctx, 1); + write_merge_idx(s, idx as usize); + } + CuMode::Merge { idx } => { + s.decision(CU_SKIP + skip_ctx, 0); + s.decision(PRED_MODE, 0); + s.decision(PART_MODE, 1); // PART_2Nx2N + s.decision(MERGE_FLAG, 1); + write_merge_idx(s, idx as usize); + let mut ti = 0; + self.write_tt(s, x, y, x, y, log2, 0, 0, [true, true], false, false, &mut ti); + } + CuMode::Amvp { mv, mvp } => { + s.decision(CU_SKIP + skip_ctx, 0); + s.decision(PRED_MODE, 0); + s.decision(PART_MODE, 1); + s.decision(MERGE_FLAG, 0); + let p = amvp[mvp as usize]; + write_mvd(s, [mv[0] as i32 - p[0] as i32, mv[1] as i32 - p[1] as i32]); + s.decision(MVP_FLAG, mvp as u32); + s.decision(RQT_ROOT_CBF, root as u32); + if root { + let mut ti = 0; + self.write_tt(s, x, y, x, y, log2, 0, 0, [true, true], false, false, &mut ti); + } + } + CuMode::Intra { .. } => unreachable!(), + } + (eff, self.cu_dist(x, y, n)) + } + } + } + + /// Transform units of a 2Nx2N CU (split to the maximum TB size), luma then Cb, Cr per TU. + fn code_tus(&mut self, x: usize, y: usize, log2: u32, intra_mode: Option) { + let tl = log2.min(LOG2_MAX_TB); + let t = 1usize << tl; + let k = 1usize << (log2 - tl); + for i in 0..k * k { + // z-order of at most 2x2 TUs + let (tx, ty) = (x + (i & 1) * t, y + (i >> 1) * t); + self.code_tb(0, tx, ty, tl, intra_mode, (tx, ty)); + self.code_tb(1, tx / 2, ty / 2, tl - 1, intra_mode, (tx, ty)); + self.code_tb(2, tx / 2, ty / 2, tl - 1, intra_mode, (tx, ty)); + } + } + + fn cu_dist(&self, x: usize, y: usize, n: usize) -> u64 { + let mut d = 0u64; + for c in 0..3 { + let (px, py, pn, stride) = if c == 0 { (x, y, n, self.w) } else { (x / 2, y / 2, n / 2, self.cw) }; + for r in 0..pn { + let o = (py + r) * stride + px; + let a = &self.src[c][o..o + pn]; + let b = &self.rec[c][o..o + pn]; + d += a.iter().zip(b).map(|(&p, &q)| (p as i64 - q as i64).pow(2) as u64).sum::(); + } + } + d + } + + /// Code one transform block of component `c` at component position (x, y): optional intra + /// prediction, residual, transform, quantisation and reconstruction. + fn code_tb(&mut self, c: usize, x: usize, y: usize, log2: u32, intra_mode: Option, lpos: (usize, usize)) { + let n = 1usize << log2; + let stride = if c == 0 { self.w } else { self.cw }; + if let Some(m) = intra_mode { + let refs = self.build_refs(c, x, y, log2); + let bd = self.bd; + predict_from_refs(&refs, c, m, n, bd, &mut self.rec[c][y * stride + x..], stride); + } + let mut scr = std::mem::take(&mut self.scratch); + let [r, coef] = &mut scr; + for yy in 0..n { + let o = (y + yy) * stride + x; + for xx in 0..n { + r[yy * n + xx] = self.src[c][o + xx] as i32 - self.rec[c][o + xx] as i32; + } + } + let intra = intra_mode.is_some(); + let dst = intra && c == 0 && n == 4; + transform::forward(&r[..n * n], &mut coef[..n * n], n, dst, self.bd); + let qp = if c == 0 { self.qp_y } else { self.qp_c }; + let off = self.coefs.len(); + self.coefs.resize(off + n * n, 0); + let lv = &mut self.coefs[off..off + n * n]; + let scan = match intra_mode { + Some(m) if log2 == 2 || (log2 == 3 && c == 0) => scan_idx_for_mode(m), + _ => 0, + }; + let nz = transform::quantize(&coef[..n * n], lv, n, qp, intra, self.bd) > 0; + if nz { + transform::sign_hiding(&coef[..n * n], lv, log2, scan as usize, qp, self.bd); + } + if nz { + let d = &mut r[..n * n]; + transform::dequantize(lv, d, n, qp, self.bd); + transform::inverse(d, n, dst, self.bd); + let max = (1i32 << self.bd) - 1; + for yy in 0..n { + let o = (y + yy) * stride + x; + for xx in 0..n { + let v = &mut self.rec[c][o + xx]; + *v = (*v as i32 + d[yy * n + xx]).clamp(0, max) as u16; + } + } + } + self.scratch = scr; + if nz && c == 0 { + self.fill(x, y, n, |b| b.flags |= F_CBF); + } + self.tbs.push(Tb { c: c as u8, lx: lpos.0 as u32, ly: lpos.1 as u32, log2, nz, off, scan }); + } + + /// Reference samples (substituted, unfiltered) of an intra block, as in 8.4.4.2.1 / 8.4.4.2.2. + fn build_refs(&self, c: usize, x0: usize, y0: usize, log2: u32) -> Refs { + let n = 1usize << log2; + let sc = if c == 0 { 1 } else { 2 }; + let (x0, y0) = (x0 as i32, y0 as i32); + let (xl, yl) = (x0 * sc, y0 * sc); + let unit = 4 / sc as usize; + let stride = if c == 0 { self.w } else { self.cw }; + let mut al = [false; 129]; + let mut at = [false; 129]; + let plane = &self.rec[c]; + let mut r = Refs::default(); + if self.zavail(xl, yl, xl - 1, yl - 1) { + al[0] = true; + at[0] = true; + let v = plane[(y0 - 1) as usize * stride + (x0 - 1) as usize]; + r.left[0] = v; + r.top[0] = v; + } + let mut k = 0; + while k < 2 * n { + let yn = y0 + k as i32; + if self.zavail(xl, yl, xl - 1, yn * sc) { + for i in 0..unit { + al[1 + k + i] = true; + r.left[1 + k + i] = plane[(yn as usize + i) * stride + (x0 - 1) as usize]; + } + } + let xn = x0 + k as i32; + if self.zavail(xl, yl, xn * sc, yl - 1) { + let o = (y0 - 1) as usize * stride + xn as usize; + for i in 0..unit { + at[1 + k + i] = true; + r.top[1 + k + i] = plane[o + i]; + } + } + k += unit; + } + intra::substitute(&mut r, &al, &at, n, self.bd); + r + } + + // --------------------------------------------------------------------------------------- + // syntax writers + + /// Candidate modes candModeList (8.4.2) of the prediction block at (xp, yp). + fn mpm(&self, xp: usize, yp: usize) -> [u32; 3] { + let (xp, yp) = (xp as i32, yp as i32); + let cand_a = if self.zavail(xp, yp, xp - 1, yp) { + let b = self.blk_at(xp - 1, yp); + if b.flags & F_INTRA != 0 { b.ipm as u32 } else { 1 } + } else { + 1 + }; + let cand_b = if self.zavail(xp, yp, xp, yp - 1) && yp > ((yp >> LOG2_CTB) << LOG2_CTB) { + let b = self.blk_at(xp, yp - 1); + if b.flags & F_INTRA != 0 { b.ipm as u32 } else { 1 } + } else { + 1 + }; + if cand_a == cand_b { + if cand_a < 2 { [0, 1, 26] } else { [cand_a, 2 + ((cand_a + 29) % 32), 2 + ((cand_a - 2 + 1) % 32)] } + } else { + let c = if cand_a != 0 && cand_b != 0 { + 0 + } else if cand_a != 1 && cand_b != 1 { + 1 + } else { + 26 + }; + [cand_a, cand_b, c] + } + } + + fn write_intra_modes(&self, s: &mut S, x: usize, y: usize, n: usize, nxn: bool, modes: &[u8; 4]) { + let nb = if nxn { 2 } else { 1 }; + let pb = n / nb; + let mut codes = [(false, 0u32); 4]; + for j in 0..nb { + for i in 0..nb { + let k = j * nb + i; + let list = self.mpm(x + i * pb, y + j * pb); + let m = modes[k] as u32; + codes[k] = match list.iter().position(|&c| c == m) { + Some(idx) => (true, idx as u32), + None => { + let mut sorted = list; + sorted.sort_unstable(); + let mut r = m; + for &c in sorted.iter().rev() { + if r > c { + r -= 1; + } + } + (false, r) + } + }; + } + } + for code in codes.iter().take(nb * nb) { + s.decision(PREV_INTRA_LUMA, code.0 as u32); + } + for &(prev, v) in codes.iter().take(nb * nb) { + if prev { + s.bypass((v > 0) as u32); + if v > 0 { + s.bypass((v > 1) as u32); + } + } else { + s.bypass_bits(v, 5); + } + } + } + + fn chroma_cbf(&self, c: u8, x0: usize, y0: usize, n: usize) -> bool { + self.tbs.iter().any(|t| t.c == c && t.nz && (t.lx as usize) >= x0 && (t.lx as usize) < x0 + n && (t.ly as usize) >= y0 && (t.ly as usize) < y0 + n) + } + + /// transform_tree() (7.3.8.8) over the coded transform blocks of the CU; no split flag is ever + /// coded (max_transform_hierarchy_depth_* = 0), splits are inferred. + #[allow(clippy::too_many_arguments)] + fn write_tt( + &self, + s: &mut S, + x0: usize, + y0: usize, + xb: usize, + yb: usize, + log2: u32, + depth: u32, + blk_idx: u32, + parent: [bool; 2], + intra: bool, + nxn: bool, + ti: &mut usize, + ) { + let n = 1usize << log2; + let split = log2 > LOG2_MAX_TB || (nxn && depth == 0); + let mut cbf_c = [false; 2]; + if log2 > 2 { + for c in 0..2 { + if depth == 0 || parent[c] { + cbf_c[c] = self.chroma_cbf(c as u8 + 1, x0, y0, n); + s.decision(CBF_CHROMA + depth as usize, cbf_c[c] as u32); + } + } + } else if depth > 0 { + cbf_c = parent; + } + if split { + let h = n / 2; + for (i, (dx, dy)) in [(0, 0), (h, 0), (0, h), (h, h)].into_iter().enumerate() { + self.write_tt(s, x0 + dx, y0 + dy, x0, y0, log2 - 1, depth + 1, i as u32, cbf_c, intra, nxn, ti); + } + return; + } + let _ = (xb, yb); + let tb = self.tbs[*ti]; + *ti += 1; + debug_assert_eq!(tb.c, 0); + if intra || depth != 0 || cbf_c[0] || cbf_c[1] { + s.decision(CBF_LUMA + (depth == 0) as usize, tb.nz as u32); + } else { + debug_assert!(tb.nz, "inferred cbf_luma needs coefficients"); + } + if tb.nz { + let n2 = 1usize << (2 * tb.log2); + residual_coding(s, &self.coefs[tb.off..tb.off + n2], tb.log2, 0, tb.scan as usize); + } + if log2 > 2 || blk_idx == 3 { + for c in 0..2 { + let tb = self.tbs[*ti]; + *ti += 1; + debug_assert_eq!(tb.c as usize, c + 1); + debug_assert_eq!(tb.nz, cbf_c[c]); + if cbf_c[c] { + let n2 = 1usize << (2 * tb.log2); + residual_coding(s, &self.coefs[tb.off..tb.off + n2], tb.log2, c + 1, tb.scan as usize); + } + } + } + } + + // --------------------------------------------------------------------------------------- + // inter prediction + + /// Prediction block availability (6.4.2) for a 2Nx2N CU: available and not intra. + #[inline] + fn pb_avail(&self, xp: i32, yp: i32, xn: i32, yn: i32) -> bool { + self.zavail(xp, yp, xn, yn) && self.blk_at(xn, yn).flags & F_INTRA == 0 + } + + /// Merge candidate list (8.5.3.2.2 - 8.5.3.2.5) of a 2Nx2N P-slice CU: spatial candidates and + /// zero candidates (no temporal candidate, one reference picture). + pub fn merge_list(&self, x: usize, y: usize, n: usize) -> [[i16; 2]; MAX_MERGE_CAND] { + let (xp, yp, w, h) = (x as i32, y as i32, n as i32, n as i32); + let mv = |p: (i32, i32)| self.blk_at(p.0, p.1).mv; + let mut list = [[0i16; 2]; MAX_MERGE_CAND]; + let mut k = 0; + let pa1 = (xp - 1, yp + h - 1); + let av_a1 = self.pb_avail(xp, yp, pa1.0, pa1.1); + if av_a1 { + list[k] = mv(pa1); + k += 1; + } + let pb1 = (xp + w - 1, yp - 1); + let av_b1 = self.pb_avail(xp, yp, pb1.0, pb1.1); + let fl_b1 = av_b1 && !(av_a1 && mv(pa1) == mv(pb1)); + if fl_b1 { + list[k] = mv(pb1); + k += 1; + } + let pb0 = (xp + w, yp - 1); + let fl_b0 = self.pb_avail(xp, yp, pb0.0, pb0.1) && !(av_b1 && mv(pb1) == mv(pb0)); + if fl_b0 { + list[k] = mv(pb0); + k += 1; + } + let pa0 = (xp - 1, yp + h); + let fl_a0 = self.pb_avail(xp, yp, pa0.0, pa0.1) && !(av_a1 && mv(pa1) == mv(pa0)); + if fl_a0 { + list[k] = mv(pa0); + k += 1; + } + let pb2 = (xp - 1, yp - 1); + let fl_b2 = self.pb_avail(xp, yp, pb2.0, pb2.1) + && !((av_a1 && mv(pa1) == mv(pb2)) || (av_b1 && mv(pb1) == mv(pb2))) + && (fl_a0 as u32 + av_a1 as u32 + fl_b0 as u32 + fl_b1 as u32) != 4; + if fl_b2 { + list[k] = mv(pb2); + } + // remaining entries are zero candidates with refIdx 0 + list + } + + /// AMVP candidates (8.5.3.2.6, 8.5.3.2.7) of a 2Nx2N CU with one reference picture. + pub fn amvp(&self, x: usize, y: usize, n: usize) -> [[i16; 2]; 2] { + let (xp, yp, w, h) = (x as i32, y as i32, n as i32, n as i32); + let pa = [(xp - 1, yp + h), (xp - 1, yp + h - 1)]; + let av_a = [self.pb_avail(xp, yp, pa[0].0, pa[0].1), self.pb_avail(xp, yp, pa[1].0, pa[1].1)]; + let is_scaled = av_a[0] || av_a[1]; + let mut a = None; + for k in 0..2 { + if av_a[k] && a.is_none() { + a = Some(self.blk_at(pa[k].0, pa[k].1).mv); + } + } + let pb = [(xp + w, yp - 1), (xp + w - 1, yp - 1), (xp - 1, yp - 1)]; + let mut b = None; + for p in pb { + if b.is_none() && self.pb_avail(xp, yp, p.0, p.1) { + b = Some(self.blk_at(p.0, p.1).mv); + } + } + if !is_scaled && b.is_some() { + a = b; + } + let mut list = [[0i16; 2]; 2]; + match (a, b) { + (Some(a), Some(b)) => { + list[0] = a; + if b != a { + list[1] = b; + } + } + (Some(a), None) => list[0] = a, + (None, Some(b)) => list[0] = b, + (None, None) => {} + } + list + } + + fn predict_inter(&mut self, x: usize, y: usize, n: usize, mv: [i16; 2]) { + let refp = self.refp.expect("reference picture"); + for c in 0..3 { + let (px, py, pn, pw, ph) = if c == 0 { (x, y, n, self.w, self.h) } else { (x / 2, y / 2, n / 2, self.cw, self.ch) }; + let pr = PlaneRef { data: &refp[c], w: pw, h: ph }; + if c == 0 { + inter::mc_luma(pr, self.bd, px as i32, py as i32, mv, pn, pn, &mut self.pred); + } else { + inter::mc_chroma(pr, self.bd, px as i32, py as i32, mv, pn, pn, &mut self.pred); + } + inter::put_uni(&self.pred, pn, pn, self.bd, &mut self.rec[c][py * pw + px..], pw); + } + } + + // --------------------------------------------------------------------------------------- + // estimation helpers + + /// Best merge index by SATD of the (approximate) luma prediction. + fn best_merge(&self, x: usize, y: usize, n: usize, list: &[[i16; 2]; MAX_MERGE_CAND]) -> (usize, u64) { + let me = self.me.expect("motion search reference"); + let mut buf = vec![0u16; n * n]; + let mut best = (0, u64::MAX); + for (i, &mv) in list.iter().enumerate() { + if list[..i].contains(&mv) { + continue; + } + me.approx_pred(x as i32, y as i32, n, mv, &mut buf); + let satd = satd(&self.src[0][y * self.w + x..], self.w, &buf, n, n) as u64; + let cost = satd + (self.lambda_sad * (i + 1) as f64) as u64; + if cost < best.1 { + best = (i, cost); + } + } + best + } + + /// Integer then fractional motion search. Returns (mv, mvp index, SATD cost). + fn motion_search(&self, x: usize, y: usize, n: usize, amvp: &[[i16; 2]; 2], merge: &[[i16; 2]; MAX_MERGE_CAND]) -> ([i16; 2], u8, u64) { + let me = self.me.expect("motion search reference"); + let (xi, yi) = (x as i32, y as i32); + let src = &self.src[0][y * self.w + x..]; + let ls = self.lambda_sad; + let mv_bits = |mv: [i32; 2]| -> f64 { amvp.iter().map(|p| mvd_bits(mv[0] - p[0] as i32) + mvd_bits(mv[1] - p[1] as i32)).fold(f64::MAX, f64::min) }; + let (lo_x, hi_x, lo_y, hi_y) = me.int_range(xi, yi, n); + let clamp_i = |v: [i32; 2]| [v[0].clamp(lo_x, hi_x), v[1].clamp(lo_y, hi_y)]; + let sad_cost = |v: [i32; 2]| -> f64 { me.sad(src, self.w, xi + v[0], yi + v[1], n) as f64 + ls * mv_bits([v[0] * 4, v[1] * 4]) }; + // starting points + let mut best = [0i32; 2]; + let mut best_cost = sad_cost(best); + for mv in amvp.iter().chain(merge.iter()) { + let v = clamp_i([(mv[0] as i32 + 2) >> 2, (mv[1] as i32 + 2) >> 2]); + let c = sad_cost(v); + if c < best_cost { + best_cost = c; + best = v; + } + } + // square pattern search with decreasing steps + let mut step = 16; + while step >= 1 { + let mut moved = true; + let mut iters = 0; + while moved && iters < 4 { + moved = false; + iters += 1; + let center = best; + for (dx, dy) in [(-1, -1), (0, -1), (1, -1), (-1, 0), (1, 0), (-1, 1), (0, 1), (1, 1)] { + let v = clamp_i([center[0] + dx * step, center[1] + dy * step]); + if v == center { + continue; + } + let c = sad_cost(v); + if c < best_cost { + best_cost = c; + best = v; + moved = true; + } + } + } + step /= 2; + } + // fractional refinement with SATD on the approximate interpolated planes + let mut buf = vec![0u16; n * n]; + let mut satd_cost = |mv: [i32; 2]| -> f64 { + me.approx_pred(xi, yi, n, [mv[0] as i16, mv[1] as i16], &mut buf); + satd(src, self.w, &buf, n, n) as f64 + ls * mv_bits(mv) + }; + let mut bq = [best[0] * 4, best[1] * 4]; + let mut bc = satd_cost(bq); + for step in [2, 1] { + let center = bq; + for (dx, dy) in [(-1, -1), (0, -1), (1, -1), (-1, 0), (1, 0), (-1, 1), (0, 1), (1, 1)] { + let v = [center[0] + dx * step, center[1] + dy * step]; + let c = satd_cost(v); + if c < bc { + bc = c; + bq = v; + } + } + } + let mv = [bq[0] as i16, bq[1] as i16]; + let bits = |p: &[i16; 2]| mvd_bits(bq[0] - p[0] as i32) + mvd_bits(bq[1] - p[1] as i32); + let mvp = if bits(&amvp[1]) < bits(&amvp[0]) { 1 } else { 0 }; + (mv, mvp, bc as u64) + } + + /// Best 2Nx2N intra luma mode by SATD (coarse grid, then refinement around the best angle). + fn search_intra_mode(&self, x: usize, y: usize, log2: u32) -> (u32, u64) { + let n = 1usize << log2; + let refs = self.build_refs(0, x, y, log2); + let mpm = self.mpm(x, y); + let mut buf = vec![0u16; n * n]; + let src = &self.src[0][y * self.w + x..]; + let mut eval = |m: u32| -> u64 { + predict_from_refs(&refs, 0, m, n, self.bd, &mut buf, n); + let bits = match mpm.iter().position(|&c| c == m) { + Some(0) => 2.0, + Some(_) => 3.0, + None => 6.0, + }; + satd(src, self.w, &buf, n, n) as u64 + (self.lambda_sad * bits) as u64 + }; + let mut best = (1u32, u64::MAX); + let mut tried = [false; 35]; + for &m in COARSE_MODES.iter().chain(mpm.iter()) { + if !tried[m as usize] { + tried[m as usize] = true; + let c = eval(m); + if c < best.1 { + best = (m, c); + } + } + } + if best.0 >= 2 { + for d in [2i32, 1] { + let center = best.0 as i32; + for m in [center - d, center + d] { + if (2..=34).contains(&m) && !tried[m as usize] { + tried[m as usize] = true; + let c = eval(m as u32); + if c < best.1 { + best = (m as u32, c); + } + } + } + } + } + best + } + + /// Modes of the four 4x4 PUs of an NxN intra CU, chosen sequentially (each PU reconstructed + /// before the next one is searched, as in the real coding order). + fn search_nxn_modes(&mut self, x: usize, y: usize) -> [u8; 4] { + let mut modes = [1u8; 4]; + self.fill(x, y, 8, |b| { + b.flags = F_INTRA; + b.ipm = 1; + }); + for k in 0..4 { + let (px, py) = (x + (k & 1) * 4, y + (k >> 1) * 4); + let (m, _) = self.search_intra_mode(px, py, 2); + modes[k] = m as u8; + self.fill(px, py, 4, |b| b.ipm = m as u8); + self.tbs.clear(); + self.coefs.clear(); + self.code_tb(0, px, py, 2, Some(m), (px, py)); + } + modes + } +} + +/// Filter (luma) and predict an intra block from substituted references. +fn predict_from_refs(refs: &Refs, c: usize, mode: u32, n: usize, bd: u32, out: &mut [u16], os: usize) { + if c == 0 { + let mut r = refs.clone(); + intra::filter(&mut r, mode, n, true, true, bd); + intra::predict(&r, mode, n, true, bd, out, os); + } else { + intra::predict(refs, mode, n, false, bd, out, os); + } +} + +/// scanIdx (7.4.9.11) for an intra prediction mode. +fn scan_idx_for_mode(m: u32) -> u8 { + if (6..=14).contains(&m) { + 2 + } else if (22..=30).contains(&m) { + 1 + } else { + 0 + } +} + +/// Approximate bits of one motion vector difference component. +fn mvd_bits(v: i32) -> f64 { + if v == 0 { 1.0 } else { (2 * (32 - (v.unsigned_abs()).leading_zeros()) + 1) as f64 } +} + +fn write_merge_idx(s: &mut S, idx: usize) { + if MAX_MERGE_CAND > 1 { + s.decision(MERGE_IDX, (idx > 0) as u32); + if idx > 0 { + let mut i = 1; + while i < MAX_MERGE_CAND - 1 { + let b = idx > i; + s.bypass(b as u32); + if !b { + break; + } + i += 1; + } + } + } +} + +/// mvd_coding() (7.3.8.9). +fn write_mvd(s: &mut S, d: [i32; 2]) { + let g0 = [d[0] != 0, d[1] != 0]; + s.decision(ABS_MVD_GT0, g0[0] as u32); + s.decision(ABS_MVD_GT0, g0[1] as u32); + for k in 0..2 { + if g0[k] { + s.decision(ABS_MVD_GT1, (d[k].abs() > 1) as u32); + } + } + for k in 0..2 { + if g0[k] { + let a = d[k].unsigned_abs(); + if a > 1 { + // abs_mvd_minus2: EG1 + let mut v = a - 2; + let mut kk = 1; + while v >= (1 << kk) { + s.bypass(1); + v -= 1 << kk; + kk += 1; + } + s.bypass(0); + s.bypass_bits(v, kk); + } + s.bypass((d[k] < 0) as u32); + } + } +} + +/// coeff_abs_level_remaining binarisation (9.3.3.11): TR prefix up to 3 with Rice suffix, then +/// an escape with an Exp-Golomb-like suffix. +fn write_remaining(s: &mut S, v: u32, rice: u32) { + if v < (4 << rice) { + let p = v >> rice; + for _ in 0..p { + s.bypass(1); + } + s.bypass(0); + s.bypass_bits(v & ((1 << rice) - 1), rice); + } else { + let u = v - (2 << rice); + let kr = 31 - u.leading_zeros(); // k + rice + let k = kr - rice; + for _ in 0..k + 3 { + s.bypass(1); + } + s.bypass(0); + s.bypass_bits(u - (1 << kr), kr); + } +} + +/// last_sig_coeff_{x,y}_prefix value and suffix for a position. +fn last_prefix(v: u32) -> (u32, u32, u32) { + if v < 4 { + return (v, 0, 0); + } + let mut p = 4; + loop { + let nb = (p >> 1) - 1; + let min = (1 << nb) * (2 + (p & 1)); + let next_nb = ((p + 1) >> 1) - 1; + let next = (1 << next_nb) * (2 + ((p + 1) & 1)); + if v < next { + return (p, v - min, nb); + } + p += 1; + } +} + +/// residual_coding() (7.3.8.11) of one transform block of levels (row-major, n x n). No transform +/// skip, no sign data hiding. +fn residual_coding(s: &mut S, lv: &[i32], log2: u32, c: usize, scan_idx: usize) { + let n = 1usize << log2; + let log2sb = log2 - 2; + let sb_w = 1usize << log2sb; + let sb_scan = scan_order(log2sb, scan_idx); + let pos_scan = &SCAN_4[scan_idx]; + let at = |xs: usize, ys: usize, p: usize| lv[(ys * 4 + pos_scan[p].1 as usize) * n + xs * 4 + pos_scan[p].0 as usize]; + // last significant coefficient in scan order + let mut last = None; + 'outer: for i in (0..sb_scan.len()).rev() { + let (xs, ys) = (sb_scan[i].0 as usize, sb_scan[i].1 as usize); + for p in (0..16).rev() { + if at(xs, ys, p) != 0 { + last = Some((i, p)); + break 'outer; + } + } + } + let (last_sb, last_pos) = last.expect("residual_coding of an all-zero block"); + let lx = sb_scan[last_sb].0 as u32 * 4 + pos_scan[last_pos].0 as u32; + let ly = sb_scan[last_sb].1 as u32 * 4 + pos_scan[last_pos].1 as u32; + let (cx, cy) = if scan_idx == 2 { (ly, lx) } else { (lx, ly) }; + let cmax = (log2 << 1) - 1; + let (off, shift) = if c == 0 { (3 * (log2 - 2) + ((log2 - 1) >> 2), (log2 + 1) >> 2) } else { (15, log2 - 2) }; + let (px, sx, nbx) = last_prefix(cx); + let (py, sy, nby) = last_prefix(cy); + for (p, base) in [(px, LAST_X_PREFIX), (py, LAST_Y_PREFIX)] { + for i in 0..p { + s.decision(base + (off + (i >> shift)) as usize, 1); + } + if p < cmax { + s.decision(base + (off + (p >> shift)) as usize, 0); + } + } + if px > 3 { + s.bypass_bits(sx, nbx); + } + if py > 3 { + s.bypass_bits(sy, nby); + } + let mut csbf = [0u8; 64]; + let mut prev_g1: Option = None; + let c_off_sig = if c == 0 { 0 } else { 27 }; + for i in (0..=last_sb).rev() { + let (xs, ys) = (sb_scan[i].0 as usize, sb_scan[i].1 as usize); + let right = if xs + 1 < sb_w { csbf[ys * 8 + xs + 1] } else { 0 }; + let below = if ys + 1 < sb_w { csbf[(ys + 1) * 8 + xs] } else { 0 }; + let mut infer_dc = false; + if i < last_sb && i > 0 { + let has = (0..16).any(|p| at(xs, ys, p) != 0); + let inc = (right | below).min(1) as usize + if c > 0 { 2 } else { 0 }; + s.decision(CODED_SUB_BLOCK + inc, has as u32); + csbf[ys * 8 + xs] = has as u8; + infer_dc = true; + } else { + csbf[ys * 8 + xs] = 1; + } + if csbf[ys * 8 + xs] == 0 { + continue; + } + let mut sig = [0u8; 16]; + let mut nsig = 0; + let start: i32 = if i == last_sb { + sig[0] = last_pos as u8; + nsig = 1; + last_pos as i32 - 1 + } else { + 15 + }; + let prev_csbf = right + (below << 1); + let mut np = start; + while np >= 0 { + let p = np as usize; + let (xp, yp) = (pos_scan[p].0 as usize, pos_scan[p].1 as usize); + let (xc, yc) = (xs * 4 + xp, ys * 4 + yp); + let v = lv[yc * n + xc] != 0; + if np > 0 || !infer_dc { + let sig_ctx = if log2 == 2 { + CTX_IDX_MAP[(yc << 2) + xc] as usize + } else if xc + yc == 0 { + 0 + } else { + let mut sc = match prev_csbf { + 0 => { + if xp + yp == 0 { + 2 + } else if xp + yp < 3 { + 1 + } else { + 0 + } + } + 1 => { + if yp == 0 { + 2 + } else if yp == 1 { + 1 + } else { + 0 + } + } + 2 => { + if xp == 0 { + 2 + } else if xp == 1 { + 1 + } else { + 0 + } + } + _ => 2, + }; + if c == 0 { + if xs + ys > 0 { + sc += 3; + } + sc += if log2 == 3 { if scan_idx == 0 { 9 } else { 15 } } else { 21 }; + } else { + sc += if log2 == 3 { 9 } else { 12 }; + } + sc + }; + s.decision(SIG_COEFF + c_off_sig + sig_ctx, v as u32); + if v { + sig[nsig] = p as u8; + nsig += 1; + infer_dc = false; + } + } else { + debug_assert!(v, "inferred DC coefficient must be non-zero"); + sig[nsig] = 0; + nsig += 1; + } + np -= 1; + } + if nsig == 0 { + continue; + } + let abs_at = |k: usize| at(xs, ys, sig[k] as usize).unsigned_abs(); + let mut ctx_set = if i == 0 || c > 0 { 0 } else { 2 }; + if prev_g1 == Some(0) { + ctx_set += 1; + } + let mut g1ctx = 1u32; + let mut gt1 = [0u8; 16]; + let mut first_g1: Option = None; + let c_off_g1 = if c > 0 { 16 } else { 0 }; + for k in 0..nsig.min(8) { + let f = abs_at(k) > 1; + s.decision(GT1 + ctx_set * 4 + g1ctx.min(3) as usize + c_off_g1, f as u32); + gt1[k] = f as u8; + if f { + g1ctx = 0; + if first_g1.is_none() { + first_g1 = Some(k); + } + } else if g1ctx > 0 { + g1ctx += 1; + } + } + prev_g1 = Some(g1ctx); + let mut gt2 = 0u32; + if let Some(k) = first_g1 { + gt2 = (abs_at(k) > 2) as u32; + s.decision(GT2 + ctx_set + if c > 0 { 4 } else { 0 }, gt2); + } + // sign data hiding: the sign of the first coefficient in scan order (sig[nsig - 1]) is + // inferred from the parity of the sum of absolute levels + let hidden = sig[0] as i32 - sig[nsig - 1] as i32 > 3; + let nsigns = if hidden { nsig - 1 } else { nsig }; + for k in 0..nsigns { + s.bypass((at(xs, ys, sig[k] as usize) < 0) as u32); + } + if hidden { + let sum: u32 = (0..nsig).map(abs_at).sum(); + debug_assert_eq!(at(xs, ys, sig[nsig - 1] as usize) < 0, sum & 1 == 1, "sign hiding parity"); + } + let mut rice = 0u32; + for k in 0..nsig { + let mut base = 1 + gt1[k] as u32; + if Some(k) == first_g1 { + base += gt2; + } + let thresh = if k < 8 { if Some(k) == first_g1 { 3 } else { 2 } } else { 1 }; + if base == thresh { + let level = abs_at(k); + write_remaining(s, level - base, rice); + if level > 3 * (1 << rice) { + rice = (rice + 1).min(4); + } + } + } + } +} + +/// Sum of absolute Hadamard-transformed differences (8x8 blocks, 4x4 for 4-sample blocks). +pub fn satd(src: &[u16], ss: usize, pred: &[u16], ps: usize, n: usize) -> u32 { + if n == 4 { + return satd4(src, ss, pred, ps); + } + let mut total = 0; + for by in (0..n).step_by(8) { + for bx in (0..n).step_by(8) { + total += satd8(&src[by * ss + bx..], ss, &pred[by * ps + bx..], ps); + } + } + total +} + +fn satd4(src: &[u16], ss: usize, pred: &[u16], ps: usize) -> u32 { + let mut d = [0i32; 16]; + for y in 0..4 { + for x in 0..4 { + d[y * 4 + x] = src[y * ss + x] as i32 - pred[y * ps + x] as i32; + } + } + let mut m = [0i32; 16]; + for y in 0..4 { + let r = &d[y * 4..y * 4 + 4]; + let (a, b, c, e) = (r[0] + r[3], r[1] + r[2], r[1] - r[2], r[0] - r[3]); + m[y * 4] = a + b; + m[y * 4 + 1] = e + c; + m[y * 4 + 2] = a - b; + m[y * 4 + 3] = e - c; + } + let mut sum = 0; + for x in 0..4 { + let (r0, r1, r2, r3) = (m[x], m[4 + x], m[8 + x], m[12 + x]); + let (a, b, c, e) = (r0 + r3, r1 + r2, r1 - r2, r0 - r3); + sum += (a + b).abs() + (e + c).abs() + (a - b).abs() + (e - c).abs(); + } + ((sum + 1) >> 1) as u32 +} + +fn hadamard8(v: &mut [i32; 8]) { + let mut t = *v; + let mut h = 1; + while h < 8 { + for i in (0..8).step_by(h * 2) { + for j in i..i + h { + let (a, b) = (t[j], t[j + h]); + t[j] = a + b; + t[j + h] = a - b; + } + } + h *= 2; + } + *v = t; +} + +fn satd8(src: &[u16], ss: usize, pred: &[u16], ps: usize) -> u32 { + let mut m = [[0i32; 8]; 8]; + for y in 0..8 { + for x in 0..8 { + m[y][x] = src[y * ss + x] as i32 - pred[y * ps + x] as i32; + } + hadamard8(&mut m[y]); + } + let mut sum = 0; + for x in 0..8 { + let mut col = [0i32; 8]; + for y in 0..8 { + col[y] = m[y][x]; + } + hadamard8(&mut col); + sum += col.iter().map(|v| v.abs()).sum::(); + } + ((sum + 2) >> 2) as u32 +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn last_prefix_groups() { + assert_eq!(last_prefix(3), (3, 0, 0)); + assert_eq!(last_prefix(4), (4, 0, 1)); + assert_eq!(last_prefix(5), (4, 1, 1)); + assert_eq!(last_prefix(7), (5, 1, 1)); + assert_eq!(last_prefix(8), (6, 0, 2)); + assert_eq!(last_prefix(15), (7, 3, 2)); + assert_eq!(last_prefix(16), (8, 0, 3)); + assert_eq!(last_prefix(31), (9, 7, 3)); + } +} diff --git a/crates/hevcenc/src/inter.rs b/crates/hevcenc/src/inter.rs new file mode 100644 index 00000000..e363871b --- /dev/null +++ b/crates/hevcenc/src/inter.rs @@ -0,0 +1,155 @@ +//! Fractional sample interpolation (8.5.3.3.3) and default weighted prediction (8-262), mirrored +//! from the filmcraft-hevc decoder. Reference sample fetches clamp to the picture (8-228, 8-229). + +use crate::tables::{CHROMA_FILTER, LUMA_FILTER}; + +/// One plane of a reference picture. +#[derive(Clone, Copy)] +pub struct PlaneRef<'a> { + pub data: &'a [u16], + pub w: usize, + pub h: usize, +} + +impl PlaneRef<'_> { + /// Copy a w x h window at (x0, y0) with edge replication. + fn window(&self, x0: i32, y0: i32, w: usize, h: usize, out: &mut [i16], os: usize) { + let (pw, ph) = (self.w as i32, self.h as i32); + let inside_x = x0 >= 0 && x0 + w as i32 <= pw; + for r in 0..h { + let yy = (y0 + r as i32).clamp(0, ph - 1) as usize; + let row = &self.data[yy * self.w..yy * self.w + self.w]; + let dst = &mut out[r * os..r * os + w]; + if inside_x { + for (d, &s) in dst.iter_mut().zip(&row[x0 as usize..x0 as usize + w]) { + *d = s as i16; + } + } else { + for (c, d) in dst.iter_mut().enumerate() { + *d = row[(x0 + c as i32).clamp(0, pw - 1) as usize] as i16; + } + } + } + } +} + +/// Luma prediction samples (14-bit intermediate) of a w x h block at (x, y) displaced by the +/// quarter-sample vector `mv`, into `out` (stride w). +pub fn mc_luma(f: PlaneRef, bd: u32, x: i32, y: i32, mv: [i16; 2], w: usize, h: usize, out: &mut [i16]) { + let (fx, fy) = ((mv[0] & 3) as usize, (mv[1] & 3) as usize); + let xi = x + (mv[0] as i32 >> 2); + let yi = y + (mv[1] as i32 >> 2); + let shift1 = bd.min(12) - 8; + let shift3 = 14 - bd; + let ws = w + 7; + let mut win = vec![0i16; ws * (h + 7)]; + if fx == 0 && fy == 0 { + f.window(xi, yi, w, h, &mut win, ws); + for r in 0..h { + for c in 0..w { + out[r * w + c] = win[r * ws + c] << shift3; + } + } + return; + } + f.window(xi - 3, yi - 3, w + 7, h + 7, &mut win, ws); + let hf = &LUMA_FILTER[fx]; + let vf = &LUMA_FILTER[fy]; + if fy == 0 { + for r in 0..h { + let row = &win[(r + 3) * ws..]; + for c in 0..w { + let s: i32 = (0..8).map(|k| hf[k] as i32 * row[c + k] as i32).sum(); + out[r * w + c] = (s >> shift1) as i16; + } + } + } else if fx == 0 { + for r in 0..h { + for c in 0..w { + let s: i32 = (0..8).map(|k| vf[k] as i32 * win[(r + k) * ws + c + 3] as i32).sum(); + out[r * w + c] = (s >> shift1) as i16; + } + } + } else { + let mut tmp = vec![0i16; ws * (h + 7)]; + for r in 0..h + 7 { + let row = &win[r * ws..]; + for c in 0..w { + let s: i32 = (0..8).map(|k| hf[k] as i32 * row[c + k] as i32).sum(); + tmp[r * ws + c] = (s >> shift1) as i16; + } + } + for r in 0..h { + for c in 0..w { + let s: i32 = (0..8).map(|k| vf[k] as i32 * tmp[(r + k) * ws + c] as i32).sum(); + out[r * w + c] = (s >> 6) as i16; + } + } + } +} + +/// Chroma prediction samples of a w x h block at chroma position (x, y) with a 1/8-sample vector. +pub fn mc_chroma(f: PlaneRef, bd: u32, x: i32, y: i32, mv: [i16; 2], w: usize, h: usize, out: &mut [i16]) { + let (fx, fy) = ((mv[0] & 7) as usize, (mv[1] & 7) as usize); + let xi = x + (mv[0] as i32 >> 3); + let yi = y + (mv[1] as i32 >> 3); + let shift1 = bd.min(12) - 8; + let shift3 = 14 - bd; + let ws = w + 7; + let mut win = vec![0i16; ws * (h + 7)]; + if fx == 0 && fy == 0 { + f.window(xi, yi, w, h, &mut win, ws); + for r in 0..h { + for c in 0..w { + out[r * w + c] = win[r * ws + c] << shift3; + } + } + return; + } + f.window(xi - 1, yi - 1, w + 3, h + 3, &mut win, ws); + let hf = &CHROMA_FILTER[fx]; + let vf = &CHROMA_FILTER[fy]; + if fy == 0 { + for r in 0..h { + let row = &win[(r + 1) * ws..]; + for c in 0..w { + let s: i32 = (0..4).map(|k| hf[k] as i32 * row[c + k] as i32).sum(); + out[r * w + c] = (s >> shift1) as i16; + } + } + } else if fx == 0 { + for r in 0..h { + for c in 0..w { + let s: i32 = (0..4).map(|k| vf[k] as i32 * win[(r + k) * ws + c + 1] as i32).sum(); + out[r * w + c] = (s >> shift1) as i16; + } + } + } else { + let mut tmp = vec![0i16; ws * (h + 7)]; + for r in 0..h + 3 { + let row = &win[r * ws..]; + for c in 0..w { + let s: i32 = (0..4).map(|k| hf[k] as i32 * row[c + k] as i32).sum(); + tmp[r * ws + c] = (s >> shift1) as i16; + } + } + for r in 0..h { + for c in 0..w { + let s: i32 = (0..4).map(|k| vf[k] as i32 * tmp[(r + k) * ws + c] as i32).sum(); + out[r * w + c] = (s >> 6) as i16; + } + } + } +} + +/// Default weighted prediction, one list (8-262): 14-bit samples to `bd`-bit samples. +pub fn put_uni(p: &[i16], w: usize, h: usize, bd: u32, dst: &mut [u16], ds: usize) { + let shift = 14 - bd; + let off = if shift > 0 { 1 << (shift - 1) } else { 0 }; + let max = (1i32 << bd) - 1; + for r in 0..h { + for c in 0..w { + dst[r * ds + c] = ((p[r * w + c] as i32 + off) >> shift).clamp(0, max) as u16; + } + } +} diff --git a/crates/hevcenc/src/intra.rs b/crates/hevcenc/src/intra.rs new file mode 100644 index 00000000..f1fe9e4c --- /dev/null +++ b/crates/hevcenc/src/intra.rs @@ -0,0 +1,254 @@ +//! Intra sample prediction (8.4.4.2), mirrored from the filmcraft-hevc decoder so the encoder +//! reconstructs exactly like a decoder: reference substitution, filtering (incl. strong intra +//! smoothing), planar, DC and the 33 angular modes. + +use crate::tables::{INTRA_PRED_ANGLE, INV_ANGLE}; + +/// Reference samples of an nTbS block: `left[0]` = `top[0]` = p[-1][-1], `left[1 + y]` = p[-1][y], +/// `top[1 + x]` = p[x][-1] for x, y = 0..2*nTbS-1. +#[derive(Clone)] +pub struct Refs { + pub left: [u16; 129], + pub top: [u16; 129], +} + +impl Default for Refs { + fn default() -> Self { + Refs { left: [0; 129], top: [0; 129] } + } +} + +/// Substitution process (8.4.4.2.2). `avail_left[1 + y]` / `avail_top[1 + x]` (index 0 = corner) tell +/// which samples are available; unavailable ones are replaced. +pub fn substitute(r: &mut Refs, avail_left: &[bool; 129], avail_top: &[bool; 129], n: usize, bit_depth: u32) { + let n2 = 2 * n; + // Sequence order: p[-1][2n-1] .. p[-1][-1], p[0][-1] .. p[2n-1][-1] + let any = avail_left[..=n2].iter().any(|&a| a) || avail_top[1..=n2].iter().any(|&a| a); + if !any { + let v = 1u16 << (bit_depth - 1); + r.left[..=n2].fill(v); + r.top[..=n2].fill(v); + return; + } + if !avail_left[n2] { + // search upwards then along the top + let mut found = None; + for y in (0..n2).rev() { + if avail_left[y] { + found = Some(r.left[y]); + break; + } + } + if found.is_none() { + for x in 1..=n2 { + if avail_top[x] { + found = Some(r.top[x]); + break; + } + } + } + r.left[n2] = found.unwrap_or(0); + } + for y in (0..n2).rev() { + if !avail_left[y] { + r.left[y] = r.left[y + 1]; + } + } + r.top[0] = r.left[0]; + for x in 1..=n2 { + if !avail_top[x] { + r.top[x] = r.top[x - 1]; + } + } +} + +/// Filtering process of neighbouring samples (8.4.4.2.3) for luma (and 4:4:4 chroma). +pub fn filter(r: &mut Refs, mode: u32, n: usize, strong_enabled: bool, is_luma: bool, bit_depth: u32) { + if mode == 1 || n == 4 { + return; + } + let min_dist = (mode as i32 - 26).abs().min((mode as i32 - 10).abs()); + let thres = match n { + 8 => 7, + 16 => 1, + _ => 0, + }; + if min_dist <= thres { + return; + } + let n2 = 2 * n; + let c = r.left[0] as i32; + if strong_enabled && is_luma && n == 32 { + let (bl, tr) = (r.left[n2] as i32, r.top[n2] as i32); + let thr = 1 << (bit_depth - 5); + if (c + tr - 2 * r.top[n] as i32).abs() < thr && (c + bl - 2 * r.left[n] as i32).abs() < thr { + for i in 0..63 { + r.left[1 + i] = (((63 - i as i32) * c + (i as i32 + 1) * bl + 32) >> 6) as u16; + r.top[1 + i] = (((63 - i as i32) * c + (i as i32 + 1) * tr + 32) >> 6) as u16; + } + return; + } + } + let (l, t) = (r.left, r.top); + let corner = ((l[1] as u32 + 2 * c as u32 + t[1] as u32 + 2) >> 2) as u16; + for i in 1..n2 { + r.left[i] = ((l[i - 1] as u32 + 2 * l[i] as u32 + l[i + 1] as u32 + 2) >> 2) as u16; + r.top[i] = ((t[i - 1] as u32 + 2 * t[i] as u32 + t[i + 1] as u32 + 2) >> 2) as u16; + } + r.left[0] = corner; + r.top[0] = corner; +} + +/// Predict an n x n block with `mode` into `out` (stride `os`). `edge_filters`: DC / horizontal / +/// vertical boundary smoothing (luma blocks smaller than 32x32). +pub fn predict(r: &Refs, mode: u32, n: usize, edge_filters: bool, bit_depth: u32, out: &mut [u16], os: usize) { + let max = (1i32 << bit_depth) - 1; + match mode { + 0 => { + // planar + let log2 = n.trailing_zeros(); + let tr = r.top[1 + n] as i32; + let bl = r.left[1 + n] as i32; + for y in 0..n { + let l = r.left[1 + y] as i32; + for x in 0..n { + let t = r.top[1 + x] as i32; + let v = ((n - 1 - x) as i32 * l + (x as i32 + 1) * tr + (n - 1 - y) as i32 * t + (y as i32 + 1) * bl + n as i32) >> (log2 + 1); + out[y * os + x] = v as u16; + } + } + } + 1 => { + let log2 = n.trailing_zeros(); + let sum: u32 = r.top[1..=n].iter().map(|&v| v as u32).sum::() + r.left[1..=n].iter().map(|&v| v as u32).sum::(); + let dc = ((sum + n as u32) >> (log2 + 1)) as u16; + for y in 0..n { + out[y * os..y * os + n].fill(dc); + } + if edge_filters && n < 32 { + let d = dc as u32; + out[0] = ((r.left[1] as u32 + 2 * d + r.top[1] as u32 + 2) >> 2) as u16; + for x in 1..n { + out[x] = ((r.top[1 + x] as u32 + 3 * d + 2) >> 2) as u16; + } + for y in 1..n { + out[y * os] = ((r.left[1 + y] as u32 + 3 * d + 2) >> 2) as u16; + } + } + } + _ => { + let angle = INTRA_PRED_ANGLE[mode as usize] as i32; + // reference array ref[-n..=2n] stored at offset n + let mut refa = [0i32; 3 * 64 + 1]; + let base = n as i32; + let (main, side) = if mode >= 18 { (&r.top, &r.left) } else { (&r.left, &r.top) }; + for x in 0..=n { + refa[(base + x as i32) as usize] = main[x] as i32; // ref[x] = p[-1+x][-1] + } + if angle < 0 { + let lim = (n as i32 * angle) >> 5; + if lim < -1 { + let inv = INV_ANGLE[(mode - 11) as usize] as i32; + for x in lim..=-1 { + let idx = -1 + ((x * inv + 128) >> 8); + refa[(base + x) as usize] = side[(idx + 1) as usize] as i32; + } + } + } else { + for x in n + 1..=2 * n { + refa[(base + x as i32) as usize] = main[x] as i32; + } + } + let horizontal = mode < 18; + for j in 0..n { + let pos = (j as i32 + 1) * angle; + let idx = pos >> 5; + let fact = pos & 31; + for i in 0..n { + let k = (base + i as i32 + idx + 1) as usize; + let v = if fact != 0 { ((32 - fact) * refa[k] + fact * refa[k + 1] + 16) >> 5 } else { refa[k] }; + if horizontal { + out[i * os + j] = v as u16; + } else { + out[j * os + i] = v as u16; + } + } + } + if edge_filters && n < 32 { + if mode == 26 { + for y in 0..n { + let v = r.top[1] as i32 + ((r.left[1 + y] as i32 - r.left[0] as i32) >> 1); + out[y * os] = v.clamp(0, max) as u16; + } + } else if mode == 10 { + for x in 0..n { + let v = r.left[1] as i32 + ((r.top[1 + x] as i32 - r.top[0] as i32) >> 1); + out[x] = v.clamp(0, max) as u16; + } + } + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn refs(f: impl Fn(i32) -> u16) -> Refs { + let mut r = Refs::default(); + r.left[0] = f(0); + r.top[0] = f(0); + for i in 1..129 { + r.left[i] = f(-(i as i32)); + r.top[i] = f(i as i32); + } + r + } + + #[test] + fn dc_and_planar_flat() { + let r = refs(|_| 100); + let mut out = [0u16; 64]; + predict(&r, 1, 8, true, 8, &mut out, 8); + assert!(out.iter().all(|&v| v == 100)); + predict(&r, 0, 8, true, 8, &mut out, 8); + assert!(out.iter().all(|&v| v == 100)); + } + + #[test] + fn pure_vertical_and_horizontal() { + let r = refs(|i| (128 + i) as u16); + let mut out = [0u16; 16]; + predict(&r, 26, 4, false, 8, &mut out, 4); + for y in 0..4 { + for x in 0..4 { + assert_eq!(out[y * 4 + x], r.top[1 + x]); + } + } + predict(&r, 10, 4, false, 8, &mut out, 4); + for y in 0..4 { + for x in 0..4 { + assert_eq!(out[y * 4 + x], r.left[1 + y]); + } + } + // diagonal mode 34 reads top[x + y + 2] + predict(&r, 34, 4, false, 8, &mut out, 4); + assert_eq!(out[4 + 1], r.top[1 + 1 + 1 + 1]); + } + + #[test] + fn substitution() { + let mut r = Refs::default(); + let mut al = [false; 129]; + let at = [false; 129]; + r.left[3] = 77; + al[3] = true; + substitute(&mut r, &al, &at, 4, 8); + assert!(r.left[..=8].iter().all(|&v| v == 77)); + assert!(r.top[..=8].iter().all(|&v| v == 77)); + let mut r = Refs::default(); + substitute(&mut r, &[false; 129], &[false; 129], 4, 10); + assert_eq!(r.top[5], 512); + } +} diff --git a/crates/hevcenc/src/lib.rs b/crates/hevcenc/src/lib.rs new file mode 100644 index 00000000..3a5b9b47 --- /dev/null +++ b/crates/hevcenc/src/lib.rs @@ -0,0 +1,356 @@ +//! Clean-room pure-Rust HEVC (H.265) encoder, implemented from ITU-T Rec. H.265 (ISO/IEC 23008-2). +//! +//! Main and Main 10 profiles, 4:2:0, one slice per picture: IDR pictures and single-reference P +//! pictures (no reordering), 64x64 CTUs with rate-distortion coding-unit decisions (8x8 .. 64x64), +//! intra (35 modes, NxN at 8x8), merge / skip and AMVP inter prediction with quarter-sample motion, +//! DCT / DST transforms and CABAC. The deblocking filter is on; SAO is not used. +//! +//! ``` +//! use effectcraft_hevcenc::{Encoder, EncoderConfig, Frame}; +//! let mut enc = Encoder::new(EncoderConfig::new(64, 48, 30, 1)).unwrap(); +//! let (y, c) = (vec![128u16; 64 * 48], vec![128u16; 32 * 24]); +//! let packet = enc.encode(&Frame { y: &y, u: &c, v: &c, y_stride: 64, uv_stride: 32 }); +//! assert!(packet.keyframe && !packet.data.is_empty()); +//! let _hvcc = enc.hvcc(); +//! ``` + +// Index loops over fixed-size blocks read more clearly than iterator chains in codec code. +#![allow(clippy::needless_range_loop, clippy::too_many_arguments)] + +mod bitstream; +mod cabac; +mod deblock; +mod frame; +mod inter; +mod intra; +mod me; +mod params; +#[rustfmt::skip] +mod tables; +mod transform; + +use bitstream::{BitWriter, NAL_IDR_N_LP, NAL_TRAIL_R, nal_unit}; +use frame::{FrameCoder, SliceKind}; +use params::{LOG2_MAX_POC_LSB, LOG2_MIN_CB, MAX_MERGE_CAND, SeqParams}; + +/// Profile written in the parameter sets. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub enum Profile { + /// Main: 8-bit 4:2:0. + #[default] + Main, + /// Main 10: 10-bit 4:2:0. + Main10, +} + +impl Profile { + pub fn bit_depth(self) -> u8 { + match self { + Profile::Main => 8, + Profile::Main10 => 10, + } + } + fn idc(self) -> u8 { + match self { + Profile::Main => 1, + Profile::Main10 => 2, + } + } +} + +/// Rate control. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum RateControl { + /// Constant QP (0–51; lower is better). + ConstantQp(u8), + /// Average bitrate target in kbit/s (frame-level QP adaptation). + Bitrate { kbps: u32 }, +} + +/// Encoder settings. +#[derive(Clone, Debug, PartialEq)] +pub struct EncoderConfig { + /// Picture size; must be even (4:2:0 cropping is in units of two luma samples). + pub width: u32, + pub height: u32, + /// Frame rate numerator / denominator (VUI timing and rate control). + pub fps_num: u32, + pub fps_den: u32, + pub profile: Profile, + /// `general_level_idc` (30 × level, e.g. 93 = level 3.1); `None` picks the lowest level that fits. + pub level_idc: Option, + pub rate: RateControl, + /// Frames between IDR pictures (1 = all intra, 0 = only the first picture). + pub keyint: u32, + /// BT.709 limited range is signalled in the VUI unless this is set. + pub full_range: bool, +} + +impl EncoderConfig { + pub fn new(width: u32, height: u32, fps_num: u32, fps_den: u32) -> Self { + EncoderConfig { + width, + height, + fps_num, + fps_den, + profile: Profile::Main, + level_idc: None, + rate: RateControl::ConstantQp(28), + keyint: 60, + full_range: false, + } + } +} + +/// One input picture, 4:2:0, samples at the profile's bit depth (8-bit values 0–255 for Main). +/// Luma is `width` x `height`, chroma `width / 2` x `height / 2`. +#[derive(Clone, Copy, Debug)] +pub struct Frame<'a> { + pub y: &'a [u16], + pub u: &'a [u16], + pub v: &'a [u16], + pub y_stride: usize, + pub uv_stride: usize, +} + +/// One coded picture (an access unit): NAL units, each prefixed by a 4-byte big-endian length +/// (ISO/IEC 14496-15 sample format; parameter sets are only in [`Encoder::hvcc`]). +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Packet { + pub data: Vec, + /// IDR picture. + pub keyframe: bool, +} + +#[derive(Debug, PartialEq, Eq)] +pub enum Error { + InvalidConfig(String), +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::InvalidConfig(s) => write!(f, "invalid HEVC encoder config: {s}"), + } + } +} + +impl std::error::Error for Error {} + +/// Frame-level rate control for [`RateControl::Bitrate`]: a bits ≈ X / Qstep model per picture type +/// and a buffer that spreads over- / undershoot over the following pictures. +struct RateCtl { + target: f64, + /// sum(actual) - sum(target), bits. + buffer: f64, + /// Complexity X = bits * Qstep for I and P pictures. + x: [Option; 2], + last_qp: [i32; 2], + init_qp: i32, +} + +fn qstep(qp: i32) -> f64 { + 2f64.powf((qp as f64 - 4.0) / 6.0) +} + +impl RateCtl { + fn new(kbps: u32, cfg: &EncoderConfig) -> Self { + let fps = cfg.fps_num as f64 / cfg.fps_den.max(1) as f64; + let target = kbps as f64 * 1000.0 / fps.max(0.001); + let bpp = target / (cfg.width as f64 * cfg.height as f64); + let init_qp = (28.0 - 6.0 * (bpp / 0.08).log2()).round().clamp(10.0, 46.0) as i32; + RateCtl { target, buffer: 0.0, x: [None, None], last_qp: [init_qp - 3, init_qp], init_qp } + } + + fn qp(&self, idr: bool) -> i32 { + let t = idr as usize ^ 1; // 0 = I, 1 = P + let tp = (self.target - self.buffer / 12.0).clamp(0.3 * self.target, 2.0 * self.target); + let want = if idr { 4.0 * tp } else { tp }; + let x = match (self.x[t], self.x[0]) { + (Some(x), _) => x, + (None, Some(xi)) => xi / 5.0, + (None, None) => return if idr { self.init_qp - 3 } else { self.init_qp }, + }; + let qp = (4.0 + 6.0 * (x / want).log2()).round() as i32; + qp.clamp(self.last_qp[t] - 6, self.last_qp[t] + 6).clamp(1, 51) + } + + fn update(&mut self, idr: bool, qp: i32, bits: f64) { + let t = idr as usize ^ 1; + let x = bits * qstep(qp); + self.x[t] = Some(match self.x[t] { + Some(old) => 0.5 * old + 0.5 * x, + None => x, + }); + self.last_qp[t] = qp; + self.buffer += bits - self.target; + } +} + +/// HEVC encoder. Pictures come out in input order (no reordering: I and P only). +pub struct Encoder { + cfg: EncoderConfig, + seq: SeqParams, + sets: [Vec; 3], + frames: u64, + /// POC of the previous picture (relative to the last IDR). + poc: u32, + reference: Option<[Vec; 3]>, + recon: Option<[Vec; 3]>, + rc: Option, +} + +impl Encoder { + pub fn new(cfg: EncoderConfig) -> Result { + if cfg.width == 0 || cfg.height == 0 { + return Err(Error::InvalidConfig("empty frame".into())); + } + if !cfg.width.is_multiple_of(2) || !cfg.height.is_multiple_of(2) { + return Err(Error::InvalidConfig(format!("{}x{}: 4:2:0 HEVC needs even dimensions", cfg.width, cfg.height))); + } + if cfg.width > 16384 || cfg.height > 16384 { + return Err(Error::InvalidConfig("picture larger than 16384 samples".into())); + } + if cfg.fps_num == 0 || cfg.fps_den == 0 { + return Err(Error::InvalidConfig("zero frame rate".into())); + } + match cfg.rate { + RateControl::ConstantQp(q) if q > 51 => return Err(Error::InvalidConfig(format!("QP {q} > 51"))), + RateControl::Bitrate { kbps: 0 } => return Err(Error::InvalidConfig("zero bitrate".into())), + _ => {} + } + let min_cb = 1u32 << LOG2_MIN_CB; + let (coded_width, coded_height) = (cfg.width.div_ceil(min_cb) * min_cb, cfg.height.div_ceil(min_cb) * min_cb); + let kbps = match cfg.rate { + RateControl::Bitrate { kbps } => Some(kbps), + RateControl::ConstantQp(_) => None, + }; + let level_idc = cfg.level_idc.unwrap_or_else(|| params::choose_level(coded_width, coded_height, cfg.fps_num, cfg.fps_den, kbps)); + let seq = SeqParams { + coded_width, + coded_height, + width: cfg.width, + height: cfg.height, + bit_depth: cfg.profile.bit_depth() as u32, + profile_idc: cfg.profile.idc(), + level_idc, + full_range: cfg.full_range, + fps_num: cfg.fps_num, + fps_den: cfg.fps_den, + }; + let sets = [params::vps(&seq), params::sps(&seq), params::pps()]; + let rc = kbps.map(|k| RateCtl::new(k, &cfg)); + Ok(Encoder { cfg, seq, sets, frames: 0, poc: 0, reference: None, recon: None, rc }) + } + + pub fn config(&self) -> &EncoderConfig { + &self.cfg + } + + /// VPS, SPS, PPS NAL units (2-byte NAL header, emulation prevention, no start codes or length + /// prefixes). + pub fn parameter_sets(&self) -> [Vec; 3] { + self.sets.clone() + } + + /// HEVCDecoderConfigurationRecord (`hvcC` payload) with the parameter sets. + pub fn hvcc(&self) -> Vec { + params::hvcc(&self.seq, &self.sets) + } + + /// `general_level_idc` written in the parameter sets. + pub fn level_idc(&self) -> u8 { + self.seq.level_idc + } + + /// Reconstruction of the last encoded picture (what a decoder outputs), cropped planes + /// `[Y, Cb, Cr]` with strides `width` and `width / 2`. For tests. + #[doc(hidden)] + pub fn last_reconstruction(&self) -> Option<&[Vec; 3]> { + self.recon.as_ref() + } + + /// Copy the input into coded-size planes, replicating the right / bottom edges. + fn pad_input(&self, f: &Frame) -> [Vec; 3] { + let max = (1u32 << self.seq.bit_depth) - 1; + let mut out: [Vec; 3] = Default::default(); + for (c, plane) in out.iter_mut().enumerate() { + let (src, stride, w, h, cw, ch) = if c == 0 { + (f.y, f.y_stride, self.seq.width, self.seq.height, self.seq.coded_width, self.seq.coded_height) + } else { + let s = if c == 1 { f.u } else { f.v }; + (s, f.uv_stride, self.seq.width / 2, self.seq.height / 2, self.seq.coded_width / 2, self.seq.coded_height / 2) + }; + let (w, h, cw, ch) = (w as usize, h as usize, cw as usize, ch as usize); + assert!(stride >= w && src.len() >= (h - 1) * stride + w, "HEVC encoder: plane {c} smaller than {w}x{h} (stride {stride})"); + plane.reserve(cw * ch); + for y in 0..ch { + let row = &src[y.min(h - 1) * stride..][..w]; + plane.extend(row.iter().map(|&v| (v as u32).min(max) as u16)); + let last = *plane.last().unwrap(); + plane.extend(std::iter::repeat_n(last, cw - w)); + } + } + out + } + + /// Encodes one picture. + pub fn encode(&mut self, frame: &Frame) -> Packet { + let src = self.pad_input(frame); + let keyint = self.cfg.keyint as u64; + let idr = self.reference.is_none() || (keyint > 0 && self.frames.is_multiple_of(keyint)); + self.poc = if idr { 0 } else { self.poc + 1 }; + let qp = match (&self.rc, self.cfg.rate) { + (Some(rc), _) => rc.qp(idr), + (None, RateControl::ConstantQp(q)) => q as i32, + (None, RateControl::Bitrate { .. }) => 28, + }; + let (w, h) = (self.seq.coded_width as usize, self.seq.coded_height as usize); + let bd = self.seq.bit_depth; + let kind = if idr { SliceKind::I } else { SliceKind::P }; + let me = if idr { None } else { self.reference.as_ref().map(|r| me::MeRef::new(&r[0], w, h, bd)) }; + let refp = if idr { None } else { self.reference.as_ref() }; + let mut fc = FrameCoder::new(bd, w, h, kind, qp, &src, refp, me.as_ref()); + + // slice_segment_header() (7.3.6.1) + let mut bw = BitWriter::new(); + bw.put(1, 1); // first_slice_segment_in_pic_flag + if idr { + bw.put(0, 1); // no_output_of_prior_pics_flag + } + bw.ue(0); // slice_pic_parameter_set_id + bw.ue(if idr { 2 } else { 1 }); // slice_type I / P + if !idr { + bw.put(self.poc & ((1 << LOG2_MAX_POC_LSB) - 1), LOG2_MAX_POC_LSB); // slice_pic_order_cnt_lsb + bw.put(1, 1); // short_term_ref_pic_set_sps_flag (the only SPS set, no index coded) + bw.put(0, 1); // num_ref_idx_active_override_flag + bw.ue(5 - MAX_MERGE_CAND as u32); // five_minus_max_num_merge_cand + } + bw.se(qp - 26); // slice_qp_delta (init_qp = 26) + // byte_alignment() + bw.put(1, 1); + bw.align_zero(); + + let ctx = cabac::init_contexts(qp, if idr { 0 } else { 1 }); + let mut enc = cabac::CabacEncoder::new(bw, ctx); + fc.encode(&mut enc); + fc.deblock(); + let rbsp = enc.finish().into_bytes(); + let nal = nal_unit(if idr { NAL_IDR_N_LP } else { NAL_TRAIL_R }, &rbsp); + let mut data = Vec::with_capacity(nal.len() + 4); + data.extend_from_slice(&(nal.len() as u32).to_be_bytes()); + data.extend_from_slice(&nal); + + if let Some(rc) = &mut self.rc { + rc.update(idr, qp, data.len() as f64 * 8.0); + } + // output picture: cropped reconstruction + let rec = std::mem::take(&mut fc.rec); + let (ow, oh) = (self.seq.width as usize, self.seq.height as usize); + let crop = |p: &[u16], stride: usize, w: usize, h: usize| -> Vec { (0..h).flat_map(|y| p[y * stride..y * stride + w].iter().copied()).collect() }; + self.recon = Some([crop(&rec[0], w, ow, oh), crop(&rec[1], w / 2, ow / 2, oh / 2), crop(&rec[2], w / 2, ow / 2, oh / 2)]); + self.reference = Some(rec); + self.frames += 1; + Packet { data, keyframe: idr } + } +} diff --git a/crates/hevcenc/src/me.rs b/crates/hevcenc/src/me.rs new file mode 100644 index 00000000..5f4cb2ce --- /dev/null +++ b/crates/hevcenc/src/me.rs @@ -0,0 +1,144 @@ +//! Motion search reference: the previous reconstructed luma plane padded by edge replication, plus +//! half-sample planes interpolated with the 8-tap half-sample filter. Quarter-sample positions are +//! approximated by averaging neighbouring half-grid samples. Used for estimation only; the coded +//! prediction always uses the normative interpolation in `inter`. + +use crate::tables::LUMA_FILTER; + +const MARGIN: i32 = 80; + +pub struct MeRef { + /// [integer, horizontal half, vertical half, diagonal half] samples, each `stride` x `rows`. + planes: [Vec; 4], + stride: usize, + rows: usize, + w: i32, + h: i32, +} + +impl MeRef { + pub fn new(y: &[u16], w: usize, h: usize, bd: u32) -> MeRef { + let m = MARGIN as usize; + let (stride, rows) = (w + 2 * m, h + 2 * m); + let mut pad = vec![0u16; stride * rows]; + for r in 0..rows { + let sy = (r as i32 - MARGIN).clamp(0, h as i32 - 1) as usize; + let src = &y[sy * w..sy * w + w]; + let row = &mut pad[r * stride..r * stride + stride]; + row[..m].fill(src[0]); + row[m..m + w].copy_from_slice(src); + row[m + w..].fill(src[w - 1]); + } + let max = (1i32 << bd) - 1; + let f = &LUMA_FILTER[2]; + let filt_h = |p: &[u16]| -> Vec { + let mut o = vec![0u16; stride * rows]; + for r in 0..rows { + let row = &p[r * stride..r * stride + stride]; + for x in 0..stride { + let mut s = 0i32; + for k in 0..8 { + let xx = (x as i32 - 3 + k as i32).clamp(0, stride as i32 - 1) as usize; + s += f[k] as i32 * row[xx] as i32; + } + o[r * stride + x] = ((s + 32) >> 6).clamp(0, max) as u16; + } + } + o + }; + let filt_v = |p: &[u16]| -> Vec { + let mut o = vec![0u16; stride * rows]; + let mut acc = vec![0i32; stride]; + for r in 0..rows { + acc.fill(0); + for k in 0..8 { + let rr = (r as i32 - 3 + k as i32).clamp(0, rows as i32 - 1) as usize; + let src = &p[rr * stride..rr * stride + stride]; + let coef = f[k] as i32; + for (a, &s) in acc.iter_mut().zip(src) { + *a += coef * s as i32; + } + } + for (d, &a) in o[r * stride..r * stride + stride].iter_mut().zip(&acc) { + *d = ((a + 32) >> 6).clamp(0, max) as u16; + } + } + o + }; + let hp = filt_h(&pad); + let vp = filt_v(&pad); + let dp = filt_v(&hp); + MeRef { planes: [pad, hp, vp, dp], stride, rows, w: w as i32, h: h as i32 } + } + + /// Integer motion vector range keeping an n x n block (plus filter margin) inside the padding. + pub fn int_range(&self, x: i32, y: i32, n: usize) -> (i32, i32, i32, i32) { + let n = n as i32; + let lo_x = -MARGIN + 4 - x; + let hi_x = self.w + MARGIN - n - 4 - x; + let lo_y = -MARGIN + 4 - y; + let hi_y = self.h + MARGIN - n - 4 - y; + (lo_x.max(-256), hi_x.min(256), lo_y.max(-256), hi_y.min(256)) + } + + /// SAD of the source block against the integer-position reference block at (rx, ry). + pub fn sad(&self, src: &[u16], ss: usize, rx: i32, ry: i32, n: usize) -> u32 { + let p = &self.planes[0]; + let base = (ry + MARGIN) as usize * self.stride + (rx + MARGIN) as usize; + let mut s = 0u32; + for r in 0..n { + let a = &src[r * ss..r * ss + n]; + let b = &p[base + r * self.stride..base + r * self.stride + n]; + s += a.iter().zip(b).map(|(&x, &y)| (x as i32 - y as i32).unsigned_abs()).sum::(); + } + s + } + + /// Approximate luma prediction of an n x n block at (x, y) with quarter-sample vector `mv`. + pub fn approx_pred(&self, x: i32, y: i32, n: usize, mv: [i16; 2], out: &mut [u16]) { + // per axis: one or two (half-sample fraction, integer offset) taps + let axis = |v: i32| -> [(usize, i32); 2] { + if v & 1 == 0 { + let hv = v >> 1; + [((hv & 1) as usize, hv >> 1); 2] + } else { + let (a, b) = ((v - 1) >> 1, (v + 1) >> 1); + [((a & 1) as usize, a >> 1), ((b & 1) as usize, b >> 1)] + } + }; + let (mx, my) = (mv[0] as i32, mv[1] as i32); + let ax = axis(mx); + let ay = axis(my); + let two = mx & 1 != 0 || my & 1 != 0; + let taps = [(ax[0], ay[0]), (ax[1], ay[1])]; + let ntaps = if two { 2 } else { 1 }; + let mut first = true; + for &((fx, ox), (fy, oy)) in taps.iter().take(ntaps) { + let plane = &self.planes[fx + 2 * fy]; + let (bx, by) = (x + ox + MARGIN, y + oy + MARGIN); + let inside = bx >= 0 && by >= 0 && bx as usize + n <= self.stride && by as usize + n <= self.rows; + for r in 0..n { + let dst = &mut out[r * n..r * n + n]; + if inside { + let o = (by as usize + r) * self.stride + bx as usize; + let src = &plane[o..o + n]; + if first { + dst.copy_from_slice(src); + } else { + for (d, &s) in dst.iter_mut().zip(src) { + *d = ((*d as u32 + s as u32 + 1) >> 1) as u16; + } + } + } else { + let yy = (by + r as i32).clamp(0, self.rows as i32 - 1) as usize; + for (c, d) in dst.iter_mut().enumerate() { + let xx = (bx + c as i32).clamp(0, self.stride as i32 - 1) as usize; + let s = plane[yy * self.stride + xx]; + *d = if first { s } else { ((*d as u32 + s as u32 + 1) >> 1) as u16 }; + } + } + } + first = false; + } + } +} diff --git a/crates/hevcenc/src/params.rs b/crates/hevcenc/src/params.rs new file mode 100644 index 00000000..f1b89f73 --- /dev/null +++ b/crates/hevcenc/src/params.rs @@ -0,0 +1,262 @@ +//! Parameter sets (7.3.2.1 VPS, 7.3.2.2 SPS with E.2.1 VUI, 7.3.2.3 PPS), profile_tier_level (7.3.3), +//! level selection (Annex A, Table A.8) and the `hvcC` record (ISO/IEC 14496-15 §8.3.3.1). + +use crate::bitstream::{BitWriter, NAL_PPS, NAL_SPS, NAL_VPS, nal_unit}; + +/// Fixed coding structure of the encoder. +pub const LOG2_CTB: u32 = 6; +pub const LOG2_MIN_CB: u32 = 3; +pub const LOG2_MIN_TB: u32 = 2; +pub const LOG2_MAX_TB: u32 = 5; +pub const LOG2_MAX_POC_LSB: u32 = 8; +/// MaxNumMergeCand. +pub const MAX_MERGE_CAND: usize = 5; + +/// Sequence-level settings shared by the parameter-set writers. +#[derive(Clone, Debug)] +pub struct SeqParams { + /// Coded (padded) size, multiples of the minimum CB size. + pub coded_width: u32, + pub coded_height: u32, + /// Output (cropped) size. + pub width: u32, + pub height: u32, + pub bit_depth: u32, + pub profile_idc: u8, + pub level_idc: u8, + pub full_range: bool, + pub fps_num: u32, + pub fps_den: u32, +} + +/// (level_idc, MaxLumaPs, MaxLumaSr, MaxBR main tier in kbit/s) of Table A.8 (and A.9 for luma rates). +const LEVELS: [(u8, u64, u64, u64); 13] = [ + (30, 36_864, 552_960, 128), + (60, 122_880, 3_686_400, 1_500), + (63, 245_760, 7_372_800, 3_000), + (90, 552_960, 16_588_800, 6_000), + (93, 983_040, 33_177_600, 10_000), + (120, 2_228_224, 66_846_720, 12_000), + (123, 2_228_224, 133_693_440, 20_000), + (150, 8_912_896, 267_386_880, 25_000), + (153, 8_912_896, 534_773_760, 40_000), + (156, 8_912_896, 1_069_547_520, 60_000), + (180, 35_651_584, 1_069_547_520, 60_000), + (183, 35_651_584, 2_139_095_040, 120_000), + (186, 35_651_584, 4_278_190_080, 240_000), +]; + +/// Lowest level whose picture size, sample rate and (when known) bitrate limits fit. +pub fn choose_level(width: u32, height: u32, fps_num: u32, fps_den: u32, kbps: Option) -> u8 { + let ps = width as u64 * height as u64; + let sr = (ps as f64 * fps_num as f64 / fps_den.max(1) as f64).ceil() as u64; + for &(idc, max_ps, max_sr, max_br) in &LEVELS { + let max_dim = ((max_ps * 8) as f64).sqrt() as u64; + if ps <= max_ps && (width as u64) <= max_dim && (height as u64) <= max_dim && sr <= max_sr && kbps.is_none_or(|k| k as u64 <= max_br) { + return idc; + } + } + 186 +} + +fn profile_tier_level(w: &mut BitWriter, p: &SeqParams) { + w.put(0, 2); // general_profile_space + w.put(0, 1); // general_tier_flag (Main tier) + w.put(p.profile_idc as u32, 5); + w.put(compat_flags(p.profile_idc), 32); + w.put(1, 1); // general_progressive_source_flag + w.put(0, 1); // general_interlaced_source_flag + w.put(0, 1); // general_non_packed_constraint_flag + w.put(1, 1); // general_frame_only_constraint_flag + w.put(0, 32); // general_reserved_zero_43bits ... + w.put(0, 11); + w.put(0, 1); // general_inbld_flag / reserved + w.put(p.level_idc as u32, 8); + // sps_max_sub_layers_minus1 == 0: no sub-layer flags +} + +/// general_profile_compatibility_flag[j] as a 32-bit word (flag 0 is the MSB). A Main stream is also +/// Main 10 compatible. +fn compat_flags(profile_idc: u8) -> u32 { + let mut f = 1u32 << (31 - profile_idc as u32); + if profile_idc == 1 { + f |= 1 << (31 - 2); + } + f +} + +pub fn vps(p: &SeqParams) -> Vec { + let mut w = BitWriter::new(); + w.put(0, 4); // vps_video_parameter_set_id + w.put(1, 1); // vps_base_layer_internal_flag + w.put(1, 1); // vps_base_layer_available_flag + w.put(0, 6); // vps_max_layers_minus1 + w.put(0, 3); // vps_max_sub_layers_minus1 + w.put(1, 1); // vps_temporal_id_nesting_flag + w.put(0xffff, 16); + profile_tier_level(&mut w, p); + w.put(1, 1); // vps_sub_layer_ordering_info_present_flag + w.ue(1); // vps_max_dec_pic_buffering_minus1 + w.ue(0); // vps_max_num_reorder_pics + w.ue(0); // vps_max_latency_increase_plus1 + w.put(0, 6); // vps_max_layer_id + w.ue(0); // vps_num_layer_sets_minus1 + w.put(0, 1); // vps_timing_info_present_flag + w.put(0, 1); // vps_extension_flag + w.trailing(); + nal_unit(NAL_VPS, &w.into_bytes()) +} + +pub fn sps(p: &SeqParams) -> Vec { + let mut w = BitWriter::new(); + w.put(0, 4); // sps_video_parameter_set_id + w.put(0, 3); // sps_max_sub_layers_minus1 + w.put(1, 1); // sps_temporal_id_nesting_flag + profile_tier_level(&mut w, p); + w.ue(0); // sps_seq_parameter_set_id + w.ue(1); // chroma_format_idc 4:2:0 + w.ue(p.coded_width); + w.ue(p.coded_height); + let crop = p.coded_width != p.width || p.coded_height != p.height; + w.flag(crop); // conformance_window_flag + if crop { + // offsets in chroma sample units (SubWidthC = SubHeightC = 2) + w.ue(0); + w.ue((p.coded_width - p.width) / 2); + w.ue(0); + w.ue((p.coded_height - p.height) / 2); + } + w.ue(p.bit_depth - 8); // bit_depth_luma_minus8 + w.ue(p.bit_depth - 8); // bit_depth_chroma_minus8 + w.ue(LOG2_MAX_POC_LSB - 4); + w.put(1, 1); // sps_sub_layer_ordering_info_present_flag + w.ue(1); // sps_max_dec_pic_buffering_minus1 + w.ue(0); // sps_max_num_reorder_pics + w.ue(0); // sps_max_latency_increase_plus1 + w.ue(LOG2_MIN_CB - 3); + w.ue(LOG2_CTB - LOG2_MIN_CB); + w.ue(LOG2_MIN_TB - 2); + w.ue(LOG2_MAX_TB - LOG2_MIN_TB); + w.ue(0); // max_transform_hierarchy_depth_inter + w.ue(0); // max_transform_hierarchy_depth_intra + w.put(0, 1); // scaling_list_enabled_flag + w.put(0, 1); // amp_enabled_flag + w.put(0, 1); // sample_adaptive_offset_enabled_flag + w.put(0, 1); // pcm_enabled_flag + w.ue(1); // num_short_term_ref_pic_sets + // st_ref_pic_set(0): one negative picture at delta POC -1, used by the current picture + w.ue(1); // num_negative_pics + w.ue(0); // num_positive_pics + w.ue(0); // delta_poc_s0_minus1 + w.put(1, 1); // used_by_curr_pic_s0_flag + w.put(0, 1); // long_term_ref_pics_present_flag + w.put(0, 1); // sps_temporal_mvp_enabled_flag + w.put(1, 1); // strong_intra_smoothing_enabled_flag + w.put(1, 1); // vui_parameters_present_flag + vui(&mut w, p); + w.put(0, 1); // sps_extension_present_flag + w.trailing(); + nal_unit(NAL_SPS, &w.into_bytes()) +} + +fn vui(w: &mut BitWriter, p: &SeqParams) { + w.put(0, 1); // aspect_ratio_info_present_flag + w.put(0, 1); // overscan_info_present_flag + w.put(1, 1); // video_signal_type_present_flag + w.put(5, 3); // video_format: unspecified + w.flag(p.full_range); + w.put(1, 1); // colour_description_present_flag + w.put(1, 8); // colour_primaries BT.709 + w.put(1, 8); // transfer_characteristics BT.709 + w.put(1, 8); // matrix_coeffs BT.709 + w.put(0, 1); // chroma_loc_info_present_flag + w.put(0, 1); // neutral_chroma_indication_flag + w.put(0, 1); // field_seq_flag + w.put(0, 1); // frame_field_info_present_flag + w.put(0, 1); // default_display_window_flag + w.put(1, 1); // vui_timing_info_present_flag + w.put(p.fps_den, 32); // vui_num_units_in_tick + w.put(p.fps_num, 32); // vui_time_scale + w.put(0, 1); // vui_poc_proportional_to_timing_flag + w.put(0, 1); // vui_hrd_parameters_present_flag + w.put(0, 1); // bitstream_restriction_flag +} + +pub fn pps() -> Vec { + let mut w = BitWriter::new(); + w.ue(0); // pps_pic_parameter_set_id + w.ue(0); // pps_seq_parameter_set_id + w.put(0, 1); // dependent_slice_segments_enabled_flag + w.put(0, 1); // output_flag_present_flag + w.put(0, 3); // num_extra_slice_header_bits + w.put(1, 1); // sign_data_hiding_enabled_flag + w.put(0, 1); // cabac_init_present_flag + w.ue(0); // num_ref_idx_l0_default_active_minus1 + w.ue(0); // num_ref_idx_l1_default_active_minus1 + w.se(0); // init_qp_minus26 + w.put(0, 1); // constrained_intra_pred_flag + w.put(0, 1); // transform_skip_enabled_flag + w.put(0, 1); // cu_qp_delta_enabled_flag + w.se(0); // pps_cb_qp_offset + w.se(0); // pps_cr_qp_offset + w.put(0, 1); // pps_slice_chroma_qp_offsets_present_flag + w.put(0, 1); // weighted_pred_flag + w.put(0, 1); // weighted_bipred_flag + w.put(0, 1); // transquant_bypass_enabled_flag + w.put(0, 1); // tiles_enabled_flag + w.put(0, 1); // entropy_coding_sync_enabled_flag + w.put(0, 1); // pps_loop_filter_across_slices_enabled_flag + w.put(1, 1); // deblocking_filter_control_present_flag + w.put(0, 1); // deblocking_filter_override_enabled_flag + w.put(0, 1); // pps_deblocking_filter_disabled_flag + w.se(0); // pps_beta_offset_div2 + w.se(0); // pps_tc_offset_div2 + w.put(0, 1); // pps_scaling_list_data_present_flag + w.put(0, 1); // lists_modification_present_flag + w.ue(0); // log2_parallel_merge_level_minus2 + w.put(0, 1); // slice_segment_header_extension_present_flag + w.put(0, 1); // pps_extension_present_flag + w.trailing(); + nal_unit(NAL_PPS, &w.into_bytes()) +} + +/// HEVCDecoderConfigurationRecord with one VPS, SPS and PPS (lengthSizeMinusOne = 3). +pub fn hvcc(p: &SeqParams, sets: &[Vec; 3]) -> Vec { + let mut o = vec![1u8]; // configurationVersion + o.push(p.profile_idc); // profile_space 0, tier 0, profile_idc + o.extend_from_slice(&compat_flags(p.profile_idc).to_be_bytes()); + // general_constraint_indicator_flags (48 bits): progressive, interlaced, non_packed, frame_only + o.extend_from_slice(&[0x90, 0, 0, 0, 0, 0]); + o.push(p.level_idc); + o.extend_from_slice(&[0xf0, 0x00]); // reserved + min_spatial_segmentation_idc 0 + o.push(0xfc); // reserved + parallelismType 0 + o.push(0xfc | 1); // reserved + chroma_format_idc 1 + o.push(0xf8 | (p.bit_depth - 8) as u8); + o.push(0xf8 | (p.bit_depth - 8) as u8); + o.extend_from_slice(&[0, 0]); // avgFrameRate unspecified + // constantFrameRate 0, numTemporalLayers 1, temporalIdNested 1, lengthSizeMinusOne 3 + o.push((1 << 3) | (1 << 2) | 3); + o.push(3); // numOfArrays + for (set, ty) in sets.iter().zip([NAL_VPS, NAL_SPS, NAL_PPS]) { + o.push(0x80 | ty); // array_completeness 1 + o.extend_from_slice(&1u16.to_be_bytes()); + o.extend_from_slice(&(set.len() as u16).to_be_bytes()); + o.extend_from_slice(set); + } + o +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn levels() { + assert_eq!(choose_level(1920, 1080, 30, 1, None), 120); + assert_eq!(choose_level(1920, 1080, 60, 1, None), 123); + assert_eq!(choose_level(3840, 2160, 30, 1, None), 150); + assert_eq!(choose_level(1280, 720, 30, 1, None), 93); + assert_eq!(choose_level(64, 64, 30, 1, None), 30); + assert_eq!(choose_level(1920, 1080, 30, 1, Some(15_000)), 123); + } +} diff --git a/crates/hevcenc/src/tables.rs b/crates/hevcenc/src/tables.rs new file mode 100644 index 00000000..7c2fc447 --- /dev/null +++ b/crates/hevcenc/src/tables.rs @@ -0,0 +1,236 @@ +//! Constant tables of ITU-T H.265, mirrored from the first-party, spec-derived `filmcraft-hevc` +//! decoder (same organisation, MIT OR Apache-2.0): CABAC context layout and initValues (9.3.2.2), +//! rangeTabLPS / transIdx (Tables 9-52, 9-53), scans (6.5.3 - 6.5.5), transform matrices (8.6.4.2), +//! intra angles (Tables 8-5, 8-6), interpolation filters (8.5.3.3.3) and the chroma QP mapping (Table 8-10). + +#![allow(dead_code)] + +/// Table 9-52 (same values as H.264) rangeTabLPS[pStateIdx][qCodIRangeIdx]. +#[rustfmt::skip] +pub static RANGE_TAB_LPS: [[u8; 4]; 64] = [ + [128, 176, 208, 240], [128, 167, 197, 227], [128, 158, 187, 216], [123, 150, 178, 205], + [116, 142, 169, 195], [111, 135, 160, 185], [105, 128, 152, 175], [100, 122, 144, 166], + [95, 116, 137, 158], [90, 110, 130, 150], [85, 104, 123, 142], [81, 99, 117, 135], + [77, 94, 111, 128], [73, 89, 105, 122], [69, 85, 100, 116], [66, 80, 95, 110], + [62, 76, 90, 104], [59, 72, 86, 99], [56, 69, 81, 94], [53, 65, 77, 89], + [51, 62, 73, 85], [48, 59, 69, 80], [46, 56, 66, 76], [43, 53, 63, 72], + [41, 50, 59, 69], [39, 48, 56, 65], [37, 45, 54, 62], [35, 43, 51, 59], + [33, 41, 48, 56], [32, 39, 46, 53], [30, 37, 43, 50], [29, 35, 41, 48], + [27, 33, 39, 45], [26, 31, 37, 43], [24, 30, 35, 41], [23, 28, 33, 39], + [22, 27, 32, 37], [21, 26, 30, 35], [20, 24, 29, 33], [19, 23, 27, 31], + [18, 22, 26, 30], [17, 21, 25, 28], [16, 20, 23, 27], [15, 19, 22, 25], + [14, 18, 21, 24], [14, 17, 20, 23], [13, 16, 19, 22], [12, 15, 18, 21], + [12, 14, 17, 20], [11, 14, 16, 19], [11, 13, 15, 18], [10, 12, 15, 17], + [10, 12, 14, 16], [9, 11, 13, 15], [9, 11, 12, 14], [8, 10, 12, 14], + [8, 9, 11, 13], [7, 9, 11, 12], [7, 9, 10, 12], [7, 8, 10, 11], + [6, 8, 9, 11], [6, 7, 9, 10], [6, 7, 8, 9], [2, 2, 2, 2], +]; + +/// Table 9-53 transIdxLPS. +#[rustfmt::skip] +static TRANS_IDX_LPS: [u8; 64] = [ + 0, 0, 1, 2, 2, 4, 4, 5, 6, 7, 8, 9, 9, 11, 11, 12, + 13, 13, 15, 15, 16, 16, 18, 18, 19, 19, 21, 21, 22, 22, 23, 24, + 24, 25, 26, 26, 27, 27, 28, 29, 29, 30, 30, 30, 31, 32, 32, 33, + 33, 33, 34, 34, 35, 35, 35, 36, 36, 36, 37, 37, 37, 38, 38, 63, +]; + +/// Table 9-53 transIdxMPS. +#[rustfmt::skip] +static TRANS_IDX_MPS: [u8; 64] = [ + 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, 62, 63, +]; + +/// Combined state transition: index = (pStateIdx << 1 | valMPS) * 2 + bin_is_lps. +/// Each context is stored as `pStateIdx << 1 | valMPS`. +pub static NEXT_STATE: [[u8; 2]; 128] = { + let mut t = [[0u8; 2]; 128]; + let mut s = 0; + while s < 128 { + let p = s >> 1; + let mps = s & 1; + // MPS path + t[s][0] = (TRANS_IDX_MPS[p] << 1) | mps as u8; + // LPS path + let new_mps = if p == 0 { 1 - mps } else { mps }; + t[s][1] = (TRANS_IDX_LPS[p] << 1) | new_mps as u8; + s += 1; + } + t +}; + +/// Up-right diagonal scan (6.5.3) of a `N x N` block as (x, y) pairs. +const fn diag(n: usize) -> [(u8, u8); L] { + let mut out = [(0u8, 0u8); L]; + let mut i = 0; + let mut x: i32 = 0; + let mut y: i32 = 0; + while i < L { + while y >= 0 { + if (x as usize) < n && (y as usize) < n { + out[i] = (x as u8, y as u8); + i += 1; + } + y -= 1; + x += 1; + } + y = x; + x = 0; + } + out +} + +/// Horizontal (6.5.4, `vertical == false`) or vertical (6.5.5) scan. +const fn hv(n: usize, vertical: bool) -> [(u8, u8); L] { + let mut out = [(0u8, 0u8); L]; + let mut i = 0; + while i < L { + let (a, b) = ((i % n) as u8, (i / n) as u8); + out[i] = if vertical { (b, a) } else { (a, b) }; + i += 1; + } + out +} + +pub static SCAN_1: [[(u8, u8); 1]; 3] = [[(0, 0)], [(0, 0)], [(0, 0)]]; +pub static SCAN_2: [[(u8, u8); 4]; 3] = [diag::<4>(2), hv::<4>(2, false), hv::<4>(2, true)]; +pub static SCAN_4: [[(u8, u8); 16]; 3] = [diag::<16>(4), hv::<16>(4, false), hv::<16>(4, true)]; +pub static SCAN_8: [[(u8, u8); 64]; 3] = [diag::<64>(8), hv::<64>(8, false), hv::<64>(8, true)]; + +/// ScanOrder[log2BlockSize][scanIdx] (log2BlockSize 0..=3). +#[inline] +pub fn scan_order(log2: u32, scan_idx: usize) -> &'static [(u8, u8)] { + match log2 { + 0 => &SCAN_1[scan_idx], + 1 => &SCAN_2[scan_idx], + 2 => &SCAN_4[scan_idx], + _ => &SCAN_8[scan_idx], + } +} + +/// Up-right diagonal scan for log2 size 2 or 3. +pub fn scan_diag(log2: u32) -> &'static [(u8, u8)] { + scan_order(log2, 0) +} + +/// Table 8-10: QpC as a function of qPi (ChromaArrayType 1). +#[inline] +pub fn qpc_420(qpi: i32) -> i32 { + if qpi < 30 { + qpi + } else if qpi > 43 { + qpi - 6 + } else { + QPC_30_43[(qpi - 30) as usize] as i32 + } +} + +/// Table 9-50 ctxIdxMap for 4x4 sig_coeff_flag contexts. +pub static CTX_IDX_MAP: [u8; 16] = [0, 1, 4, 5, 2, 3, 4, 5, 6, 6, 8, 8, 7, 7, 8, 8]; + +/// levelScale (8-309). +pub static LEVEL_SCALE: [i32; 6] = [40, 45, 51, 57, 64, 72]; + +/// DST-VII 4x4 matrix (8.6.4.2, trType 1). +pub static DST_MATRIX: [[i8; 4]; 4] = [[29, 55, 74, 84], [74, 74, 0, -74], [84, -29, -74, 55], [55, -84, 74, -29]]; + + +// CABAC context layout: offset of each syntax element in the context array (Table 9-4). +pub const SAO_MERGE: usize = 0; +pub const SAO_TYPE: usize = 1; +pub const SPLIT_CU: usize = 2; +pub const CU_TRANSQUANT_BYPASS: usize = 5; +pub const CU_SKIP: usize = 6; +pub const PRED_MODE: usize = 9; +pub const PART_MODE: usize = 10; +pub const PREV_INTRA_LUMA: usize = 14; +pub const INTRA_CHROMA: usize = 15; +pub const RQT_ROOT_CBF: usize = 16; +pub const MERGE_FLAG: usize = 17; +pub const MERGE_IDX: usize = 18; +pub const INTER_PRED_IDC: usize = 19; +pub const REF_IDX: usize = 24; +pub const MVP_FLAG: usize = 26; +pub const SPLIT_TRANSFORM: usize = 27; +pub const CBF_LUMA: usize = 30; +pub const CBF_CHROMA: usize = 32; +pub const ABS_MVD_GT0: usize = 37; +pub const ABS_MVD_GT1: usize = 38; +pub const CU_QP_DELTA_ABS: usize = 39; +pub const TRANSFORM_SKIP: usize = 41; +pub const LAST_X_PREFIX: usize = 43; +pub const LAST_Y_PREFIX: usize = 61; +pub const CODED_SUB_BLOCK: usize = 79; +pub const SIG_COEFF: usize = 83; +pub const GT1: usize = 127; +pub const GT2: usize = 151; +pub const EXPLICIT_RDPCM: usize = 157; +pub const EXPLICIT_RDPCM_DIR: usize = 159; +pub const CU_CHROMA_QP_OFFSET_FLAG: usize = 161; +pub const CU_CHROMA_QP_OFFSET_IDX: usize = 162; +pub const NUM_CTX: usize = 163; + +/// initValue per initType (9.3.2.2). +pub static CABAC_INIT: [[u8; NUM_CTX]; 3] = [ + [153, 200, 139, 141, 157, 154, 154, 154, 154, 154, 184, 154, 154, 154, 184, 63, 154, 154, 154, 154, 154, 154, 154, 154, 154, 154, 154, 153, 138, 138, 111, 141, 94, 138, 182, 154, 154, 154, 154, 154, 154, 139, 139, 110, 110, 124, 125, 140, 153, 125, 127, 140, 109, 111, 143, 127, 111, 79, 108, 123, 63, 110, 110, 124, 125, 140, 153, 125, 127, 140, 109, 111, 143, 127, 111, 79, 108, 123, 63, 91, 171, 134, 141, 111, 111, 125, 110, 110, 94, 124, 108, 124, 107, 125, 141, 179, 153, 125, 107, 125, 141, 179, 153, 125, 107, 125, 141, 179, 153, 125, 140, 139, 182, 182, 152, 136, 152, 136, 153, 136, 139, 111, 136, 139, 111, 141, 111, 140, 92, 137, 138, 140, 152, 138, 139, 153, 74, 149, 92, 139, 107, 122, 152, 140, 179, 166, 182, 140, 227, 122, 197, 138, 153, 136, 167, 152, 152, 154, 154, 154, 154, 154, 154], + [153, 185, 107, 139, 126, 154, 197, 185, 201, 149, 154, 139, 154, 154, 154, 152, 79, 110, 122, 95, 79, 63, 31, 31, 153, 153, 168, 124, 138, 94, 153, 111, 149, 107, 167, 154, 154, 140, 198, 154, 154, 139, 139, 125, 110, 94, 110, 95, 79, 125, 111, 110, 78, 110, 111, 111, 95, 94, 108, 123, 108, 125, 110, 94, 110, 95, 79, 125, 111, 110, 78, 110, 111, 111, 95, 94, 108, 123, 108, 121, 140, 61, 154, 155, 154, 139, 153, 139, 123, 123, 63, 153, 166, 183, 140, 136, 153, 154, 166, 183, 140, 136, 153, 154, 166, 183, 140, 136, 153, 154, 170, 153, 123, 123, 107, 121, 107, 121, 167, 151, 183, 140, 151, 183, 140, 140, 140, 154, 196, 196, 167, 154, 152, 167, 182, 182, 134, 149, 136, 153, 121, 136, 137, 169, 194, 166, 167, 154, 167, 137, 182, 107, 167, 91, 122, 107, 167, 139, 139, 139, 139, 154, 154], + [153, 160, 107, 139, 126, 154, 197, 185, 201, 134, 154, 139, 154, 154, 183, 152, 79, 154, 137, 95, 79, 63, 31, 31, 153, 153, 168, 224, 167, 122, 153, 111, 149, 92, 167, 154, 154, 169, 198, 154, 154, 139, 139, 125, 110, 124, 110, 95, 94, 125, 111, 111, 79, 125, 126, 111, 111, 79, 108, 123, 93, 125, 110, 124, 110, 95, 94, 125, 111, 111, 79, 125, 126, 111, 111, 79, 108, 123, 93, 121, 140, 61, 154, 170, 154, 139, 153, 139, 123, 123, 63, 124, 166, 183, 140, 136, 153, 154, 166, 183, 140, 136, 153, 154, 166, 183, 140, 136, 153, 154, 170, 153, 138, 138, 122, 121, 122, 121, 167, 151, 183, 140, 151, 183, 140, 140, 140, 154, 196, 167, 167, 154, 152, 167, 182, 182, 134, 149, 136, 153, 121, 136, 122, 169, 208, 166, 167, 154, 152, 167, 182, 107, 167, 91, 107, 107, 167, 139, 139, 139, 139, 154, 154], +]; + + +/// Table 8-5: intraPredAngle for predModeIntra 0..34 (0 for planar / DC). +pub static INTRA_PRED_ANGLE: [i8; 35] = [0, 0, 32, 26, 21, 17, 13, 9, 5, 2, 0, -2, -5, -9, -13, -17, -21, -26, -32, -26, -21, -17, -13, -9, -5, -2, 0, 2, 5, 9, 13, 17, 21, 26, 32]; +/// Table 8-6: invAngle for predModeIntra 11..25 (index = mode - 11). +pub static INV_ANGLE: [i16; 15] = [-4096, -1638, -910, -630, -482, -390, -315, -256, -315, -390, -482, -630, -910, -1638, -4096]; + +/// Table 8-10: QpC for qPi = 30..43 (qPi < 30: QpC = qPi; qPi > 43: QpC = qPi - 6). +pub static QPC_30_43: [u8; 14] = [29, 30, 31, 32, 33, 33, 34, 34, 35, 35, 36, 36, 37, 37]; + + +/// Luma 8-tap interpolation filter fL[xFrac][k] (8-236..8-238), taps at offsets -3..4. +pub static LUMA_FILTER: [[i8; 8]; 4] = [[0, 0, 0, 64, 0, 0, 0, 0], [-1, 4, -10, 58, 17, -5, 1, 0], [-1, 4, -11, 40, 40, -11, 4, -1], [0, 1, -5, 17, 58, -10, 4, -1]]; +/// Chroma 4-tap interpolation filter fC[xFrac][k] (8-255..8-261), taps at offsets -1..2. +pub static CHROMA_FILTER: [[i8; 4]; 8] = [[0, 64, 0, 0], [-2, 58, 10, -2], [-4, 54, 16, -2], [-6, 46, 28, -4], [-4, 36, 36, -4], [-4, 28, 46, -6], [-2, 16, 54, -4], [-2, 10, 58, -2]]; + +/// DCT matrix (8-319, 8-321): TRANS_MATRIX[n][m] = transMatrix[m][n] (row n = basis function). +pub static TRANS_MATRIX: [[i8; 32]; 32] = [ + [64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64], + [90, 90, 88, 85, 82, 78, 73, 67, 61, 54, 46, 38, 31, 22, 13, 4, -4, -13, -22, -31, -38, -46, -54, -61, -67, -73, -78, -82, -85, -88, -90, -90], + [90, 87, 80, 70, 57, 43, 25, 9, -9, -25, -43, -57, -70, -80, -87, -90, -90, -87, -80, -70, -57, -43, -25, -9, 9, 25, 43, 57, 70, 80, 87, 90], + [90, 82, 67, 46, 22, -4, -31, -54, -73, -85, -90, -88, -78, -61, -38, -13, 13, 38, 61, 78, 88, 90, 85, 73, 54, 31, 4, -22, -46, -67, -82, -90], + [89, 75, 50, 18, -18, -50, -75, -89, -89, -75, -50, -18, 18, 50, 75, 89, 89, 75, 50, 18, -18, -50, -75, -89, -89, -75, -50, -18, 18, 50, 75, 89], + [88, 67, 31, -13, -54, -82, -90, -78, -46, -4, 38, 73, 90, 85, 61, 22, -22, -61, -85, -90, -73, -38, 4, 46, 78, 90, 82, 54, 13, -31, -67, -88], + [87, 57, 9, -43, -80, -90, -70, -25, 25, 70, 90, 80, 43, -9, -57, -87, -87, -57, -9, 43, 80, 90, 70, 25, -25, -70, -90, -80, -43, 9, 57, 87], + [85, 46, -13, -67, -90, -73, -22, 38, 82, 88, 54, -4, -61, -90, -78, -31, 31, 78, 90, 61, 4, -54, -88, -82, -38, 22, 73, 90, 67, 13, -46, -85], + [83, 36, -36, -83, -83, -36, 36, 83, 83, 36, -36, -83, -83, -36, 36, 83, 83, 36, -36, -83, -83, -36, 36, 83, 83, 36, -36, -83, -83, -36, 36, 83], + [82, 22, -54, -90, -61, 13, 78, 85, 31, -46, -90, -67, 4, 73, 88, 38, -38, -88, -73, -4, 67, 90, 46, -31, -85, -78, -13, 61, 90, 54, -22, -82], + [80, 9, -70, -87, -25, 57, 90, 43, -43, -90, -57, 25, 87, 70, -9, -80, -80, -9, 70, 87, 25, -57, -90, -43, 43, 90, 57, -25, -87, -70, 9, 80], + [78, -4, -82, -73, 13, 85, 67, -22, -88, -61, 31, 90, 54, -38, -90, -46, 46, 90, 38, -54, -90, -31, 61, 88, 22, -67, -85, -13, 73, 82, 4, -78], + [75, -18, -89, -50, 50, 89, 18, -75, -75, 18, 89, 50, -50, -89, -18, 75, 75, -18, -89, -50, 50, 89, 18, -75, -75, 18, 89, 50, -50, -89, -18, 75], + [73, -31, -90, -22, 78, 67, -38, -90, -13, 82, 61, -46, -88, -4, 85, 54, -54, -85, 4, 88, 46, -61, -82, 13, 90, 38, -67, -78, 22, 90, 31, -73], + [70, -43, -87, 9, 90, 25, -80, -57, 57, 80, -25, -90, -9, 87, 43, -70, -70, 43, 87, -9, -90, -25, 80, 57, -57, -80, 25, 90, 9, -87, -43, 70], + [67, -54, -78, 38, 85, -22, -90, 4, 90, 13, -88, -31, 82, 46, -73, -61, 61, 73, -46, -82, 31, 88, -13, -90, -4, 90, 22, -85, -38, 78, 54, -67], + [64, -64, -64, 64, 64, -64, -64, 64, 64, -64, -64, 64, 64, -64, -64, 64, 64, -64, -64, 64, 64, -64, -64, 64, 64, -64, -64, 64, 64, -64, -64, 64], + [61, -73, -46, 82, 31, -88, -13, 90, -4, -90, 22, 85, -38, -78, 54, 67, -67, -54, 78, 38, -85, -22, 90, 4, -90, 13, 88, -31, -82, 46, 73, -61], + [57, -80, -25, 90, -9, -87, 43, 70, -70, -43, 87, 9, -90, 25, 80, -57, -57, 80, 25, -90, 9, 87, -43, -70, 70, 43, -87, -9, 90, -25, -80, 57], + [54, -85, -4, 88, -46, -61, 82, 13, -90, 38, 67, -78, -22, 90, -31, -73, 73, 31, -90, 22, 78, -67, -38, 90, -13, -82, 61, 46, -88, 4, 85, -54], + [50, -89, 18, 75, -75, -18, 89, -50, -50, 89, -18, -75, 75, 18, -89, 50, 50, -89, 18, 75, -75, -18, 89, -50, -50, 89, -18, -75, 75, 18, -89, 50], + [46, -90, 38, 54, -90, 31, 61, -88, 22, 67, -85, 13, 73, -82, 4, 78, -78, -4, 82, -73, -13, 85, -67, -22, 88, -61, -31, 90, -54, -38, 90, -46], + [43, -90, 57, 25, -87, 70, 9, -80, 80, -9, -70, 87, -25, -57, 90, -43, -43, 90, -57, -25, 87, -70, -9, 80, -80, 9, 70, -87, 25, 57, -90, 43], + [38, -88, 73, -4, -67, 90, -46, -31, 85, -78, 13, 61, -90, 54, 22, -82, 82, -22, -54, 90, -61, -13, 78, -85, 31, 46, -90, 67, 4, -73, 88, -38], + [36, -83, 83, -36, -36, 83, -83, 36, 36, -83, 83, -36, -36, 83, -83, 36, 36, -83, 83, -36, -36, 83, -83, 36, 36, -83, 83, -36, -36, 83, -83, 36], + [31, -78, 90, -61, 4, 54, -88, 82, -38, -22, 73, -90, 67, -13, -46, 85, -85, 46, 13, -67, 90, -73, 22, 38, -82, 88, -54, -4, 61, -90, 78, -31], + [25, -70, 90, -80, 43, 9, -57, 87, -87, 57, -9, -43, 80, -90, 70, -25, -25, 70, -90, 80, -43, -9, 57, -87, 87, -57, 9, 43, -80, 90, -70, 25], + [22, -61, 85, -90, 73, -38, -4, 46, -78, 90, -82, 54, -13, -31, 67, -88, 88, -67, 31, 13, -54, 82, -90, 78, -46, 4, 38, -73, 90, -85, 61, -22], + [18, -50, 75, -89, 89, -75, 50, -18, -18, 50, -75, 89, -89, 75, -50, 18, 18, -50, 75, -89, 89, -75, 50, -18, -18, 50, -75, 89, -89, 75, -50, 18], + [13, -38, 61, -78, 88, -90, 85, -73, 54, -31, 4, 22, -46, 67, -82, 90, -90, 82, -67, 46, -22, -4, 31, -54, 73, -85, 90, -88, 78, -61, 38, -13], + [9, -25, 43, -57, 70, -80, 87, -90, 90, -87, 80, -70, 57, -43, 25, -9, -9, 25, -43, 57, -70, 80, -87, 90, -90, 87, -80, 70, -57, 43, -25, 9], + [4, -13, 22, -31, 38, -46, 54, -61, 67, -73, 78, -82, 85, -88, 90, -90, 90, -90, 88, -85, 82, -78, 73, -67, 61, -54, 46, -38, 31, -22, 13, -4], +]; +/// Table 8-12: beta' for Q = 0..51. +pub static BETA_TABLE: [u8; 52] = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 20, 22, 24, 26, 28, 30, 32, 34, 36, 38, 40, 42, 44, 46, 48, 50, 52, 54, 56, 58, 60, 62, 64]; +/// Table 8-12: tC' for Q = 0..53. +pub static TC_TABLE: [u8; 54] = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 5, 5, 6, 6, 7, 8, 9, 10, 11, 13, 14, 16, 18, 20, 22, 24]; diff --git a/crates/hevcenc/src/transform.rs b/crates/hevcenc/src/transform.rs new file mode 100644 index 00000000..cffea75c --- /dev/null +++ b/crates/hevcenc/src/transform.rs @@ -0,0 +1,328 @@ +//! Forward transforms (encoder side, any integer approximation is allowed), quantisation, and the +//! normative scaling (8.6.3) and inverse transforms (8.6.4.2) the decoder applies. The inverse path +//! is exact integer arithmetic: the even/odd decomposition computes the same sums as the matrix +//! products of the specification. + +use crate::tables::{DST_MATRIX, LEVEL_SCALE, TRANS_MATRIX}; + +#[inline] +fn m(n: usize, k: usize, i: usize) -> i32 { + // k-th basis function of the n-point DCT, sample i + TRANS_MATRIX[k * (32 / n)][i] as i32 +} + +/// 1-D forward DCT of `x` (length n) into `out`. +fn fdct1d(x: &[i32], out: &mut [i32], n: usize) { + if n <= 4 { + for k in 0..n { + let mut s = 0; + for i in 0..n { + s += m(n, k, i) * x[i]; + } + out[k] = s; + } + return; + } + let h = n / 2; + let mut e = [0i32; 16]; + let mut o = [0i32; 16]; + for i in 0..h { + e[i] = x[i] + x[n - 1 - i]; + o[i] = x[i] - x[n - 1 - i]; + } + let mut ev = [0i32; 16]; + fdct1d(&e[..h], &mut ev[..h], h); + for k in 0..h { + out[2 * k] = ev[k]; + let kk = 2 * k + 1; + let mut s = 0; + for i in 0..h { + s += m(n, kk, i) * o[i]; + } + out[kk] = s; + } +} + +/// 1-D inverse DCT: x[i] = sum_k M[k][i] * c[k]. +fn idct1d(c: &[i32], out: &mut [i32], n: usize) { + if n <= 4 { + for i in 0..n { + let mut s = 0; + for k in 0..n { + s += m(n, k, i) * c[k]; + } + out[i] = s; + } + return; + } + let h = n / 2; + let mut ce = [0i32; 16]; + for k in 0..h { + ce[k] = c[2 * k]; + } + let mut e = [0i32; 16]; + idct1d(&ce[..h], &mut e[..h], h); + for i in 0..h { + let mut o = 0; + let mut k = 1; + while k < n { + let v = c[k]; + if v != 0 { + o += m(n, k, i) * v; + } + k += 2; + } + out[i] = e[i] + o; + out[n - 1 - i] = e[i] - o; + } +} + +fn fdst1d(x: &[i32], out: &mut [i32]) { + for k in 0..4 { + out[k] = (0..4).map(|i| DST_MATRIX[k][i] as i32 * x[i]).sum(); + } +} + +fn idst1d(c: &[i32], out: &mut [i32]) { + for i in 0..4 { + out[i] = (0..4).map(|k| DST_MATRIX[k][i] as i32 * c[k]).sum(); + } +} + +/// Forward 2-D transform of the n x n residual `r` (row-major) into `c`. +pub fn forward(r: &[i32], c: &mut [i32], n: usize, dst: bool, bit_depth: u32) { + let log2 = n.trailing_zeros(); + let shift1 = log2 + bit_depth - 9; + let shift2 = log2 + 6; + let (r1, r2) = (1i32 << (shift1 - 1), 1i32 << (shift2 - 1)); + let mut tmp = vec![0i32; n * n]; + let mut out = [0i32; 32]; + // rows (horizontal frequencies) + for y in 0..n { + let row = &r[y * n..y * n + n]; + if dst { + fdst1d(row, &mut out); + } else { + fdct1d(row, &mut out, n); + } + for k in 0..n { + tmp[k * n + y] = (out[k] + r1) >> shift1; // transposed: tmp[kx][y] + } + } + // columns + let mut col = [0i32; 32]; + for kx in 0..n { + col[..n].copy_from_slice(&tmp[kx * n..kx * n + n]); + if dst { + fdst1d(&col, &mut out); + } else { + fdct1d(&col, &mut out, n); + } + for ky in 0..n { + c[ky * n + kx] = (out[ky] + r2) >> shift2; + } + } +} + +/// Inverse 2-D transform (8.6.4.2) of scaled coefficients `d` in place, producing residuals. +pub fn inverse(d: &mut [i32], n: usize, dst: bool, bit_depth: u32) { + let bd_shift = 20 - bit_depth; + let mut tmp = vec![0i32; n * n]; + let mut col = [0i32; 32]; + let mut out = [0i32; 32]; + // max non-zero row, to skip empty work + let mut nz_cols = [false; 32]; + for y in 0..n { + for x in 0..n { + if d[y * n + x] != 0 { + nz_cols[x] = true; + } + } + } + // 1. columns + for x in 0..n { + if !nz_cols[x] { + continue; // tmp column stays zero + } + for j in 0..n { + col[j] = d[j * n + x]; + } + if dst { + idst1d(&col, &mut out); + } else { + idct1d(&col, &mut out, n); + } + for y in 0..n { + tmp[y * n + x] = ((out[y] + 64) >> 7).clamp(-32768, 32767); + } + } + // 2. rows + let round = 1 << (bd_shift - 1); + for y in 0..n { + let row = &tmp[y * n..y * n + n]; + if dst { + idst1d(row, &mut out); + } else { + idct1d(row, &mut out, n); + } + for x in 0..n { + d[y * n + x] = (out[x] + round) >> bd_shift; + } + } +} + +const QUANT_SCALE: [i64; 6] = [26214, 23302, 20560, 18396, 16384, 14564]; + +/// Quantise transform coefficients `c` into levels `l` with a dead-zone rounding offset +/// (`intra`: 1/3, otherwise 1/6). `qp` includes QpBdOffset. Returns the number of non-zero levels. +pub fn quantize(c: &[i32], l: &mut [i32], n: usize, qp: i32, intra: bool, bit_depth: u32) -> usize { + let log2 = n.trailing_zeros() as i32; + let tshift = 15 - bit_depth as i32 - log2; + let qbits = 14 + qp / 6 + tshift; + let scale = QUANT_SCALE[(qp % 6) as usize]; + let offset = (if intra { 171i64 } else { 85 }) << (qbits - 9); + let mut nz = 0; + for i in 0..n * n { + let v = c[i] as i64; + let a = ((v.abs() * scale + offset) >> qbits).min(32767); + if a != 0 { + nz += 1; + } + l[i] = if v < 0 { -(a as i32) } else { a as i32 }; + } + nz +} + +/// Sign data hiding (7.3.8.11 signHidden): in every 4x4 sub-block whose first and last non-zero +/// coefficients (in scan order) are more than 3 positions apart, the sign of the first one is not +/// coded but inferred from the parity of the sum of absolute levels. Fix the parity where needed by +/// the +-1 level change that adds the least quantisation error, without moving the first / last +/// non-zero positions. +pub fn sign_hiding(c: &[i32], l: &mut [i32], log2: u32, scan_idx: usize, qp: i32, bit_depth: u32) { + use crate::tables::{SCAN_4, scan_order}; + let n = 1usize << log2; + let tshift = 15 - bit_depth as i32 - log2 as i32; + let qbits = 14 + qp / 6 + tshift; + let scale = QUANT_SCALE[(qp % 6) as usize]; + let sb_scan = scan_order(log2 - 2, scan_idx); + let pos = &SCAN_4[scan_idx]; + for &(xs, ys) in sb_scan { + let idx = |p: usize| (ys as usize * 4 + pos[p].1 as usize) * n + xs as usize * 4 + pos[p].0 as usize; + let (Some(first), Some(last)) = ((0..16).find(|&p| l[idx(p)] != 0), (0..16).rev().find(|&p| l[idx(p)] != 0)) else { continue }; + if last - first <= 3 { + continue; + } + let sum: i32 = (0..16).map(|p| l[idx(p)].abs()).sum(); + let negative = l[idx(first)] < 0; + if negative == (sum & 1 == 1) { + continue; + } + // cost of +-1 changes in 1/256 units of quantisation error + let mut best: Option<(i64, usize, i32)> = None; + for p in first..=last { + let i = idx(p); + let q = ((c[i] as i64).abs() * scale) >> (qbits - 8); + let a = l[i].abs() as i64; + let err = |lv: i64| (q - (lv << 8)).abs(); + let mut consider = |cost: i64, d: i32| { + if best.is_none_or(|b| cost < b.0) { + best = Some((cost, i, d)); + } + }; + if a < 32767 && (a > 0 || (p > first && p < last)) { + consider(err(a + 1) - err(a), 1); + } + if a > 1 || (a == 1 && p > first && p < last) { + consider(err(a - 1) - err(a), -1); + } + } + if let Some((_, i, d)) = best { + let a = l[i].abs() + d; + l[i] = if c[i] < 0 { -a } else { a }; + } + } +} + +/// Scaling process (8.6.2 / 8.6.3, flat scaling matrix): levels to scaled coefficients. +pub fn dequantize(l: &[i32], d: &mut [i32], n: usize, qp: i32, bit_depth: u32) { + let log2 = n.trailing_zeros(); + let bd_shift = bit_depth + log2 - 5; + let round = 1i64 << (bd_shift - 1); + let scale = (LEVEL_SCALE[(qp % 6) as usize] as i64) << (qp / 6); + for i in 0..n * n { + let v = l[i]; + d[i] = if v == 0 { 0 } else { ((v as i64 * 16 * scale + round) >> bd_shift).clamp(-32768, 32767) as i32 }; + } +} + +#[cfg(test)] +mod tests { + use super::*; + + /// Direct matrix form of the inverse transform, as written in the specification. + fn inverse_ref(c: &mut [i32], n: usize, dst: bool, bd: u32) { + let mut tmp = vec![0i32; n * n]; + let step = 32 / n; + for x in 0..n { + for y in 0..n { + let mut s = 0i32; + for j in 0..n { + let m = if dst { DST_MATRIX[j][y] as i32 } else { TRANS_MATRIX[j * step][y] as i32 }; + s += m * c[j * n + x]; + } + tmp[y * n + x] = ((s + 64) >> 7).clamp(-32768, 32767); + } + } + let bd_shift = 20 - bd; + for y in 0..n { + for x in 0..n { + let mut s = 0i32; + for j in 0..n { + let m = if dst { DST_MATRIX[j][x] as i32 } else { TRANS_MATRIX[j * step][x] as i32 }; + s += m * tmp[y * n + j]; + } + c[y * n + x] = (s + (1 << (bd_shift - 1))) >> bd_shift; + } + } + } + + #[test] + fn inverse_matches_matrix_form() { + let mut seed = 7u32; + for &(n, dst) in &[(4usize, true), (4, false), (8, false), (16, false), (32, false)] { + for _ in 0..20 { + let mut c = vec![0i32; n * n]; + for v in c.iter_mut() { + seed = seed.wrapping_mul(1664525).wrapping_add(1013904223); + if (seed >> 24).is_multiple_of(3) { + *v = ((seed >> 8) % 2001) as i32 - 1000; + } + } + let mut a = c.clone(); + let mut b = c.clone(); + inverse(&mut a, n, dst, 8); + inverse_ref(&mut b, n, dst, 8); + assert_eq!(a, b, "n {n} dst {dst}"); + } + } + } + + #[test] + fn round_trip_is_close() { + for &(n, dst) in &[(4usize, true), (8, false), (32, false)] { + for bd in [8u32, 10] { + let r: Vec = (0..n * n).map(|i| ((i * 37 + i / n * 11) % 61) as i32 - 30).collect(); + let mut c = vec![0; n * n]; + forward(&r, &mut c, n, dst, bd); + let mut l = vec![0; n * n]; + let qp = 4; + quantize(&c, &mut l, n, qp, true, bd); + let mut d = vec![0; n * n]; + dequantize(&l, &mut d, n, qp, bd); + inverse(&mut d, n, dst, bd); + let err: i32 = r.iter().zip(&d).map(|(a, b)| (a - b).abs()).max().unwrap(); + assert!(err <= 2, "n {n} bd {bd} err {err}"); + } + } + } +} diff --git a/crates/hevcenc/target/agent-codec-hevc/.rustc_info.json b/crates/hevcenc/target/agent-codec-hevc/.rustc_info.json new file mode 100644 index 00000000..5d9c147e --- /dev/null +++ b/crates/hevcenc/target/agent-codec-hevc/.rustc_info.json @@ -0,0 +1 @@ +{"rustc_fingerprint":9559367331539902066,"outputs":{"4233666815994275565":{"success":true,"status":"","code":0,"stdout":"rustc 1.96.0 (ac68faa20 2026-05-25)\nbinary: rustc\ncommit-hash: ac68faa20c58cbccd01ee7208bf3b6e93a7d7f96\ncommit-date: 2026-05-25\nhost: aarch64-apple-darwin\nrelease: 1.96.0\nLLVM version: 22.1.2\n","stderr":""},"7971740275564407648":{"success":true,"status":"","code":0,"stdout":"___\nlib___.rlib\nlib___.dylib\nlib___.dylib\nlib___.a\nlib___.dylib\n/Users/bt/.rustup/toolchains/stable-aarch64-apple-darwin\noff\npacked\nunpacked\n___\ndebug_assertions\npanic=\"unwind\"\nproc_macro\ntarget_abi=\"\"\ntarget_arch=\"aarch64\"\ntarget_endian=\"little\"\ntarget_env=\"\"\ntarget_family=\"unix\"\ntarget_feature=\"aes\"\ntarget_feature=\"crc\"\ntarget_feature=\"dit\"\ntarget_feature=\"dotprod\"\ntarget_feature=\"dpb\"\ntarget_feature=\"dpb2\"\ntarget_feature=\"fcma\"\ntarget_feature=\"fhm\"\ntarget_feature=\"flagm\"\ntarget_feature=\"fp16\"\ntarget_feature=\"frintts\"\ntarget_feature=\"jsconv\"\ntarget_feature=\"lor\"\ntarget_feature=\"lse\"\ntarget_feature=\"neon\"\ntarget_feature=\"paca\"\ntarget_feature=\"pacg\"\ntarget_feature=\"pan\"\ntarget_feature=\"pmuv3\"\ntarget_feature=\"ras\"\ntarget_feature=\"rcpc\"\ntarget_feature=\"rcpc2\"\ntarget_feature=\"rdm\"\ntarget_feature=\"sb\"\ntarget_feature=\"sha2\"\ntarget_feature=\"sha3\"\ntarget_feature=\"ssbs\"\ntarget_feature=\"vh\"\ntarget_has_atomic=\"128\"\ntarget_has_atomic=\"16\"\ntarget_has_atomic=\"32\"\ntarget_has_atomic=\"64\"\ntarget_has_atomic=\"8\"\ntarget_has_atomic=\"ptr\"\ntarget_os=\"macos\"\ntarget_pointer_width=\"64\"\ntarget_vendor=\"apple\"\nunix\n","stderr":""}},"successes":{}} \ No newline at end of file diff --git a/crates/hevcenc/target/agent-codec-hevc/CACHEDIR.TAG b/crates/hevcenc/target/agent-codec-hevc/CACHEDIR.TAG new file mode 100644 index 00000000..20d7c319 --- /dev/null +++ b/crates/hevcenc/target/agent-codec-hevc/CACHEDIR.TAG @@ -0,0 +1,3 @@ +Signature: 8a477f597d28d172789f06886806bc55 +# This file is a cache directory tag created by cargo. +# For information about cache directory tags see https://bford.info/cachedir/ diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/.cargo-artifact-lock b/crates/hevcenc/target/agent-codec-hevc/debug/.cargo-artifact-lock new file mode 100644 index 00000000..e69de29b diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/.cargo-build-lock b/crates/hevcenc/target/agent-codec-hevc/debug/.cargo-build-lock new file mode 100644 index 00000000..e69de29b diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/.cargo-lock b/crates/hevcenc/target/agent-codec-hevc/debug/.cargo-lock new file mode 100644 index 00000000..e69de29b diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/crossbeam-deque-5b3c4336788f7174/build-script-build-script-build b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/crossbeam-deque-5b3c4336788f7174/build-script-build-script-build new file mode 100644 index 00000000..427290bf --- /dev/null +++ b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/crossbeam-deque-5b3c4336788f7174/build-script-build-script-build @@ -0,0 +1 @@ +096ac002c23a6667 \ No newline at end of file diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/crossbeam-deque-5b3c4336788f7174/build-script-build-script-build.json b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/crossbeam-deque-5b3c4336788f7174/build-script-build-script-build.json new file mode 100644 index 00000000..a72a5bb4 --- /dev/null +++ b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/crossbeam-deque-5b3c4336788f7174/build-script-build-script-build.json @@ -0,0 +1 @@ +{"rustc":17467972547956852243,"features":"[\"default\", \"std\"]","declared_features":"[\"default\", \"std\"]","target":5408242616063297496,"profile":3185167420583542146,"path":6080947815111469725,"deps":[],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/crossbeam-deque-5b3c4336788f7174/dep-build-script-build-script-build","checksum":false}}],"rustflags":[],"config":8247474407144887393,"compile_kind":0} \ No newline at end of file diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/crossbeam-deque-5b3c4336788f7174/dep-build-script-build-script-build b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/crossbeam-deque-5b3c4336788f7174/dep-build-script-build-script-build new file mode 100644 index 00000000..ec3cb8bf Binary files /dev/null and b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/crossbeam-deque-5b3c4336788f7174/dep-build-script-build-script-build differ diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/crossbeam-deque-5b3c4336788f7174/invoked.timestamp b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/crossbeam-deque-5b3c4336788f7174/invoked.timestamp new file mode 100644 index 00000000..e00328da --- /dev/null +++ b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/crossbeam-deque-5b3c4336788f7174/invoked.timestamp @@ -0,0 +1 @@ +This file has an mtime of when this was started. \ No newline at end of file diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/crossbeam-deque-d9775930f3a59590/dep-lib-crossbeam_deque b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/crossbeam-deque-d9775930f3a59590/dep-lib-crossbeam_deque new file mode 100644 index 00000000..ec3cb8bf Binary files /dev/null and b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/crossbeam-deque-d9775930f3a59590/dep-lib-crossbeam_deque differ diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/crossbeam-deque-d9775930f3a59590/invoked.timestamp b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/crossbeam-deque-d9775930f3a59590/invoked.timestamp new file mode 100644 index 00000000..e00328da --- /dev/null +++ b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/crossbeam-deque-d9775930f3a59590/invoked.timestamp @@ -0,0 +1 @@ +This file has an mtime of when this was started. \ No newline at end of file diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/crossbeam-deque-d9775930f3a59590/lib-crossbeam_deque b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/crossbeam-deque-d9775930f3a59590/lib-crossbeam_deque new file mode 100644 index 00000000..9162efb3 --- /dev/null +++ b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/crossbeam-deque-d9775930f3a59590/lib-crossbeam_deque @@ -0,0 +1 @@ +a673fdef9376ddb1 \ No newline at end of file diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/crossbeam-deque-d9775930f3a59590/lib-crossbeam_deque.json b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/crossbeam-deque-d9775930f3a59590/lib-crossbeam_deque.json new file mode 100644 index 00000000..0e08d299 --- /dev/null +++ b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/crossbeam-deque-d9775930f3a59590/lib-crossbeam_deque.json @@ -0,0 +1 @@ +{"rustc":17467972547956852243,"features":"[\"default\", \"std\"]","declared_features":"[\"default\", \"std\"]","target":15353977948366730291,"profile":3185167420583542146,"path":2714696676876033032,"deps":[[2543204310390312751,"crossbeam_epoch",false,15863831904650651516],[11050506297539643678,"crossbeam_utils",false,11092378776446818952],[15481973119957668846,"build_script_build",false,7352081084529830974]],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/crossbeam-deque-d9775930f3a59590/dep-lib-crossbeam_deque","checksum":false}}],"rustflags":[],"config":8247474407144887393,"compile_kind":0} \ No newline at end of file diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/crossbeam-deque-f51f1776f2c514b1/run-build-script-build-script-build b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/crossbeam-deque-f51f1776f2c514b1/run-build-script-build-script-build new file mode 100644 index 00000000..dc01c242 --- /dev/null +++ b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/crossbeam-deque-f51f1776f2c514b1/run-build-script-build-script-build @@ -0,0 +1 @@ +3eec441bcbd60766 \ No newline at end of file diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/crossbeam-deque-f51f1776f2c514b1/run-build-script-build-script-build.json b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/crossbeam-deque-f51f1776f2c514b1/run-build-script-build-script-build.json new file mode 100644 index 00000000..b86b2c74 --- /dev/null +++ b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/crossbeam-deque-f51f1776f2c514b1/run-build-script-build-script-build.json @@ -0,0 +1 @@ +{"rustc":17467972547956852243,"features":"","declared_features":"","target":0,"profile":0,"path":0,"deps":[[15481973119957668846,"build_script_build",false,7450707238475295241]],"local":[{"RerunIfChanged":{"output":"debug/build/crossbeam-deque-f51f1776f2c514b1/output","paths":["build.rs"]}}],"rustflags":[],"config":0,"compile_kind":0} \ No newline at end of file diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/crossbeam-epoch-1347d2207ea061ce/build-script-build-script-build b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/crossbeam-epoch-1347d2207ea061ce/build-script-build-script-build new file mode 100644 index 00000000..89173559 --- /dev/null +++ b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/crossbeam-epoch-1347d2207ea061ce/build-script-build-script-build @@ -0,0 +1 @@ +16d4cd1f76c06772 \ No newline at end of file diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/crossbeam-epoch-1347d2207ea061ce/build-script-build-script-build.json b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/crossbeam-epoch-1347d2207ea061ce/build-script-build-script-build.json new file mode 100644 index 00000000..aed09422 --- /dev/null +++ b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/crossbeam-epoch-1347d2207ea061ce/build-script-build-script-build.json @@ -0,0 +1 @@ +{"rustc":17467972547956852243,"features":"[\"alloc\", \"std\"]","declared_features":"[\"alloc\", \"default\", \"loom\", \"loom-crate\", \"nightly\", \"std\"]","target":5408242616063297496,"profile":3185167420583542146,"path":5028293854929153649,"deps":[],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/crossbeam-epoch-1347d2207ea061ce/dep-build-script-build-script-build","checksum":false}}],"rustflags":[],"config":8247474407144887393,"compile_kind":0} \ No newline at end of file diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/crossbeam-epoch-1347d2207ea061ce/dep-build-script-build-script-build b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/crossbeam-epoch-1347d2207ea061ce/dep-build-script-build-script-build new file mode 100644 index 00000000..ec3cb8bf Binary files /dev/null and b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/crossbeam-epoch-1347d2207ea061ce/dep-build-script-build-script-build differ diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/crossbeam-epoch-1347d2207ea061ce/invoked.timestamp b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/crossbeam-epoch-1347d2207ea061ce/invoked.timestamp new file mode 100644 index 00000000..e00328da --- /dev/null +++ b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/crossbeam-epoch-1347d2207ea061ce/invoked.timestamp @@ -0,0 +1 @@ +This file has an mtime of when this was started. \ No newline at end of file diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/crossbeam-epoch-2092bf031b9d9a01/run-build-script-build-script-build b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/crossbeam-epoch-2092bf031b9d9a01/run-build-script-build-script-build new file mode 100644 index 00000000..3a26abad --- /dev/null +++ b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/crossbeam-epoch-2092bf031b9d9a01/run-build-script-build-script-build @@ -0,0 +1 @@ +c36dd00ce8039b4d \ No newline at end of file diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/crossbeam-epoch-2092bf031b9d9a01/run-build-script-build-script-build.json b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/crossbeam-epoch-2092bf031b9d9a01/run-build-script-build-script-build.json new file mode 100644 index 00000000..714a1d1e --- /dev/null +++ b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/crossbeam-epoch-2092bf031b9d9a01/run-build-script-build-script-build.json @@ -0,0 +1 @@ +{"rustc":17467972547956852243,"features":"","declared_features":"","target":0,"profile":0,"path":0,"deps":[[2543204310390312751,"build_script_build",false,8243769256497239062]],"local":[{"RerunIfChanged":{"output":"debug/build/crossbeam-epoch-2092bf031b9d9a01/output","paths":["build.rs"]}}],"rustflags":[],"config":0,"compile_kind":0} \ No newline at end of file diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/crossbeam-epoch-72c18a120600cf42/dep-lib-crossbeam_epoch b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/crossbeam-epoch-72c18a120600cf42/dep-lib-crossbeam_epoch new file mode 100644 index 00000000..ec3cb8bf Binary files /dev/null and b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/crossbeam-epoch-72c18a120600cf42/dep-lib-crossbeam_epoch differ diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/crossbeam-epoch-72c18a120600cf42/invoked.timestamp b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/crossbeam-epoch-72c18a120600cf42/invoked.timestamp new file mode 100644 index 00000000..e00328da --- /dev/null +++ b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/crossbeam-epoch-72c18a120600cf42/invoked.timestamp @@ -0,0 +1 @@ +This file has an mtime of when this was started. \ No newline at end of file diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/crossbeam-epoch-72c18a120600cf42/lib-crossbeam_epoch b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/crossbeam-epoch-72c18a120600cf42/lib-crossbeam_epoch new file mode 100644 index 00000000..7cf5e27b --- /dev/null +++ b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/crossbeam-epoch-72c18a120600cf42/lib-crossbeam_epoch @@ -0,0 +1 @@ +7ce7352d11a727dc \ No newline at end of file diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/crossbeam-epoch-72c18a120600cf42/lib-crossbeam_epoch.json b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/crossbeam-epoch-72c18a120600cf42/lib-crossbeam_epoch.json new file mode 100644 index 00000000..c133fb7a --- /dev/null +++ b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/crossbeam-epoch-72c18a120600cf42/lib-crossbeam_epoch.json @@ -0,0 +1 @@ +{"rustc":17467972547956852243,"features":"[\"alloc\", \"std\"]","declared_features":"[\"alloc\", \"default\", \"loom\", \"loom-crate\", \"nightly\", \"std\"]","target":16242420667881341737,"profile":3185167420583542146,"path":7266900391393874633,"deps":[[2543204310390312751,"build_script_build",false,5592067657492884931],[11050506297539643678,"crossbeam_utils",false,11092378776446818952]],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/crossbeam-epoch-72c18a120600cf42/dep-lib-crossbeam_epoch","checksum":false}}],"rustflags":[],"config":8247474407144887393,"compile_kind":0} \ No newline at end of file diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/crossbeam-utils-5b2ff6828cdce4c3/build-script-build-script-build b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/crossbeam-utils-5b2ff6828cdce4c3/build-script-build-script-build new file mode 100644 index 00000000..1790b224 --- /dev/null +++ b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/crossbeam-utils-5b2ff6828cdce4c3/build-script-build-script-build @@ -0,0 +1 @@ +bdec41fc81c92fa2 \ No newline at end of file diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/crossbeam-utils-5b2ff6828cdce4c3/build-script-build-script-build.json b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/crossbeam-utils-5b2ff6828cdce4c3/build-script-build-script-build.json new file mode 100644 index 00000000..69147ada --- /dev/null +++ b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/crossbeam-utils-5b2ff6828cdce4c3/build-script-build-script-build.json @@ -0,0 +1 @@ +{"rustc":17467972547956852243,"features":"[\"default\", \"std\"]","declared_features":"[\"default\", \"loom\", \"nightly\", \"std\"]","target":5408242616063297496,"profile":3185167420583542146,"path":14844284517995534797,"deps":[],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/crossbeam-utils-5b2ff6828cdce4c3/dep-build-script-build-script-build","checksum":false}}],"rustflags":[],"config":8247474407144887393,"compile_kind":0} \ No newline at end of file diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/crossbeam-utils-5b2ff6828cdce4c3/dep-build-script-build-script-build b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/crossbeam-utils-5b2ff6828cdce4c3/dep-build-script-build-script-build new file mode 100644 index 00000000..ec3cb8bf Binary files /dev/null and b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/crossbeam-utils-5b2ff6828cdce4c3/dep-build-script-build-script-build differ diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/crossbeam-utils-5b2ff6828cdce4c3/invoked.timestamp b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/crossbeam-utils-5b2ff6828cdce4c3/invoked.timestamp new file mode 100644 index 00000000..e00328da --- /dev/null +++ b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/crossbeam-utils-5b2ff6828cdce4c3/invoked.timestamp @@ -0,0 +1 @@ +This file has an mtime of when this was started. \ No newline at end of file diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/crossbeam-utils-799e464d44773a7d/dep-lib-crossbeam_utils b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/crossbeam-utils-799e464d44773a7d/dep-lib-crossbeam_utils new file mode 100644 index 00000000..ec3cb8bf Binary files /dev/null and b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/crossbeam-utils-799e464d44773a7d/dep-lib-crossbeam_utils differ diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/crossbeam-utils-799e464d44773a7d/invoked.timestamp b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/crossbeam-utils-799e464d44773a7d/invoked.timestamp new file mode 100644 index 00000000..e00328da --- /dev/null +++ b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/crossbeam-utils-799e464d44773a7d/invoked.timestamp @@ -0,0 +1 @@ +This file has an mtime of when this was started. \ No newline at end of file diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/crossbeam-utils-799e464d44773a7d/lib-crossbeam_utils b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/crossbeam-utils-799e464d44773a7d/lib-crossbeam_utils new file mode 100644 index 00000000..e425ee19 --- /dev/null +++ b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/crossbeam-utils-799e464d44773a7d/lib-crossbeam_utils @@ -0,0 +1 @@ +88da312cba0bf099 \ No newline at end of file diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/crossbeam-utils-799e464d44773a7d/lib-crossbeam_utils.json b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/crossbeam-utils-799e464d44773a7d/lib-crossbeam_utils.json new file mode 100644 index 00000000..bc49c405 --- /dev/null +++ b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/crossbeam-utils-799e464d44773a7d/lib-crossbeam_utils.json @@ -0,0 +1 @@ +{"rustc":17467972547956852243,"features":"[\"default\", \"std\"]","declared_features":"[\"default\", \"loom\", \"nightly\", \"std\"]","target":9626079250877207070,"profile":3185167420583542146,"path":16937405451120399764,"deps":[[11050506297539643678,"build_script_build",false,8108134710804475658]],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/crossbeam-utils-799e464d44773a7d/dep-lib-crossbeam_utils","checksum":false}}],"rustflags":[],"config":8247474407144887393,"compile_kind":0} \ No newline at end of file diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/crossbeam-utils-99f1d686cf22430f/run-build-script-build-script-build b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/crossbeam-utils-99f1d686cf22430f/run-build-script-build-script-build new file mode 100644 index 00000000..c31310a1 --- /dev/null +++ b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/crossbeam-utils-99f1d686cf22430f/run-build-script-build-script-build @@ -0,0 +1 @@ +0aef838c8fe18570 \ No newline at end of file diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/crossbeam-utils-99f1d686cf22430f/run-build-script-build-script-build.json b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/crossbeam-utils-99f1d686cf22430f/run-build-script-build-script-build.json new file mode 100644 index 00000000..2b02b78b --- /dev/null +++ b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/crossbeam-utils-99f1d686cf22430f/run-build-script-build-script-build.json @@ -0,0 +1 @@ +{"rustc":17467972547956852243,"features":"","declared_features":"","target":0,"profile":0,"path":0,"deps":[[11050506297539643678,"build_script_build",false,11686781118169869501]],"local":[{"RerunIfChanged":{"output":"debug/build/crossbeam-utils-99f1d686cf22430f/output","paths":["no_atomic.rs"]}}],"rustflags":[],"config":0,"compile_kind":0} \ No newline at end of file diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/effectcraft-hevcenc-335f1f822197d831/dep-test-integration-test-roundtrip b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/effectcraft-hevcenc-335f1f822197d831/dep-test-integration-test-roundtrip new file mode 100644 index 00000000..73f42b52 Binary files /dev/null and b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/effectcraft-hevcenc-335f1f822197d831/dep-test-integration-test-roundtrip differ diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/effectcraft-hevcenc-335f1f822197d831/invoked.timestamp b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/effectcraft-hevcenc-335f1f822197d831/invoked.timestamp new file mode 100644 index 00000000..e00328da --- /dev/null +++ b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/effectcraft-hevcenc-335f1f822197d831/invoked.timestamp @@ -0,0 +1 @@ +This file has an mtime of when this was started. \ No newline at end of file diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/effectcraft-hevcenc-335f1f822197d831/test-integration-test-roundtrip b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/effectcraft-hevcenc-335f1f822197d831/test-integration-test-roundtrip new file mode 100644 index 00000000..5f22e76e --- /dev/null +++ b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/effectcraft-hevcenc-335f1f822197d831/test-integration-test-roundtrip @@ -0,0 +1 @@ +e64536e1e71b0acd \ No newline at end of file diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/effectcraft-hevcenc-335f1f822197d831/test-integration-test-roundtrip.json b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/effectcraft-hevcenc-335f1f822197d831/test-integration-test-roundtrip.json new file mode 100644 index 00000000..e2674a32 --- /dev/null +++ b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/effectcraft-hevcenc-335f1f822197d831/test-integration-test-roundtrip.json @@ -0,0 +1 @@ +{"rustc":17467972547956852243,"features":"[]","declared_features":"[]","target":13741281541045977916,"profile":14861435456621666076,"path":1524816848624354599,"deps":[[5706705931987278984,"filmcraft_hevc",false,1065779467293311985],[10103226027885832861,"effectcraft_hevcenc",false,3919018516886026431]],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/effectcraft-hevcenc-335f1f822197d831/dep-test-integration-test-roundtrip","checksum":false}}],"rustflags":[],"config":8247474407144887393,"compile_kind":0} \ No newline at end of file diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/effectcraft-hevcenc-955aee250b2c2a4e/dep-lib-effectcraft_hevcenc b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/effectcraft-hevcenc-955aee250b2c2a4e/dep-lib-effectcraft_hevcenc new file mode 100644 index 00000000..13bf633d Binary files /dev/null and b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/effectcraft-hevcenc-955aee250b2c2a4e/dep-lib-effectcraft_hevcenc differ diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/effectcraft-hevcenc-955aee250b2c2a4e/invoked.timestamp b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/effectcraft-hevcenc-955aee250b2c2a4e/invoked.timestamp new file mode 100644 index 00000000..e00328da --- /dev/null +++ b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/effectcraft-hevcenc-955aee250b2c2a4e/invoked.timestamp @@ -0,0 +1 @@ +This file has an mtime of when this was started. \ No newline at end of file diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/effectcraft-hevcenc-955aee250b2c2a4e/lib-effectcraft_hevcenc b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/effectcraft-hevcenc-955aee250b2c2a4e/lib-effectcraft_hevcenc new file mode 100644 index 00000000..14901e19 --- /dev/null +++ b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/effectcraft-hevcenc-955aee250b2c2a4e/lib-effectcraft_hevcenc @@ -0,0 +1 @@ +bf782bc793266336 \ No newline at end of file diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/effectcraft-hevcenc-955aee250b2c2a4e/lib-effectcraft_hevcenc.json b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/effectcraft-hevcenc-955aee250b2c2a4e/lib-effectcraft_hevcenc.json new file mode 100644 index 00000000..461b185a --- /dev/null +++ b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/effectcraft-hevcenc-955aee250b2c2a4e/lib-effectcraft_hevcenc.json @@ -0,0 +1 @@ +{"rustc":17467972547956852243,"features":"[]","declared_features":"[]","target":11125424694902762418,"profile":15209061013077869454,"path":6858098780819335099,"deps":[],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/effectcraft-hevcenc-955aee250b2c2a4e/dep-lib-effectcraft_hevcenc","checksum":false}}],"rustflags":[],"config":8247474407144887393,"compile_kind":0} \ No newline at end of file diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/either-72952a75ef45a16d/dep-lib-either b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/either-72952a75ef45a16d/dep-lib-either new file mode 100644 index 00000000..ec3cb8bf Binary files /dev/null and b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/either-72952a75ef45a16d/dep-lib-either differ diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/either-72952a75ef45a16d/invoked.timestamp b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/either-72952a75ef45a16d/invoked.timestamp new file mode 100644 index 00000000..e00328da --- /dev/null +++ b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/either-72952a75ef45a16d/invoked.timestamp @@ -0,0 +1 @@ +This file has an mtime of when this was started. \ No newline at end of file diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/either-72952a75ef45a16d/lib-either b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/either-72952a75ef45a16d/lib-either new file mode 100644 index 00000000..464bb799 --- /dev/null +++ b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/either-72952a75ef45a16d/lib-either @@ -0,0 +1 @@ +a449c4ea4d7d8d17 \ No newline at end of file diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/either-72952a75ef45a16d/lib-either.json b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/either-72952a75ef45a16d/lib-either.json new file mode 100644 index 00000000..51e6f5f4 --- /dev/null +++ b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/either-72952a75ef45a16d/lib-either.json @@ -0,0 +1 @@ +{"rustc":17467972547956852243,"features":"[]","declared_features":"[\"default\", \"serde\", \"std\", \"use_std\"]","target":17124342308084364240,"profile":2785734998891497023,"path":5546998507963859579,"deps":[],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/either-72952a75ef45a16d/dep-lib-either","checksum":false}}],"rustflags":[],"config":8247474407144887393,"compile_kind":0} \ No newline at end of file diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/filmcraft-bitstream-2ad770771b023a77/dep-lib-filmcraft_bitstream b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/filmcraft-bitstream-2ad770771b023a77/dep-lib-filmcraft_bitstream new file mode 100644 index 00000000..ec3cb8bf Binary files /dev/null and b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/filmcraft-bitstream-2ad770771b023a77/dep-lib-filmcraft_bitstream differ diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/filmcraft-bitstream-2ad770771b023a77/invoked.timestamp b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/filmcraft-bitstream-2ad770771b023a77/invoked.timestamp new file mode 100644 index 00000000..e00328da --- /dev/null +++ b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/filmcraft-bitstream-2ad770771b023a77/invoked.timestamp @@ -0,0 +1 @@ +This file has an mtime of when this was started. \ No newline at end of file diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/filmcraft-bitstream-2ad770771b023a77/lib-filmcraft_bitstream b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/filmcraft-bitstream-2ad770771b023a77/lib-filmcraft_bitstream new file mode 100644 index 00000000..51b08d80 --- /dev/null +++ b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/filmcraft-bitstream-2ad770771b023a77/lib-filmcraft_bitstream @@ -0,0 +1 @@ +059b6b4655b5b2c5 \ No newline at end of file diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/filmcraft-bitstream-2ad770771b023a77/lib-filmcraft_bitstream.json b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/filmcraft-bitstream-2ad770771b023a77/lib-filmcraft_bitstream.json new file mode 100644 index 00000000..8f431364 --- /dev/null +++ b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/filmcraft-bitstream-2ad770771b023a77/lib-filmcraft_bitstream.json @@ -0,0 +1 @@ +{"rustc":17467972547956852243,"features":"[]","declared_features":"[]","target":18127681992696119206,"profile":12938959104297781942,"path":12854803809928081657,"deps":[],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/filmcraft-bitstream-2ad770771b023a77/dep-lib-filmcraft_bitstream","checksum":false}}],"rustflags":[],"config":8247474407144887393,"compile_kind":0} \ No newline at end of file diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/filmcraft-hevc-71cf1ac5fd5debba/dep-lib-filmcraft_hevc b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/filmcraft-hevc-71cf1ac5fd5debba/dep-lib-filmcraft_hevc new file mode 100644 index 00000000..ec3cb8bf Binary files /dev/null and b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/filmcraft-hevc-71cf1ac5fd5debba/dep-lib-filmcraft_hevc differ diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/filmcraft-hevc-71cf1ac5fd5debba/invoked.timestamp b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/filmcraft-hevc-71cf1ac5fd5debba/invoked.timestamp new file mode 100644 index 00000000..e00328da --- /dev/null +++ b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/filmcraft-hevc-71cf1ac5fd5debba/invoked.timestamp @@ -0,0 +1 @@ +This file has an mtime of when this was started. \ No newline at end of file diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/filmcraft-hevc-71cf1ac5fd5debba/lib-filmcraft_hevc b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/filmcraft-hevc-71cf1ac5fd5debba/lib-filmcraft_hevc new file mode 100644 index 00000000..05871b3f --- /dev/null +++ b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/filmcraft-hevc-71cf1ac5fd5debba/lib-filmcraft_hevc @@ -0,0 +1 @@ +f143ec5cc768ca0e \ No newline at end of file diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/filmcraft-hevc-71cf1ac5fd5debba/lib-filmcraft_hevc.json b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/filmcraft-hevc-71cf1ac5fd5debba/lib-filmcraft_hevc.json new file mode 100644 index 00000000..75da585b --- /dev/null +++ b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/filmcraft-hevc-71cf1ac5fd5debba/lib-filmcraft_hevc.json @@ -0,0 +1 @@ +{"rustc":17467972547956852243,"features":"[\"default\", \"threads\"]","declared_features":"[\"default\", \"threads\"]","target":9847609124140232008,"profile":15209061013077869454,"path":2590569095805419776,"deps":[[1957009224993739128,"thiserror",false,17538703420373504327],[4943083844709074312,"filmcraft_bitstream",false,14245647949184604933],[11910974697091955563,"rayon",false,13044872296819726823]],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/filmcraft-hevc-71cf1ac5fd5debba/dep-lib-filmcraft_hevc","checksum":false}}],"rustflags":[],"config":8247474407144887393,"compile_kind":0} \ No newline at end of file diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/proc-macro2-67c058b976f66fb4/dep-lib-proc_macro2 b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/proc-macro2-67c058b976f66fb4/dep-lib-proc_macro2 new file mode 100644 index 00000000..ec3cb8bf Binary files /dev/null and b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/proc-macro2-67c058b976f66fb4/dep-lib-proc_macro2 differ diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/proc-macro2-67c058b976f66fb4/invoked.timestamp b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/proc-macro2-67c058b976f66fb4/invoked.timestamp new file mode 100644 index 00000000..e00328da --- /dev/null +++ b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/proc-macro2-67c058b976f66fb4/invoked.timestamp @@ -0,0 +1 @@ +This file has an mtime of when this was started. \ No newline at end of file diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/proc-macro2-67c058b976f66fb4/lib-proc_macro2 b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/proc-macro2-67c058b976f66fb4/lib-proc_macro2 new file mode 100644 index 00000000..7f0dfbf3 --- /dev/null +++ b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/proc-macro2-67c058b976f66fb4/lib-proc_macro2 @@ -0,0 +1 @@ +3d6cecd1d5ecd76f \ No newline at end of file diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/proc-macro2-67c058b976f66fb4/lib-proc_macro2.json b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/proc-macro2-67c058b976f66fb4/lib-proc_macro2.json new file mode 100644 index 00000000..269e2805 --- /dev/null +++ b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/proc-macro2-67c058b976f66fb4/lib-proc_macro2.json @@ -0,0 +1 @@ +{"rustc":17467972547956852243,"features":"[\"default\", \"proc-macro\"]","declared_features":"[\"default\", \"nightly\", \"proc-macro\", \"span-locations\"]","target":369203346396300798,"profile":2785734998891497023,"path":2809009335364014821,"deps":[[12333832803962989937,"unicode_ident",false,16793811267078206154],[16346726298725429545,"build_script_build",false,12224304350208236698]],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/proc-macro2-67c058b976f66fb4/dep-lib-proc_macro2","checksum":false}}],"rustflags":[],"config":8247474407144887393,"compile_kind":0} \ No newline at end of file diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/proc-macro2-73f2303f90bf5dd8/run-build-script-build-script-build b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/proc-macro2-73f2303f90bf5dd8/run-build-script-build-script-build new file mode 100644 index 00000000..6a76c040 --- /dev/null +++ b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/proc-macro2-73f2303f90bf5dd8/run-build-script-build-script-build @@ -0,0 +1 @@ +9a9c6c140a74a5a9 \ No newline at end of file diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/proc-macro2-73f2303f90bf5dd8/run-build-script-build-script-build.json b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/proc-macro2-73f2303f90bf5dd8/run-build-script-build-script-build.json new file mode 100644 index 00000000..01850cf9 --- /dev/null +++ b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/proc-macro2-73f2303f90bf5dd8/run-build-script-build-script-build.json @@ -0,0 +1 @@ +{"rustc":17467972547956852243,"features":"","declared_features":"","target":0,"profile":0,"path":0,"deps":[[16346726298725429545,"build_script_build",false,11266185680237871916]],"local":[{"RerunIfChanged":{"output":"debug/build/proc-macro2-73f2303f90bf5dd8/output","paths":["src/probe/proc_macro_span.rs","src/probe/proc_macro_span_location.rs","src/probe/proc_macro_span_file.rs"]}},{"RerunIfEnvChanged":{"var":"RUSTC_BOOTSTRAP","val":null}}],"rustflags":[],"config":0,"compile_kind":0} \ No newline at end of file diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/proc-macro2-8b5400504772b527/build-script-build-script-build b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/proc-macro2-8b5400504772b527/build-script-build-script-build new file mode 100644 index 00000000..20c7065d --- /dev/null +++ b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/proc-macro2-8b5400504772b527/build-script-build-script-build @@ -0,0 +1 @@ +2cc32b742f88599c \ No newline at end of file diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/proc-macro2-8b5400504772b527/build-script-build-script-build.json b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/proc-macro2-8b5400504772b527/build-script-build-script-build.json new file mode 100644 index 00000000..0a8e349c --- /dev/null +++ b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/proc-macro2-8b5400504772b527/build-script-build-script-build.json @@ -0,0 +1 @@ +{"rustc":17467972547956852243,"features":"[\"default\", \"proc-macro\"]","declared_features":"[\"default\", \"nightly\", \"proc-macro\", \"span-locations\"]","target":5408242616063297496,"profile":2785734998891497023,"path":15930204402656880288,"deps":[],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/proc-macro2-8b5400504772b527/dep-build-script-build-script-build","checksum":false}}],"rustflags":[],"config":8247474407144887393,"compile_kind":0} \ No newline at end of file diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/proc-macro2-8b5400504772b527/dep-build-script-build-script-build b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/proc-macro2-8b5400504772b527/dep-build-script-build-script-build new file mode 100644 index 00000000..ec3cb8bf Binary files /dev/null and b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/proc-macro2-8b5400504772b527/dep-build-script-build-script-build differ diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/proc-macro2-8b5400504772b527/invoked.timestamp b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/proc-macro2-8b5400504772b527/invoked.timestamp new file mode 100644 index 00000000..e00328da --- /dev/null +++ b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/proc-macro2-8b5400504772b527/invoked.timestamp @@ -0,0 +1 @@ +This file has an mtime of when this was started. \ No newline at end of file diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/quote-0f85966d6f134e44/dep-lib-quote b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/quote-0f85966d6f134e44/dep-lib-quote new file mode 100644 index 00000000..ec3cb8bf Binary files /dev/null and b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/quote-0f85966d6f134e44/dep-lib-quote differ diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/quote-0f85966d6f134e44/invoked.timestamp b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/quote-0f85966d6f134e44/invoked.timestamp new file mode 100644 index 00000000..e00328da --- /dev/null +++ b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/quote-0f85966d6f134e44/invoked.timestamp @@ -0,0 +1 @@ +This file has an mtime of when this was started. \ No newline at end of file diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/quote-0f85966d6f134e44/lib-quote b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/quote-0f85966d6f134e44/lib-quote new file mode 100644 index 00000000..b4b6ba8b --- /dev/null +++ b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/quote-0f85966d6f134e44/lib-quote @@ -0,0 +1 @@ +618b025593b80093 \ No newline at end of file diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/quote-0f85966d6f134e44/lib-quote.json b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/quote-0f85966d6f134e44/lib-quote.json new file mode 100644 index 00000000..631fa766 --- /dev/null +++ b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/quote-0f85966d6f134e44/lib-quote.json @@ -0,0 +1 @@ +{"rustc":17467972547956852243,"features":"[\"default\", \"proc-macro\"]","declared_features":"[\"default\", \"proc-macro\"]","target":8313845041260779044,"profile":2785734998891497023,"path":8077693039016294807,"deps":[[8949245912927223590,"build_script_build",false,2599661241357163625],[16346726298725429545,"proc_macro2",false,8059170461296913469]],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/quote-0f85966d6f134e44/dep-lib-quote","checksum":false}}],"rustflags":[],"config":8247474407144887393,"compile_kind":0} \ No newline at end of file diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/quote-4b484c95cbb303f6/build-script-build-script-build b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/quote-4b484c95cbb303f6/build-script-build-script-build new file mode 100644 index 00000000..01fcc6df --- /dev/null +++ b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/quote-4b484c95cbb303f6/build-script-build-script-build @@ -0,0 +1 @@ +14dc0fcc1148625e \ No newline at end of file diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/quote-4b484c95cbb303f6/build-script-build-script-build.json b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/quote-4b484c95cbb303f6/build-script-build-script-build.json new file mode 100644 index 00000000..4e1f4926 --- /dev/null +++ b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/quote-4b484c95cbb303f6/build-script-build-script-build.json @@ -0,0 +1 @@ +{"rustc":17467972547956852243,"features":"[\"default\", \"proc-macro\"]","declared_features":"[\"default\", \"proc-macro\"]","target":5408242616063297496,"profile":2785734998891497023,"path":2672923304195493942,"deps":[],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/quote-4b484c95cbb303f6/dep-build-script-build-script-build","checksum":false}}],"rustflags":[],"config":8247474407144887393,"compile_kind":0} \ No newline at end of file diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/quote-4b484c95cbb303f6/dep-build-script-build-script-build b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/quote-4b484c95cbb303f6/dep-build-script-build-script-build new file mode 100644 index 00000000..ec3cb8bf Binary files /dev/null and b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/quote-4b484c95cbb303f6/dep-build-script-build-script-build differ diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/quote-4b484c95cbb303f6/invoked.timestamp b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/quote-4b484c95cbb303f6/invoked.timestamp new file mode 100644 index 00000000..e00328da --- /dev/null +++ b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/quote-4b484c95cbb303f6/invoked.timestamp @@ -0,0 +1 @@ +This file has an mtime of when this was started. \ No newline at end of file diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/quote-d3fa970117425bd7/run-build-script-build-script-build b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/quote-d3fa970117425bd7/run-build-script-build-script-build new file mode 100644 index 00000000..33392f0e --- /dev/null +++ b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/quote-d3fa970117425bd7/run-build-script-build-script-build @@ -0,0 +1 @@ +69dc711b20da1324 \ No newline at end of file diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/quote-d3fa970117425bd7/run-build-script-build-script-build.json b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/quote-d3fa970117425bd7/run-build-script-build-script-build.json new file mode 100644 index 00000000..2c5faf03 --- /dev/null +++ b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/quote-d3fa970117425bd7/run-build-script-build-script-build.json @@ -0,0 +1 @@ +{"rustc":17467972547956852243,"features":"","declared_features":"","target":0,"profile":0,"path":0,"deps":[[8949245912927223590,"build_script_build",false,6801077628558105620]],"local":[{"RerunIfChanged":{"output":"debug/build/quote-d3fa970117425bd7/output","paths":["build.rs"]}}],"rustflags":[],"config":0,"compile_kind":0} \ No newline at end of file diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/rayon-6e2dc17155d9fd34/dep-lib-rayon b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/rayon-6e2dc17155d9fd34/dep-lib-rayon new file mode 100644 index 00000000..ec3cb8bf Binary files /dev/null and b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/rayon-6e2dc17155d9fd34/dep-lib-rayon differ diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/rayon-6e2dc17155d9fd34/invoked.timestamp b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/rayon-6e2dc17155d9fd34/invoked.timestamp new file mode 100644 index 00000000..e00328da --- /dev/null +++ b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/rayon-6e2dc17155d9fd34/invoked.timestamp @@ -0,0 +1 @@ +This file has an mtime of when this was started. \ No newline at end of file diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/rayon-6e2dc17155d9fd34/lib-rayon b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/rayon-6e2dc17155d9fd34/lib-rayon new file mode 100644 index 00000000..5108ffda --- /dev/null +++ b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/rayon-6e2dc17155d9fd34/lib-rayon @@ -0,0 +1 @@ +e71946403db208b5 \ No newline at end of file diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/rayon-6e2dc17155d9fd34/lib-rayon.json b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/rayon-6e2dc17155d9fd34/lib-rayon.json new file mode 100644 index 00000000..dd274fe6 --- /dev/null +++ b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/rayon-6e2dc17155d9fd34/lib-rayon.json @@ -0,0 +1 @@ +{"rustc":17467972547956852243,"features":"[]","declared_features":"[\"web_spin_lock\"]","target":4732152328429177609,"profile":2785734998891497023,"path":17431103954642750794,"deps":[[3746573929696391749,"rayon_core",false,11362863269611761524],[13370710369771896710,"either",false,1697150408193231268]],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/rayon-6e2dc17155d9fd34/dep-lib-rayon","checksum":false}}],"rustflags":[],"config":8247474407144887393,"compile_kind":0} \ No newline at end of file diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/rayon-core-1b6fdb81fee9810e/run-build-script-build-script-build b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/rayon-core-1b6fdb81fee9810e/run-build-script-build-script-build new file mode 100644 index 00000000..60eca530 --- /dev/null +++ b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/rayon-core-1b6fdb81fee9810e/run-build-script-build-script-build @@ -0,0 +1 @@ +0e9aa1a7bb8410a4 \ No newline at end of file diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/rayon-core-1b6fdb81fee9810e/run-build-script-build-script-build.json b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/rayon-core-1b6fdb81fee9810e/run-build-script-build-script-build.json new file mode 100644 index 00000000..ae4bfa9a --- /dev/null +++ b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/rayon-core-1b6fdb81fee9810e/run-build-script-build-script-build.json @@ -0,0 +1 @@ +{"rustc":17467972547956852243,"features":"","declared_features":"","target":0,"profile":0,"path":0,"deps":[[3746573929696391749,"build_script_build",false,5512398614385402082]],"local":[{"RerunIfChanged":{"output":"debug/build/rayon-core-1b6fdb81fee9810e/output","paths":["build.rs"]}}],"rustflags":[],"config":0,"compile_kind":0} \ No newline at end of file diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/rayon-core-b409a671b98b8570/build-script-build-script-build b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/rayon-core-b409a671b98b8570/build-script-build-script-build new file mode 100644 index 00000000..4958ae8b --- /dev/null +++ b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/rayon-core-b409a671b98b8570/build-script-build-script-build @@ -0,0 +1 @@ +e220cc7655f97f4c \ No newline at end of file diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/rayon-core-b409a671b98b8570/build-script-build-script-build.json b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/rayon-core-b409a671b98b8570/build-script-build-script-build.json new file mode 100644 index 00000000..c2e1f0f8 --- /dev/null +++ b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/rayon-core-b409a671b98b8570/build-script-build-script-build.json @@ -0,0 +1 @@ +{"rustc":17467972547956852243,"features":"[]","declared_features":"[\"web_spin_lock\"]","target":5408242616063297496,"profile":2785734998891497023,"path":10372009197711997420,"deps":[],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/rayon-core-b409a671b98b8570/dep-build-script-build-script-build","checksum":false}}],"rustflags":[],"config":8247474407144887393,"compile_kind":0} \ No newline at end of file diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/rayon-core-b409a671b98b8570/dep-build-script-build-script-build b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/rayon-core-b409a671b98b8570/dep-build-script-build-script-build new file mode 100644 index 00000000..ec3cb8bf Binary files /dev/null and b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/rayon-core-b409a671b98b8570/dep-build-script-build-script-build differ diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/rayon-core-b409a671b98b8570/invoked.timestamp b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/rayon-core-b409a671b98b8570/invoked.timestamp new file mode 100644 index 00000000..e00328da --- /dev/null +++ b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/rayon-core-b409a671b98b8570/invoked.timestamp @@ -0,0 +1 @@ +This file has an mtime of when this was started. \ No newline at end of file diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/rayon-core-d9d479d6cc7ea71c/dep-lib-rayon_core b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/rayon-core-d9d479d6cc7ea71c/dep-lib-rayon_core new file mode 100644 index 00000000..ec3cb8bf Binary files /dev/null and b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/rayon-core-d9d479d6cc7ea71c/dep-lib-rayon_core differ diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/rayon-core-d9d479d6cc7ea71c/invoked.timestamp b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/rayon-core-d9d479d6cc7ea71c/invoked.timestamp new file mode 100644 index 00000000..e00328da --- /dev/null +++ b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/rayon-core-d9d479d6cc7ea71c/invoked.timestamp @@ -0,0 +1 @@ +This file has an mtime of when this was started. \ No newline at end of file diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/rayon-core-d9d479d6cc7ea71c/lib-rayon_core b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/rayon-core-d9d479d6cc7ea71c/lib-rayon_core new file mode 100644 index 00000000..c20ab603 --- /dev/null +++ b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/rayon-core-d9d479d6cc7ea71c/lib-rayon_core @@ -0,0 +1 @@ +74ef8ad0f0ffb09d \ No newline at end of file diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/rayon-core-d9d479d6cc7ea71c/lib-rayon_core.json b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/rayon-core-d9d479d6cc7ea71c/lib-rayon_core.json new file mode 100644 index 00000000..33f3e03a --- /dev/null +++ b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/rayon-core-d9d479d6cc7ea71c/lib-rayon_core.json @@ -0,0 +1 @@ +{"rustc":17467972547956852243,"features":"[]","declared_features":"[\"web_spin_lock\"]","target":12465439074827573786,"profile":2785734998891497023,"path":7184092575742886946,"deps":[[3746573929696391749,"build_script_build",false,11822094963353688590],[11050506297539643678,"crossbeam_utils",false,11092378776446818952],[15481973119957668846,"crossbeam_deque",false,12816530492324934566]],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/rayon-core-d9d479d6cc7ea71c/dep-lib-rayon_core","checksum":false}}],"rustflags":[],"config":8247474407144887393,"compile_kind":0} \ No newline at end of file diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/syn-86f5aa6c6ace4240/dep-lib-syn b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/syn-86f5aa6c6ace4240/dep-lib-syn new file mode 100644 index 00000000..ec3cb8bf Binary files /dev/null and b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/syn-86f5aa6c6ace4240/dep-lib-syn differ diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/syn-86f5aa6c6ace4240/invoked.timestamp b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/syn-86f5aa6c6ace4240/invoked.timestamp new file mode 100644 index 00000000..e00328da --- /dev/null +++ b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/syn-86f5aa6c6ace4240/invoked.timestamp @@ -0,0 +1 @@ +This file has an mtime of when this was started. \ No newline at end of file diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/syn-86f5aa6c6ace4240/lib-syn b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/syn-86f5aa6c6ace4240/lib-syn new file mode 100644 index 00000000..fe54c7c4 --- /dev/null +++ b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/syn-86f5aa6c6ace4240/lib-syn @@ -0,0 +1 @@ +dfcabe0f565d5911 \ No newline at end of file diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/syn-86f5aa6c6ace4240/lib-syn.json b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/syn-86f5aa6c6ace4240/lib-syn.json new file mode 100644 index 00000000..6c15c911 --- /dev/null +++ b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/syn-86f5aa6c6ace4240/lib-syn.json @@ -0,0 +1 @@ +{"rustc":17467972547956852243,"features":"[\"clone-impls\", \"default\", \"derive\", \"parsing\", \"printing\", \"proc-macro\"]","declared_features":"[\"clone-impls\", \"default\", \"derive\", \"extra-traits\", \"fold\", \"full\", \"parsing\", \"printing\", \"proc-macro\", \"test\", \"visit\", \"visit-mut\"]","target":9442126953582868550,"profile":2785734998891497023,"path":8687046859460428135,"deps":[[8949245912927223590,"quote",false,10592669266501340001],[12333832803962989937,"unicode_ident",false,16793811267078206154],[16346726298725429545,"proc_macro2",false,8059170461296913469]],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/syn-86f5aa6c6ace4240/dep-lib-syn","checksum":false}}],"rustflags":[],"config":8247474407144887393,"compile_kind":0} \ No newline at end of file diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/thiserror-572b221996b5f3ee/dep-lib-thiserror b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/thiserror-572b221996b5f3ee/dep-lib-thiserror new file mode 100644 index 00000000..e6c51cac Binary files /dev/null and b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/thiserror-572b221996b5f3ee/dep-lib-thiserror differ diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/thiserror-572b221996b5f3ee/invoked.timestamp b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/thiserror-572b221996b5f3ee/invoked.timestamp new file mode 100644 index 00000000..e00328da --- /dev/null +++ b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/thiserror-572b221996b5f3ee/invoked.timestamp @@ -0,0 +1 @@ +This file has an mtime of when this was started. \ No newline at end of file diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/thiserror-572b221996b5f3ee/lib-thiserror b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/thiserror-572b221996b5f3ee/lib-thiserror new file mode 100644 index 00000000..a75b31b5 --- /dev/null +++ b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/thiserror-572b221996b5f3ee/lib-thiserror @@ -0,0 +1 @@ +47914a38d6fd65f3 \ No newline at end of file diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/thiserror-572b221996b5f3ee/lib-thiserror.json b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/thiserror-572b221996b5f3ee/lib-thiserror.json new file mode 100644 index 00000000..02c09d11 --- /dev/null +++ b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/thiserror-572b221996b5f3ee/lib-thiserror.json @@ -0,0 +1 @@ +{"rustc":17467972547956852243,"features":"[\"default\", \"std\"]","declared_features":"[\"default\", \"std\"]","target":13586076721141200315,"profile":2785734998891497023,"path":7642888677821054070,"deps":[[1957009224993739128,"build_script_build",false,9625043333480330270],[7717134694032034306,"thiserror_impl",false,12308587709611057643]],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/thiserror-572b221996b5f3ee/dep-lib-thiserror","checksum":false}}],"rustflags":[],"config":8247474407144887393,"compile_kind":0} \ No newline at end of file diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/thiserror-6b4c49fb40990cd8/build-script-build-script-build b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/thiserror-6b4c49fb40990cd8/build-script-build-script-build new file mode 100644 index 00000000..b5966d71 --- /dev/null +++ b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/thiserror-6b4c49fb40990cd8/build-script-build-script-build @@ -0,0 +1 @@ +7a920618d44fdd59 \ No newline at end of file diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/thiserror-6b4c49fb40990cd8/build-script-build-script-build.json b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/thiserror-6b4c49fb40990cd8/build-script-build-script-build.json new file mode 100644 index 00000000..366699bc --- /dev/null +++ b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/thiserror-6b4c49fb40990cd8/build-script-build-script-build.json @@ -0,0 +1 @@ +{"rustc":17467972547956852243,"features":"[\"default\", \"std\"]","declared_features":"[\"default\", \"std\"]","target":5408242616063297496,"profile":2785734998891497023,"path":11777605584080991800,"deps":[],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/thiserror-6b4c49fb40990cd8/dep-build-script-build-script-build","checksum":false}}],"rustflags":[],"config":8247474407144887393,"compile_kind":0} \ No newline at end of file diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/thiserror-6b4c49fb40990cd8/dep-build-script-build-script-build b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/thiserror-6b4c49fb40990cd8/dep-build-script-build-script-build new file mode 100644 index 00000000..ec3cb8bf Binary files /dev/null and b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/thiserror-6b4c49fb40990cd8/dep-build-script-build-script-build differ diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/thiserror-6b4c49fb40990cd8/invoked.timestamp b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/thiserror-6b4c49fb40990cd8/invoked.timestamp new file mode 100644 index 00000000..e00328da --- /dev/null +++ b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/thiserror-6b4c49fb40990cd8/invoked.timestamp @@ -0,0 +1 @@ +This file has an mtime of when this was started. \ No newline at end of file diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/thiserror-ea6c25606e27ac64/run-build-script-build-script-build b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/thiserror-ea6c25606e27ac64/run-build-script-build-script-build new file mode 100644 index 00000000..47d0a4fa --- /dev/null +++ b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/thiserror-ea6c25606e27ac64/run-build-script-build-script-build @@ -0,0 +1 @@ +1e34d687ea059385 \ No newline at end of file diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/thiserror-ea6c25606e27ac64/run-build-script-build-script-build.json b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/thiserror-ea6c25606e27ac64/run-build-script-build-script-build.json new file mode 100644 index 00000000..28a46706 --- /dev/null +++ b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/thiserror-ea6c25606e27ac64/run-build-script-build-script-build.json @@ -0,0 +1 @@ +{"rustc":17467972547956852243,"features":"","declared_features":"","target":0,"profile":0,"path":0,"deps":[[1957009224993739128,"build_script_build",false,6475419611583386234]],"local":[{"RerunIfChanged":{"output":"debug/build/thiserror-ea6c25606e27ac64/output","paths":["build/probe.rs"]}},{"RerunIfEnvChanged":{"var":"RUSTC_BOOTSTRAP","val":null}}],"rustflags":[],"config":0,"compile_kind":0} \ No newline at end of file diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/thiserror-impl-40d32b56c8547825/dep-lib-thiserror_impl b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/thiserror-impl-40d32b56c8547825/dep-lib-thiserror_impl new file mode 100644 index 00000000..ec3cb8bf Binary files /dev/null and b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/thiserror-impl-40d32b56c8547825/dep-lib-thiserror_impl differ diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/thiserror-impl-40d32b56c8547825/invoked.timestamp b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/thiserror-impl-40d32b56c8547825/invoked.timestamp new file mode 100644 index 00000000..e00328da --- /dev/null +++ b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/thiserror-impl-40d32b56c8547825/invoked.timestamp @@ -0,0 +1 @@ +This file has an mtime of when this was started. \ No newline at end of file diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/thiserror-impl-40d32b56c8547825/lib-thiserror_impl b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/thiserror-impl-40d32b56c8547825/lib-thiserror_impl new file mode 100644 index 00000000..c9a73312 --- /dev/null +++ b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/thiserror-impl-40d32b56c8547825/lib-thiserror_impl @@ -0,0 +1 @@ +eb211be64ee3d0aa \ No newline at end of file diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/thiserror-impl-40d32b56c8547825/lib-thiserror_impl.json b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/thiserror-impl-40d32b56c8547825/lib-thiserror_impl.json new file mode 100644 index 00000000..d013eade --- /dev/null +++ b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/thiserror-impl-40d32b56c8547825/lib-thiserror_impl.json @@ -0,0 +1 @@ +{"rustc":17467972547956852243,"features":"[]","declared_features":"[]","target":6216210811039475267,"profile":2785734998891497023,"path":17504287886612828789,"deps":[[8949245912927223590,"quote",false,10592669266501340001],[16346726298725429545,"proc_macro2",false,8059170461296913469],[16468994220052032074,"syn",false,1250132995784755935]],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/thiserror-impl-40d32b56c8547825/dep-lib-thiserror_impl","checksum":false}}],"rustflags":[],"config":8247474407144887393,"compile_kind":0} \ No newline at end of file diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/unicode-ident-fb577f96b63f8d5a/dep-lib-unicode_ident b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/unicode-ident-fb577f96b63f8d5a/dep-lib-unicode_ident new file mode 100644 index 00000000..ec3cb8bf Binary files /dev/null and b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/unicode-ident-fb577f96b63f8d5a/dep-lib-unicode_ident differ diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/unicode-ident-fb577f96b63f8d5a/invoked.timestamp b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/unicode-ident-fb577f96b63f8d5a/invoked.timestamp new file mode 100644 index 00000000..e00328da --- /dev/null +++ b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/unicode-ident-fb577f96b63f8d5a/invoked.timestamp @@ -0,0 +1 @@ +This file has an mtime of when this was started. \ No newline at end of file diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/unicode-ident-fb577f96b63f8d5a/lib-unicode_ident b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/unicode-ident-fb577f96b63f8d5a/lib-unicode_ident new file mode 100644 index 00000000..aece5c19 --- /dev/null +++ b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/unicode-ident-fb577f96b63f8d5a/lib-unicode_ident @@ -0,0 +1 @@ +ca3226b75e9a0fe9 \ No newline at end of file diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/unicode-ident-fb577f96b63f8d5a/lib-unicode_ident.json b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/unicode-ident-fb577f96b63f8d5a/lib-unicode_ident.json new file mode 100644 index 00000000..08cf61b2 --- /dev/null +++ b/crates/hevcenc/target/agent-codec-hevc/debug/.fingerprint/unicode-ident-fb577f96b63f8d5a/lib-unicode_ident.json @@ -0,0 +1 @@ +{"rustc":17467972547956852243,"features":"[]","declared_features":"[]","target":14045917370260632744,"profile":2785734998891497023,"path":13952268360258147237,"deps":[],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/unicode-ident-fb577f96b63f8d5a/dep-lib-unicode_ident","checksum":false}}],"rustflags":[],"config":8247474407144887393,"compile_kind":0} \ No newline at end of file diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/build/crossbeam-deque-5b3c4336788f7174/build-script-build b/crates/hevcenc/target/agent-codec-hevc/debug/build/crossbeam-deque-5b3c4336788f7174/build-script-build new file mode 100755 index 00000000..eb48decd Binary files /dev/null and b/crates/hevcenc/target/agent-codec-hevc/debug/build/crossbeam-deque-5b3c4336788f7174/build-script-build differ diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/build/crossbeam-deque-5b3c4336788f7174/build_script_build-5b3c4336788f7174 b/crates/hevcenc/target/agent-codec-hevc/debug/build/crossbeam-deque-5b3c4336788f7174/build_script_build-5b3c4336788f7174 new file mode 100755 index 00000000..eb48decd Binary files /dev/null and b/crates/hevcenc/target/agent-codec-hevc/debug/build/crossbeam-deque-5b3c4336788f7174/build_script_build-5b3c4336788f7174 differ diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/build/crossbeam-deque-5b3c4336788f7174/build_script_build-5b3c4336788f7174.d b/crates/hevcenc/target/agent-codec-hevc/debug/build/crossbeam-deque-5b3c4336788f7174/build_script_build-5b3c4336788f7174.d new file mode 100644 index 00000000..01540bc1 --- /dev/null +++ b/crates/hevcenc/target/agent-codec-hevc/debug/build/crossbeam-deque-5b3c4336788f7174/build_script_build-5b3c4336788f7174.d @@ -0,0 +1,5 @@ +/Users/bt/dev/storyteller/craft-apps/effectcraft/.claude/worktrees/agent-a5ec24c82e394a621/crates/hevcenc/target/agent-codec-hevc/debug/build/crossbeam-deque-5b3c4336788f7174/build_script_build-5b3c4336788f7174.d: /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/crossbeam-deque-0.8.8/build.rs + +/Users/bt/dev/storyteller/craft-apps/effectcraft/.claude/worktrees/agent-a5ec24c82e394a621/crates/hevcenc/target/agent-codec-hevc/debug/build/crossbeam-deque-5b3c4336788f7174/build_script_build-5b3c4336788f7174: /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/crossbeam-deque-0.8.8/build.rs + +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/crossbeam-deque-0.8.8/build.rs: diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/build/crossbeam-deque-f51f1776f2c514b1/invoked.timestamp b/crates/hevcenc/target/agent-codec-hevc/debug/build/crossbeam-deque-f51f1776f2c514b1/invoked.timestamp new file mode 100644 index 00000000..e00328da --- /dev/null +++ b/crates/hevcenc/target/agent-codec-hevc/debug/build/crossbeam-deque-f51f1776f2c514b1/invoked.timestamp @@ -0,0 +1 @@ +This file has an mtime of when this was started. \ No newline at end of file diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/build/crossbeam-deque-f51f1776f2c514b1/output b/crates/hevcenc/target/agent-codec-hevc/debug/build/crossbeam-deque-f51f1776f2c514b1/output new file mode 100644 index 00000000..57fc2854 --- /dev/null +++ b/crates/hevcenc/target/agent-codec-hevc/debug/build/crossbeam-deque-f51f1776f2c514b1/output @@ -0,0 +1,2 @@ +cargo:rerun-if-changed=build.rs +cargo:rustc-check-cfg=cfg(crossbeam_sanitize_thread) diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/build/crossbeam-deque-f51f1776f2c514b1/root-output b/crates/hevcenc/target/agent-codec-hevc/debug/build/crossbeam-deque-f51f1776f2c514b1/root-output new file mode 100644 index 00000000..91782756 --- /dev/null +++ b/crates/hevcenc/target/agent-codec-hevc/debug/build/crossbeam-deque-f51f1776f2c514b1/root-output @@ -0,0 +1 @@ +/Users/bt/dev/storyteller/craft-apps/effectcraft/.claude/worktrees/agent-a5ec24c82e394a621/crates/hevcenc/target/agent-codec-hevc/debug/build/crossbeam-deque-f51f1776f2c514b1/out \ No newline at end of file diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/build/crossbeam-deque-f51f1776f2c514b1/stderr b/crates/hevcenc/target/agent-codec-hevc/debug/build/crossbeam-deque-f51f1776f2c514b1/stderr new file mode 100644 index 00000000..e69de29b diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/build/crossbeam-epoch-1347d2207ea061ce/build-script-build b/crates/hevcenc/target/agent-codec-hevc/debug/build/crossbeam-epoch-1347d2207ea061ce/build-script-build new file mode 100755 index 00000000..986aecc0 Binary files /dev/null and b/crates/hevcenc/target/agent-codec-hevc/debug/build/crossbeam-epoch-1347d2207ea061ce/build-script-build differ diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/build/crossbeam-epoch-1347d2207ea061ce/build_script_build-1347d2207ea061ce b/crates/hevcenc/target/agent-codec-hevc/debug/build/crossbeam-epoch-1347d2207ea061ce/build_script_build-1347d2207ea061ce new file mode 100755 index 00000000..986aecc0 Binary files /dev/null and b/crates/hevcenc/target/agent-codec-hevc/debug/build/crossbeam-epoch-1347d2207ea061ce/build_script_build-1347d2207ea061ce differ diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/build/crossbeam-epoch-1347d2207ea061ce/build_script_build-1347d2207ea061ce.d b/crates/hevcenc/target/agent-codec-hevc/debug/build/crossbeam-epoch-1347d2207ea061ce/build_script_build-1347d2207ea061ce.d new file mode 100644 index 00000000..e2181c8f --- /dev/null +++ b/crates/hevcenc/target/agent-codec-hevc/debug/build/crossbeam-epoch-1347d2207ea061ce/build_script_build-1347d2207ea061ce.d @@ -0,0 +1,5 @@ +/Users/bt/dev/storyteller/craft-apps/effectcraft/.claude/worktrees/agent-a5ec24c82e394a621/crates/hevcenc/target/agent-codec-hevc/debug/build/crossbeam-epoch-1347d2207ea061ce/build_script_build-1347d2207ea061ce.d: /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/crossbeam-epoch-0.9.21/build.rs + +/Users/bt/dev/storyteller/craft-apps/effectcraft/.claude/worktrees/agent-a5ec24c82e394a621/crates/hevcenc/target/agent-codec-hevc/debug/build/crossbeam-epoch-1347d2207ea061ce/build_script_build-1347d2207ea061ce: /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/crossbeam-epoch-0.9.21/build.rs + +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/crossbeam-epoch-0.9.21/build.rs: diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/build/crossbeam-epoch-2092bf031b9d9a01/invoked.timestamp b/crates/hevcenc/target/agent-codec-hevc/debug/build/crossbeam-epoch-2092bf031b9d9a01/invoked.timestamp new file mode 100644 index 00000000..e00328da --- /dev/null +++ b/crates/hevcenc/target/agent-codec-hevc/debug/build/crossbeam-epoch-2092bf031b9d9a01/invoked.timestamp @@ -0,0 +1 @@ +This file has an mtime of when this was started. \ No newline at end of file diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/build/crossbeam-epoch-2092bf031b9d9a01/output b/crates/hevcenc/target/agent-codec-hevc/debug/build/crossbeam-epoch-2092bf031b9d9a01/output new file mode 100644 index 00000000..57fc2854 --- /dev/null +++ b/crates/hevcenc/target/agent-codec-hevc/debug/build/crossbeam-epoch-2092bf031b9d9a01/output @@ -0,0 +1,2 @@ +cargo:rerun-if-changed=build.rs +cargo:rustc-check-cfg=cfg(crossbeam_sanitize_thread) diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/build/crossbeam-epoch-2092bf031b9d9a01/root-output b/crates/hevcenc/target/agent-codec-hevc/debug/build/crossbeam-epoch-2092bf031b9d9a01/root-output new file mode 100644 index 00000000..bb48420d --- /dev/null +++ b/crates/hevcenc/target/agent-codec-hevc/debug/build/crossbeam-epoch-2092bf031b9d9a01/root-output @@ -0,0 +1 @@ +/Users/bt/dev/storyteller/craft-apps/effectcraft/.claude/worktrees/agent-a5ec24c82e394a621/crates/hevcenc/target/agent-codec-hevc/debug/build/crossbeam-epoch-2092bf031b9d9a01/out \ No newline at end of file diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/build/crossbeam-epoch-2092bf031b9d9a01/stderr b/crates/hevcenc/target/agent-codec-hevc/debug/build/crossbeam-epoch-2092bf031b9d9a01/stderr new file mode 100644 index 00000000..e69de29b diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/build/crossbeam-utils-5b2ff6828cdce4c3/build-script-build b/crates/hevcenc/target/agent-codec-hevc/debug/build/crossbeam-utils-5b2ff6828cdce4c3/build-script-build new file mode 100755 index 00000000..c1b1362d Binary files /dev/null and b/crates/hevcenc/target/agent-codec-hevc/debug/build/crossbeam-utils-5b2ff6828cdce4c3/build-script-build differ diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/build/crossbeam-utils-5b2ff6828cdce4c3/build_script_build-5b2ff6828cdce4c3 b/crates/hevcenc/target/agent-codec-hevc/debug/build/crossbeam-utils-5b2ff6828cdce4c3/build_script_build-5b2ff6828cdce4c3 new file mode 100755 index 00000000..c1b1362d Binary files /dev/null and b/crates/hevcenc/target/agent-codec-hevc/debug/build/crossbeam-utils-5b2ff6828cdce4c3/build_script_build-5b2ff6828cdce4c3 differ diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/build/crossbeam-utils-5b2ff6828cdce4c3/build_script_build-5b2ff6828cdce4c3.d b/crates/hevcenc/target/agent-codec-hevc/debug/build/crossbeam-utils-5b2ff6828cdce4c3/build_script_build-5b2ff6828cdce4c3.d new file mode 100644 index 00000000..55e49280 --- /dev/null +++ b/crates/hevcenc/target/agent-codec-hevc/debug/build/crossbeam-utils-5b2ff6828cdce4c3/build_script_build-5b2ff6828cdce4c3.d @@ -0,0 +1,9 @@ +/Users/bt/dev/storyteller/craft-apps/effectcraft/.claude/worktrees/agent-a5ec24c82e394a621/crates/hevcenc/target/agent-codec-hevc/debug/build/crossbeam-utils-5b2ff6828cdce4c3/build_script_build-5b2ff6828cdce4c3.d: /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/crossbeam-utils-0.8.23/build.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/crossbeam-utils-0.8.23/no_atomic.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/crossbeam-utils-0.8.23/build-common.rs + +/Users/bt/dev/storyteller/craft-apps/effectcraft/.claude/worktrees/agent-a5ec24c82e394a621/crates/hevcenc/target/agent-codec-hevc/debug/build/crossbeam-utils-5b2ff6828cdce4c3/build_script_build-5b2ff6828cdce4c3: /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/crossbeam-utils-0.8.23/build.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/crossbeam-utils-0.8.23/no_atomic.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/crossbeam-utils-0.8.23/build-common.rs + +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/crossbeam-utils-0.8.23/build.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/crossbeam-utils-0.8.23/no_atomic.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/crossbeam-utils-0.8.23/build-common.rs: + +# env-dep:CARGO_PKG_NAME=crossbeam-utils diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/build/crossbeam-utils-99f1d686cf22430f/invoked.timestamp b/crates/hevcenc/target/agent-codec-hevc/debug/build/crossbeam-utils-99f1d686cf22430f/invoked.timestamp new file mode 100644 index 00000000..e00328da --- /dev/null +++ b/crates/hevcenc/target/agent-codec-hevc/debug/build/crossbeam-utils-99f1d686cf22430f/invoked.timestamp @@ -0,0 +1 @@ +This file has an mtime of when this was started. \ No newline at end of file diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/build/crossbeam-utils-99f1d686cf22430f/output b/crates/hevcenc/target/agent-codec-hevc/debug/build/crossbeam-utils-99f1d686cf22430f/output new file mode 100644 index 00000000..d0bad9fd --- /dev/null +++ b/crates/hevcenc/target/agent-codec-hevc/debug/build/crossbeam-utils-99f1d686cf22430f/output @@ -0,0 +1,2 @@ +cargo:rerun-if-changed=no_atomic.rs +cargo:rustc-check-cfg=cfg(crossbeam_no_atomic,crossbeam_sanitize_thread) diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/build/crossbeam-utils-99f1d686cf22430f/root-output b/crates/hevcenc/target/agent-codec-hevc/debug/build/crossbeam-utils-99f1d686cf22430f/root-output new file mode 100644 index 00000000..27a89239 --- /dev/null +++ b/crates/hevcenc/target/agent-codec-hevc/debug/build/crossbeam-utils-99f1d686cf22430f/root-output @@ -0,0 +1 @@ +/Users/bt/dev/storyteller/craft-apps/effectcraft/.claude/worktrees/agent-a5ec24c82e394a621/crates/hevcenc/target/agent-codec-hevc/debug/build/crossbeam-utils-99f1d686cf22430f/out \ No newline at end of file diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/build/crossbeam-utils-99f1d686cf22430f/stderr b/crates/hevcenc/target/agent-codec-hevc/debug/build/crossbeam-utils-99f1d686cf22430f/stderr new file mode 100644 index 00000000..e69de29b diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/build/proc-macro2-73f2303f90bf5dd8/invoked.timestamp b/crates/hevcenc/target/agent-codec-hevc/debug/build/proc-macro2-73f2303f90bf5dd8/invoked.timestamp new file mode 100644 index 00000000..e00328da --- /dev/null +++ b/crates/hevcenc/target/agent-codec-hevc/debug/build/proc-macro2-73f2303f90bf5dd8/invoked.timestamp @@ -0,0 +1 @@ +This file has an mtime of when this was started. \ No newline at end of file diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/build/proc-macro2-73f2303f90bf5dd8/output b/crates/hevcenc/target/agent-codec-hevc/debug/build/proc-macro2-73f2303f90bf5dd8/output new file mode 100644 index 00000000..d3d235a5 --- /dev/null +++ b/crates/hevcenc/target/agent-codec-hevc/debug/build/proc-macro2-73f2303f90bf5dd8/output @@ -0,0 +1,23 @@ +cargo:rustc-check-cfg=cfg(fuzzing) +cargo:rustc-check-cfg=cfg(no_is_available) +cargo:rustc-check-cfg=cfg(no_literal_byte_character) +cargo:rustc-check-cfg=cfg(no_literal_c_string) +cargo:rustc-check-cfg=cfg(no_source_text) +cargo:rustc-check-cfg=cfg(proc_macro_span) +cargo:rustc-check-cfg=cfg(proc_macro_span_file) +cargo:rustc-check-cfg=cfg(proc_macro_span_location) +cargo:rustc-check-cfg=cfg(procmacro2_backtrace) +cargo:rustc-check-cfg=cfg(procmacro2_build_probe) +cargo:rustc-check-cfg=cfg(procmacro2_nightly_testing) +cargo:rustc-check-cfg=cfg(procmacro2_semver_exempt) +cargo:rustc-check-cfg=cfg(randomize_layout) +cargo:rustc-check-cfg=cfg(span_locations) +cargo:rustc-check-cfg=cfg(super_unstable) +cargo:rustc-check-cfg=cfg(wrap_proc_macro) +cargo:rerun-if-changed=src/probe/proc_macro_span.rs +cargo:rustc-cfg=wrap_proc_macro +cargo:rerun-if-changed=src/probe/proc_macro_span_location.rs +cargo:rustc-cfg=proc_macro_span_location +cargo:rerun-if-changed=src/probe/proc_macro_span_file.rs +cargo:rustc-cfg=proc_macro_span_file +cargo:rerun-if-env-changed=RUSTC_BOOTSTRAP diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/build/proc-macro2-73f2303f90bf5dd8/root-output b/crates/hevcenc/target/agent-codec-hevc/debug/build/proc-macro2-73f2303f90bf5dd8/root-output new file mode 100644 index 00000000..545de268 --- /dev/null +++ b/crates/hevcenc/target/agent-codec-hevc/debug/build/proc-macro2-73f2303f90bf5dd8/root-output @@ -0,0 +1 @@ +/Users/bt/dev/storyteller/craft-apps/effectcraft/.claude/worktrees/agent-a5ec24c82e394a621/crates/hevcenc/target/agent-codec-hevc/debug/build/proc-macro2-73f2303f90bf5dd8/out \ No newline at end of file diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/build/proc-macro2-73f2303f90bf5dd8/stderr b/crates/hevcenc/target/agent-codec-hevc/debug/build/proc-macro2-73f2303f90bf5dd8/stderr new file mode 100644 index 00000000..e69de29b diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/build/proc-macro2-8b5400504772b527/build-script-build b/crates/hevcenc/target/agent-codec-hevc/debug/build/proc-macro2-8b5400504772b527/build-script-build new file mode 100755 index 00000000..0b58654c Binary files /dev/null and b/crates/hevcenc/target/agent-codec-hevc/debug/build/proc-macro2-8b5400504772b527/build-script-build differ diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/build/proc-macro2-8b5400504772b527/build_script_build-8b5400504772b527 b/crates/hevcenc/target/agent-codec-hevc/debug/build/proc-macro2-8b5400504772b527/build_script_build-8b5400504772b527 new file mode 100755 index 00000000..0b58654c Binary files /dev/null and b/crates/hevcenc/target/agent-codec-hevc/debug/build/proc-macro2-8b5400504772b527/build_script_build-8b5400504772b527 differ diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/build/proc-macro2-8b5400504772b527/build_script_build-8b5400504772b527.d b/crates/hevcenc/target/agent-codec-hevc/debug/build/proc-macro2-8b5400504772b527/build_script_build-8b5400504772b527.d new file mode 100644 index 00000000..2a84a739 --- /dev/null +++ b/crates/hevcenc/target/agent-codec-hevc/debug/build/proc-macro2-8b5400504772b527/build_script_build-8b5400504772b527.d @@ -0,0 +1,5 @@ +/Users/bt/dev/storyteller/craft-apps/effectcraft/.claude/worktrees/agent-a5ec24c82e394a621/crates/hevcenc/target/agent-codec-hevc/debug/build/proc-macro2-8b5400504772b527/build_script_build-8b5400504772b527.d: /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/proc-macro2-1.0.107/build.rs + +/Users/bt/dev/storyteller/craft-apps/effectcraft/.claude/worktrees/agent-a5ec24c82e394a621/crates/hevcenc/target/agent-codec-hevc/debug/build/proc-macro2-8b5400504772b527/build_script_build-8b5400504772b527: /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/proc-macro2-1.0.107/build.rs + +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/proc-macro2-1.0.107/build.rs: diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/build/quote-4b484c95cbb303f6/build-script-build b/crates/hevcenc/target/agent-codec-hevc/debug/build/quote-4b484c95cbb303f6/build-script-build new file mode 100755 index 00000000..8ff96310 Binary files /dev/null and b/crates/hevcenc/target/agent-codec-hevc/debug/build/quote-4b484c95cbb303f6/build-script-build differ diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/build/quote-4b484c95cbb303f6/build_script_build-4b484c95cbb303f6 b/crates/hevcenc/target/agent-codec-hevc/debug/build/quote-4b484c95cbb303f6/build_script_build-4b484c95cbb303f6 new file mode 100755 index 00000000..8ff96310 Binary files /dev/null and b/crates/hevcenc/target/agent-codec-hevc/debug/build/quote-4b484c95cbb303f6/build_script_build-4b484c95cbb303f6 differ diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/build/quote-4b484c95cbb303f6/build_script_build-4b484c95cbb303f6.d b/crates/hevcenc/target/agent-codec-hevc/debug/build/quote-4b484c95cbb303f6/build_script_build-4b484c95cbb303f6.d new file mode 100644 index 00000000..5e8a8fd8 --- /dev/null +++ b/crates/hevcenc/target/agent-codec-hevc/debug/build/quote-4b484c95cbb303f6/build_script_build-4b484c95cbb303f6.d @@ -0,0 +1,5 @@ +/Users/bt/dev/storyteller/craft-apps/effectcraft/.claude/worktrees/agent-a5ec24c82e394a621/crates/hevcenc/target/agent-codec-hevc/debug/build/quote-4b484c95cbb303f6/build_script_build-4b484c95cbb303f6.d: /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/quote-1.0.47/build.rs + +/Users/bt/dev/storyteller/craft-apps/effectcraft/.claude/worktrees/agent-a5ec24c82e394a621/crates/hevcenc/target/agent-codec-hevc/debug/build/quote-4b484c95cbb303f6/build_script_build-4b484c95cbb303f6: /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/quote-1.0.47/build.rs + +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/quote-1.0.47/build.rs: diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/build/quote-d3fa970117425bd7/invoked.timestamp b/crates/hevcenc/target/agent-codec-hevc/debug/build/quote-d3fa970117425bd7/invoked.timestamp new file mode 100644 index 00000000..e00328da --- /dev/null +++ b/crates/hevcenc/target/agent-codec-hevc/debug/build/quote-d3fa970117425bd7/invoked.timestamp @@ -0,0 +1 @@ +This file has an mtime of when this was started. \ No newline at end of file diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/build/quote-d3fa970117425bd7/output b/crates/hevcenc/target/agent-codec-hevc/debug/build/quote-d3fa970117425bd7/output new file mode 100644 index 00000000..6d81eca2 --- /dev/null +++ b/crates/hevcenc/target/agent-codec-hevc/debug/build/quote-d3fa970117425bd7/output @@ -0,0 +1,2 @@ +cargo:rerun-if-changed=build.rs +cargo:rustc-check-cfg=cfg(no_diagnostic_namespace) diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/build/quote-d3fa970117425bd7/root-output b/crates/hevcenc/target/agent-codec-hevc/debug/build/quote-d3fa970117425bd7/root-output new file mode 100644 index 00000000..323737b3 --- /dev/null +++ b/crates/hevcenc/target/agent-codec-hevc/debug/build/quote-d3fa970117425bd7/root-output @@ -0,0 +1 @@ +/Users/bt/dev/storyteller/craft-apps/effectcraft/.claude/worktrees/agent-a5ec24c82e394a621/crates/hevcenc/target/agent-codec-hevc/debug/build/quote-d3fa970117425bd7/out \ No newline at end of file diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/build/quote-d3fa970117425bd7/stderr b/crates/hevcenc/target/agent-codec-hevc/debug/build/quote-d3fa970117425bd7/stderr new file mode 100644 index 00000000..e69de29b diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/build/rayon-core-1b6fdb81fee9810e/invoked.timestamp b/crates/hevcenc/target/agent-codec-hevc/debug/build/rayon-core-1b6fdb81fee9810e/invoked.timestamp new file mode 100644 index 00000000..e00328da --- /dev/null +++ b/crates/hevcenc/target/agent-codec-hevc/debug/build/rayon-core-1b6fdb81fee9810e/invoked.timestamp @@ -0,0 +1 @@ +This file has an mtime of when this was started. \ No newline at end of file diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/build/rayon-core-1b6fdb81fee9810e/output b/crates/hevcenc/target/agent-codec-hevc/debug/build/rayon-core-1b6fdb81fee9810e/output new file mode 100644 index 00000000..d15ba9ab --- /dev/null +++ b/crates/hevcenc/target/agent-codec-hevc/debug/build/rayon-core-1b6fdb81fee9810e/output @@ -0,0 +1 @@ +cargo:rerun-if-changed=build.rs diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/build/rayon-core-1b6fdb81fee9810e/root-output b/crates/hevcenc/target/agent-codec-hevc/debug/build/rayon-core-1b6fdb81fee9810e/root-output new file mode 100644 index 00000000..4de1f8ae --- /dev/null +++ b/crates/hevcenc/target/agent-codec-hevc/debug/build/rayon-core-1b6fdb81fee9810e/root-output @@ -0,0 +1 @@ +/Users/bt/dev/storyteller/craft-apps/effectcraft/.claude/worktrees/agent-a5ec24c82e394a621/crates/hevcenc/target/agent-codec-hevc/debug/build/rayon-core-1b6fdb81fee9810e/out \ No newline at end of file diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/build/rayon-core-1b6fdb81fee9810e/stderr b/crates/hevcenc/target/agent-codec-hevc/debug/build/rayon-core-1b6fdb81fee9810e/stderr new file mode 100644 index 00000000..e69de29b diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/build/rayon-core-b409a671b98b8570/build-script-build b/crates/hevcenc/target/agent-codec-hevc/debug/build/rayon-core-b409a671b98b8570/build-script-build new file mode 100755 index 00000000..73a8ef6f Binary files /dev/null and b/crates/hevcenc/target/agent-codec-hevc/debug/build/rayon-core-b409a671b98b8570/build-script-build differ diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/build/rayon-core-b409a671b98b8570/build_script_build-b409a671b98b8570 b/crates/hevcenc/target/agent-codec-hevc/debug/build/rayon-core-b409a671b98b8570/build_script_build-b409a671b98b8570 new file mode 100755 index 00000000..73a8ef6f Binary files /dev/null and b/crates/hevcenc/target/agent-codec-hevc/debug/build/rayon-core-b409a671b98b8570/build_script_build-b409a671b98b8570 differ diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/build/rayon-core-b409a671b98b8570/build_script_build-b409a671b98b8570.d b/crates/hevcenc/target/agent-codec-hevc/debug/build/rayon-core-b409a671b98b8570/build_script_build-b409a671b98b8570.d new file mode 100644 index 00000000..ff779b97 --- /dev/null +++ b/crates/hevcenc/target/agent-codec-hevc/debug/build/rayon-core-b409a671b98b8570/build_script_build-b409a671b98b8570.d @@ -0,0 +1,5 @@ +/Users/bt/dev/storyteller/craft-apps/effectcraft/.claude/worktrees/agent-a5ec24c82e394a621/crates/hevcenc/target/agent-codec-hevc/debug/build/rayon-core-b409a671b98b8570/build_script_build-b409a671b98b8570.d: /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-core-1.13.0/build.rs + +/Users/bt/dev/storyteller/craft-apps/effectcraft/.claude/worktrees/agent-a5ec24c82e394a621/crates/hevcenc/target/agent-codec-hevc/debug/build/rayon-core-b409a671b98b8570/build_script_build-b409a671b98b8570: /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-core-1.13.0/build.rs + +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-core-1.13.0/build.rs: diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/build/thiserror-6b4c49fb40990cd8/build-script-build b/crates/hevcenc/target/agent-codec-hevc/debug/build/thiserror-6b4c49fb40990cd8/build-script-build new file mode 100755 index 00000000..3ff8ed41 Binary files /dev/null and b/crates/hevcenc/target/agent-codec-hevc/debug/build/thiserror-6b4c49fb40990cd8/build-script-build differ diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/build/thiserror-6b4c49fb40990cd8/build_script_build-6b4c49fb40990cd8 b/crates/hevcenc/target/agent-codec-hevc/debug/build/thiserror-6b4c49fb40990cd8/build_script_build-6b4c49fb40990cd8 new file mode 100755 index 00000000..3ff8ed41 Binary files /dev/null and b/crates/hevcenc/target/agent-codec-hevc/debug/build/thiserror-6b4c49fb40990cd8/build_script_build-6b4c49fb40990cd8 differ diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/build/thiserror-6b4c49fb40990cd8/build_script_build-6b4c49fb40990cd8.d b/crates/hevcenc/target/agent-codec-hevc/debug/build/thiserror-6b4c49fb40990cd8/build_script_build-6b4c49fb40990cd8.d new file mode 100644 index 00000000..3171e69c --- /dev/null +++ b/crates/hevcenc/target/agent-codec-hevc/debug/build/thiserror-6b4c49fb40990cd8/build_script_build-6b4c49fb40990cd8.d @@ -0,0 +1,5 @@ +/Users/bt/dev/storyteller/craft-apps/effectcraft/.claude/worktrees/agent-a5ec24c82e394a621/crates/hevcenc/target/agent-codec-hevc/debug/build/thiserror-6b4c49fb40990cd8/build_script_build-6b4c49fb40990cd8.d: /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/thiserror-2.0.21/build.rs + +/Users/bt/dev/storyteller/craft-apps/effectcraft/.claude/worktrees/agent-a5ec24c82e394a621/crates/hevcenc/target/agent-codec-hevc/debug/build/thiserror-6b4c49fb40990cd8/build_script_build-6b4c49fb40990cd8: /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/thiserror-2.0.21/build.rs + +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/thiserror-2.0.21/build.rs: diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/build/thiserror-ea6c25606e27ac64/invoked.timestamp b/crates/hevcenc/target/agent-codec-hevc/debug/build/thiserror-ea6c25606e27ac64/invoked.timestamp new file mode 100644 index 00000000..e00328da --- /dev/null +++ b/crates/hevcenc/target/agent-codec-hevc/debug/build/thiserror-ea6c25606e27ac64/invoked.timestamp @@ -0,0 +1 @@ +This file has an mtime of when this was started. \ No newline at end of file diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/build/thiserror-ea6c25606e27ac64/out/private.rs b/crates/hevcenc/target/agent-codec-hevc/debug/build/thiserror-ea6c25606e27ac64/out/private.rs new file mode 100644 index 00000000..036d1e64 --- /dev/null +++ b/crates/hevcenc/target/agent-codec-hevc/debug/build/thiserror-ea6c25606e27ac64/out/private.rs @@ -0,0 +1,5 @@ +#[doc(hidden)] +pub mod __private21 { + #[doc(hidden)] + pub use crate::private::*; +} diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/build/thiserror-ea6c25606e27ac64/output b/crates/hevcenc/target/agent-codec-hevc/debug/build/thiserror-ea6c25606e27ac64/output new file mode 100644 index 00000000..f62a8d10 --- /dev/null +++ b/crates/hevcenc/target/agent-codec-hevc/debug/build/thiserror-ea6c25606e27ac64/output @@ -0,0 +1,5 @@ +cargo:rerun-if-changed=build/probe.rs +cargo:rustc-check-cfg=cfg(error_generic_member_access) +cargo:rustc-check-cfg=cfg(thiserror_nightly_testing) +cargo:rustc-check-cfg=cfg(thiserror_no_backtrace_type) +cargo:rerun-if-env-changed=RUSTC_BOOTSTRAP diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/build/thiserror-ea6c25606e27ac64/root-output b/crates/hevcenc/target/agent-codec-hevc/debug/build/thiserror-ea6c25606e27ac64/root-output new file mode 100644 index 00000000..374068c0 --- /dev/null +++ b/crates/hevcenc/target/agent-codec-hevc/debug/build/thiserror-ea6c25606e27ac64/root-output @@ -0,0 +1 @@ +/Users/bt/dev/storyteller/craft-apps/effectcraft/.claude/worktrees/agent-a5ec24c82e394a621/crates/hevcenc/target/agent-codec-hevc/debug/build/thiserror-ea6c25606e27ac64/out \ No newline at end of file diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/build/thiserror-ea6c25606e27ac64/stderr b/crates/hevcenc/target/agent-codec-hevc/debug/build/thiserror-ea6c25606e27ac64/stderr new file mode 100644 index 00000000..e69de29b diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/deps/crossbeam_deque-d9775930f3a59590.d b/crates/hevcenc/target/agent-codec-hevc/debug/deps/crossbeam_deque-d9775930f3a59590.d new file mode 100644 index 00000000..7b261062 --- /dev/null +++ b/crates/hevcenc/target/agent-codec-hevc/debug/deps/crossbeam_deque-d9775930f3a59590.d @@ -0,0 +1,8 @@ +/Users/bt/dev/storyteller/craft-apps/effectcraft/.claude/worktrees/agent-a5ec24c82e394a621/crates/hevcenc/target/agent-codec-hevc/debug/deps/crossbeam_deque-d9775930f3a59590.d: /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/crossbeam-deque-0.8.8/src/lib.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/crossbeam-deque-0.8.8/src/deque.rs + +/Users/bt/dev/storyteller/craft-apps/effectcraft/.claude/worktrees/agent-a5ec24c82e394a621/crates/hevcenc/target/agent-codec-hevc/debug/deps/libcrossbeam_deque-d9775930f3a59590.rlib: /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/crossbeam-deque-0.8.8/src/lib.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/crossbeam-deque-0.8.8/src/deque.rs + +/Users/bt/dev/storyteller/craft-apps/effectcraft/.claude/worktrees/agent-a5ec24c82e394a621/crates/hevcenc/target/agent-codec-hevc/debug/deps/libcrossbeam_deque-d9775930f3a59590.rmeta: /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/crossbeam-deque-0.8.8/src/lib.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/crossbeam-deque-0.8.8/src/deque.rs + +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/crossbeam-deque-0.8.8/src/lib.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/crossbeam-deque-0.8.8/src/deque.rs: diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/deps/crossbeam_epoch-72c18a120600cf42.d b/crates/hevcenc/target/agent-codec-hevc/debug/deps/crossbeam_epoch-72c18a120600cf42.d new file mode 100644 index 00000000..93d1b139 --- /dev/null +++ b/crates/hevcenc/target/agent-codec-hevc/debug/deps/crossbeam_epoch-72c18a120600cf42.d @@ -0,0 +1,18 @@ +/Users/bt/dev/storyteller/craft-apps/effectcraft/.claude/worktrees/agent-a5ec24c82e394a621/crates/hevcenc/target/agent-codec-hevc/debug/deps/crossbeam_epoch-72c18a120600cf42.d: /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/crossbeam-epoch-0.9.21/src/lib.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/crossbeam-epoch-0.9.21/src/atomic.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/crossbeam-epoch-0.9.21/src/collector.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/crossbeam-epoch-0.9.21/src/deferred.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/crossbeam-epoch-0.9.21/src/epoch.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/crossbeam-epoch-0.9.21/src/guard.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/crossbeam-epoch-0.9.21/src/internal.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/crossbeam-epoch-0.9.21/src/sync/mod.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/crossbeam-epoch-0.9.21/src/sync/list.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/crossbeam-epoch-0.9.21/src/sync/once_lock.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/crossbeam-epoch-0.9.21/src/sync/queue.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/crossbeam-epoch-0.9.21/src/default.rs + +/Users/bt/dev/storyteller/craft-apps/effectcraft/.claude/worktrees/agent-a5ec24c82e394a621/crates/hevcenc/target/agent-codec-hevc/debug/deps/libcrossbeam_epoch-72c18a120600cf42.rlib: /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/crossbeam-epoch-0.9.21/src/lib.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/crossbeam-epoch-0.9.21/src/atomic.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/crossbeam-epoch-0.9.21/src/collector.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/crossbeam-epoch-0.9.21/src/deferred.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/crossbeam-epoch-0.9.21/src/epoch.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/crossbeam-epoch-0.9.21/src/guard.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/crossbeam-epoch-0.9.21/src/internal.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/crossbeam-epoch-0.9.21/src/sync/mod.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/crossbeam-epoch-0.9.21/src/sync/list.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/crossbeam-epoch-0.9.21/src/sync/once_lock.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/crossbeam-epoch-0.9.21/src/sync/queue.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/crossbeam-epoch-0.9.21/src/default.rs + +/Users/bt/dev/storyteller/craft-apps/effectcraft/.claude/worktrees/agent-a5ec24c82e394a621/crates/hevcenc/target/agent-codec-hevc/debug/deps/libcrossbeam_epoch-72c18a120600cf42.rmeta: /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/crossbeam-epoch-0.9.21/src/lib.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/crossbeam-epoch-0.9.21/src/atomic.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/crossbeam-epoch-0.9.21/src/collector.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/crossbeam-epoch-0.9.21/src/deferred.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/crossbeam-epoch-0.9.21/src/epoch.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/crossbeam-epoch-0.9.21/src/guard.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/crossbeam-epoch-0.9.21/src/internal.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/crossbeam-epoch-0.9.21/src/sync/mod.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/crossbeam-epoch-0.9.21/src/sync/list.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/crossbeam-epoch-0.9.21/src/sync/once_lock.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/crossbeam-epoch-0.9.21/src/sync/queue.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/crossbeam-epoch-0.9.21/src/default.rs + +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/crossbeam-epoch-0.9.21/src/lib.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/crossbeam-epoch-0.9.21/src/atomic.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/crossbeam-epoch-0.9.21/src/collector.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/crossbeam-epoch-0.9.21/src/deferred.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/crossbeam-epoch-0.9.21/src/epoch.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/crossbeam-epoch-0.9.21/src/guard.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/crossbeam-epoch-0.9.21/src/internal.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/crossbeam-epoch-0.9.21/src/sync/mod.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/crossbeam-epoch-0.9.21/src/sync/list.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/crossbeam-epoch-0.9.21/src/sync/once_lock.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/crossbeam-epoch-0.9.21/src/sync/queue.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/crossbeam-epoch-0.9.21/src/default.rs: diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/deps/crossbeam_utils-799e464d44773a7d.d b/crates/hevcenc/target/agent-codec-hevc/debug/deps/crossbeam_utils-799e464d44773a7d.d new file mode 100644 index 00000000..2574a612 --- /dev/null +++ b/crates/hevcenc/target/agent-codec-hevc/debug/deps/crossbeam_utils-799e464d44773a7d.d @@ -0,0 +1,19 @@ +/Users/bt/dev/storyteller/craft-apps/effectcraft/.claude/worktrees/agent-a5ec24c82e394a621/crates/hevcenc/target/agent-codec-hevc/debug/deps/crossbeam_utils-799e464d44773a7d.d: /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/crossbeam-utils-0.8.23/src/lib.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/crossbeam-utils-0.8.23/src/atomic/mod.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/crossbeam-utils-0.8.23/src/atomic/seq_lock.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/crossbeam-utils-0.8.23/src/atomic/atomic_cell.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/crossbeam-utils-0.8.23/src/atomic/consume.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/crossbeam-utils-0.8.23/src/cache_padded.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/crossbeam-utils-0.8.23/src/backoff.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/crossbeam-utils-0.8.23/src/sync/mod.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/crossbeam-utils-0.8.23/src/sync/once_lock.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/crossbeam-utils-0.8.23/src/sync/parker.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/crossbeam-utils-0.8.23/src/sync/sharded_lock.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/crossbeam-utils-0.8.23/src/sync/wait_group.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/crossbeam-utils-0.8.23/src/thread.rs + +/Users/bt/dev/storyteller/craft-apps/effectcraft/.claude/worktrees/agent-a5ec24c82e394a621/crates/hevcenc/target/agent-codec-hevc/debug/deps/libcrossbeam_utils-799e464d44773a7d.rlib: /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/crossbeam-utils-0.8.23/src/lib.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/crossbeam-utils-0.8.23/src/atomic/mod.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/crossbeam-utils-0.8.23/src/atomic/seq_lock.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/crossbeam-utils-0.8.23/src/atomic/atomic_cell.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/crossbeam-utils-0.8.23/src/atomic/consume.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/crossbeam-utils-0.8.23/src/cache_padded.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/crossbeam-utils-0.8.23/src/backoff.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/crossbeam-utils-0.8.23/src/sync/mod.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/crossbeam-utils-0.8.23/src/sync/once_lock.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/crossbeam-utils-0.8.23/src/sync/parker.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/crossbeam-utils-0.8.23/src/sync/sharded_lock.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/crossbeam-utils-0.8.23/src/sync/wait_group.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/crossbeam-utils-0.8.23/src/thread.rs + +/Users/bt/dev/storyteller/craft-apps/effectcraft/.claude/worktrees/agent-a5ec24c82e394a621/crates/hevcenc/target/agent-codec-hevc/debug/deps/libcrossbeam_utils-799e464d44773a7d.rmeta: /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/crossbeam-utils-0.8.23/src/lib.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/crossbeam-utils-0.8.23/src/atomic/mod.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/crossbeam-utils-0.8.23/src/atomic/seq_lock.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/crossbeam-utils-0.8.23/src/atomic/atomic_cell.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/crossbeam-utils-0.8.23/src/atomic/consume.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/crossbeam-utils-0.8.23/src/cache_padded.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/crossbeam-utils-0.8.23/src/backoff.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/crossbeam-utils-0.8.23/src/sync/mod.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/crossbeam-utils-0.8.23/src/sync/once_lock.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/crossbeam-utils-0.8.23/src/sync/parker.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/crossbeam-utils-0.8.23/src/sync/sharded_lock.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/crossbeam-utils-0.8.23/src/sync/wait_group.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/crossbeam-utils-0.8.23/src/thread.rs + +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/crossbeam-utils-0.8.23/src/lib.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/crossbeam-utils-0.8.23/src/atomic/mod.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/crossbeam-utils-0.8.23/src/atomic/seq_lock.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/crossbeam-utils-0.8.23/src/atomic/atomic_cell.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/crossbeam-utils-0.8.23/src/atomic/consume.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/crossbeam-utils-0.8.23/src/cache_padded.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/crossbeam-utils-0.8.23/src/backoff.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/crossbeam-utils-0.8.23/src/sync/mod.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/crossbeam-utils-0.8.23/src/sync/once_lock.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/crossbeam-utils-0.8.23/src/sync/parker.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/crossbeam-utils-0.8.23/src/sync/sharded_lock.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/crossbeam-utils-0.8.23/src/sync/wait_group.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/crossbeam-utils-0.8.23/src/thread.rs: diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/deps/effectcraft_hevcenc-955aee250b2c2a4e.d b/crates/hevcenc/target/agent-codec-hevc/debug/deps/effectcraft_hevcenc-955aee250b2c2a4e.d new file mode 100644 index 00000000..f618c9ca --- /dev/null +++ b/crates/hevcenc/target/agent-codec-hevc/debug/deps/effectcraft_hevcenc-955aee250b2c2a4e.d @@ -0,0 +1,16 @@ +/Users/bt/dev/storyteller/craft-apps/effectcraft/.claude/worktrees/agent-a5ec24c82e394a621/crates/hevcenc/target/agent-codec-hevc/debug/deps/effectcraft_hevcenc-955aee250b2c2a4e.d: crates/hevcenc/src/lib.rs crates/hevcenc/src/bitstream.rs crates/hevcenc/src/cabac.rs crates/hevcenc/src/frame.rs crates/hevcenc/src/inter.rs crates/hevcenc/src/intra.rs crates/hevcenc/src/me.rs crates/hevcenc/src/params.rs crates/hevcenc/src/transform.rs crates/hevcenc/src/tables.rs + +/Users/bt/dev/storyteller/craft-apps/effectcraft/.claude/worktrees/agent-a5ec24c82e394a621/crates/hevcenc/target/agent-codec-hevc/debug/deps/libeffectcraft_hevcenc-955aee250b2c2a4e.rlib: crates/hevcenc/src/lib.rs crates/hevcenc/src/bitstream.rs crates/hevcenc/src/cabac.rs crates/hevcenc/src/frame.rs crates/hevcenc/src/inter.rs crates/hevcenc/src/intra.rs crates/hevcenc/src/me.rs crates/hevcenc/src/params.rs crates/hevcenc/src/transform.rs crates/hevcenc/src/tables.rs + +/Users/bt/dev/storyteller/craft-apps/effectcraft/.claude/worktrees/agent-a5ec24c82e394a621/crates/hevcenc/target/agent-codec-hevc/debug/deps/libeffectcraft_hevcenc-955aee250b2c2a4e.rmeta: crates/hevcenc/src/lib.rs crates/hevcenc/src/bitstream.rs crates/hevcenc/src/cabac.rs crates/hevcenc/src/frame.rs crates/hevcenc/src/inter.rs crates/hevcenc/src/intra.rs crates/hevcenc/src/me.rs crates/hevcenc/src/params.rs crates/hevcenc/src/transform.rs crates/hevcenc/src/tables.rs + +crates/hevcenc/src/lib.rs: +crates/hevcenc/src/bitstream.rs: +crates/hevcenc/src/cabac.rs: +crates/hevcenc/src/frame.rs: +crates/hevcenc/src/inter.rs: +crates/hevcenc/src/intra.rs: +crates/hevcenc/src/me.rs: +crates/hevcenc/src/params.rs: +crates/hevcenc/src/transform.rs: +crates/hevcenc/src/tables.rs: diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/deps/either-72952a75ef45a16d.d b/crates/hevcenc/target/agent-codec-hevc/debug/deps/either-72952a75ef45a16d.d new file mode 100644 index 00000000..20801a0d --- /dev/null +++ b/crates/hevcenc/target/agent-codec-hevc/debug/deps/either-72952a75ef45a16d.d @@ -0,0 +1,9 @@ +/Users/bt/dev/storyteller/craft-apps/effectcraft/.claude/worktrees/agent-a5ec24c82e394a621/crates/hevcenc/target/agent-codec-hevc/debug/deps/either-72952a75ef45a16d.d: /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/either-1.18.0/src/lib.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/either-1.18.0/src/iterator.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/either-1.18.0/src/into_either.rs + +/Users/bt/dev/storyteller/craft-apps/effectcraft/.claude/worktrees/agent-a5ec24c82e394a621/crates/hevcenc/target/agent-codec-hevc/debug/deps/libeither-72952a75ef45a16d.rlib: /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/either-1.18.0/src/lib.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/either-1.18.0/src/iterator.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/either-1.18.0/src/into_either.rs + +/Users/bt/dev/storyteller/craft-apps/effectcraft/.claude/worktrees/agent-a5ec24c82e394a621/crates/hevcenc/target/agent-codec-hevc/debug/deps/libeither-72952a75ef45a16d.rmeta: /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/either-1.18.0/src/lib.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/either-1.18.0/src/iterator.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/either-1.18.0/src/into_either.rs + +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/either-1.18.0/src/lib.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/either-1.18.0/src/iterator.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/either-1.18.0/src/into_either.rs: diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/deps/filmcraft_bitstream-2ad770771b023a77.d b/crates/hevcenc/target/agent-codec-hevc/debug/deps/filmcraft_bitstream-2ad770771b023a77.d new file mode 100644 index 00000000..63971aef --- /dev/null +++ b/crates/hevcenc/target/agent-codec-hevc/debug/deps/filmcraft_bitstream-2ad770771b023a77.d @@ -0,0 +1,7 @@ +/Users/bt/dev/storyteller/craft-apps/effectcraft/.claude/worktrees/agent-a5ec24c82e394a621/crates/hevcenc/target/agent-codec-hevc/debug/deps/filmcraft_bitstream-2ad770771b023a77.d: /Users/bt/.cargo/git/checkouts/filmcraft-58288263de9b27d9/89cfbf2/crates/bitstream/src/lib.rs + +/Users/bt/dev/storyteller/craft-apps/effectcraft/.claude/worktrees/agent-a5ec24c82e394a621/crates/hevcenc/target/agent-codec-hevc/debug/deps/libfilmcraft_bitstream-2ad770771b023a77.rlib: /Users/bt/.cargo/git/checkouts/filmcraft-58288263de9b27d9/89cfbf2/crates/bitstream/src/lib.rs + +/Users/bt/dev/storyteller/craft-apps/effectcraft/.claude/worktrees/agent-a5ec24c82e394a621/crates/hevcenc/target/agent-codec-hevc/debug/deps/libfilmcraft_bitstream-2ad770771b023a77.rmeta: /Users/bt/.cargo/git/checkouts/filmcraft-58288263de9b27d9/89cfbf2/crates/bitstream/src/lib.rs + +/Users/bt/.cargo/git/checkouts/filmcraft-58288263de9b27d9/89cfbf2/crates/bitstream/src/lib.rs: diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/deps/filmcraft_hevc-71cf1ac5fd5debba.d b/crates/hevcenc/target/agent-codec-hevc/debug/deps/filmcraft_hevc-71cf1ac5fd5debba.d new file mode 100644 index 00000000..e6fcfb02 --- /dev/null +++ b/crates/hevcenc/target/agent-codec-hevc/debug/deps/filmcraft_hevc-71cf1ac5fd5debba.d @@ -0,0 +1,22 @@ +/Users/bt/dev/storyteller/craft-apps/effectcraft/.claude/worktrees/agent-a5ec24c82e394a621/crates/hevcenc/target/agent-codec-hevc/debug/deps/filmcraft_hevc-71cf1ac5fd5debba.d: /Users/bt/.cargo/git/checkouts/filmcraft-58288263de9b27d9/89cfbf2/crates/hevc/src/lib.rs /Users/bt/.cargo/git/checkouts/filmcraft-58288263de9b27d9/89cfbf2/crates/hevc/src/cabac.rs /Users/bt/.cargo/git/checkouts/filmcraft-58288263de9b27d9/89cfbf2/crates/hevc/src/decoder.rs /Users/bt/.cargo/git/checkouts/filmcraft-58288263de9b27d9/89cfbf2/crates/hevc/src/dpb.rs /Users/bt/.cargo/git/checkouts/filmcraft-58288263de9b27d9/89cfbf2/crates/hevc/src/error.rs /Users/bt/.cargo/git/checkouts/filmcraft-58288263de9b27d9/89cfbf2/crates/hevc/src/filter.rs /Users/bt/.cargo/git/checkouts/filmcraft-58288263de9b27d9/89cfbf2/crates/hevc/src/inter.rs /Users/bt/.cargo/git/checkouts/filmcraft-58288263de9b27d9/89cfbf2/crates/hevc/src/intra.rs /Users/bt/.cargo/git/checkouts/filmcraft-58288263de9b27d9/89cfbf2/crates/hevc/src/mvpred.rs /Users/bt/.cargo/git/checkouts/filmcraft-58288263de9b27d9/89cfbf2/crates/hevc/src/params.rs /Users/bt/.cargo/git/checkouts/filmcraft-58288263de9b27d9/89cfbf2/crates/hevc/src/picture.rs /Users/bt/.cargo/git/checkouts/filmcraft-58288263de9b27d9/89cfbf2/crates/hevc/src/slice.rs /Users/bt/.cargo/git/checkouts/filmcraft-58288263de9b27d9/89cfbf2/crates/hevc/src/slicedec.rs /Users/bt/.cargo/git/checkouts/filmcraft-58288263de9b27d9/89cfbf2/crates/hevc/src/tables.rs /Users/bt/.cargo/git/checkouts/filmcraft-58288263de9b27d9/89cfbf2/crates/hevc/src/transform.rs /Users/bt/.cargo/git/checkouts/filmcraft-58288263de9b27d9/89cfbf2/crates/hevc/src/spec_tables.rs + +/Users/bt/dev/storyteller/craft-apps/effectcraft/.claude/worktrees/agent-a5ec24c82e394a621/crates/hevcenc/target/agent-codec-hevc/debug/deps/libfilmcraft_hevc-71cf1ac5fd5debba.rlib: /Users/bt/.cargo/git/checkouts/filmcraft-58288263de9b27d9/89cfbf2/crates/hevc/src/lib.rs /Users/bt/.cargo/git/checkouts/filmcraft-58288263de9b27d9/89cfbf2/crates/hevc/src/cabac.rs /Users/bt/.cargo/git/checkouts/filmcraft-58288263de9b27d9/89cfbf2/crates/hevc/src/decoder.rs /Users/bt/.cargo/git/checkouts/filmcraft-58288263de9b27d9/89cfbf2/crates/hevc/src/dpb.rs /Users/bt/.cargo/git/checkouts/filmcraft-58288263de9b27d9/89cfbf2/crates/hevc/src/error.rs /Users/bt/.cargo/git/checkouts/filmcraft-58288263de9b27d9/89cfbf2/crates/hevc/src/filter.rs /Users/bt/.cargo/git/checkouts/filmcraft-58288263de9b27d9/89cfbf2/crates/hevc/src/inter.rs /Users/bt/.cargo/git/checkouts/filmcraft-58288263de9b27d9/89cfbf2/crates/hevc/src/intra.rs /Users/bt/.cargo/git/checkouts/filmcraft-58288263de9b27d9/89cfbf2/crates/hevc/src/mvpred.rs /Users/bt/.cargo/git/checkouts/filmcraft-58288263de9b27d9/89cfbf2/crates/hevc/src/params.rs /Users/bt/.cargo/git/checkouts/filmcraft-58288263de9b27d9/89cfbf2/crates/hevc/src/picture.rs /Users/bt/.cargo/git/checkouts/filmcraft-58288263de9b27d9/89cfbf2/crates/hevc/src/slice.rs /Users/bt/.cargo/git/checkouts/filmcraft-58288263de9b27d9/89cfbf2/crates/hevc/src/slicedec.rs /Users/bt/.cargo/git/checkouts/filmcraft-58288263de9b27d9/89cfbf2/crates/hevc/src/tables.rs /Users/bt/.cargo/git/checkouts/filmcraft-58288263de9b27d9/89cfbf2/crates/hevc/src/transform.rs /Users/bt/.cargo/git/checkouts/filmcraft-58288263de9b27d9/89cfbf2/crates/hevc/src/spec_tables.rs + +/Users/bt/dev/storyteller/craft-apps/effectcraft/.claude/worktrees/agent-a5ec24c82e394a621/crates/hevcenc/target/agent-codec-hevc/debug/deps/libfilmcraft_hevc-71cf1ac5fd5debba.rmeta: /Users/bt/.cargo/git/checkouts/filmcraft-58288263de9b27d9/89cfbf2/crates/hevc/src/lib.rs /Users/bt/.cargo/git/checkouts/filmcraft-58288263de9b27d9/89cfbf2/crates/hevc/src/cabac.rs /Users/bt/.cargo/git/checkouts/filmcraft-58288263de9b27d9/89cfbf2/crates/hevc/src/decoder.rs /Users/bt/.cargo/git/checkouts/filmcraft-58288263de9b27d9/89cfbf2/crates/hevc/src/dpb.rs /Users/bt/.cargo/git/checkouts/filmcraft-58288263de9b27d9/89cfbf2/crates/hevc/src/error.rs /Users/bt/.cargo/git/checkouts/filmcraft-58288263de9b27d9/89cfbf2/crates/hevc/src/filter.rs /Users/bt/.cargo/git/checkouts/filmcraft-58288263de9b27d9/89cfbf2/crates/hevc/src/inter.rs /Users/bt/.cargo/git/checkouts/filmcraft-58288263de9b27d9/89cfbf2/crates/hevc/src/intra.rs /Users/bt/.cargo/git/checkouts/filmcraft-58288263de9b27d9/89cfbf2/crates/hevc/src/mvpred.rs /Users/bt/.cargo/git/checkouts/filmcraft-58288263de9b27d9/89cfbf2/crates/hevc/src/params.rs /Users/bt/.cargo/git/checkouts/filmcraft-58288263de9b27d9/89cfbf2/crates/hevc/src/picture.rs /Users/bt/.cargo/git/checkouts/filmcraft-58288263de9b27d9/89cfbf2/crates/hevc/src/slice.rs /Users/bt/.cargo/git/checkouts/filmcraft-58288263de9b27d9/89cfbf2/crates/hevc/src/slicedec.rs /Users/bt/.cargo/git/checkouts/filmcraft-58288263de9b27d9/89cfbf2/crates/hevc/src/tables.rs /Users/bt/.cargo/git/checkouts/filmcraft-58288263de9b27d9/89cfbf2/crates/hevc/src/transform.rs /Users/bt/.cargo/git/checkouts/filmcraft-58288263de9b27d9/89cfbf2/crates/hevc/src/spec_tables.rs + +/Users/bt/.cargo/git/checkouts/filmcraft-58288263de9b27d9/89cfbf2/crates/hevc/src/lib.rs: +/Users/bt/.cargo/git/checkouts/filmcraft-58288263de9b27d9/89cfbf2/crates/hevc/src/cabac.rs: +/Users/bt/.cargo/git/checkouts/filmcraft-58288263de9b27d9/89cfbf2/crates/hevc/src/decoder.rs: +/Users/bt/.cargo/git/checkouts/filmcraft-58288263de9b27d9/89cfbf2/crates/hevc/src/dpb.rs: +/Users/bt/.cargo/git/checkouts/filmcraft-58288263de9b27d9/89cfbf2/crates/hevc/src/error.rs: +/Users/bt/.cargo/git/checkouts/filmcraft-58288263de9b27d9/89cfbf2/crates/hevc/src/filter.rs: +/Users/bt/.cargo/git/checkouts/filmcraft-58288263de9b27d9/89cfbf2/crates/hevc/src/inter.rs: +/Users/bt/.cargo/git/checkouts/filmcraft-58288263de9b27d9/89cfbf2/crates/hevc/src/intra.rs: +/Users/bt/.cargo/git/checkouts/filmcraft-58288263de9b27d9/89cfbf2/crates/hevc/src/mvpred.rs: +/Users/bt/.cargo/git/checkouts/filmcraft-58288263de9b27d9/89cfbf2/crates/hevc/src/params.rs: +/Users/bt/.cargo/git/checkouts/filmcraft-58288263de9b27d9/89cfbf2/crates/hevc/src/picture.rs: +/Users/bt/.cargo/git/checkouts/filmcraft-58288263de9b27d9/89cfbf2/crates/hevc/src/slice.rs: +/Users/bt/.cargo/git/checkouts/filmcraft-58288263de9b27d9/89cfbf2/crates/hevc/src/slicedec.rs: +/Users/bt/.cargo/git/checkouts/filmcraft-58288263de9b27d9/89cfbf2/crates/hevc/src/tables.rs: +/Users/bt/.cargo/git/checkouts/filmcraft-58288263de9b27d9/89cfbf2/crates/hevc/src/transform.rs: +/Users/bt/.cargo/git/checkouts/filmcraft-58288263de9b27d9/89cfbf2/crates/hevc/src/spec_tables.rs: diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/deps/libcrossbeam_deque-d9775930f3a59590.rlib b/crates/hevcenc/target/agent-codec-hevc/debug/deps/libcrossbeam_deque-d9775930f3a59590.rlib new file mode 100644 index 00000000..0b2cd1e6 Binary files /dev/null and b/crates/hevcenc/target/agent-codec-hevc/debug/deps/libcrossbeam_deque-d9775930f3a59590.rlib differ diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/deps/libcrossbeam_deque-d9775930f3a59590.rmeta b/crates/hevcenc/target/agent-codec-hevc/debug/deps/libcrossbeam_deque-d9775930f3a59590.rmeta new file mode 100644 index 00000000..9f91813c Binary files /dev/null and b/crates/hevcenc/target/agent-codec-hevc/debug/deps/libcrossbeam_deque-d9775930f3a59590.rmeta differ diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/deps/libcrossbeam_epoch-72c18a120600cf42.rlib b/crates/hevcenc/target/agent-codec-hevc/debug/deps/libcrossbeam_epoch-72c18a120600cf42.rlib new file mode 100644 index 00000000..1deb140e Binary files /dev/null and b/crates/hevcenc/target/agent-codec-hevc/debug/deps/libcrossbeam_epoch-72c18a120600cf42.rlib differ diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/deps/libcrossbeam_epoch-72c18a120600cf42.rmeta b/crates/hevcenc/target/agent-codec-hevc/debug/deps/libcrossbeam_epoch-72c18a120600cf42.rmeta new file mode 100644 index 00000000..f969e16e Binary files /dev/null and b/crates/hevcenc/target/agent-codec-hevc/debug/deps/libcrossbeam_epoch-72c18a120600cf42.rmeta differ diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/deps/libcrossbeam_utils-799e464d44773a7d.rlib b/crates/hevcenc/target/agent-codec-hevc/debug/deps/libcrossbeam_utils-799e464d44773a7d.rlib new file mode 100644 index 00000000..f1da5d81 Binary files /dev/null and b/crates/hevcenc/target/agent-codec-hevc/debug/deps/libcrossbeam_utils-799e464d44773a7d.rlib differ diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/deps/libcrossbeam_utils-799e464d44773a7d.rmeta b/crates/hevcenc/target/agent-codec-hevc/debug/deps/libcrossbeam_utils-799e464d44773a7d.rmeta new file mode 100644 index 00000000..ca423860 Binary files /dev/null and b/crates/hevcenc/target/agent-codec-hevc/debug/deps/libcrossbeam_utils-799e464d44773a7d.rmeta differ diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/deps/libeffectcraft_hevcenc-955aee250b2c2a4e.rlib b/crates/hevcenc/target/agent-codec-hevc/debug/deps/libeffectcraft_hevcenc-955aee250b2c2a4e.rlib new file mode 100644 index 00000000..5e5e2f50 Binary files /dev/null and b/crates/hevcenc/target/agent-codec-hevc/debug/deps/libeffectcraft_hevcenc-955aee250b2c2a4e.rlib differ diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/deps/libeffectcraft_hevcenc-955aee250b2c2a4e.rmeta b/crates/hevcenc/target/agent-codec-hevc/debug/deps/libeffectcraft_hevcenc-955aee250b2c2a4e.rmeta new file mode 100644 index 00000000..03d053ff Binary files /dev/null and b/crates/hevcenc/target/agent-codec-hevc/debug/deps/libeffectcraft_hevcenc-955aee250b2c2a4e.rmeta differ diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/deps/libeither-72952a75ef45a16d.rlib b/crates/hevcenc/target/agent-codec-hevc/debug/deps/libeither-72952a75ef45a16d.rlib new file mode 100644 index 00000000..c8e72a48 Binary files /dev/null and b/crates/hevcenc/target/agent-codec-hevc/debug/deps/libeither-72952a75ef45a16d.rlib differ diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/deps/libeither-72952a75ef45a16d.rmeta b/crates/hevcenc/target/agent-codec-hevc/debug/deps/libeither-72952a75ef45a16d.rmeta new file mode 100644 index 00000000..6430cb84 Binary files /dev/null and b/crates/hevcenc/target/agent-codec-hevc/debug/deps/libeither-72952a75ef45a16d.rmeta differ diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/deps/libfilmcraft_bitstream-2ad770771b023a77.rlib b/crates/hevcenc/target/agent-codec-hevc/debug/deps/libfilmcraft_bitstream-2ad770771b023a77.rlib new file mode 100644 index 00000000..495842e6 Binary files /dev/null and b/crates/hevcenc/target/agent-codec-hevc/debug/deps/libfilmcraft_bitstream-2ad770771b023a77.rlib differ diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/deps/libfilmcraft_bitstream-2ad770771b023a77.rmeta b/crates/hevcenc/target/agent-codec-hevc/debug/deps/libfilmcraft_bitstream-2ad770771b023a77.rmeta new file mode 100644 index 00000000..3185389e Binary files /dev/null and b/crates/hevcenc/target/agent-codec-hevc/debug/deps/libfilmcraft_bitstream-2ad770771b023a77.rmeta differ diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/deps/libfilmcraft_hevc-71cf1ac5fd5debba.rlib b/crates/hevcenc/target/agent-codec-hevc/debug/deps/libfilmcraft_hevc-71cf1ac5fd5debba.rlib new file mode 100644 index 00000000..cb28bbc2 Binary files /dev/null and b/crates/hevcenc/target/agent-codec-hevc/debug/deps/libfilmcraft_hevc-71cf1ac5fd5debba.rlib differ diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/deps/libfilmcraft_hevc-71cf1ac5fd5debba.rmeta b/crates/hevcenc/target/agent-codec-hevc/debug/deps/libfilmcraft_hevc-71cf1ac5fd5debba.rmeta new file mode 100644 index 00000000..39c9f55a Binary files /dev/null and b/crates/hevcenc/target/agent-codec-hevc/debug/deps/libfilmcraft_hevc-71cf1ac5fd5debba.rmeta differ diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/deps/libproc_macro2-67c058b976f66fb4.rlib b/crates/hevcenc/target/agent-codec-hevc/debug/deps/libproc_macro2-67c058b976f66fb4.rlib new file mode 100644 index 00000000..01bf2210 Binary files /dev/null and b/crates/hevcenc/target/agent-codec-hevc/debug/deps/libproc_macro2-67c058b976f66fb4.rlib differ diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/deps/libproc_macro2-67c058b976f66fb4.rmeta b/crates/hevcenc/target/agent-codec-hevc/debug/deps/libproc_macro2-67c058b976f66fb4.rmeta new file mode 100644 index 00000000..adb9c960 Binary files /dev/null and b/crates/hevcenc/target/agent-codec-hevc/debug/deps/libproc_macro2-67c058b976f66fb4.rmeta differ diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/deps/libquote-0f85966d6f134e44.rlib b/crates/hevcenc/target/agent-codec-hevc/debug/deps/libquote-0f85966d6f134e44.rlib new file mode 100644 index 00000000..247a152c Binary files /dev/null and b/crates/hevcenc/target/agent-codec-hevc/debug/deps/libquote-0f85966d6f134e44.rlib differ diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/deps/libquote-0f85966d6f134e44.rmeta b/crates/hevcenc/target/agent-codec-hevc/debug/deps/libquote-0f85966d6f134e44.rmeta new file mode 100644 index 00000000..91bcca08 Binary files /dev/null and b/crates/hevcenc/target/agent-codec-hevc/debug/deps/libquote-0f85966d6f134e44.rmeta differ diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/deps/librayon-6e2dc17155d9fd34.rlib b/crates/hevcenc/target/agent-codec-hevc/debug/deps/librayon-6e2dc17155d9fd34.rlib new file mode 100644 index 00000000..a84d8dc5 Binary files /dev/null and b/crates/hevcenc/target/agent-codec-hevc/debug/deps/librayon-6e2dc17155d9fd34.rlib differ diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/deps/librayon-6e2dc17155d9fd34.rmeta b/crates/hevcenc/target/agent-codec-hevc/debug/deps/librayon-6e2dc17155d9fd34.rmeta new file mode 100644 index 00000000..3775073c Binary files /dev/null and b/crates/hevcenc/target/agent-codec-hevc/debug/deps/librayon-6e2dc17155d9fd34.rmeta differ diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/deps/librayon_core-d9d479d6cc7ea71c.rlib b/crates/hevcenc/target/agent-codec-hevc/debug/deps/librayon_core-d9d479d6cc7ea71c.rlib new file mode 100644 index 00000000..23605122 Binary files /dev/null and b/crates/hevcenc/target/agent-codec-hevc/debug/deps/librayon_core-d9d479d6cc7ea71c.rlib differ diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/deps/librayon_core-d9d479d6cc7ea71c.rmeta b/crates/hevcenc/target/agent-codec-hevc/debug/deps/librayon_core-d9d479d6cc7ea71c.rmeta new file mode 100644 index 00000000..a9861567 Binary files /dev/null and b/crates/hevcenc/target/agent-codec-hevc/debug/deps/librayon_core-d9d479d6cc7ea71c.rmeta differ diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/deps/libsyn-86f5aa6c6ace4240.rlib b/crates/hevcenc/target/agent-codec-hevc/debug/deps/libsyn-86f5aa6c6ace4240.rlib new file mode 100644 index 00000000..50a6d640 Binary files /dev/null and b/crates/hevcenc/target/agent-codec-hevc/debug/deps/libsyn-86f5aa6c6ace4240.rlib differ diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/deps/libsyn-86f5aa6c6ace4240.rmeta b/crates/hevcenc/target/agent-codec-hevc/debug/deps/libsyn-86f5aa6c6ace4240.rmeta new file mode 100644 index 00000000..082563d9 Binary files /dev/null and b/crates/hevcenc/target/agent-codec-hevc/debug/deps/libsyn-86f5aa6c6ace4240.rmeta differ diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/deps/libthiserror-572b221996b5f3ee.rlib b/crates/hevcenc/target/agent-codec-hevc/debug/deps/libthiserror-572b221996b5f3ee.rlib new file mode 100644 index 00000000..68125455 Binary files /dev/null and b/crates/hevcenc/target/agent-codec-hevc/debug/deps/libthiserror-572b221996b5f3ee.rlib differ diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/deps/libthiserror-572b221996b5f3ee.rmeta b/crates/hevcenc/target/agent-codec-hevc/debug/deps/libthiserror-572b221996b5f3ee.rmeta new file mode 100644 index 00000000..35caf1b2 Binary files /dev/null and b/crates/hevcenc/target/agent-codec-hevc/debug/deps/libthiserror-572b221996b5f3ee.rmeta differ diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/deps/libthiserror_impl-40d32b56c8547825.dylib b/crates/hevcenc/target/agent-codec-hevc/debug/deps/libthiserror_impl-40d32b56c8547825.dylib new file mode 100755 index 00000000..ccb90c50 Binary files /dev/null and b/crates/hevcenc/target/agent-codec-hevc/debug/deps/libthiserror_impl-40d32b56c8547825.dylib differ diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/deps/libunicode_ident-fb577f96b63f8d5a.rlib b/crates/hevcenc/target/agent-codec-hevc/debug/deps/libunicode_ident-fb577f96b63f8d5a.rlib new file mode 100644 index 00000000..386642d7 Binary files /dev/null and b/crates/hevcenc/target/agent-codec-hevc/debug/deps/libunicode_ident-fb577f96b63f8d5a.rlib differ diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/deps/libunicode_ident-fb577f96b63f8d5a.rmeta b/crates/hevcenc/target/agent-codec-hevc/debug/deps/libunicode_ident-fb577f96b63f8d5a.rmeta new file mode 100644 index 00000000..e816378f Binary files /dev/null and b/crates/hevcenc/target/agent-codec-hevc/debug/deps/libunicode_ident-fb577f96b63f8d5a.rmeta differ diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/deps/proc_macro2-67c058b976f66fb4.d b/crates/hevcenc/target/agent-codec-hevc/debug/deps/proc_macro2-67c058b976f66fb4.d new file mode 100644 index 00000000..ddbe1a4f --- /dev/null +++ b/crates/hevcenc/target/agent-codec-hevc/debug/deps/proc_macro2-67c058b976f66fb4.d @@ -0,0 +1,17 @@ +/Users/bt/dev/storyteller/craft-apps/effectcraft/.claude/worktrees/agent-a5ec24c82e394a621/crates/hevcenc/target/agent-codec-hevc/debug/deps/proc_macro2-67c058b976f66fb4.d: /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/proc-macro2-1.0.107/src/lib.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/proc-macro2-1.0.107/src/marker.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/proc-macro2-1.0.107/src/parse.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/proc-macro2-1.0.107/src/probe.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/proc-macro2-1.0.107/src/probe/proc_macro_span_file.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/proc-macro2-1.0.107/src/probe/proc_macro_span_location.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/proc-macro2-1.0.107/src/rcvec.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/proc-macro2-1.0.107/src/detection.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/proc-macro2-1.0.107/src/fallback.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/proc-macro2-1.0.107/src/extra.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/proc-macro2-1.0.107/src/wrapper.rs + +/Users/bt/dev/storyteller/craft-apps/effectcraft/.claude/worktrees/agent-a5ec24c82e394a621/crates/hevcenc/target/agent-codec-hevc/debug/deps/libproc_macro2-67c058b976f66fb4.rlib: /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/proc-macro2-1.0.107/src/lib.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/proc-macro2-1.0.107/src/marker.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/proc-macro2-1.0.107/src/parse.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/proc-macro2-1.0.107/src/probe.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/proc-macro2-1.0.107/src/probe/proc_macro_span_file.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/proc-macro2-1.0.107/src/probe/proc_macro_span_location.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/proc-macro2-1.0.107/src/rcvec.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/proc-macro2-1.0.107/src/detection.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/proc-macro2-1.0.107/src/fallback.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/proc-macro2-1.0.107/src/extra.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/proc-macro2-1.0.107/src/wrapper.rs + +/Users/bt/dev/storyteller/craft-apps/effectcraft/.claude/worktrees/agent-a5ec24c82e394a621/crates/hevcenc/target/agent-codec-hevc/debug/deps/libproc_macro2-67c058b976f66fb4.rmeta: /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/proc-macro2-1.0.107/src/lib.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/proc-macro2-1.0.107/src/marker.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/proc-macro2-1.0.107/src/parse.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/proc-macro2-1.0.107/src/probe.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/proc-macro2-1.0.107/src/probe/proc_macro_span_file.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/proc-macro2-1.0.107/src/probe/proc_macro_span_location.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/proc-macro2-1.0.107/src/rcvec.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/proc-macro2-1.0.107/src/detection.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/proc-macro2-1.0.107/src/fallback.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/proc-macro2-1.0.107/src/extra.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/proc-macro2-1.0.107/src/wrapper.rs + +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/proc-macro2-1.0.107/src/lib.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/proc-macro2-1.0.107/src/marker.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/proc-macro2-1.0.107/src/parse.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/proc-macro2-1.0.107/src/probe.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/proc-macro2-1.0.107/src/probe/proc_macro_span_file.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/proc-macro2-1.0.107/src/probe/proc_macro_span_location.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/proc-macro2-1.0.107/src/rcvec.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/proc-macro2-1.0.107/src/detection.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/proc-macro2-1.0.107/src/fallback.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/proc-macro2-1.0.107/src/extra.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/proc-macro2-1.0.107/src/wrapper.rs: diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/deps/quote-0f85966d6f134e44.d b/crates/hevcenc/target/agent-codec-hevc/debug/deps/quote-0f85966d6f134e44.d new file mode 100644 index 00000000..abc8bb75 --- /dev/null +++ b/crates/hevcenc/target/agent-codec-hevc/debug/deps/quote-0f85966d6f134e44.d @@ -0,0 +1,13 @@ +/Users/bt/dev/storyteller/craft-apps/effectcraft/.claude/worktrees/agent-a5ec24c82e394a621/crates/hevcenc/target/agent-codec-hevc/debug/deps/quote-0f85966d6f134e44.d: /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/quote-1.0.47/src/lib.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/quote-1.0.47/src/ext.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/quote-1.0.47/src/format.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/quote-1.0.47/src/ident_fragment.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/quote-1.0.47/src/to_tokens.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/quote-1.0.47/src/runtime.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/quote-1.0.47/src/spanned.rs + +/Users/bt/dev/storyteller/craft-apps/effectcraft/.claude/worktrees/agent-a5ec24c82e394a621/crates/hevcenc/target/agent-codec-hevc/debug/deps/libquote-0f85966d6f134e44.rlib: /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/quote-1.0.47/src/lib.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/quote-1.0.47/src/ext.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/quote-1.0.47/src/format.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/quote-1.0.47/src/ident_fragment.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/quote-1.0.47/src/to_tokens.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/quote-1.0.47/src/runtime.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/quote-1.0.47/src/spanned.rs + +/Users/bt/dev/storyteller/craft-apps/effectcraft/.claude/worktrees/agent-a5ec24c82e394a621/crates/hevcenc/target/agent-codec-hevc/debug/deps/libquote-0f85966d6f134e44.rmeta: /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/quote-1.0.47/src/lib.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/quote-1.0.47/src/ext.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/quote-1.0.47/src/format.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/quote-1.0.47/src/ident_fragment.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/quote-1.0.47/src/to_tokens.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/quote-1.0.47/src/runtime.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/quote-1.0.47/src/spanned.rs + +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/quote-1.0.47/src/lib.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/quote-1.0.47/src/ext.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/quote-1.0.47/src/format.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/quote-1.0.47/src/ident_fragment.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/quote-1.0.47/src/to_tokens.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/quote-1.0.47/src/runtime.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/quote-1.0.47/src/spanned.rs: diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/deps/rayon-6e2dc17155d9fd34.d b/crates/hevcenc/target/agent-codec-hevc/debug/deps/rayon-6e2dc17155d9fd34.d new file mode 100644 index 00000000..899eea5c --- /dev/null +++ b/crates/hevcenc/target/agent-codec-hevc/debug/deps/rayon-6e2dc17155d9fd34.d @@ -0,0 +1,104 @@ +/Users/bt/dev/storyteller/craft-apps/effectcraft/.claude/worktrees/agent-a5ec24c82e394a621/crates/hevcenc/target/agent-codec-hevc/debug/deps/rayon-6e2dc17155d9fd34.d: /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/lib.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/delegate.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/private.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/split_producer.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/array.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/collections/mod.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/collections/binary_heap.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/collections/btree_map.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/collections/btree_set.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/collections/hash_map.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/collections/hash_set.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/collections/linked_list.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/collections/vec_deque.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/mod.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/plumbing/mod.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/blocks.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/chain.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/chunks.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/cloned.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/collect/mod.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/collect/consumer.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/collect/test.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/copied.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/empty.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/enumerate.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/extend.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/filter.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/filter_map.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/find.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/find_first_last/mod.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/flat_map.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/flat_map_iter.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/flatten.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/flatten_iter.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/fold.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/fold_chunks.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/fold_chunks_with.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/for_each.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/from_par_iter.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/inspect.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/interleave.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/interleave_shortest.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/intersperse.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/len.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/map.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/map_with.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/multizip.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/noop.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/once.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/panic_fuse.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/par_bridge.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/positions.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/product.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/reduce.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/repeat.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/rev.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/skip.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/skip_any.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/skip_any_while.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/splitter.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/step_by.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/sum.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/take.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/take_any.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/take_any_while.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/try_fold.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/try_reduce.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/try_reduce_with.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/unzip.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/update.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/walk_tree.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/while_some.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/zip.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/zip_eq.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/option.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/prelude.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/range.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/range_inclusive.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/result.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/slice/mod.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/slice/chunk_by.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/slice/chunks.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/slice/rchunks.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/slice/sort.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/slice/windows.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/slice/test.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/str.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/string.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/vec.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/math.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/par_either.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/compile_fail/mod.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/compile_fail/cannot_collect_filtermap_data.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/compile_fail/cannot_zip_filtered_data.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/compile_fail/cell_par_iter.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/compile_fail/must_use.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/compile_fail/no_send_par_iter.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/compile_fail/rc_par_iter.rs + +/Users/bt/dev/storyteller/craft-apps/effectcraft/.claude/worktrees/agent-a5ec24c82e394a621/crates/hevcenc/target/agent-codec-hevc/debug/deps/librayon-6e2dc17155d9fd34.rlib: /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/lib.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/delegate.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/private.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/split_producer.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/array.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/collections/mod.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/collections/binary_heap.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/collections/btree_map.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/collections/btree_set.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/collections/hash_map.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/collections/hash_set.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/collections/linked_list.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/collections/vec_deque.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/mod.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/plumbing/mod.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/blocks.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/chain.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/chunks.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/cloned.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/collect/mod.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/collect/consumer.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/collect/test.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/copied.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/empty.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/enumerate.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/extend.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/filter.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/filter_map.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/find.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/find_first_last/mod.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/flat_map.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/flat_map_iter.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/flatten.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/flatten_iter.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/fold.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/fold_chunks.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/fold_chunks_with.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/for_each.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/from_par_iter.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/inspect.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/interleave.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/interleave_shortest.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/intersperse.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/len.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/map.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/map_with.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/multizip.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/noop.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/once.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/panic_fuse.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/par_bridge.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/positions.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/product.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/reduce.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/repeat.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/rev.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/skip.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/skip_any.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/skip_any_while.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/splitter.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/step_by.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/sum.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/take.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/take_any.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/take_any_while.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/try_fold.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/try_reduce.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/try_reduce_with.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/unzip.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/update.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/walk_tree.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/while_some.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/zip.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/zip_eq.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/option.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/prelude.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/range.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/range_inclusive.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/result.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/slice/mod.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/slice/chunk_by.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/slice/chunks.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/slice/rchunks.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/slice/sort.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/slice/windows.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/slice/test.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/str.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/string.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/vec.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/math.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/par_either.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/compile_fail/mod.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/compile_fail/cannot_collect_filtermap_data.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/compile_fail/cannot_zip_filtered_data.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/compile_fail/cell_par_iter.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/compile_fail/must_use.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/compile_fail/no_send_par_iter.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/compile_fail/rc_par_iter.rs + +/Users/bt/dev/storyteller/craft-apps/effectcraft/.claude/worktrees/agent-a5ec24c82e394a621/crates/hevcenc/target/agent-codec-hevc/debug/deps/librayon-6e2dc17155d9fd34.rmeta: /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/lib.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/delegate.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/private.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/split_producer.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/array.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/collections/mod.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/collections/binary_heap.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/collections/btree_map.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/collections/btree_set.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/collections/hash_map.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/collections/hash_set.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/collections/linked_list.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/collections/vec_deque.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/mod.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/plumbing/mod.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/blocks.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/chain.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/chunks.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/cloned.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/collect/mod.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/collect/consumer.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/collect/test.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/copied.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/empty.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/enumerate.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/extend.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/filter.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/filter_map.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/find.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/find_first_last/mod.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/flat_map.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/flat_map_iter.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/flatten.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/flatten_iter.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/fold.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/fold_chunks.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/fold_chunks_with.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/for_each.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/from_par_iter.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/inspect.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/interleave.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/interleave_shortest.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/intersperse.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/len.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/map.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/map_with.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/multizip.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/noop.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/once.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/panic_fuse.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/par_bridge.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/positions.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/product.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/reduce.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/repeat.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/rev.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/skip.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/skip_any.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/skip_any_while.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/splitter.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/step_by.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/sum.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/take.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/take_any.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/take_any_while.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/try_fold.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/try_reduce.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/try_reduce_with.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/unzip.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/update.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/walk_tree.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/while_some.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/zip.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/zip_eq.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/option.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/prelude.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/range.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/range_inclusive.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/result.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/slice/mod.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/slice/chunk_by.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/slice/chunks.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/slice/rchunks.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/slice/sort.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/slice/windows.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/slice/test.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/str.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/string.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/vec.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/math.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/par_either.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/compile_fail/mod.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/compile_fail/cannot_collect_filtermap_data.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/compile_fail/cannot_zip_filtered_data.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/compile_fail/cell_par_iter.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/compile_fail/must_use.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/compile_fail/no_send_par_iter.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/compile_fail/rc_par_iter.rs + +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/lib.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/delegate.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/private.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/split_producer.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/array.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/collections/mod.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/collections/binary_heap.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/collections/btree_map.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/collections/btree_set.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/collections/hash_map.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/collections/hash_set.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/collections/linked_list.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/collections/vec_deque.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/mod.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/plumbing/mod.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/blocks.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/chain.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/chunks.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/cloned.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/collect/mod.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/collect/consumer.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/collect/test.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/copied.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/empty.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/enumerate.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/extend.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/filter.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/filter_map.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/find.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/find_first_last/mod.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/flat_map.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/flat_map_iter.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/flatten.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/flatten_iter.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/fold.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/fold_chunks.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/fold_chunks_with.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/for_each.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/from_par_iter.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/inspect.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/interleave.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/interleave_shortest.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/intersperse.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/len.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/map.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/map_with.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/multizip.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/noop.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/once.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/panic_fuse.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/par_bridge.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/positions.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/product.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/reduce.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/repeat.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/rev.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/skip.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/skip_any.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/skip_any_while.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/splitter.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/step_by.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/sum.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/take.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/take_any.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/take_any_while.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/try_fold.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/try_reduce.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/try_reduce_with.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/unzip.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/update.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/walk_tree.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/while_some.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/zip.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/iter/zip_eq.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/option.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/prelude.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/range.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/range_inclusive.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/result.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/slice/mod.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/slice/chunk_by.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/slice/chunks.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/slice/rchunks.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/slice/sort.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/slice/windows.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/slice/test.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/str.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/string.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/vec.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/math.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/par_either.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/compile_fail/mod.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/compile_fail/cannot_collect_filtermap_data.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/compile_fail/cannot_zip_filtered_data.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/compile_fail/cell_par_iter.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/compile_fail/must_use.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/compile_fail/no_send_par_iter.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-1.12.0/src/compile_fail/rc_par_iter.rs: diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/deps/rayon_core-d9d479d6cc7ea71c.d b/crates/hevcenc/target/agent-codec-hevc/debug/deps/rayon_core-d9d479d6cc7ea71c.d new file mode 100644 index 00000000..dbdcd0ae --- /dev/null +++ b/crates/hevcenc/target/agent-codec-hevc/debug/deps/rayon_core-d9d479d6cc7ea71c.d @@ -0,0 +1,29 @@ +/Users/bt/dev/storyteller/craft-apps/effectcraft/.claude/worktrees/agent-a5ec24c82e394a621/crates/hevcenc/target/agent-codec-hevc/debug/deps/rayon_core-d9d479d6cc7ea71c.d: /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-core-1.13.0/src/lib.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-core-1.13.0/src/private.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-core-1.13.0/src/broadcast/mod.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-core-1.13.0/src/broadcast/test.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-core-1.13.0/src/job.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-core-1.13.0/src/join/mod.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-core-1.13.0/src/latch.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-core-1.13.0/src/registry.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-core-1.13.0/src/scope/mod.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-core-1.13.0/src/sleep/mod.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-core-1.13.0/src/sleep/counters.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-core-1.13.0/src/spawn/mod.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-core-1.13.0/src/thread_pool/mod.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-core-1.13.0/src/thread_pool/test.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-core-1.13.0/src/unwind.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-core-1.13.0/src/compile_fail/mod.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-core-1.13.0/src/compile_fail/quicksort_race1.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-core-1.13.0/src/compile_fail/quicksort_race2.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-core-1.13.0/src/compile_fail/quicksort_race3.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-core-1.13.0/src/compile_fail/rc_return.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-core-1.13.0/src/compile_fail/rc_upvar.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-core-1.13.0/src/compile_fail/scope_join_bad.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-core-1.13.0/src/test.rs + +/Users/bt/dev/storyteller/craft-apps/effectcraft/.claude/worktrees/agent-a5ec24c82e394a621/crates/hevcenc/target/agent-codec-hevc/debug/deps/librayon_core-d9d479d6cc7ea71c.rlib: /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-core-1.13.0/src/lib.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-core-1.13.0/src/private.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-core-1.13.0/src/broadcast/mod.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-core-1.13.0/src/broadcast/test.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-core-1.13.0/src/job.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-core-1.13.0/src/join/mod.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-core-1.13.0/src/latch.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-core-1.13.0/src/registry.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-core-1.13.0/src/scope/mod.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-core-1.13.0/src/sleep/mod.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-core-1.13.0/src/sleep/counters.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-core-1.13.0/src/spawn/mod.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-core-1.13.0/src/thread_pool/mod.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-core-1.13.0/src/thread_pool/test.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-core-1.13.0/src/unwind.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-core-1.13.0/src/compile_fail/mod.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-core-1.13.0/src/compile_fail/quicksort_race1.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-core-1.13.0/src/compile_fail/quicksort_race2.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-core-1.13.0/src/compile_fail/quicksort_race3.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-core-1.13.0/src/compile_fail/rc_return.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-core-1.13.0/src/compile_fail/rc_upvar.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-core-1.13.0/src/compile_fail/scope_join_bad.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-core-1.13.0/src/test.rs + +/Users/bt/dev/storyteller/craft-apps/effectcraft/.claude/worktrees/agent-a5ec24c82e394a621/crates/hevcenc/target/agent-codec-hevc/debug/deps/librayon_core-d9d479d6cc7ea71c.rmeta: /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-core-1.13.0/src/lib.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-core-1.13.0/src/private.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-core-1.13.0/src/broadcast/mod.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-core-1.13.0/src/broadcast/test.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-core-1.13.0/src/job.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-core-1.13.0/src/join/mod.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-core-1.13.0/src/latch.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-core-1.13.0/src/registry.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-core-1.13.0/src/scope/mod.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-core-1.13.0/src/sleep/mod.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-core-1.13.0/src/sleep/counters.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-core-1.13.0/src/spawn/mod.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-core-1.13.0/src/thread_pool/mod.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-core-1.13.0/src/thread_pool/test.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-core-1.13.0/src/unwind.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-core-1.13.0/src/compile_fail/mod.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-core-1.13.0/src/compile_fail/quicksort_race1.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-core-1.13.0/src/compile_fail/quicksort_race2.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-core-1.13.0/src/compile_fail/quicksort_race3.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-core-1.13.0/src/compile_fail/rc_return.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-core-1.13.0/src/compile_fail/rc_upvar.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-core-1.13.0/src/compile_fail/scope_join_bad.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-core-1.13.0/src/test.rs + +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-core-1.13.0/src/lib.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-core-1.13.0/src/private.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-core-1.13.0/src/broadcast/mod.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-core-1.13.0/src/broadcast/test.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-core-1.13.0/src/job.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-core-1.13.0/src/join/mod.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-core-1.13.0/src/latch.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-core-1.13.0/src/registry.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-core-1.13.0/src/scope/mod.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-core-1.13.0/src/sleep/mod.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-core-1.13.0/src/sleep/counters.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-core-1.13.0/src/spawn/mod.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-core-1.13.0/src/thread_pool/mod.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-core-1.13.0/src/thread_pool/test.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-core-1.13.0/src/unwind.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-core-1.13.0/src/compile_fail/mod.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-core-1.13.0/src/compile_fail/quicksort_race1.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-core-1.13.0/src/compile_fail/quicksort_race2.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-core-1.13.0/src/compile_fail/quicksort_race3.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-core-1.13.0/src/compile_fail/rc_return.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-core-1.13.0/src/compile_fail/rc_upvar.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-core-1.13.0/src/compile_fail/scope_join_bad.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rayon-core-1.13.0/src/test.rs: diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/deps/roundtrip-335f1f822197d831 b/crates/hevcenc/target/agent-codec-hevc/debug/deps/roundtrip-335f1f822197d831 new file mode 100755 index 00000000..93b321a3 Binary files /dev/null and b/crates/hevcenc/target/agent-codec-hevc/debug/deps/roundtrip-335f1f822197d831 differ diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/deps/roundtrip-335f1f822197d831.d b/crates/hevcenc/target/agent-codec-hevc/debug/deps/roundtrip-335f1f822197d831.d new file mode 100644 index 00000000..f0407a53 --- /dev/null +++ b/crates/hevcenc/target/agent-codec-hevc/debug/deps/roundtrip-335f1f822197d831.d @@ -0,0 +1,5 @@ +/Users/bt/dev/storyteller/craft-apps/effectcraft/.claude/worktrees/agent-a5ec24c82e394a621/crates/hevcenc/target/agent-codec-hevc/debug/deps/roundtrip-335f1f822197d831.d: crates/hevcenc/tests/roundtrip.rs + +/Users/bt/dev/storyteller/craft-apps/effectcraft/.claude/worktrees/agent-a5ec24c82e394a621/crates/hevcenc/target/agent-codec-hevc/debug/deps/roundtrip-335f1f822197d831: crates/hevcenc/tests/roundtrip.rs + +crates/hevcenc/tests/roundtrip.rs: diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/deps/syn-86f5aa6c6ace4240.d b/crates/hevcenc/target/agent-codec-hevc/debug/deps/syn-86f5aa6c6ace4240.d new file mode 100644 index 00000000..f4d68481 --- /dev/null +++ b/crates/hevcenc/target/agent-codec-hevc/debug/deps/syn-86f5aa6c6ace4240.d @@ -0,0 +1,49 @@ +/Users/bt/dev/storyteller/craft-apps/effectcraft/.claude/worktrees/agent-a5ec24c82e394a621/crates/hevcenc/target/agent-codec-hevc/debug/deps/syn-86f5aa6c6ace4240.d: /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/syn-3.0.6/src/lib.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/syn-3.0.6/src/macros.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/syn-3.0.6/src/group.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/syn-3.0.6/src/token.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/syn-3.0.6/src/attr.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/syn-3.0.6/src/bigint.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/syn-3.0.6/src/buffer.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/syn-3.0.6/src/classify.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/syn-3.0.6/src/custom_keyword.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/syn-3.0.6/src/custom_punctuation.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/syn-3.0.6/src/data.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/syn-3.0.6/src/derive.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/syn-3.0.6/src/drops.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/syn-3.0.6/src/error.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/syn-3.0.6/src/expr.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/syn-3.0.6/src/ext.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/syn-3.0.6/src/fixup.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/syn-3.0.6/src/generics.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/syn-3.0.6/src/ident.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/syn-3.0.6/src/lifetime.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/syn-3.0.6/src/lit.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/syn-3.0.6/src/lookahead.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/syn-3.0.6/src/mac.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/syn-3.0.6/src/meta.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/syn-3.0.6/src/op.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/syn-3.0.6/src/parse.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/syn-3.0.6/src/discouraged.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/syn-3.0.6/src/parse_macro_input.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/syn-3.0.6/src/parse_quote.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/syn-3.0.6/src/path.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/syn-3.0.6/src/precedence.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/syn-3.0.6/src/print.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/syn-3.0.6/src/punctuated.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/syn-3.0.6/src/restriction.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/syn-3.0.6/src/sealed.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/syn-3.0.6/src/scan_expr.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/syn-3.0.6/src/span.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/syn-3.0.6/src/spanned.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/syn-3.0.6/src/thread.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/syn-3.0.6/src/ty.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/syn-3.0.6/src/verbatim.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/syn-3.0.6/src/export.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/syn-3.0.6/src/gen/clone.rs + +/Users/bt/dev/storyteller/craft-apps/effectcraft/.claude/worktrees/agent-a5ec24c82e394a621/crates/hevcenc/target/agent-codec-hevc/debug/deps/libsyn-86f5aa6c6ace4240.rlib: /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/syn-3.0.6/src/lib.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/syn-3.0.6/src/macros.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/syn-3.0.6/src/group.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/syn-3.0.6/src/token.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/syn-3.0.6/src/attr.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/syn-3.0.6/src/bigint.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/syn-3.0.6/src/buffer.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/syn-3.0.6/src/classify.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/syn-3.0.6/src/custom_keyword.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/syn-3.0.6/src/custom_punctuation.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/syn-3.0.6/src/data.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/syn-3.0.6/src/derive.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/syn-3.0.6/src/drops.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/syn-3.0.6/src/error.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/syn-3.0.6/src/expr.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/syn-3.0.6/src/ext.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/syn-3.0.6/src/fixup.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/syn-3.0.6/src/generics.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/syn-3.0.6/src/ident.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/syn-3.0.6/src/lifetime.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/syn-3.0.6/src/lit.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/syn-3.0.6/src/lookahead.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/syn-3.0.6/src/mac.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/syn-3.0.6/src/meta.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/syn-3.0.6/src/op.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/syn-3.0.6/src/parse.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/syn-3.0.6/src/discouraged.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/syn-3.0.6/src/parse_macro_input.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/syn-3.0.6/src/parse_quote.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/syn-3.0.6/src/path.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/syn-3.0.6/src/precedence.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/syn-3.0.6/src/print.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/syn-3.0.6/src/punctuated.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/syn-3.0.6/src/restriction.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/syn-3.0.6/src/sealed.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/syn-3.0.6/src/scan_expr.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/syn-3.0.6/src/span.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/syn-3.0.6/src/spanned.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/syn-3.0.6/src/thread.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/syn-3.0.6/src/ty.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/syn-3.0.6/src/verbatim.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/syn-3.0.6/src/export.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/syn-3.0.6/src/gen/clone.rs + +/Users/bt/dev/storyteller/craft-apps/effectcraft/.claude/worktrees/agent-a5ec24c82e394a621/crates/hevcenc/target/agent-codec-hevc/debug/deps/libsyn-86f5aa6c6ace4240.rmeta: /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/syn-3.0.6/src/lib.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/syn-3.0.6/src/macros.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/syn-3.0.6/src/group.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/syn-3.0.6/src/token.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/syn-3.0.6/src/attr.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/syn-3.0.6/src/bigint.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/syn-3.0.6/src/buffer.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/syn-3.0.6/src/classify.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/syn-3.0.6/src/custom_keyword.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/syn-3.0.6/src/custom_punctuation.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/syn-3.0.6/src/data.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/syn-3.0.6/src/derive.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/syn-3.0.6/src/drops.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/syn-3.0.6/src/error.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/syn-3.0.6/src/expr.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/syn-3.0.6/src/ext.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/syn-3.0.6/src/fixup.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/syn-3.0.6/src/generics.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/syn-3.0.6/src/ident.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/syn-3.0.6/src/lifetime.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/syn-3.0.6/src/lit.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/syn-3.0.6/src/lookahead.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/syn-3.0.6/src/mac.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/syn-3.0.6/src/meta.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/syn-3.0.6/src/op.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/syn-3.0.6/src/parse.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/syn-3.0.6/src/discouraged.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/syn-3.0.6/src/parse_macro_input.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/syn-3.0.6/src/parse_quote.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/syn-3.0.6/src/path.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/syn-3.0.6/src/precedence.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/syn-3.0.6/src/print.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/syn-3.0.6/src/punctuated.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/syn-3.0.6/src/restriction.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/syn-3.0.6/src/sealed.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/syn-3.0.6/src/scan_expr.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/syn-3.0.6/src/span.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/syn-3.0.6/src/spanned.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/syn-3.0.6/src/thread.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/syn-3.0.6/src/ty.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/syn-3.0.6/src/verbatim.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/syn-3.0.6/src/export.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/syn-3.0.6/src/gen/clone.rs + +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/syn-3.0.6/src/lib.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/syn-3.0.6/src/macros.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/syn-3.0.6/src/group.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/syn-3.0.6/src/token.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/syn-3.0.6/src/attr.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/syn-3.0.6/src/bigint.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/syn-3.0.6/src/buffer.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/syn-3.0.6/src/classify.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/syn-3.0.6/src/custom_keyword.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/syn-3.0.6/src/custom_punctuation.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/syn-3.0.6/src/data.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/syn-3.0.6/src/derive.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/syn-3.0.6/src/drops.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/syn-3.0.6/src/error.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/syn-3.0.6/src/expr.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/syn-3.0.6/src/ext.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/syn-3.0.6/src/fixup.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/syn-3.0.6/src/generics.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/syn-3.0.6/src/ident.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/syn-3.0.6/src/lifetime.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/syn-3.0.6/src/lit.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/syn-3.0.6/src/lookahead.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/syn-3.0.6/src/mac.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/syn-3.0.6/src/meta.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/syn-3.0.6/src/op.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/syn-3.0.6/src/parse.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/syn-3.0.6/src/discouraged.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/syn-3.0.6/src/parse_macro_input.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/syn-3.0.6/src/parse_quote.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/syn-3.0.6/src/path.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/syn-3.0.6/src/precedence.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/syn-3.0.6/src/print.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/syn-3.0.6/src/punctuated.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/syn-3.0.6/src/restriction.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/syn-3.0.6/src/sealed.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/syn-3.0.6/src/scan_expr.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/syn-3.0.6/src/span.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/syn-3.0.6/src/spanned.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/syn-3.0.6/src/thread.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/syn-3.0.6/src/ty.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/syn-3.0.6/src/verbatim.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/syn-3.0.6/src/export.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/syn-3.0.6/src/gen/clone.rs: diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/deps/thiserror-572b221996b5f3ee.d b/crates/hevcenc/target/agent-codec-hevc/debug/deps/thiserror-572b221996b5f3ee.d new file mode 100644 index 00000000..a8d25a5c --- /dev/null +++ b/crates/hevcenc/target/agent-codec-hevc/debug/deps/thiserror-572b221996b5f3ee.d @@ -0,0 +1,14 @@ +/Users/bt/dev/storyteller/craft-apps/effectcraft/.claude/worktrees/agent-a5ec24c82e394a621/crates/hevcenc/target/agent-codec-hevc/debug/deps/thiserror-572b221996b5f3ee.d: /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/thiserror-2.0.21/src/lib.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/thiserror-2.0.21/src/aserror.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/thiserror-2.0.21/src/display.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/thiserror-2.0.21/src/var.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/thiserror-2.0.21/src/private.rs /Users/bt/dev/storyteller/craft-apps/effectcraft/.claude/worktrees/agent-a5ec24c82e394a621/crates/hevcenc/target/agent-codec-hevc/debug/build/thiserror-ea6c25606e27ac64/out/private.rs + +/Users/bt/dev/storyteller/craft-apps/effectcraft/.claude/worktrees/agent-a5ec24c82e394a621/crates/hevcenc/target/agent-codec-hevc/debug/deps/libthiserror-572b221996b5f3ee.rlib: /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/thiserror-2.0.21/src/lib.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/thiserror-2.0.21/src/aserror.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/thiserror-2.0.21/src/display.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/thiserror-2.0.21/src/var.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/thiserror-2.0.21/src/private.rs /Users/bt/dev/storyteller/craft-apps/effectcraft/.claude/worktrees/agent-a5ec24c82e394a621/crates/hevcenc/target/agent-codec-hevc/debug/build/thiserror-ea6c25606e27ac64/out/private.rs + +/Users/bt/dev/storyteller/craft-apps/effectcraft/.claude/worktrees/agent-a5ec24c82e394a621/crates/hevcenc/target/agent-codec-hevc/debug/deps/libthiserror-572b221996b5f3ee.rmeta: /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/thiserror-2.0.21/src/lib.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/thiserror-2.0.21/src/aserror.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/thiserror-2.0.21/src/display.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/thiserror-2.0.21/src/var.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/thiserror-2.0.21/src/private.rs /Users/bt/dev/storyteller/craft-apps/effectcraft/.claude/worktrees/agent-a5ec24c82e394a621/crates/hevcenc/target/agent-codec-hevc/debug/build/thiserror-ea6c25606e27ac64/out/private.rs + +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/thiserror-2.0.21/src/lib.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/thiserror-2.0.21/src/aserror.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/thiserror-2.0.21/src/display.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/thiserror-2.0.21/src/var.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/thiserror-2.0.21/src/private.rs: +/Users/bt/dev/storyteller/craft-apps/effectcraft/.claude/worktrees/agent-a5ec24c82e394a621/crates/hevcenc/target/agent-codec-hevc/debug/build/thiserror-ea6c25606e27ac64/out/private.rs: + +# env-dep:OUT_DIR=/Users/bt/dev/storyteller/craft-apps/effectcraft/.claude/worktrees/agent-a5ec24c82e394a621/crates/hevcenc/target/agent-codec-hevc/debug/build/thiserror-ea6c25606e27ac64/out diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/deps/thiserror_impl-40d32b56c8547825.d b/crates/hevcenc/target/agent-codec-hevc/debug/deps/thiserror_impl-40d32b56c8547825.d new file mode 100644 index 00000000..781aff08 --- /dev/null +++ b/crates/hevcenc/target/agent-codec-hevc/debug/deps/thiserror_impl-40d32b56c8547825.d @@ -0,0 +1,17 @@ +/Users/bt/dev/storyteller/craft-apps/effectcraft/.claude/worktrees/agent-a5ec24c82e394a621/crates/hevcenc/target/agent-codec-hevc/debug/deps/thiserror_impl-40d32b56c8547825.d: /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/thiserror-impl-2.0.21/src/lib.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/thiserror-impl-2.0.21/src/ast.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/thiserror-impl-2.0.21/src/attr.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/thiserror-impl-2.0.21/src/expand.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/thiserror-impl-2.0.21/src/fallback.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/thiserror-impl-2.0.21/src/fmt.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/thiserror-impl-2.0.21/src/generics.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/thiserror-impl-2.0.21/src/prop.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/thiserror-impl-2.0.21/src/scan_expr.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/thiserror-impl-2.0.21/src/unraw.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/thiserror-impl-2.0.21/src/valid.rs + +/Users/bt/dev/storyteller/craft-apps/effectcraft/.claude/worktrees/agent-a5ec24c82e394a621/crates/hevcenc/target/agent-codec-hevc/debug/deps/libthiserror_impl-40d32b56c8547825.dylib: /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/thiserror-impl-2.0.21/src/lib.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/thiserror-impl-2.0.21/src/ast.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/thiserror-impl-2.0.21/src/attr.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/thiserror-impl-2.0.21/src/expand.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/thiserror-impl-2.0.21/src/fallback.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/thiserror-impl-2.0.21/src/fmt.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/thiserror-impl-2.0.21/src/generics.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/thiserror-impl-2.0.21/src/prop.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/thiserror-impl-2.0.21/src/scan_expr.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/thiserror-impl-2.0.21/src/unraw.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/thiserror-impl-2.0.21/src/valid.rs + +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/thiserror-impl-2.0.21/src/lib.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/thiserror-impl-2.0.21/src/ast.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/thiserror-impl-2.0.21/src/attr.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/thiserror-impl-2.0.21/src/expand.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/thiserror-impl-2.0.21/src/fallback.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/thiserror-impl-2.0.21/src/fmt.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/thiserror-impl-2.0.21/src/generics.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/thiserror-impl-2.0.21/src/prop.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/thiserror-impl-2.0.21/src/scan_expr.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/thiserror-impl-2.0.21/src/unraw.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/thiserror-impl-2.0.21/src/valid.rs: + +# env-dep:CARGO_PKG_VERSION_PATCH=21 diff --git a/crates/hevcenc/target/agent-codec-hevc/debug/deps/unicode_ident-fb577f96b63f8d5a.d b/crates/hevcenc/target/agent-codec-hevc/debug/deps/unicode_ident-fb577f96b63f8d5a.d new file mode 100644 index 00000000..b07086d8 --- /dev/null +++ b/crates/hevcenc/target/agent-codec-hevc/debug/deps/unicode_ident-fb577f96b63f8d5a.d @@ -0,0 +1,8 @@ +/Users/bt/dev/storyteller/craft-apps/effectcraft/.claude/worktrees/agent-a5ec24c82e394a621/crates/hevcenc/target/agent-codec-hevc/debug/deps/unicode_ident-fb577f96b63f8d5a.d: /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/unicode-ident-1.0.26/src/lib.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/unicode-ident-1.0.26/src/tables.rs + +/Users/bt/dev/storyteller/craft-apps/effectcraft/.claude/worktrees/agent-a5ec24c82e394a621/crates/hevcenc/target/agent-codec-hevc/debug/deps/libunicode_ident-fb577f96b63f8d5a.rlib: /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/unicode-ident-1.0.26/src/lib.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/unicode-ident-1.0.26/src/tables.rs + +/Users/bt/dev/storyteller/craft-apps/effectcraft/.claude/worktrees/agent-a5ec24c82e394a621/crates/hevcenc/target/agent-codec-hevc/debug/deps/libunicode_ident-fb577f96b63f8d5a.rmeta: /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/unicode-ident-1.0.26/src/lib.rs /Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/unicode-ident-1.0.26/src/tables.rs + +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/unicode-ident-1.0.26/src/lib.rs: +/Users/bt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/unicode-ident-1.0.26/src/tables.rs: diff --git a/crates/hevcenc/tests/roundtrip.rs b/crates/hevcenc/tests/roundtrip.rs new file mode 100644 index 00000000..777bc455 --- /dev/null +++ b/crates/hevcenc/tests/roundtrip.rs @@ -0,0 +1,413 @@ +//! Round trips through the filmcraft-hevc decoder and (when installed) ffmpeg: decoded pictures must +//! equal the encoder's own reconstruction bit-exactly. + +use effectcraft_hevcenc::{Encoder, EncoderConfig, Frame, Profile, RateControl}; +use std::process::Command; + +struct Pic { + w: usize, + y: Vec, + u: Vec, + v: Vec, +} + +impl Pic { + fn frame(&self) -> Frame<'_> { + Frame { y: &self.y, u: &self.u, v: &self.v, y_stride: self.w, uv_stride: self.w / 2 } + } +} + +/// Smooth gradients with a few hard-edged shapes, translated by (dx, dy) samples. +fn synth(w: usize, h: usize, bd: u32, dx: i32, dy: i32) -> Pic { + let max = ((1u32 << bd) - 1) as f64; + let mut y = vec![0u16; w * h]; + let mut u = vec![0u16; w * h / 4]; + let mut v = vec![0u16; w * h / 4]; + let luma = |x: i32, yy: i32| -> f64 { + let (fx, fy) = ((x - dx) as f64, (yy - dy) as f64); + let mut l = 0.25 + 0.5 * (fx / 97.0).sin().abs() * 0.6 + 0.3 * (fy / 61.0).cos().powi(2); + // a disc and a rectangle + if (fx - 40.0).powi(2) + (fy - 30.0).powi(2) < 18.0f64.powi(2) { + l = 0.85; + } + if (70.0..110.0).contains(&fx) && (50.0..64.0).contains(&fy) { + l = 0.12; + } + // fine texture + l += 0.03 * ((fx * 0.9).sin() * (fy * 0.7).cos()); + l.clamp(0.0, 1.0) + }; + for yy in 0..h { + for x in 0..w { + y[yy * w + x] = (luma(x as i32, yy as i32) * max).round() as u16; + } + } + for yy in 0..h / 2 { + for x in 0..w / 2 { + let (fx, fy) = ((2 * x) as i32 - dx, (2 * yy) as i32 - dy); + let cu = 0.5 + 0.2 * ((fx as f64) / 53.0).sin(); + let cv = 0.5 + 0.2 * ((fy as f64) / 37.0).cos() * if (fx / 16 + fy / 16) % 2 == 0 { 1.0 } else { -0.5 }; + u[yy * w / 2 + x] = (cu * max).round() as u16; + v[yy * w / 2 + x] = (cv * max).round() as u16; + } + } + Pic { w, y, u, v } +} + +fn psnr(a: &[u16], b: &[u16], bd: u32) -> f64 { + let max = ((1u32 << bd) - 1) as f64; + let mse = a.iter().zip(b).map(|(&x, &y)| (x as f64 - y as f64).powi(2)).sum::() / a.len() as f64; + if mse == 0.0 { 99.0 } else { 10.0 * (max * max / mse).log10() } +} + +struct Run { + packets: Vec>, + keys: Vec, + recons: Vec<[Vec; 3]>, + hvcc: Vec, + sets: [Vec; 3], + psnr_y: Vec, +} + +fn encode(cfg: EncoderConfig, pics: &[Pic]) -> Run { + let bd = cfg.profile.bit_depth() as u32; + let mut enc = Encoder::new(cfg).unwrap(); + let mut run = Run { packets: vec![], keys: vec![], recons: vec![], hvcc: enc.hvcc(), sets: enc.parameter_sets(), psnr_y: vec![] }; + for p in pics { + let pkt = enc.encode(&p.frame()); + let rec = enc.last_reconstruction().unwrap().clone(); + run.psnr_y.push(psnr(&p.y, &rec[0], bd)); + run.packets.push(pkt.data); + run.keys.push(pkt.keyframe); + run.recons.push(rec); + } + run +} + +/// Decode with filmcraft-hevc and compare with the reconstructions. +fn check_filmcraft(run: &Run, w: usize, h: usize) { + let mut dec = filmcraft_hevc::Decoder::from_hvcc(&run.hvcc).unwrap(); + let mut pics = Vec::new(); + for (i, p) in run.packets.iter().enumerate() { + pics.extend(dec.decode(p, i as i64).unwrap()); + } + pics.extend(dec.flush()); + assert_eq!(pics.len(), run.packets.len(), "decoded picture count"); + for (i, pic) in pics.iter().enumerate() { + assert_eq!((pic.width as usize, pic.height as usize), (w, h)); + assert_eq!(pic.pts, i as i64); + let rec = &run.recons[i]; + for (c, plane) in [&pic.y, &pic.u, &pic.v].into_iter().enumerate() { + let (pw, ph, stride) = if c == 0 { (w, h, pic.y_stride) } else { (w / 2, h / 2, pic.uv_stride) }; + for yy in 0..ph { + for x in 0..pw { + let got = plane.get(yy * stride + x); + let want = rec[c][yy * pw + x]; + assert_eq!(got, want, "picture {i} plane {c} at ({x}, {yy})"); + } + } + } + } +} + +fn annexb(run: &Run) -> Vec { + let mut out = Vec::new(); + for s in &run.sets { + out.extend_from_slice(&[0, 0, 0, 1]); + out.extend_from_slice(s); + } + for p in &run.packets { + let mut i = 0; + while i < p.len() { + let n = u32::from_be_bytes([p[i], p[i + 1], p[i + 2], p[i + 3]]) as usize; + out.extend_from_slice(&[0, 0, 0, 1]); + out.extend_from_slice(&p[i + 4..i + 4 + n]); + i += 4 + n; + } + } + out +} + +fn ffmpeg_available() -> bool { + Command::new("ffmpeg").arg("-version").output().map(|o| o.status.success()).unwrap_or(false) +} + +/// Decode the Annex-B stream with ffmpeg and require bit-exact equality and an empty stderr. +fn check_ffmpeg(run: &Run, w: usize, h: usize, bd: u32, tag: &str) { + if !ffmpeg_available() { + eprintln!("ffmpeg not found; skipping the ffmpeg oracle"); + return; + } + let dir = std::env::temp_dir(); + let uniq = format!("{}-{}-{}", tag, std::process::id(), std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_nanos()); + let inp = dir.join(format!("hevcenc-{uniq}.hevc")); + let out = dir.join(format!("hevcenc-{uniq}.yuv")); + std::fs::write(&inp, annexb(run)).unwrap(); + let pix = if bd == 8 { "yuv420p" } else { "yuv420p10le" }; + let o = Command::new("ffmpeg") + .args(["-v", "error", "-xerror", "-y", "-f", "hevc", "-i"]) + .arg(&inp) + .args(["-f", "rawvideo", "-pix_fmt", pix]) + .arg(&out) + .output() + .unwrap(); + let stderr = String::from_utf8_lossy(&o.stderr).to_string(); + let data = std::fs::read(&out).unwrap_or_default(); + let _ = std::fs::remove_file(&inp); + let _ = std::fs::remove_file(&out); + assert!(o.status.success() && stderr.trim().is_empty(), "ffmpeg failed: {stderr}"); + let bps = if bd == 8 { 1 } else { 2 }; + let frame_bytes = (w * h + 2 * (w / 2) * (h / 2)) * bps; + assert_eq!(data.len(), frame_bytes * run.packets.len(), "ffmpeg output size"); + for (i, rec) in run.recons.iter().enumerate() { + let mut off = i * frame_bytes; + for (c, plane) in rec.iter().enumerate() { + let pw = if c == 0 { w } else { w / 2 }; + for (k, &want) in plane.iter().enumerate() { + let got = if bd == 8 { data[off] as u16 } else { u16::from_le_bytes([data[off], data[off + 1]]) }; + assert_eq!(got, want, "ffmpeg: picture {i} plane {c} at ({}, {})", k % pw, k / pw); + off += bps; + } + } + } +} + +fn cfg(w: u32, h: u32, qp: u8, keyint: u32, profile: Profile) -> EncoderConfig { + let mut c = EncoderConfig::new(w, h, 30, 1); + c.rate = RateControl::ConstantQp(qp); + c.keyint = keyint; + c.profile = profile; + c +} + +#[test] +fn intra_8bit_quality_and_decoders() { + let (w, h) = (192, 128); + let pics = vec![synth(w, h, 8, 0, 0)]; + let run = encode(cfg(w as u32, h as u32, 22, 1, Profile::Main), &pics); + eprintln!("intra QP22: {} bytes, PSNR-Y {:.2}", run.packets[0].len(), run.psnr_y[0]); + assert!(run.psnr_y[0] > 35.0, "PSNR {}", run.psnr_y[0]); + check_filmcraft(&run, w, h); + check_ffmpeg(&run, w, h, 8, "intra8"); +} + +/// ffprobe reads the profile, level, colour description and timing from the parameter sets. +#[test] +fn ffprobe_stream_info() { + if !ffmpeg_available() { + return; + } + let (w, h) = (130, 72); + let pics: Vec = (0..2).map(|i| synth(w, h, 10, i, 0)).collect(); + let mut c = cfg(w as u32, h as u32, 30, 30, Profile::Main10); + c.fps_num = 30000; + c.fps_den = 1001; + c.full_range = true; + let run = encode(c, &pics); + let path = std::env::temp_dir().join(format!("hevcenc-probe-{}.hevc", std::process::id())); + std::fs::write(&path, annexb(&run)).unwrap(); + let o = Command::new("ffprobe").args(["-v", "error", "-show_streams", "-of", "flat"]).arg(&path).output().unwrap(); + let _ = std::fs::remove_file(&path); + let s = String::from_utf8_lossy(&o.stdout).to_string(); + for want in [ + "codec_name=\"hevc\"", + "profile=\"Main 10\"", + "width=130", + "height=72", + "coded_width=136", + "coded_height=72", + "pix_fmt=\"yuv420p10le\"", + "level=30", + "color_range=\"pc\"", + "color_space=\"bt709\"", + "color_transfer=\"bt709\"", + "color_primaries=\"bt709\"", + "r_frame_rate=\"30000/1001\"", + ] { + assert!(s.contains(want), "ffprobe output lacks {want}:\n{s}"); + } +} + +#[test] +fn inter_8bit_moving_pattern() { + let (w, h) = (160, 96); + let pics: Vec = (0..6).map(|i| synth(w, h, 8, 3 * i, i)).collect(); + let run = encode(cfg(w as u32, h as u32, 27, 30, Profile::Main), &pics); + let sizes: Vec = run.packets.iter().map(|p| p.len()).collect(); + eprintln!("inter sizes {sizes:?} psnr {:?}", run.psnr_y); + assert_eq!(run.keys, vec![true, false, false, false, false, false]); + let p_avg = sizes[1..].iter().sum::() as f64 / 5.0; + assert!(p_avg < sizes[0] as f64 * 0.6, "P frames {p_avg} vs I {}", sizes[0]); + assert!(run.psnr_y.iter().all(|&p| p > 33.0)); + check_filmcraft(&run, w, h); + check_ffmpeg(&run, w, h, 8, "inter8"); +} + +#[test] +fn odd_multiple_sizes_and_cropping() { + for (w, h) in [(34usize, 18usize), (66, 38), (8, 8), (130, 72), (2, 2), (2, 70), (264, 6)] { + let pics: Vec = (0..3).map(|i| synth(w, h, 8, i, 2 * i)).collect(); + let run = encode(cfg(w as u32, h as u32, 30, 2, Profile::Main), &pics); + check_filmcraft(&run, w, h); + check_ffmpeg(&run, w, h, 8, &format!("size{w}x{h}")); + } +} + +/// White noise at QP 0: large levels, escape codes with the maximum Rice parameter. +#[test] +fn noise_low_qp() { + let (w, h) = (48, 40); + let mut seed = 0x1234_5678u32; + let mut rnd = move || { + seed ^= seed << 13; + seed ^= seed >> 17; + seed ^= seed << 5; + seed + }; + for (profile, bd) in [(Profile::Main, 8u32), (Profile::Main10, 10)] { + let max = (1u32 << bd) - 1; + let pics: Vec = (0..3) + .map(|_| { + let mut p = synth(w, h, bd, 0, 0); + for v in p.y.iter_mut().chain(p.u.iter_mut()).chain(p.v.iter_mut()) { + *v = (rnd() % (max + 1)) as u16; + } + p + }) + .collect(); + let run = encode(cfg(w as u32, h as u32, 0, 2, profile), &pics); + assert!(run.psnr_y.iter().all(|&p| p > 45.0), "{:?}", run.psnr_y); + check_filmcraft(&run, w, h); + check_ffmpeg(&run, w, h, bd, &format!("noise{bd}")); + } +} + +/// High QPs: strong deblocking, large skipped CUs. +#[test] +fn high_qp_deblocking() { + let (w, h) = (200, 136); + for (qp, profile, bd) in [(38u8, Profile::Main, 8u32), (44, Profile::Main, 8), (37, Profile::Main10, 10)] { + let pics: Vec = (0..5).map(|i| synth(w, h, bd, 2 * i, i)).collect(); + let run = encode(cfg(w as u32, h as u32, qp, 4, profile), &pics); + check_filmcraft(&run, w, h); + check_ffmpeg(&run, w, h, bd, &format!("hq{qp}")); + } +} + +/// More pictures than MaxPicOrderCntLsb (256) without an IDR, then a periodic IDR: POC wrap. +#[test] +fn long_gop_poc_wrap() { + let (w, h) = (16, 16); + let pics: Vec = (0..300).map(|i| synth(w, h, 8, i % 7, i % 5)).collect(); + let run = encode(cfg(w as u32, h as u32, 30, 0, Profile::Main), &pics); + assert_eq!(run.keys.iter().filter(|&&k| k).count(), 1); + check_filmcraft(&run, w, h); + check_ffmpeg(&run, w, h, 8, "pocwrap"); + let run = encode(cfg(w as u32, h as u32, 30, 260, Profile::Main), &pics); + assert_eq!(run.keys.iter().filter(|&&k| k).count(), 2); + check_ffmpeg(&run, w, h, 8, "pocwrap2"); +} + +#[test] +fn odd_dimensions_are_rejected() { + assert!(Encoder::new(EncoderConfig::new(33, 17, 30, 1)).is_err()); +} + +#[test] +fn main10_round_trip() { + let (w, h) = (96, 64); + let pics: Vec = (0..4).map(|i| synth(w, h, 10, 2 * i, -i)).collect(); + let run = encode(cfg(w as u32, h as u32, 24, 3, Profile::Main10), &pics); + eprintln!("main10 psnr {:?}", run.psnr_y); + assert!(run.psnr_y[0] > 35.0); + check_filmcraft(&run, w, h); + check_ffmpeg(&run, w, h, 10, "main10"); +} + +#[test] +fn qp_extremes() { + let (w, h) = (64, 64); + for qp in [0u8, 51] { + let pics: Vec = (0..2).map(|i| synth(w, h, 8, i, i)).collect(); + let run = encode(cfg(w as u32, h as u32, qp, 30, Profile::Main), &pics); + check_filmcraft(&run, w, h); + check_ffmpeg(&run, w, h, 8, &format!("qp{qp}")); + } +} + +#[test] +fn bitrate_mode_hits_target() { + let (w, h) = (192, 128); + let pics: Vec = (0..30).map(|i| synth(w, h, 8, i, i / 2)).collect(); + let mut c = EncoderConfig::new(w as u32, h as u32, 30, 1); + c.rate = RateControl::Bitrate { kbps: 400 }; + c.keyint = 15; + let run = encode(c, &pics); + let bits: usize = run.packets.iter().map(|p| p.len() * 8).sum(); + let kbps = bits as f64 / (pics.len() as f64 / 30.0) / 1000.0; + eprintln!("bitrate mode: {kbps:.1} kbit/s for a 400 kbit/s target"); + assert!(kbps > 400.0 / 2.0 && kbps < 400.0 * 2.0, "{kbps}"); + check_filmcraft(&run, w, h); +} + +#[test] +fn hvcc_layout() { + let enc = Encoder::new(cfg(64, 48, 28, 30, Profile::Main10)).unwrap(); + let h = enc.hvcc(); + assert_eq!(h[0], 1); + assert_eq!(h[1] & 31, 2); // Main 10 + assert_eq!(h[21] & 3, 3); // lengthSizeMinusOne + assert_eq!(h[22], 3); // arrays + let sets = enc.parameter_sets(); + assert_eq!(sets[0][0] >> 1, 32); + assert_eq!(sets[1][0] >> 1, 33); + assert_eq!(sets[2][0] >> 1, 34); +} + +/// Speed / size / quality report on ffmpeg-generated 1080p test content: +/// `cargo test -p effectcraft-hevcenc --test roundtrip bench_1080p -- --ignored --nocapture` +/// (`HEVCENC_BENCH_SRC` selects another lavfi source). +#[test] +#[ignore] +fn bench_1080p() { + if !ffmpeg_available() { + return; + } + let (w, h, n) = (1920usize, 1080usize, 8usize); + let src = std::env::var("HEVCENC_BENCH_SRC").unwrap_or_else(|_| "testsrc2=size=1920x1080:rate=30".into()); + let out = std::env::temp_dir().join(format!("hevcenc-bench-{}.yuv", std::process::id())); + let st = Command::new("ffmpeg") + .args(["-v", "error", "-y", "-f", "lavfi", "-i", &src, "-frames:v", &n.to_string(), "-pix_fmt", "yuv420p", "-f", "rawvideo"]) + .arg(&out) + .status() + .unwrap(); + assert!(st.success()); + let raw = std::fs::read(&out).unwrap(); + let _ = std::fs::remove_file(&out); + let fsz = w * h * 3 / 2; + let to16 = |s: &[u8]| s.iter().map(|&v| v as u16).collect::>(); + let pics: Vec = (0..n) + .map(|i| { + let f = &raw[i * fsz..(i + 1) * fsz]; + Pic { w, y: to16(&f[..w * h]), u: to16(&f[w * h..w * h * 5 / 4]), v: to16(&f[w * h * 5 / 4..]) } + }) + .collect(); + for qp in [22u8, 32] { + let mut enc = Encoder::new(cfg(w as u32, h as u32, qp, 30, Profile::Main)).unwrap(); + for (i, p) in pics.iter().enumerate() { + let t = std::time::Instant::now(); + let pkt = enc.encode(&p.frame()); + let dt = t.elapsed(); + let rec = enc.last_reconstruction().unwrap(); + eprintln!( + "QP {qp} frame {i} ({}): {:7} bytes, {:6.1} ms, PSNR Y {:.2} U {:.2} V {:.2}", + if pkt.keyframe { "I" } else { "P" }, + pkt.data.len(), + dt.as_secs_f64() * 1000.0, + psnr(&p.y, &rec[0], 8), + psnr(&p.u, &rec[1], 8), + psnr(&p.v, &rec[2], 8) + ); + } + } +} diff --git a/crates/host/tests/plugins.rs b/crates/host/tests/plugins.rs index c8a9b368..83891148 100644 --- a/crates/host/tests/plugins.rs +++ b/crates/host/tests/plugins.rs @@ -104,3 +104,58 @@ fn example_wasm_plugin_loads_and_renders() { let list = s.execute("effect.plugins.list", json!({})).unwrap(); assert!(list["plugins"].as_array().unwrap().iter().any(|p| p["source"].as_str().unwrap_or("").ends_with(".wasm"))); } + +/// A parameterless plug-in module (WebAssembly text): `render` is `ec_render`'s body. +fn wat_plugin(id: &str, api: i32, render: &str) -> String { + let manifest = format!(r#"{{"api":1,"id":"{id}","name":"Host {id}","category":"Test Plug-ins","version":"1","params":[]}}"#); + let len = manifest.len(); + let escaped = manifest.replace('"', "\\\""); + format!( + r#"(module + (memory (export "memory") 2) + (data (i32.const 16) "{escaped}") + (func (export "ec_api_version") (result i32) i32.const {api}) + (func (export "ec_manifest_ptr") (result i32) i32.const 16) + (func (export "ec_manifest_len") (result i32) i32.const {len}) + (func (export "ec_alloc") (param i32) (result i32) i32.const 1024) + (func (export "ec_render") (param i32 i32 i32 i32 i32 f64 f64) (result i32) {render}))"# + ) +} + +#[test] +fn plugin_loading_reports_bad_modules_and_survives_failing_renders() { + let dir = std::env::temp_dir().join(format!("effectcraft-host-plugins-{}", std::process::id())); + std::fs::create_dir_all(&dir).unwrap(); + let files = [ + ("a-good.wasm", wat_plugin("org.test.host.good", 1, "i32.const 0").into_bytes()), + ("b-garbage.wasm", b"\0asm\x01\0\0\0garbage".to_vec()), + ("c-v2.wasm", wat_plugin("org.test.host.v2", 2, "i32.const 0").into_bytes()), + ("d-empty.wasm", vec![]), + ("e-trap.wasm", wat_plugin("org.test.host.trap", 1, "unreachable").into_bytes()), + ("notes.txt", b"not a plug-in".to_vec()), + ]; + for (name, bytes) in &files { + std::fs::write(dir.join(name), bytes).unwrap(); + } + let (mut s, l) = setup(); + let r = s.execute("effect.plugins.load", json!({"folder": dir.to_string_lossy()})).unwrap(); + let ids: Vec<&str> = r["loaded"].as_array().unwrap().iter().filter_map(|v| v["id"].as_str()).collect(); + assert_eq!(ids, ["org.test.host.good", "org.test.host.trap"], "{r}"); + let errs: Vec = r["errors"].as_array().unwrap().iter().map(|e| e["path"].as_str().unwrap().to_string()).collect(); + assert_eq!(errs.len(), 3, "{r}"); + assert!(errs.iter().all(|p| !p.ends_with(".txt"))); + assert!(r["errors"].to_string().contains("API 2"), "{r}"); + // A single file: errors come back as command errors; loading twice is refused. + let good = dir.join("a-good.wasm").to_string_lossy().to_string(); + assert!(s.execute("effect.plugins.load", json!({"path": good})).unwrap_err().to_string().contains("already registered")); + assert!(s.execute("effect.plugins.load", json!({"path": dir.join("missing.wasm").to_string_lossy()})).is_err()); + assert!(s.execute("effect.plugins.load", json!({"folder": dir.join("missing").to_string_lossy()})).is_err()); + assert!(s.execute("effect.plugins.load", json!({})).is_err()); + // A plug-in that traps on every frame leaves the layer as it was. + let before = px(&s); + s.execute("effect.apply", json!({"layer": l, "effect": "org.test.host.trap"})).unwrap(); + assert_eq!(px(&s), before); + s.execute("effect.apply", json!({"layer": l, "effect": "org.test.host.good"})).unwrap(); + assert_eq!(px(&s), before); + let _ = std::fs::remove_dir_all(&dir); +} diff --git a/crates/host/tests/vector_import.rs b/crates/host/tests/vector_import.rs index d8082573..44d2073f 100644 --- a/crates/host/tests/vector_import.rs +++ b/crates/host/tests/vector_import.rs @@ -461,3 +461,64 @@ fn psd_smart_objects_with_perspective_and_warp_bake_as_placed() { assert!(top[0] > 0.5 || top[1] > 0.5, "top centre covered {top:?}"); assert!(img.get(21, 11)[0] < 0.05 && img.get(21, 11)[1] < 0.05, "corner region empty {:?}", img.get(21, 11)); } + +/// A PDF with two placed images (one turned a quarter) between a background and a shape. +fn image_pdf() -> Vec { + let content = "0.9 0.9 0.9 rg 0 0 120 80 re f\nq 60 0 0 30 10 40 cm /Im Do Q\nq 0 40 -20 0 110 10 cm /Im Do Q\n0 0 1 rg 20 5 30 20 re f\n"; + // 8×8 pixels in quadrants: red, green / white, black. + let quad = |x: usize, y: usize| -> [u8; 3] { [[[255, 0, 0], [0, 255, 0]], [[255, 255, 255], [0, 0, 0]]][y / 4][x / 4] }; + let px: Vec = (0..64).flat_map(|i| quad(i % 8, i / 8)).collect(); + effectcraft_pdf::write::pdf( + &[ + (1, "<< /Type /Catalog /Pages 2 0 R >>".into(), None), + (2, "<< /Type /Pages /Kids [3 0 R] /Count 1 >>".into(), None), + (3, "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 120 80] /Resources << /XObject << /Im 5 0 R >> >> /Contents 4 0 R >>".into(), None), + (4, "<< >>".into(), Some(content.as_bytes().to_vec())), + (5, "<< /Type /XObject /Subtype /Image /Width 8 /Height 8 /ColorSpace /DeviceRGB /BitsPerComponent 8 >>".into(), Some(px)), + ], + 1, + ) +} + +#[test] +fn shapes_from_vector_layer_keeps_images_as_footage_layers() { + let bytes = image_pdf(); + let doc = effectcraft_pdf::parse(&bytes).unwrap(); + let (w, h) = doc.pixel_size(); + let direct = effectcraft_svg::rasterize(&doc, w, h, 1.0); + let path = tmp("images.pdf"); + std::fs::write(&path, &bytes).unwrap(); + let mut s = effectcraft_host::session(); + let item = s.execute_checked("file.import", json!({"paths": [path]})).unwrap()["items"][0].as_u64().unwrap(); + s.execute("comp.new", json!({"name": "I", "width": w, "height": h, "frameRate": 30, "duration": 1})).unwrap(); + let lid = s.execute_checked("layer.addItem", json!({"item": item})).unwrap()["layer"].as_u64().unwrap(); + let cid = s.active_comp_id().unwrap(); + let t = effectcraft_time::Tick::ZERO; + let foot = s.render(cid, t, RenderOpts::default()); + assert!(mean_diff(&foot, &direct) < 1.0 / 255.0); + s.execute("layer.select", json!({"layers": [lid]})).unwrap(); + let r = s.execute_checked("layer.create", json!({"op": "shapesFromVector"})).unwrap(); + assert_eq!(r["skipped"], json!([]), "{r}"); + let sid = r["layers"][0].as_u64().unwrap(); + let c = s.active_comp().unwrap(); + // In the document's paint order: the shape drawn last, the two images (the later one + // higher), the background drawn first, then the hidden source. + let names: Vec<&str> = c.layers.iter().map(|l| l.name.as_str()).collect(); + assert_eq!(names, ["images.pdf Outlines", "Image", "Image", "images.pdf Outlines 1", "images.pdf"]); + assert_eq!(c.layers[0].id.0, sid); + assert_eq!(r["layers"].as_array().unwrap().len(), 2); + for l in &c.layers[1..3] { + assert_eq!(l.parent.map(|p| p.0), Some(sid), "{}", l.name); + assert!(matches!(l.source, effectcraft_project::LayerSource::Footage { .. })); + } + assert!(!c.layers[4].switches.video); + let shapes = s.render(cid, t, RenderOpts::default()); + let d = mean_diff(&shapes, &direct); + assert!(d < 0.02, "converted vs page: mean diff {d}"); + // Image pixels in place: quarters of the upright image and of the turned one, and the + // shape over the background. + for (x, y) in [(25, 17), (55, 17), (25, 32), (55, 32), (95, 60), (95, 40), (105, 40), (30, 65)] { + let (a, b) = (shapes.get(x, y), direct.get(x, y)); + assert!((0..3).all(|k| (a[k] - b[k]).abs() < 0.05), "({x}, {y}): {a:?} vs {b:?}"); + } +} diff --git a/crates/opusenc/Cargo.toml b/crates/opusenc/Cargo.toml index 90b0fd02..489c648e 100644 --- a/crates/opusenc/Cargo.toml +++ b/crates/opusenc/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "effectcraft-opusenc" -description = "Clean-room pure-Rust Opus encoder (RFC 6716 CELT-only, fullband 48 kHz, 20 ms; RFC 7845 OpusHead) for WebM export audio" +description = "Clean-room pure-Rust Opus encoder (RFC 6716 SILK NB/MB/WB, hybrid SWB/FB and CELT FB modes, 48 kHz, 20 ms; RFC 7845 OpusHead) for WebM export audio" version.workspace = true edition.workspace = true license.workspace = true @@ -10,3 +10,6 @@ rust-version.workspace = true workspace = true [dependencies] + +[dev-dependencies] +filmcraft-opus = { workspace = true } diff --git a/crates/opusenc/README.md b/crates/opusenc/README.md index 0c76d49f..999d8a7e 100644 --- a/crates/opusenc/README.md +++ b/crates/opusenc/README.md @@ -1,24 +1,119 @@ # effectcraft-opusenc Clean-room, pure-Rust Opus encoder for EffectCraft's WebM export audio (layer L0, `std` only, -no `unsafe`). +no `unsafe`, builds for `wasm32-unknown-unknown`). -- **CELT-only** packets (TOC configuration 31: fullband 20 kHz, 20 ms, one frame per packet), - 48 kHz input, mono or stereo (stereo coded as dual L/R stereo), constant packet size from the - requested bitrate; all-zero frames become 3-byte silence packets. -- Forward MDCT with the low-overlap window, band energies with inter/intra coarse prediction - (cheaper of the two per frame), fine and final energy bits, the decoder's exact bit allocation, - spreading and recursive band splitting, greedy PVQ search and codeword indexing. -- Not implemented: SILK/hybrid modes, transient (short-block) frames, TF changes, pitch - post-filter, intensity stereo, dynamic allocation boosts, VBR rate control. -- `OpusEncoder::pre_skip()` is 120 samples (the MDCT overlap); `opus_head()` returns the RFC 7845 - identification header. +All three Opus modes, 20 ms packets (one frame per packet), 48 kHz float input, mono or stereo: + +- **CELT** (fullband): MDCT transform coding, constant packet size. +- **SILK** (narrowband 8 kHz, mediumband 12 kHz, wideband 16 kHz internal rate): linear + prediction with long-term (pitch) prediction; packet size varies around the target bitrate. +- **Hybrid** (super-wideband, fullband): SILK wideband codes 0–8 kHz and CELT codes bands 17 and + up (8 kHz and above) in the same range-coded frame; constant packet size. ```rust -let mut enc = effectcraft_opusenc::OpusEncoder::new(2, 128_000); -let packet = enc.encode_float(&vec![0.0; effectcraft_opusenc::OpusEncoder::FRAME_SIZE * 2]); +use effectcraft_opusenc::{Application, OpusEncoder}; +let mut enc = OpusEncoder::with_application(2, 48_000, Application::Voip); +let packet = enc.encode_float(&vec![0.0; OpusEncoder::FRAME_SIZE * 2]); +// enc.mode(), enc.bandwidth(), enc.pre_skip(), enc.opus_head() ... ``` +`OpusEncoder::new(channels, bitrate)` is `with_application(.., Application::Audio)`. +`OpusEncoder::with_mode(channels, bitrate, mode, bandwidth)` forces a mode/bandwidth pair +(SILK: NB/MB/WB; hybrid: SWB/FB; CELT: FB). + +## Mode table + +Chosen once per stream (constant-bitrate export, no in-stream switching) from the bitrate per +channel (bitrate / channels; the total is clamped to 6 kb/s per channel … 510 kb/s): + +| per-channel bitrate | `Application::Voip` | `Application::Audio` (default) | +|---------------------|-------------------------|--------------------------------| +| < 10 kb/s | SILK narrowband (8 kHz) | SILK wideband | +| 10–12 kb/s | SILK mediumband (12 kHz)| SILK wideband | +| 12–16 kb/s | SILK wideband (16 kHz) | SILK wideband | +| 16–18 kb/s | SILK wideband | hybrid fullband | +| 18–28 kb/s | hybrid super-wideband | hybrid fullband | +| 28–36 kb/s | hybrid fullband | hybrid fullband | +| 36–40 kb/s | hybrid fullband | CELT fullband | +| ≥ 40 kb/s | CELT fullband | CELT fullband | + +TOC configurations (RFC 6716 §3.1, Table 2): SILK NB/MB/WB 20 ms = 1/5/9, hybrid SWB/FB 20 ms += 13/15, CELT FB 20 ms = 31; the stereo bit is set for two channels; frame-count code 0. + +`pre_skip()` is 120 samples in every mode: the CELT MDCT overlap, and for SILK/hybrid the +decimator delay (83 samples at 48 kHz, 88 for narrowband) plus the decoder's SILK delay (one +internal-rate sample and the resampler delay of RFC 6716 Table 54). The SILK and CELT halves of a +hybrid frame are therefore aligned without extra buffering. + +## What is implemented + +**SILK** (RFC 6716 §4.2, encoder direction; own analysis and search design): + +- 48 kHz → 8/12/16 kHz decimation with a linear-phase Kaiser-windowed-sinc FIR. +- Per frame: energy VAD (inactive / unvoiced / voiced), open-loop pitch search (normalised + correlation with octave checks, joint lag + contour codebook choice, §4.2.7.6.1), windowed + autocorrelation LPC (Levinson, lag window, bandwidth expansion), LPC → NLSF by root search, + NLSF quantisation (stage-1 preselection by weighted distance, backwards-predictive stage-2 + scalar quantiser reproducing the decoder's dequantisation exactly, rate-distortion choice), + LTP codebook search over the three periodicity codebooks, independent + delta gain coding. +- Analysis-by-synthesis excitation: every sample is chosen by running the decoder's exact + fixed-point reconstruction (LTP from the decoder's own past output, LPC synthesis with the Q12 + coefficients of the *dequantised* NLSFs, gain rescaling between subframes, quantisation + offsets, the LCG sign sequence), so encoder and decoder never drift. A noise-feedback loop + shapes the coding noise by `1/A(z/γ)` (γ 0.75–0.9, coarser quantisation → more shaping), and a + small rate penalty biases pulses towards zero. +- Excitation coding: rate level, per-block pulse counts with LSB escapes, shell coding, LSBs and + signs (§4.2.7.8). +- Rate control: per-frame search over a gain offset (integer bracket, then bisection) to the + frame budget; SILK-only streams carry a bit reservoir across frames (packet sizes vary). +- Stereo: mid/side with the decoder's stereo prediction (§4.2.7.1, §4.2.8; ridge-regularised + least-squares weights, quantised to the decoder grid, interpolation mirrored), mid-only frames + when the predicted side is negligible, side gains floored at the mid gains. +- Not implemented: LBRR (in-band FEC), NLSF interpolation, 10/40/60 ms frames, DTX. + +**Hybrid** (§4.3 with start band 17): the SILK frame is written first, then the redundancy flag +(always 0) when there is room, then a CELT frame coding bands 17–18 (SWB) or 17–20 (FB) in the +same range coder with the remaining packet bytes (no silence flag or post-filter in hybrid +frames, decoder band-energy reset for uncoded bands mirrored). SILK gets 72 % (SWB) / 62 % (FB) +of the per-channel bitrate, at most the bitrate minus 6 kb/s, and never more than leaves CELT a +fifth of the packet (at least 80 bits). + +**CELT** (unchanged): forward MDCT with the low-overlap window, band energies with inter/intra +coarse prediction, fine and final energy bits, the decoder's exact bit allocation, spreading, +recursive band splitting, greedy PVQ search. Not implemented: transient frames, TF changes, +pitch post-filter, intensity stereo, dynamic allocation, VBR. All-zero CELT frames become 3-byte +silence packets. + +## Measured (tests in `tests/`, FilmCraft decoder) + +Synthetic speech (glottal pulses 100–200 Hz through three moving formants, syllable envelopes, +pauses) and music (three-note harmonic chords); waveform SNR against the input band-limited to +the mode's bandwidth (full band for hybrid/CELT), residual delay 0 (±1 for narrowband) after the +pre-skip: + +| mode / bandwidth | bitrate target → actual | speech SNR | music SNR | +|--------------------|-------------------------|------------|-----------| +| SILK NB | 8 → 7.5 kb/s | 15.9 dB | 6.9 dB (9 kb/s) | +| SILK MB | 11 → 10.1 kb/s | 21.8 dB | 7.9 dB | +| SILK WB | 12 → 10.9 kb/s | 21.5 dB | 8.1 dB | +| SILK WB | 16 → 14.3 kb/s | 28.1 dB | 12.1 dB | +| SILK WB | 24 → 21.1 kb/s | 37.0 dB | 19.7 dB | +| hybrid SWB | 24 kb/s (CBR) | 28.7 dB | 13.0 dB | +| hybrid FB | 32 kb/s (CBR) | 32.1 dB | 15.6 dB | +| CELT FB | 12 kb/s (CBR) | 13.0 dB | — | + +At 12 kb/s mono, SILK wideband beats the CELT path on speech by ~8 dB (21.0 vs 13.0 dB +full-band SNR). SILK bitrates land within −13 % … 0 % of the target (speech spends little in +pauses), hybrid is exact. Stereo SILK WB at 32 kb/s: 41 dB dual-mono, 40/37 dB panned, 22/19 dB +for two independent speech signals. + +ffmpeg (external oracle, Ogg Opus written by the test) decodes every mode with both its native +`opus` decoder and `libopus` without errors, honouring pre-skip and end trimming, with the same +SNRs (libopus residual delay 0, ffmpeg's native decoder 0–4 samples). The only message tolerated +is ffmpeg's raw-PCM muxer timestamp complaint that its native decoder also prints for +narrowband files encoded by libopus itself. + ## Specifications - RFC 6716, *Definition of the Opus Audio Codec* (September 2012), with the RFC 8251 updates @@ -26,8 +121,16 @@ let packet = enc.encode_float(&vec![0.0; effectcraft_opusenc::OpusEncoder::FRAME - RFC 7845, *Ogg Encapsulation for the Opus Audio Codec* (April 2016), §5.1 `OpusHead` and §4.2 pre-skip. -Implemented from the RFC text. Constant tables (band edges, `eMeans`, the coarse-energy Laplace -model, the allocation table, ICDFs) and the decoder-side allocation arithmetic are mirrored from -FilmCraft's first-party, spec-derived `filmcraft-opus` decoder (MIT OR Apache-2.0), which is also -the round-trip oracle in `crates/export/tests/opus_roundtrip.rs` (ffmpeg is a second, external -oracle when installed). No third-party encoder or decoder source was consulted. +## Provenance + +Implemented from the RFC text. Constant tables (CELT band edges, `eMeans`, the coarse-energy +Laplace model, the allocation table, ICDFs; all SILK probability tables and codebooks: NLSF +stage-1/2 codebooks and prediction weights, gain, pitch, contour, LTP and pulse ICDFs, stereo +weights) and the decoder-side normative arithmetic the encoder must reproduce bit-exactly (CELT +allocation; SILK NLSF dequantisation and stabilisation, NLSF → LPC conversion, `log2lin`, the +fixed-point division/inverse helpers and the LPC analysis filter) are mirrored from FilmCraft's +first-party, spec-derived `filmcraft-opus` decoder (MIT OR Apache-2.0), which is also the +round-trip oracle in `tests/` and `crates/export/tests/opus_roundtrip.rs` (ffmpeg is a second, +external oracle when installed). All encoder-side analysis and search (pitch, LPC, NLSF and +LTP search, noise feedback, rate control, stereo prediction, the decimator) is our own design. +No third-party encoder or decoder source (libopus or otherwise) was consulted. diff --git a/crates/opusenc/src/lib.rs b/crates/opusenc/src/lib.rs index 1e7a2cec..04963abf 100644 --- a/crates/opusenc/src/lib.rs +++ b/crates/opusenc/src/lib.rs @@ -1,18 +1,26 @@ -//! Clean-room pure-Rust Opus encoder (RFC 6716 CELT-only mode; RFC 7845 `OpusHead`). +//! Clean-room pure-Rust Opus encoder (RFC 6716 SILK, hybrid and CELT modes; RFC 7845 `OpusHead`). //! -//! Produces fullband (20 kHz) 48 kHz Opus packets of 20 ms, mono or stereo, at a constant -//! packet size derived from the requested bitrate. Each packet is a single CELT frame (TOC -//! configuration 31, frame-count code 0). Stereo is coded as dual (independent L/R) stereo. +//! Produces 20 ms Opus packets from 48 kHz float input, mono or stereo. The coding mode and +//! audio bandwidth are chosen once per stream from the per-channel bitrate and the +//! [`Application`] (see the crate README for the table): +//! +//! - **SILK** (narrowband/mediumband/wideband): linear-predictive speech coding at an internal +//! rate of 8/12/16 kHz; packet sizes vary around the target bitrate. +//! - **Hybrid** (super-wideband/fullband): SILK wideband codes 0–8 kHz and CELT codes the bands +//! above in the same range-coded frame; constant packet size. +//! - **CELT** (fullband): MDCT transform coding; constant packet size. //! //! Layer L0: no dependencies beyond `std`; no `unsafe`; builds for `wasm32-unknown-unknown`. //! //! ``` -//! use effectcraft_opusenc::OpusEncoder; +//! use effectcraft_opusenc::{Application, Mode, OpusEncoder}; //! let mut enc = OpusEncoder::new(2, 128_000); //! let pcm = vec![0.0f32; OpusEncoder::FRAME_SIZE * 2]; //! let packet = enc.encode_float(&pcm); //! assert_eq!(packet[0] >> 3, 31); // CELT-only, fullband, 20 ms //! assert_eq!(enc.opus_head().len(), 19); +//! let voip = OpusEncoder::with_application(1, 16_000, Application::Voip); +//! assert_eq!(voip.mode(), Mode::Silk); //! ``` // The bit-allocation code mirrors the normative integer arithmetic; keep C-like shift expressions. @@ -23,6 +31,8 @@ mod energy; mod mdct; mod range; mod rate; +mod resample; +mod silk; mod tables; use std::sync::OnceLock; @@ -31,7 +41,9 @@ use bands::{SPREAD_NORMAL, quant_all_bands}; use energy::{quant_coarse_energy, quant_energy_finalise, quant_fine_energy}; use mdct::Mdct; use range::{BITRES, RangeEncoder}; -use rate::{Mode, compute_allocation}; +use rate::{Mode as CeltMode, compute_allocation}; +use resample::Downsampler; +use silk::SilkEncoder; use tables::*; /// Samples per channel in one 20 ms frame at 48 kHz. @@ -40,18 +52,91 @@ const N: usize = 960; const LM: usize = 3; /// Smallest CELT payload the encoder produces (bytes). const MIN_FRAME_BYTES: usize = 16; -/// Largest CELT payload allowed in one frame (RFC 6716 §3.4, R2). +/// Largest payload allowed in one frame (RFC 6716 §3.4, R2). const MAX_FRAME_BYTES: usize = 1275; +/// First CELT band coded in hybrid frames (8 kHz). +const HYBRID_START_BAND: usize = 17; -fn mode() -> &'static Mode { - static MODE: OnceLock = OnceLock::new(); - MODE.get_or_init(Mode::new) +fn celt_mode() -> &'static CeltMode { + static MODE: OnceLock = OnceLock::new(); + MODE.get_or_init(CeltMode::new) +} + +/// Intended use of the stream; selects the mode/bandwidth table. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)] +pub enum Application { + /// General audio (music, mixed content): CELT unless the bitrate is low. + #[default] + Audio, + /// Speech: SILK and hybrid over a wider bitrate range, narrower bandwidths at low rates. + Voip, +} + +/// Opus coding mode (RFC 6716 §2). +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub enum Mode { + /// MDCT transform coding only. + Celt, + /// Linear-prediction coding only. + Silk, + /// SILK below 8 kHz plus CELT above. + Hybrid, +} + +/// Coded audio bandwidth (RFC 6716 §2, Table 1). +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum Bandwidth { + /// 4 kHz (8 kHz sampling). + Narrowband, + /// 6 kHz (12 kHz sampling). + Mediumband, + /// 8 kHz (16 kHz sampling). + Wideband, + /// 12 kHz (24 kHz sampling). + SuperWideband, + /// 20 kHz (48 kHz sampling). + Fullband, +} + +/// Mode and bandwidth for a per-channel bitrate (bits per second per channel). +fn choose_mode(app: Application, per_channel: u32) -> (Mode, Bandwidth) { + match app { + Application::Voip => match per_channel { + 0..10_000 => (Mode::Silk, Bandwidth::Narrowband), + 10_000..12_000 => (Mode::Silk, Bandwidth::Mediumband), + 12_000..18_000 => (Mode::Silk, Bandwidth::Wideband), + 18_000..28_000 => (Mode::Hybrid, Bandwidth::SuperWideband), + 28_000..40_000 => (Mode::Hybrid, Bandwidth::Fullband), + _ => (Mode::Celt, Bandwidth::Fullband), + }, + Application::Audio => match per_channel { + 0..16_000 => (Mode::Silk, Bandwidth::Wideband), + 16_000..36_000 => (Mode::Hybrid, Bandwidth::Fullband), + _ => (Mode::Celt, Bandwidth::Fullband), + }, + } +} + +/// SILK side of a SILK-only or hybrid stream. +struct SilkPath { + enc: SilkEncoder, + down: Vec, + /// Bits per frame aimed at (excluding the TOC byte in SILK-only mode). + target_bits: f64, + /// Bits saved (positive) or overspent by previous frames (SILK-only mode). + reservoir: f64, } /// Opus encoder for one mono or stereo stream at 48 kHz. pub struct OpusEncoder { channels: usize, - /// CELT payload bytes per packet (the packet is one byte longer: the TOC). + application: Application, + mode: Mode, + bandwidth: Bandwidth, + /// Requested bitrate (clamped), bits per second. + bitrate_bps: u32, + /// Payload bytes per packet in CELT and hybrid modes (the packet is one byte longer: the + /// TOC). frame_bytes: usize, mdct: Mdct, /// Last input sample of each channel (pre-emphasis filter memory, ×32768). @@ -62,28 +147,88 @@ pub struct OpusEncoder { old_band_e: [f32; 2 * NB_EBANDS], block: Vec, freq: Vec, + silk: Option, } impl OpusEncoder { /// Samples per channel per packet (20 ms at 48 kHz). pub const FRAME_SIZE: usize = N; - /// Creates an encoder for `channels` (1 or 2) channels of 48 kHz audio at `bitrate_bps` - /// (total for all channels; packets are constant-size, clamped to 6.8–510 kb/s). + /// Creates a general-audio encoder ([`Application::Audio`]) for `channels` (1 or 2) + /// channels of 48 kHz audio at `bitrate_bps` (total for all channels). pub fn new(channels: u8, bitrate_bps: u32) -> Self { + Self::with_application(channels, bitrate_bps, Application::Audio) + } + + /// Creates an encoder for `channels` (1 or 2) channels of 48 kHz audio at `bitrate_bps` + /// (total for all channels; clamped to 6 kb/s per channel – 510 kb/s), choosing the mode + /// and bandwidth for `application`. + pub fn with_application(channels: u8, bitrate_bps: u32, application: Application) -> Self { + let c = channels.max(1) as u32; + let (mode, bandwidth) = choose_mode(application, bitrate_bps.clamp(6_000 * c, 510_000) / c); + Self::build(channels, bitrate_bps, application, mode, bandwidth) + } + + /// Creates an encoder with an explicit mode and bandwidth instead of the application's + /// table. Valid combinations: SILK with narrowband, mediumband or wideband; hybrid with + /// super-wideband or fullband; CELT with fullband. + /// + /// # Panics + /// On any other combination, or when `channels` is not 1 or 2. + pub fn with_mode(channels: u8, bitrate_bps: u32, mode: Mode, bandwidth: Bandwidth) -> Self { + let ok = match mode { + Mode::Silk => bandwidth <= Bandwidth::Wideband, + Mode::Hybrid => bandwidth >= Bandwidth::SuperWideband, + Mode::Celt => bandwidth == Bandwidth::Fullband, + }; + assert!(ok, "unsupported mode/bandwidth combination {mode:?}/{bandwidth:?}"); + Self::build(channels, bitrate_bps, Application::Audio, mode, bandwidth) + } + + fn build(channels: u8, bitrate_bps: u32, application: Application, mode: Mode, bandwidth: Bandwidth) -> Self { assert!(channels == 1 || channels == 2, "Opus family-0 streams have 1 or 2 channels"); let c = channels as usize; + let bitrate_bps = bitrate_bps.clamp(6_000 * c as u32, 510_000); let packet_bytes = (bitrate_bps as usize).div_ceil(400); let frame_bytes = packet_bytes.saturating_sub(1).clamp(MIN_FRAME_BYTES, MAX_FRAME_BYTES); + let silk = match mode { + Mode::Celt => None, + Mode::Silk | Mode::Hybrid => { + let fs_khz = match bandwidth { + Bandwidth::Narrowband => 8, + Bandwidth::Mediumband => 12, + _ => 16, + }; + let target_bits = if mode == Mode::Silk { + bitrate_bps as f64 / 50.0 - 8.0 + } else { + // SILK gets most of a hybrid packet; CELT codes 8 kHz and up with the rest. + let per = (bitrate_bps / c as u32) as f64; + let share = if bandwidth == Bandwidth::SuperWideband { 0.72 } else { 0.62 }; + c as f64 * (per * share).min(per - 6_000.0).max(per * 0.5) / 50.0 + }; + Some(SilkPath { + enc: SilkEncoder::new(fs_khz, c), + down: (0..c).map(|_| Downsampler::new(fs_khz as u32 * 1000)).collect(), + target_bits, + reservoir: 0.0, + }) + } + }; OpusEncoder { channels: c, + application, + mode, + bandwidth, + bitrate_bps, frame_bytes, mdct: Mdct::new(N), preemph_mem: [0.0; 2], hist: vec![0.0; c * OVERLAP], - old_band_e: [-28.0; 2 * NB_EBANDS], + old_band_e: [0.0; 2 * NB_EBANDS], block: vec![0.0; N + OVERLAP], freq: vec![0.0; c * N], + silk, } } @@ -92,15 +237,43 @@ impl OpusEncoder { self.channels as u8 } - /// Actual bitrate in bits per second (packet size × 50 packets per second). - pub fn bitrate(&self) -> u32 { - ((self.frame_bytes + 1) * 8 * 50) as u32 + /// The application the encoder was created for. + pub fn application(&self) -> Application { + self.application } - /// Samples at 48 kHz the decoder must discard from the start of the stream (the MDCT - /// overlap); this is the `OpusHead` pre-skip. + /// Coding mode of every packet of the stream. + pub fn mode(&self) -> Mode { + self.mode + } + + /// Coded audio bandwidth of every packet of the stream. + pub fn bandwidth(&self) -> Bandwidth { + self.bandwidth + } + + /// Bitrate in bits per second: the constant packet size × 50 packets per second in CELT + /// and hybrid modes, the (average) target in SILK mode. + pub fn bitrate(&self) -> u32 { + match self.mode { + Mode::Silk => self.bitrate_bps, + _ => ((self.frame_bytes + 1) * 8 * 50) as u32, + } + } + + /// Samples at 48 kHz the decoder must discard from the start of the stream (the `OpusHead` + /// pre-skip): the encoder's total delay. CELT: the 120-sample MDCT overlap. SILK/hybrid: + /// the decimator (83 samples, 88 for narrowband) plus the decoder's SILK delay (one + /// internal-rate sample and the resampler delay of RFC 6716 Table 54), which also rounds + /// to 120 samples. pub fn pre_skip(&self) -> u16 { - OVERLAP as u16 + let (fs, table54_ms) = match (self.mode, self.bandwidth) { + (Mode::Celt, _) => return OVERLAP as u16, + (_, Bandwidth::Narrowband) => (8_000, 0.538), + (_, Bandwidth::Mediumband) => (12_000, 0.692), + _ => (16_000, 0.706), + }; + (resample::delay(fs) as f64 + 48_000.0 / fs as f64 + table54_ms * 48.0).round() as u16 } /// The 19-byte `OpusHead` identification header (RFC 7845 §5.1): version 1, channel count, @@ -117,14 +290,84 @@ impl OpusEncoder { h } + /// TOC byte (RFC 6716 §3.1, Table 2) of a 20 ms single-frame packet. + fn toc(&self) -> u8 { + let config: u8 = match (self.mode, self.bandwidth) { + (Mode::Silk, Bandwidth::Narrowband) => 1, + (Mode::Silk, Bandwidth::Mediumband) => 5, + (Mode::Silk, _) => 9, + (Mode::Hybrid, Bandwidth::SuperWideband) => 13, + (Mode::Hybrid, _) => 15, + (Mode::Celt, _) => 31, + }; + (config << 3) | if self.channels == 2 { 4 } else { 0 } + } + /// Encodes one 20 ms frame of interleaved f32 samples in [-1, 1] (exactly - /// `FRAME_SIZE * channels` values) and returns one Opus packet (TOC byte + CELT frame). + /// `FRAME_SIZE * channels` values) and returns one Opus packet. pub fn encode_float(&mut self, pcm: &[f32]) -> Vec { let c = self.channels; assert_eq!(pcm.len(), N * c, "encode_float takes exactly FRAME_SIZE samples per channel"); + let mut packet = vec![self.toc()]; + match self.mode { + Mode::Celt => { + let (log_e, silence) = self.celt_analysis(pcm); + let len = self.frame_bytes; + packet.extend_from_slice(&self.encode_celt(RangeEncoder::new(), len, 0, NB_EBANDS, &log_e, silence)); + } + Mode::Silk => { + let x = self.silk_input(pcm); + let sp = self.silk.as_mut().expect("SILK state"); + let target = sp.target_bits + 0.25 * sp.reservoir; + let mut enc = RangeEncoder::new(); + sp.enc.encode(&x, target.max(24.0), (MAX_FRAME_BYTES * 8) as i32, &mut enc); + let bytes = (enc.tell() as usize).div_ceil(8).max(2); + let lim = 4.0 * sp.target_bits; + sp.reservoir = (sp.reservoir + sp.target_bits - (bytes * 8) as f64).clamp(-lim, lim); + packet.extend_from_slice(&enc.done(bytes)); + } + Mode::Hybrid => { + let x = self.silk_input(pcm); + let len = self.frame_bytes; + let total = (len * 8) as i32; + let sp = self.silk.as_mut().expect("SILK state"); + let celt_min = (total / 5).max(80); + let mut enc = RangeEncoder::new(); + sp.enc.encode(&x, sp.target_bits, total - celt_min, &mut enc); + // No redundancy (RFC 6716 §4.5.1); the flag is present when there is room. + if enc.tell() + 17 + 20 <= total { + enc.bit_logp(false, 12); + } + let (log_e, _) = self.celt_analysis(pcm); + let end = if self.bandwidth == Bandwidth::SuperWideband { 19 } else { NB_EBANDS }; + packet.extend_from_slice(&self.encode_celt(enc, len, HYBRID_START_BAND, end, &log_e, false)); + } + } + packet + } - // Pre-emphasis, the MDCT and the band energies. - let mut band_e = [0f32; 2 * NB_EBANDS]; + /// Decimates the input to the SILK internal rate (16-bit scale), one vector per channel. + fn silk_input(&mut self, pcm: &[f32]) -> Vec> { + let c = self.channels; + let sp = self.silk.as_mut().expect("SILK state"); + let mut out = Vec::with_capacity(c); + let mut tmp = vec![0f32; N]; + for ch in 0..c { + for (j, t) in tmp.iter_mut().enumerate() { + let s = pcm[j * c + ch]; + *t = if s.is_finite() { s.clamp(-1.0, 1.0) * 32768.0 } else { 0.0 }; + } + let mut y = Vec::with_capacity(N / 3); + sp.down[ch].process(&tmp, &mut y); + out.push(y); + } + out + } + + /// Pre-emphasis, the MDCT, band energies (log2, relative to `E_MEANS`) and normalised + /// bands; returns the energies and whether the frame is digital silence. + fn celt_analysis(&mut self, pcm: &[f32]) -> ([f32; 2 * NB_EBANDS], bool) { + let c = self.channels; let mut log_e = [0f32; 2 * NB_EBANDS]; let mut peak = 0f32; for ch in 0..c { @@ -146,7 +389,6 @@ impl OpusEncoder { let lo = (EBANDS[i] as usize) << LM; let hi = (EBANDS[i + 1] as usize) << LM; let e = (1e-27f32 + freq[lo..hi].iter().map(|v| v * v).sum::()).sqrt(); - band_e[ch * NB_EBANDS + i] = e; log_e[ch * NB_EBANDS + i] = e.log2() - E_MEANS[i]; let g = 1.0 / e; for v in freq[lo..hi].iter_mut() { @@ -154,35 +396,33 @@ impl OpusEncoder { } } } - let silence = peak < 1e-4; - let toc = (31u8 << 3) | if c == 2 { 4 } else { 0 }; - let mut packet = vec![toc]; - packet.extend_from_slice(&self.encode_celt(&log_e, silence)); - packet + (log_e, peak < 1e-4) } - /// Codes one CELT frame (RFC 6716 §4.3, in bitstream order). - fn encode_celt(&mut self, log_e: &[f32; 2 * NB_EBANDS], silence: bool) -> Vec { - let m = mode(); + /// Codes one CELT frame (RFC 6716 §4.3, in bitstream order) of `len` bytes into `enc` + /// (which already holds the SILK frame in hybrid mode), coding bands `start..end`, and + /// finalises the frame. + #[allow(clippy::too_many_arguments)] + fn encode_celt(&mut self, mut enc: RangeEncoder, len: usize, start: usize, end: usize, log_e: &[f32; 2 * NB_EBANDS], silence: bool) -> Vec { + let m = celt_mode(); let c = self.channels; - let (start, end) = (0usize, NB_EBANDS); - let mut enc = RangeEncoder::new(); if c == 1 { for i in 0..NB_EBANDS { self.old_band_e[i] = self.old_band_e[i].max(self.old_band_e[NB_EBANDS + i]); } } - if silence { + if silence && start == 0 { // The silence flag; the decoder treats every remaining bit as consumed. enc.bit_logp(true, 15); self.old_band_e = [-28.0; 2 * NB_EBANDS]; return enc.done(2); } - let len = self.frame_bytes; let total_bits_raw = len as i32 * 8; - enc.bit_logp(false, 15); - // No pitch post-filter. - if enc.tell() + 16 <= total_bits_raw { + if enc.tell() == 1 { + enc.bit_logp(false, 15); + } + // No pitch post-filter (only signalled in CELT-only frames). + if start == 0 && enc.tell() + 16 <= total_bits_raw { enc.bit_logp(false, 1); } // Long blocks only. @@ -270,6 +510,12 @@ impl OpusEncoder { let (a, b) = self.old_band_e.split_at_mut(NB_EBANDS); b.copy_from_slice(a); } + // The decoder forgets the energies of bands it did not code. + for ch in 0..2 { + for i in (0..start).chain(end..NB_EBANDS) { + self.old_band_e[ch * NB_EBANDS + i] = 0.0; + } + } enc.done(len) } } diff --git a/crates/opusenc/src/resample.rs b/crates/opusenc/src/resample.rs new file mode 100644 index 00000000..9045cb86 --- /dev/null +++ b/crates/opusenc/src/resample.rs @@ -0,0 +1,131 @@ +//! 48 kHz → 8/12/16 kHz decimator feeding the SILK layer (own design). +//! +//! A linear-phase low-pass FIR (Kaiser-windowed sinc at 48 kHz) evaluated only at the retained +//! output phases. Its group delay ([`delay`]) is a whole number of 48 kHz samples, chosen so +//! that the SILK path (this filter + the decoder's internal-rate delay and resampler, RFC 6716 +//! Table 54) lands within a fraction of a sample of the CELT path's 120-sample MDCT overlap. + +/// Group delay of the decimator to `fs_out` in 48 kHz samples (the filter has `2 * delay + 1` +/// taps). +pub fn delay(fs_out: u32) -> usize { + if fs_out == 8_000 { 88 } else { 83 } +} + +/// Zeroth-order modified Bessel function of the first kind (power series). +fn bessel_i0(x: f64) -> f64 { + let mut sum = 1.0; + let mut term = 1.0; + let q = x * x / 4.0; + for k in 1..64 { + term *= q / (k * k) as f64; + sum += term; + if term < sum * 1e-17 { + break; + } + } + sum +} + +/// Windowed-sinc low-pass with cutoff `fc` (Hz at 48 kHz), unit DC gain. +fn design(fc: f64, beta: f64, delay: usize) -> Vec { + let wc = 2.0 * std::f64::consts::PI * fc / 48_000.0; + let i0b = bessel_i0(beta); + let mut h: Vec = (0..2 * delay + 1) + .map(|k| { + let m = k as f64 - delay as f64; + let sinc = if m == 0.0 { wc / std::f64::consts::PI } else { (wc * m).sin() / (std::f64::consts::PI * m) }; + let r = m / delay as f64; + sinc * bessel_i0(beta * (1.0 - r * r).max(0.0).sqrt()) / i0b + }) + .collect(); + let s: f64 = h.iter().sum(); + for v in h.iter_mut() { + *v /= s; + } + h.iter().map(|&v| v as f32).collect() +} + +/// Decimates one channel of 48 kHz audio by 3, 4 or 6. +#[derive(Clone)] +pub struct Downsampler { + factor: usize, + h: Vec, + taps: usize, + /// The last `taps - 1` input samples followed by the current frame. + buf: Vec, +} + +impl Downsampler { + /// `fs_out` is 8000, 12000 or 16000. + pub fn new(fs_out: u32) -> Self { + let factor = (48_000 / fs_out) as usize; + // Cutoff at 95 % of the output Nyquist frequency (pass band to ~88 %, stop band from ~110 %). + let fc = 0.95 * fs_out as f64 / 2.0; + let d = delay(fs_out); + let taps = 2 * d + 1; + Downsampler { factor, h: design(fc, 6.5, d), taps, buf: vec![0.0; taps - 1] } + } + + /// Decimates `input` (a multiple of the factor long), appending to `out`. Output sample `j` + /// is the filtered input at frame position `factor * j`, delayed by [`delay`]. + pub fn process(&mut self, input: &[f32], out: &mut Vec) { + debug_assert!(input.len().is_multiple_of(self.factor)); + let taps = self.taps; + self.buf.truncate(taps - 1); + self.buf.extend_from_slice(input); + for j in 0..input.len() / self.factor { + let t = taps - 1 + self.factor * j; + let win = &self.buf[t + 1 - taps..=t]; + // h is symmetric, so the window can be taken in forward order. + out.push(win.iter().zip(&self.h).map(|(a, b)| a * b).sum()); + } + let n = self.buf.len(); + self.buf.copy_within(n - (taps - 1).., 0); + self.buf.truncate(taps - 1); + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn tone(f: f64, n: usize) -> Vec { + (0..n).map(|i| (2.0 * std::f64::consts::PI * f * i as f64 / 48_000.0).sin() as f32).collect() + } + + fn rms(x: &[f32]) -> f64 { + (x.iter().map(|&v| (v as f64).powi(2)).sum::() / x.len() as f64).sqrt() + } + + #[test] + fn passes_band_and_rejects_aliases() { + for &(fs, pass, stop) in &[(16_000u32, 6_000.0, 8_600.0), (12_000, 4_500.0, 6_500.0), (8_000, 3_000.0, 4_500.0)] { + for &(f, want_pass) in &[(pass, true), (stop, false)] { + let mut d = Downsampler::new(fs); + let x = tone(f, 9600); + let mut y = Vec::new(); + for c in x.chunks(960) { + d.process(c, &mut y); + } + let r = rms(&y[y.len() / 2..]) * std::f64::consts::SQRT_2; + if want_pass { + assert!((r - 1.0).abs() < 0.02, "{fs}: {f} Hz gain {r}"); + } else { + assert!(r < 0.01, "{fs}: {f} Hz leaks {r}"); + } + } + } + } + + #[test] + fn delay_is_exact() { + let mut d = Downsampler::new(16_000); + let mut x = vec![0f32; 960]; + x[300] = 1.0; + let mut y = Vec::new(); + d.process(&x, &mut y); + // The impulse at 300 shows up centred at 48 kHz time 300 + 83 = 383 (output 127.67). + let peak = y.iter().enumerate().max_by(|a, b| a.1.total_cmp(b.1)).map(|(i, _)| i).unwrap_or(0); + assert!((peak as f64 - (300.0 + delay(16_000) as f64) / 3.0).abs() < 1.0, "peak at {peak}"); + } +} diff --git a/crates/opusenc/src/silk/analysis.rs b/crates/opusenc/src/silk/analysis.rs new file mode 100644 index 00000000..de510433 --- /dev/null +++ b/crates/opusenc/src/silk/analysis.rs @@ -0,0 +1,322 @@ +//! Encoder-side signal analysis for SILK (own design): windowed autocorrelation LPC, LPC → NLSF +//! conversion, open-loop pitch search and LTP codebook search. + +use super::tables::*; + +/// Short-term prediction coefficients `a` (prediction `x[n] ≈ Σ a[j] x[n-1-j]`) from the +/// windowed segment `x`, with a Gaussian lag window and white-noise correction. +pub fn lpc(x: &[f32], order: usize, fs_khz: usize) -> Vec { + let n = x.len(); + // Asymmetric window: sin² rise over the first half, flat, cos fall over the last 1/8. + let rise = n / 2; + let fall = n / 8; + let w = |i: usize| -> f64 { + if i < rise { + let s = (std::f64::consts::FRAC_PI_2 * (i as f64 + 0.5) / rise as f64).sin(); + s * s + } else if i >= n - fall { + (std::f64::consts::FRAC_PI_2 * (i + fall - n) as f64 / fall as f64 + 0.0).cos() + } else { + 1.0 + } + }; + let xw: Vec = x.iter().enumerate().map(|(i, &v)| v as f64 * w(i)).collect(); + let mut r = vec![0f64; order + 1]; + for (k, rk) in r.iter_mut().enumerate() { + *rk = xw[k..].iter().zip(&xw).map(|(a, b)| a * b).sum(); + } + if r[0] <= 1e-6 { + return vec![0.0; order]; + } + // White-noise correction (-40 dB) and a 50 Hz Gaussian lag window. + r[0] *= 1.0 + 1e-4; + let f0 = 2.0 * std::f64::consts::PI * 50.0 / (fs_khz as f64 * 1000.0); + for (k, rk) in r.iter_mut().enumerate().skip(1) { + *rk *= (-0.5 * (f0 * k as f64).powi(2)).exp(); + } + let mut a = levinson(&r, order); + // Mild bandwidth expansion keeps the NLSFs apart. + let mut g = 1.0; + for v in a.iter_mut() { + g *= 0.996; + *v *= g; + } + a +} + +/// Levinson-Durbin recursion on autocorrelation `r` (predictor convention as in [`lpc`]). +pub fn levinson(r: &[f64], order: usize) -> Vec { + let mut a = vec![0f64; order]; + let mut err = r[0]; + let mut tmp = vec![0f64; order]; + for i in 0..order { + let mut acc = r[i + 1]; + for j in 0..i { + acc -= a[j] * r[i - j]; + } + let k = (acc / err).clamp(-0.9999, 0.9999); + tmp[..i].copy_from_slice(&a[..i]); + for j in 0..i { + a[j] = tmp[j] - k * tmp[i - 1 - j]; + } + a[i] = k; + err *= 1.0 - k * k; + if err <= 0.0 { + break; + } + } + a +} + +/// Evaluates the symmetric polynomial `p` (length `d + 1`, `d` even) as the cosine series +/// `e^{jωd/2} P(e^{jω})`. +fn cos_series(p: &[f64], w: f64) -> f64 { + let h = (p.len() - 1) / 2; + let mut s = p[h]; + for k in 0..h { + s += 2.0 * p[k] * ((h - k) as f64 * w).cos(); + } + s +} + +fn roots(p: &[f64], out: &mut Vec) { + const GRID: usize = 1024; + let mut w0 = 0.0; + let mut f0 = cos_series(p, w0); + for g in 1..=GRID { + let w1 = std::f64::consts::PI * g as f64 / GRID as f64; + let f1 = cos_series(p, w1); + if f0 == 0.0 { + out.push(w0); + } else if f0 * f1 < 0.0 { + let (mut lo, mut hi, mut flo) = (w0, w1, f0); + for _ in 0..40 { + let mid = 0.5 * (lo + hi); + let fm = cos_series(p, mid); + if fm * flo <= 0.0 { + hi = mid; + } else { + lo = mid; + flo = fm; + } + } + out.push(0.5 * (lo + hi)); + } + w0 = w1; + f0 = f1; + } +} + +/// LPC → normalised line spectral frequencies in radians (ascending, `order` values), or `None` +/// when the root search fails (unstable or degenerate filter). +pub fn a2nlsf(a: &[f64]) -> Option> { + let d = a.len(); + let mut c = vec![0f64; d + 2]; + c[0] = 1.0; + for k in 0..d { + c[k + 1] = -a[k]; + } + let mut p = vec![0f64; d + 2]; + let mut q = vec![0f64; d + 2]; + for k in 0..d + 2 { + p[k] = c[k] + c[d + 1 - k]; + q[k] = c[k] - c[d + 1 - k]; + } + // Remove the trivial roots at z = -1 (P) and z = +1 (Q). + let mut p1 = vec![0f64; d + 1]; + let mut q1 = vec![0f64; d + 1]; + p1[0] = p[0]; + q1[0] = q[0]; + for k in 1..=d { + p1[k] = p[k] - p1[k - 1]; + q1[k] = q[k] + q1[k - 1]; + } + let mut r = Vec::with_capacity(d); + roots(&p1, &mut r); + roots(&q1, &mut r); + if r.len() != d { + return None; + } + r.sort_by(f64::total_cmp); + Some(r) +} + +/// Normalised cross-correlation of `x[n]` and `x[n - lag]` over `n` in `range`. +fn ncorr(x: &[f32], range: std::ops::Range, lag: usize) -> f64 { + let (mut xy, mut xx, mut yy) = (0f64, 0f64, 0f64); + for n in range { + let a = x[n] as f64; + let b = x[n - lag] as f64; + xy += a * b; + xx += a * a; + yy += b * b; + } + if xy <= 0.0 { 0.0 } else { xy / (xx * yy + 1e-9).sqrt() } +} + +pub struct Pitch { + pub lag_index: i32, + pub contour: usize, + pub lags: [i32; 4], + pub score: f64, +} + +/// Open-loop pitch search on `x` (history followed by the frame starting at `start`, `len` +/// samples, four subframes) at `fs_khz`. +pub fn pitch_search(x: &[f32], start: usize, len: usize, fs_khz: usize) -> Pitch { + let fs = fs_khz as i32; + let min_lag = 2 * fs; + let max_lag = 18 * fs - 1; + let sub = len / 4; + let nl = (max_lag - min_lag + 1) as usize; + // Whole-frame score per lag. + let full: Vec = (0..nl).map(|i| ncorr(x, start..start + len, min_lag as usize + i)).collect(); + let mut best = 0usize; + for i in 0..nl { + // Slight preference for shorter lags. + if full[i] * (1.0 - 0.0004 * i as f64) > full[best] * (1.0 - 0.0004 * best as f64) { + best = i; + } + } + let mut lag = min_lag + best as i32; + // Prefer a sub-multiple when it explains the signal almost as well (octave errors). + for div in [4, 3, 2] { + let cand = (lag as f64 / div as f64).round() as i32; + let mut cbest = (0.0, 0); + for c in cand - 1..=cand + 1 { + if c >= min_lag && c <= max_lag { + let s = full[(c - min_lag) as usize]; + if s > cbest.0 { + cbest = (s, c); + } + } + } + if cbest.1 != 0 && cbest.0 > 0.9 * full[(lag - min_lag) as usize] { + lag = cbest.1; + break; + } + } + let score = full[(lag - min_lag) as usize]; + // Contour: per-subframe lag offsets around the chosen lag. + let cb: &[[i32; 4]] = if fs_khz == 8 { &CB_NB_20 } else { &CB_WB_20 }; + let sub_score = |k: usize, l: i32| -> f64 { + let l = l.clamp(min_lag, 18 * fs); + ncorr(x, start + k * sub..start + (k + 1) * sub, l as usize) + }; + let mut best = (f64::MIN, lag, 0usize); + for l0 in (lag - 2).max(min_lag)..=(lag + 2).min(max_lag) { + for (ci, offs) in cb.iter().enumerate() { + let s: f64 = (0..4).map(|k| sub_score(k, l0 + offs[k])).sum::() - 0.002 * ci as f64; + if s > best.0 { + best = (s, l0, ci); + } + } + } + let (_, l0, ci) = best; + let mut lags = [0i32; 4]; + for k in 0..4 { + lags[k] = (l0 + cb[ci][k]).clamp(min_lag, 18 * fs); + } + Pitch { lag_index: l0 - min_lag, contour: ci, lags, score } +} + +/// Result of the LTP codebook search. +pub struct Ltp { + pub per_index: usize, + pub ltp_index: [usize; 4], + /// Residual energy after LTP prediction / before, per subframe. + pub err: [f64; 4], + pub energy: [f64; 4], +} + +fn icdf_bits(icdf: &[u8], s: usize) -> f64 { + let hi = if s == 0 { 256 } else { icdf[s - 1] as u32 }; + let p = (hi - icdf[s] as u32).max(1) as f64 / 256.0; + -p.log2() +} + +/// Searches the three LTP codebooks (RFC 6716 Tables 39–42) for the taps that best predict the +/// residual `r` (history + frame starting at `start`) from its past at the per-subframe `lags`. +pub fn ltp_search(r: &[f64], start: usize, sub: usize, lags: &[i32; 4]) -> Ltp { + let mut rr = [[0f64; 25]; 4]; + let mut cv = [[0f64; 5]; 4]; + let mut e = [0f64; 4]; + for k in 0..4 { + let s0 = start + k * sub; + let lag = lags[k] as usize; + for n in s0..s0 + sub { + let t = r[n]; + e[k] += t * t; + let base = n + 2 - lag; + for i in 0..5 { + let vi = r[base - i]; + cv[k][i] += t * vi; + for j in 0..5 { + rr[k][i * 5 + j] += vi * r[base - j]; + } + } + } + } + let books: [&[[i32; 5]]; 3] = [<P_TAPS_0, <P_TAPS_1, <P_TAPS_2]; + let mut best = (f64::MAX, 0usize, [0usize; 4], [0f64; 4]); + for (p, book) in books.iter().enumerate() { + let mut total = icdf_bits(<P_PERIODICITY, p) * 1e-3 * e.iter().sum::() / 4.0; + let mut idx = [0usize; 4]; + let mut errs = [0f64; 4]; + for k in 0..4 { + let mut bk = (f64::MAX, 0usize, 0f64); + for (i, taps) in book.iter().enumerate() { + let b: [f64; 5] = std::array::from_fn(|j| taps[j] as f64 / 128.0); + let mut err = e[k]; + for a in 0..5 { + err -= 2.0 * b[a] * cv[k][a]; + for c in 0..5 { + err += b[a] * b[c] * rr[k][a * 5 + c]; + } + } + let cost = err + icdf_bits(LTP_FILTER[p], i) * 1e-3 * e[k]; + if cost < bk.0 { + bk = (cost, i, err.max(0.0)); + } + } + total += bk.0; + idx[k] = bk.1; + errs[k] = bk.2; + } + if total < best.0 { + best = (total, p, idx, errs); + } + } + Ltp { per_index: best.1, ltp_index: best.2, err: best.3, energy: e } +} + +#[cfg(test)] +mod tests { + use super::super::lpc::nlsf2a; + use super::*; + + #[test] + fn nlsf_roundtrip_through_decoder_conversion() { + // A resonant 10th/16th-order predictor from a synthetic signal. + for &(order, fs) in &[(10usize, 8usize), (16, 16)] { + let mut st = [0f64; 4]; + let mut seed = 1u32; + let x: Vec = (0..400) + .map(|_| { + seed = seed.wrapping_mul(1_664_525).wrapping_add(1_013_904_223); + let w = (seed >> 9) as f64 / (1u32 << 23) as f64 - 1.0; + let y = w + 1.6 * st[0] - 0.9 * st[1] + 0.3 * st[2] - 0.2 * st[3]; + st = [y, st[0], st[1], st[2]]; + (y * 1000.0) as f32 + }) + .collect(); + let a = lpc(&x, order, fs); + let nlsf = a2nlsf(&a).expect("roots"); + let q15: Vec = nlsf.iter().map(|w| (w / std::f64::consts::PI * 32768.0).round() as i32).collect(); + let a_q12 = nlsf2a(&q15); + for j in 0..order { + assert!((a_q12[j] as f64 / 4096.0 - a[j]).abs() < 0.02, "order {order} coef {j}: {} vs {}", a_q12[j] as f64 / 4096.0, a[j]); + } + } + } +} diff --git a/crates/opusenc/src/silk/lpc.rs b/crates/opusenc/src/silk/lpc.rs new file mode 100644 index 00000000..7846d297 --- /dev/null +++ b/crates/opusenc/src/silk/lpc.rs @@ -0,0 +1,367 @@ +//! Fixed-point NLSF decoding, NLSF→LPC conversion and filter stabilisation +//! (RFC 6716 §4.2.7.5, with the RFC 8251 §6/§7 overflow fixes). +//! +//! This is the decoder's normative arithmetic, mirrored from FilmCraft's first-party, +//! spec-derived `filmcraft-opus` decoder (MIT OR Apache-2.0): the encoder must reconstruct the +//! quantised NLSFs and LPC coefficients bit-exactly as the decoder does. + +use super::tables::*; + +pub const MAX_LPC_ORDER: usize = 16; + +#[inline] +pub fn smulwb(a: i32, b: i32) -> i32 { + ((a as i64 * (b as i16) as i64) >> 16) as i32 +} +#[inline] +pub fn smlawb(a: i32, b: i32, c: i32) -> i32 { + a.wrapping_add(smulwb(b, c)) +} +#[inline] +pub fn smulww(a: i32, b: i32) -> i32 { + ((a as i64 * b as i64) >> 16) as i32 +} +#[inline] +pub fn smmul(a: i32, b: i32) -> i32 { + ((a as i64 * b as i64) >> 32) as i32 +} +#[inline] +pub fn rshift_round(a: i32, s: u32) -> i32 { + if s == 1 { (a >> 1) + (a & 1) } else { ((a >> (s - 1)) + 1) >> 1 } +} +#[inline] +pub fn rshift_round64(a: i64, s: u32) -> i64 { + if s == 1 { (a >> 1) + (a & 1) } else { ((a >> (s - 1)) + 1) >> 1 } +} +#[inline] +pub fn sat16(a: i32) -> i32 { + a.clamp(-32768, 32767) +} + +/// `silk_INVERSE32_varQ`: approximation of `(1 << q_res) / b32`. +pub fn inverse32_varq(b32: i32, q_res: i32) -> i32 { + let b_headrm = b32.unsigned_abs().leading_zeros() as i32 - 1; + let b32_nrm = b32.wrapping_shl(b_headrm as u32); + let b32_inv = (i32::MAX >> 2) / (b32_nrm >> 16); + let mut result = b32_inv << 16; + let err_q32 = ((1i32 << 29).wrapping_sub(smulwb(b32_nrm, b32_inv))).wrapping_shl(3); + result = result.wrapping_add(((err_q32 as i64 * b32_inv as i64) >> 16) as i32); + let lshift = 61 - b_headrm - q_res; + if lshift <= 0 { + let s = (-lshift) as u32; + let lim_hi = i32::MAX >> s; + let lim_lo = i32::MIN >> s; + result.clamp(lim_lo, lim_hi) << s + } else if lshift < 32 { + result >> lshift + } else { + 0 + } +} + +/// `silk_DIV32_varQ`: approximation of `(a32 << q_res) / b32`. +pub fn div32_varq(a32: i32, b32: i32, q_res: i32) -> i32 { + let a_headrm = a32.unsigned_abs().leading_zeros() as i32 - 1; + let a32_nrm = a32.wrapping_shl(a_headrm as u32); + let b_headrm = b32.unsigned_abs().leading_zeros() as i32 - 1; + let b32_nrm = b32.wrapping_shl(b_headrm as u32); + let b32_inv = (i32::MAX >> 2) / (b32_nrm >> 16); + let mut result = smulwb(a32_nrm, b32_inv); + let a32_nrm = a32_nrm.wrapping_sub(((smmul(b32_nrm, result) as i64) << 3) as i32); + result = smlawb(result, a32_nrm, b32_inv); + let lshift = 29 + a_headrm - b_headrm - q_res; + if lshift < 0 { + let s = (-lshift) as u32; + let lim_hi = i32::MAX >> s; + let lim_lo = i32::MIN >> s; + result.clamp(lim_lo, lim_hi) << s + } else if lshift < 32 { + result >> lshift + } else { + 0 + } +} + +pub fn sqrt_approx(x: i32) -> i32 { + if x <= 0 { + return 0; + } + let lz = x.leading_zeros() as i32; + let frac = (x.rotate_right((24 - lz).rem_euclid(32) as u32)) & 0x7f; + let mut y = if lz & 1 != 0 { 32768 } else { 46214 }; + y >>= lz >> 1; + smlawb(y, y, 213 * frac) +} + +/// Decodes the stage-1/stage-2 indices into stabilised NLSFs (Q15). +pub fn nlsf_decode(indices: &[i32], wb: bool) -> [i32; MAX_LPC_ORDER] { + let order = if wb { 16 } else { 10 }; + let i1 = indices[0] as usize; + let cb1: &[i32] = if wb { &NLSF_CB_WB[i1] } else { &NLSF_CB_NBMB[i1] }; + let qstep = if wb { 9830 } else { 11796 }; + // Residual dequantisation with backwards prediction. + let mut res_q10 = [0i32; MAX_LPC_ORDER]; + let mut out_q10 = 0i32; + for i in (0..order).rev() { + let pred = if i + 1 < order { + let sel = if wb { NLSF_PSEL_WB[i1][i] } else { NLSF_PSEL_NBMB[i1][i] } as usize; + let w = if wb { NLSF_PRED_WB[sel][i] } else { NLSF_PRED_NBMB[sel][i] }; + (out_q10 * w) >> 8 + } else { + 0 + }; + let mut v = indices[i + 1] << 10; + if v > 0 { + v -= 102; + } else if v < 0 { + v += 102; + } + out_q10 = smlawb(pred, v, qstep); + res_q10[i] = out_q10; + } + // Laroia weights. + let mut nlsf = [0i32; MAX_LPC_ORDER]; + for k in 0..order { + let prev = if k == 0 { 0 } else { cb1[k - 1] }; + let next = if k + 1 == order { 256 } else { cb1[k + 1] }; + let w = (1024 / (cb1[k] - prev).max(1) + 1024 / (next - cb1[k]).max(1)).min(32767); + let w_q9 = sqrt_approx(w << 16); + let v = (cb1[k] << 7) + (res_q10[k] << 14) / w_q9.max(1); + nlsf[k] = v.clamp(0, 32767); + } + let dmin: &[i32] = if wb { &NLSF_MIN_SPACING_WB } else { &NLSF_MIN_SPACING_NBMB }; + nlsf_stabilize(&mut nlsf[..order], dmin); + nlsf +} + +pub fn nlsf_stabilize(nlsf: &mut [i32], dmin: &[i32]) { + let l = nlsf.len(); + for _ in 0..20 { + let mut min_diff = nlsf[0] - dmin[0]; + let mut idx = 0; + for i in 1..l { + let d = nlsf[i] - (nlsf[i - 1] + dmin[i]); + if d < min_diff { + min_diff = d; + idx = i; + } + } + let d = 32768 - (nlsf[l - 1] + dmin[l]); + if d < min_diff { + min_diff = d; + idx = l; + } + if min_diff >= 0 { + return; + } + if idx == 0 { + nlsf[0] = dmin[0]; + } else if idx == l { + nlsf[l - 1] = 32768 - dmin[l]; + } else { + let mut min_center = dmin[..idx].iter().sum::(); + min_center += dmin[idx] >> 1; + let mut max_center = 32768 - dmin[idx + 1..=l].iter().sum::(); + max_center -= dmin[idx] >> 1; + let c = rshift_round(nlsf[idx - 1] + nlsf[idx], 1); + let c = if min_center > max_center { c.clamp(max_center, min_center) } else { c.clamp(min_center, max_center) }; + nlsf[idx - 1] = c - (dmin[idx] >> 1); + nlsf[idx] = nlsf[idx - 1] + dmin[idx]; + } + } + // Fallback. + nlsf.sort_unstable(); + nlsf[0] = nlsf[0].max(dmin[0]); + for i in 1..l { + nlsf[i] = nlsf[i].max(sat16(nlsf[i - 1] + dmin[i])); + } + nlsf[l - 1] = nlsf[l - 1].min(32768 - dmin[l]); + for i in (0..l - 1).rev() { + nlsf[i] = nlsf[i].min(nlsf[i + 1] - dmin[i + 1]); + } +} + +fn bwexpander_32(ar: &mut [i32], mut chirp_q16: i32) { + let d = ar.len(); + let chirp_minus_one = chirp_q16 - 65536; + for v in ar.iter_mut().take(d - 1) { + *v = smulww(chirp_q16, *v); + chirp_q16 += rshift_round(chirp_q16.wrapping_mul(chirp_minus_one), 16); + } + ar[d - 1] = smulww(chirp_q16, ar[d - 1]); +} + +fn lpc_inverse_pred_gain(a_q12: &[i32]) -> i32 { + const QA: u32 = 24; + const A_LIMIT: i32 = 16_773_022; + let order = a_q12.len(); + let mut a = [0i32; MAX_LPC_ORDER]; + let mut dc = 0; + for k in 0..order { + dc += a_q12[k]; + a[k] = a_q12[k] << (QA - 12); + } + if dc >= 4096 { + return 0; + } + let mut inv_gain: i32 = 1 << 30; + let mul32_frac_q31 = |x: i32, y: i32| rshift_round64(x as i64 * y as i64, 31) as i32; + for k in (1..order).rev() { + if a[k] > A_LIMIT || a[k] < -A_LIMIT { + return 0; + } + let rc_q31 = -(a[k] << (31 - QA)); + let rc_mult1 = (1i32 << 30) - smmul(rc_q31, rc_q31); + inv_gain = smmul(inv_gain, rc_mult1) << 2; + if inv_gain < 107_374 { + return 0; + } + let mult2q = 32 - rc_mult1.unsigned_abs().leading_zeros() as i32; + let rc_mult2 = inverse32_varq(rc_mult1, mult2q + 30); + for n in 0..(k + 1) >> 1 { + let t1 = a[n]; + let t2 = a[k - n - 1]; + let v1 = rshift_round64(t1.saturating_sub(mul32_frac_q31(t2, rc_q31)) as i64 * rc_mult2 as i64, mult2q as u32); + if v1 > i32::MAX as i64 || v1 < i32::MIN as i64 { + return 0; + } + let v2 = rshift_round64(t2.saturating_sub(mul32_frac_q31(t1, rc_q31)) as i64 * rc_mult2 as i64, mult2q as u32); + if v2 > i32::MAX as i64 || v2 < i32::MIN as i64 { + return 0; + } + a[n] = v1 as i32; + a[k - n - 1] = v2 as i32; + } + } + if a[0] > A_LIMIT || a[0] < -A_LIMIT { + return 0; + } + let rc_q31 = -(a[0] << (31 - QA)); + let rc_mult1 = (1i32 << 30) - smmul(rc_q31, rc_q31); + inv_gain = smmul(inv_gain, rc_mult1) << 2; + if inv_gain < 107_374 { + return 0; + } + inv_gain +} + +/// Converts NLSFs (Q15) to stable Q12 LPC coefficients (`silk_NLSF2A`). +pub fn nlsf2a(nlsf: &[i32]) -> [i32; MAX_LPC_ORDER] { + let d = nlsf.len(); + let ordering: &[usize] = if d == 16 { &LSF_ORDER_WB } else { &LSF_ORDER_NBMB }; + let mut c_q17 = [0i32; MAX_LPC_ORDER]; + for k in 0..d { + let i = (nlsf[k] >> 8) as usize; + let f = nlsf[k] & 255; + let c0 = COS_Q12[i]; + let c1 = COS_Q12[(i + 1).min(128)]; + c_q17[ordering[k]] = (c0 * 256 + (c1 - c0) * f + 4) >> 3; + } + let d2 = d / 2; + let find_poly = |off: usize| -> [i64; 10] { + let mut out = [0i64; 10]; + out[0] = 1 << 16; + out[1] = -(c_q17[off] as i64); + for k in 1..d2 { + let ftmp = c_q17[2 * k + off] as i64; + out[k + 1] = 2 * out[k - 1] - ((ftmp * out[k] + 32768) >> 16); + for n in (2..=k).rev() { + out[n] += out[n - 2] - ((ftmp * out[n - 1] + 32768) >> 16); + } + out[1] -= ftmp; + } + out + }; + let p = find_poly(0); + let q = find_poly(1); + let mut a32 = [0i32; MAX_LPC_ORDER]; + for k in 0..d2 { + let ptmp = p[k + 1] + p[k]; + let qtmp = q[k + 1] - q[k]; + a32[k] = (-qtmp - ptmp) as i32; + a32[d - k - 1] = (qtmp - ptmp) as i32; + } + let a32 = &mut a32[..d]; + // Limit the dynamic range (silk_LPC_fit with QIN = 17, QOUT = 12). + let mut a_q12 = [0i32; MAX_LPC_ORDER]; + let mut i = 0; + while i < 10 { + let mut maxabs = 0i32; + let mut idx = 0; + for (k, &v) in a32.iter().enumerate() { + let av = v.saturating_abs(); + if av > maxabs { + maxabs = av; + idx = k; + } + } + let maxabs = rshift_round(maxabs, 5); + if maxabs > 32767 { + let maxabs = maxabs.min(163_838); + let chirp = 65470 - ((maxabs - 32767) << 14) / ((maxabs * (idx as i32 + 1)) >> 2); + bwexpander_32(a32, chirp); + } else { + break; + } + i += 1; + } + if i == 10 { + for k in 0..d { + a_q12[k] = sat16(rshift_round(a32[k], 5)); + a32[k] = a_q12[k] << 5; + } + } else { + for k in 0..d { + a_q12[k] = rshift_round(a32[k], 5); + } + } + for i in 0..16 { + if lpc_inverse_pred_gain(&a_q12[..d]) == 0 { + bwexpander_32(a32, 65536 - (2 << i)); + for k in 0..d { + a_q12[k] = rshift_round(a32[k], 5); + } + } else { + break; + } + } + a_q12 +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn flat_nlsf_gives_small_lpc() { + // Uniformly spaced NLSFs correspond to an (almost) flat spectrum. + let nlsf: Vec = (1..=16).map(|k| k * 32768 / 17).collect(); + let a = nlsf2a(&nlsf); + assert!(a.iter().all(|v| v.abs() < 600), "{a:?}"); + let nlsf: Vec = (1..=10).map(|k| k * 32768 / 11).collect(); + let a = nlsf2a(&nlsf); + assert!(a[..10].iter().all(|v| v.abs() < 600), "{a:?}"); + } + + #[test] + fn stabilize_enforces_spacing() { + let mut v = [100, 90, 80, 30000, 30001, 30002, 31000, 32000, 32700, 32760]; + nlsf_stabilize(&mut v, &NLSF_MIN_SPACING_NBMB); + assert!(v[0] >= NLSF_MIN_SPACING_NBMB[0]); + for i in 1..10 { + assert!(v[i] - v[i - 1] >= NLSF_MIN_SPACING_NBMB[i], "{v:?}"); + } + assert!(32768 - v[9] >= NLSF_MIN_SPACING_NBMB[10]); + } + + #[test] + fn inverse_approximations() { + for &b in &[65536, 100_000, 1 << 20, 81920, 1_686_110_208] { + let inv = inverse32_varq(b, 47) as f64; + let exact = 2f64.powi(47) / b as f64; + assert!((inv - exact).abs() / exact < 1e-3, "{b}"); + } + let q = div32_varq(65536, 131072, 16) as f64; + assert!((q - 32768.0).abs() < 2.0); + } +} diff --git a/crates/opusenc/src/silk/mod.rs b/crates/opusenc/src/silk/mod.rs new file mode 100644 index 00000000..c7a4b869 --- /dev/null +++ b/crates/opusenc/src/silk/mod.rs @@ -0,0 +1,706 @@ +//! SILK layer encoder (RFC 6716 §4.2, encoder direction), 20 ms frames at 8, 12 or 16 kHz. +//! +//! Each frame is analysed (VAD, open-loop pitch, windowed-autocorrelation LPC, LTP codebook +//! search), its NLSFs are quantised, and the excitation is chosen sample by sample by running +//! the decoder's exact fixed-point reconstruction (LTP and LPC synthesis with the dequantised +//! Q12/Q14 coefficients, quantisation offsets and the pseudo-random sign sequence), so the +//! encoder always holds the decoder's state and there is no drift. A noise-feedback loop +//! shapes the coding noise with a bandwidth-expanded LPC envelope, and a per-frame search over +//! a gain offset hits the bit budget. Stereo input is coded as mid/side with the decoder's +//! stereo prediction (§4.2.8). No LBRR, no NLSF interpolation. + +pub(crate) mod analysis; +pub(crate) mod lpc; +mod nlsf; +mod pulses; +#[allow(dead_code)] +pub(crate) mod tables; + +use lpc::*; +use tables::*; + +use crate::range::RangeEncoder; + +const MAX_FRAME: usize = 320; +const LTP_ORDER: usize = 5; +/// Frame RMS (16-bit scale) below which a frame is coded as inactive (VAD off). +const VAD_RMS: f32 = 30.0; +/// Normalised pitch correlation above which a frame is a voicing candidate. +const VOICING_THRESHOLD: f64 = 0.45; +/// Minimum LTP prediction gain (energy ratio) for a frame to be coded as voiced. +const MIN_LTP_GAIN: f64 = 1.3; +/// Rate penalty (squared pulse units per unit magnitude) of the excitation quantiser. +const RD_LAMBDA: f64 = 0.15; +/// Extra gain offset (log2) for inactive frames. +const INACTIVE_OFFSET: f64 = 1.0; +/// Noise feedback: the coding noise is shaped by `1 / A(z/γ)` (A the short-term predictor). +/// γ = 0 leaves it white (closed-loop DPCM, best waveform SNR at high rates), γ = 1 shapes it +/// like the spectral envelope (open-loop residual coding, needed at low rates). γ grows with the +/// gain offset, i.e. as the quantisation gets coarser. +fn nf_gamma(offset: f64) -> f64 { + (0.75 + 0.05 * offset).clamp(0.75, 0.9) +} + +/// `silk_log2lin` (decoder arithmetic). +fn log2lin(in_log_q7: i32) -> i32 { + if in_log_q7 < 0 { + return 0; + } + if in_log_q7 >= 3967 { + return i32::MAX; + } + let out = 1i32 << (in_log_q7 >> 7); + let frac = in_log_q7 & 0x7F; + if in_log_q7 < 2048 { out + ((out * smlawb(frac, frac * (128 - frac), -174)) >> 7) } else { out + (out >> 7) * smlawb(frac, frac * (128 - frac), -174) } +} + +fn gain_q16(index: i32) -> i32 { + log2lin((smulwb(0x1D1C71, index) + 2090).min(3967)) +} + +/// The decoder's LPC analysis filter on 16-bit samples. +fn lpc_analysis_filter(out: &mut [i16], input: &[i16], b: &[i32], d: usize) { + let len = out.len(); + for ix in d..len { + let mut acc: i32 = 0; + for j in 0..d { + acc = acc.wrapping_add((input[ix - 1 - j] as i32).wrapping_mul(b[j])); + } + let v = ((input[ix] as i32) << 12).wrapping_sub(acc); + out[ix] = sat16(rshift_round(v, 12)) as i16; + } + for v in out.iter_mut().take(d.min(len)) { + *v = 0; + } +} + +/// The part of the decoder's channel state that the reconstruction depends on. +#[derive(Clone)] +struct DecState { + prev_gain_q16: i32, + s_lpc_q14: [i32; MAX_LPC_ORDER], + out_buf: [i16; MAX_FRAME], + last_gain_index: i32, +} + +impl DecState { + fn new() -> Self { + DecState { prev_gain_q16: 65536, s_lpc_q14: [0; MAX_LPC_ORDER], out_buf: [0; MAX_FRAME], last_gain_index: 10 } + } + + /// The decoder's reset of the side channel when it is coded again after mid-only frames. + fn reset_side(&mut self) { + self.out_buf = [0; MAX_FRAME]; + self.s_lpc_q14 = [0; MAX_LPC_ORDER]; + self.last_gain_index = 10; + } +} + +/// Coded parameters of one frame (everything except gains and pulses). +#[derive(Clone, Default)] +struct Params { + vad: bool, + signal_type: usize, + quant_offset_type: usize, + nlsf_ix: [i32; 17], + a_q12: [i32; MAX_LPC_ORDER], + lag_index: i32, + contour: usize, + per_index: usize, + ltp_index: [usize; 4], + pitch_l: [i32; 4], + /// RMS (16-bit scale) of the open-loop prediction residual per subframe. + res_rms: [f32; 4], +} + +/// Gains, seed and pulses chosen by the quantiser. +struct Coded { + gain_ix: [i32; 4], + gains_q16: [i32; 4], + seed: usize, + pulses: Vec, +} + +#[derive(Clone)] +struct Channel { + fs_khz: usize, + len: usize, + sub: usize, + order: usize, + dec: DecState, + /// The last two frames of input (internal rate, 16-bit scale). + hist: Vec, + /// Recent output errors (decoded − input), most recent first, for noise feedback. + nf: [f64; MAX_LPC_ORDER], +} + +impl Channel { + fn new(fs_khz: usize) -> Self { + let len = 20 * fs_khz; + Channel { + fs_khz, + len, + sub: 5 * fs_khz, + order: if fs_khz == 16 { 16 } else { 10 }, + dec: DecState::new(), + hist: vec![0.0; 2 * len], + nf: [0.0; MAX_LPC_ORDER], + } + } + + fn push_history(&mut self, x: &[f32]) { + let l = self.len; + self.hist.copy_within(l.., 0); + self.hist[l..].copy_from_slice(x); + } + + /// Frame analysis: VAD, signal type, pitch, LPC/NLSF quantisation and LTP taps. + fn analyze(&self, x: &[f32]) -> Params { + let l = self.len; + let sub = self.sub; + let order = self.order; + let wb = order == 16; + let mut buf = self.hist.clone(); + buf.extend_from_slice(x); + let start = 2 * l; + let rms = (x.iter().map(|v| v * v).sum::() / l as f32).sqrt(); + let vad = rms > VAD_RMS; + let pitch = if vad { Some(analysis::pitch_search(&buf, start, l, self.fs_khz)) } else { None }; + let mut voiced = pitch.as_ref().is_some_and(|p| p.score > VOICING_THRESHOLD); + + let a = analysis::lpc(&buf[start - l / 4..], order, self.fs_khz); + let target: Vec = match analysis::a2nlsf(&a) { + Some(w) => w.iter().map(|v| v / std::f64::consts::PI * 32768.0).collect(), + None => (1..=order).map(|k| k as f64 * 32768.0 / (order + 1) as f64).collect(), + }; + let quant = |signal_type: usize| { + let (ix, nlsf) = nlsf::quantize(&target, signal_type, wb); + (ix, nlsf2a(&nlsf[..order])) + }; + let mut p = Params { vad, ..Default::default() }; + let (mut ix, mut a_q12) = quant(if voiced { 2 } else { 1 }); + // Open-loop residual with the quantised predictor. + let mut r = vec![0f64; buf.len()]; + for n in order..buf.len() { + let mut acc = buf[n] as f64; + for j in 0..order { + acc -= a_q12[j] as f64 / 4096.0 * buf[n - 1 - j] as f64; + } + r[n] = acc; + } + if voiced && let Some(pitch) = &pitch { + let ltp = analysis::ltp_search(&r, start, sub, &pitch.lags); + let gain = ltp.energy.iter().sum::() / ltp.err.iter().sum::().max(1e-9); + if gain >= MIN_LTP_GAIN { + p.lag_index = pitch.lag_index; + p.contour = pitch.contour; + p.pitch_l = pitch.lags; + p.per_index = ltp.per_index; + p.ltp_index = ltp.ltp_index; + } else { + voiced = false; + (ix, a_q12) = quant(1); + } + } + // Gains follow the short-term residual level (the long-term predictor then only + // reduces the number of pulses), which keeps the noise-to-signal ratio steady. + for k in 0..4 { + let e: f64 = r[start + k * sub..start + (k + 1) * sub].iter().map(|v| v * v).sum(); + p.res_rms[k] = (e / sub as f64).sqrt() as f32; + } + p.signal_type = if !vad { + 0 + } else if voiced { + 2 + } else { + 1 + }; + p.quant_offset_type = 0; + p.nlsf_ix = ix; + p.a_q12 = a_q12; + p + } + + /// Chooses gains and pulses for target `x` by running the decoder's reconstruction, and + /// advances the mirrored decoder state. `offset` scales the gains (log2), `floor` gives + /// minimum gains (Q16) per subframe. Returns the coded values and the decoded frame. + fn quantize(&mut self, p: &Params, x: &[f32], offset: f64, floor: Option<&[i32; 4]>, seed: usize) -> (Coded, Vec) { + let l = self.len; + let sub = self.sub; + let order = self.order; + let st = &mut self.dec; + let nf = &mut self.nf; + let off = if p.signal_type == 0 { offset + INACTIVE_OFFSET } else { offset }; + let gamma = nf_gamma(off); + let mut nf_c = [0f64; MAX_LPC_ORDER]; + let mut gpow = 1.0; + for j in 0..order { + gpow *= gamma; + nf_c[j] = p.a_q12[j] as f64 / 4096.0 * gpow; + } + // Gains (independent coding of the first subframe, deltas for the rest). + let mut gain_ix = [0i32; 4]; + let mut gains_q16 = [0i32; 4]; + let mut prev = st.last_gain_index; + for k in 0..4 { + let mut g = p.res_rms[k] as f64 * off.exp2(); + if let Some(f) = floor { + g = g.max(f[k] as f64 / 65536.0); + } + let want = (((g * 65536.0).max(1.0).log2() * 128.0 - 2090.0) / (1_907_825.0 / 65536.0)).round().clamp(0.0, 63.0) as i32; + if k == 0 { + gain_ix[0] = want; + prev = want.max(prev - 16).clamp(0, 63); + } else { + let mut best = (i32::MAX, 0i32, prev); + for ind in 0..41 { + let tmp = ind - 4; + let thr = 2 * 36 - 64 + prev; + let d = if tmp > thr { prev + (tmp << 1) - thr } else { prev + tmp }.clamp(0, 63); + let e = (d - want).abs(); + if e < best.0 { + best = (e, ind, d); + } + } + gain_ix[k] = best.1; + prev = best.2; + } + gains_q16[k] = gain_q16(prev); + } + st.last_gain_index = prev; + + // Analysis-by-synthesis excitation quantisation through the decoder's exact synthesis. + let voiced = p.signal_type == 2; + let offset_q10 = [[100, 240], [32, 100]][p.signal_type >> 1][p.quant_offset_type]; + let off16 = offset_q10 << 4; + let mut seed_v = seed as i32; + let mut pulses = vec![0i32; l]; + let mut xq = vec![0i16; l]; + let mut s_lpc = [0i32; MAX_FRAME / 4 + MAX_LPC_ORDER]; + s_lpc[..MAX_LPC_ORDER].copy_from_slice(&st.s_lpc_q14); + let mut s_ltp = [0i16; MAX_FRAME]; + let mut s_ltp_q15 = [0i32; 2 * MAX_FRAME]; + let ltp_mem = l; + let mut ltp_buf_idx = ltp_mem; + let a_q12 = &p.a_q12; + let ltp_scale_q14 = 15565; + for k in 0..4 { + let mut b_q14 = [0i32; LTP_ORDER]; + if voiced { + let ix = p.ltp_index[k]; + let taps = match p.per_index { + 0 => LTP_TAPS_0[ix], + 1 => LTP_TAPS_1[ix], + _ => LTP_TAPS_2[ix], + }; + for i in 0..LTP_ORDER { + b_q14[i] = taps[i] << 7; + } + } + let gain_q10 = gains_q16[k] >> 6; + let mut inv_gain_q31 = inverse32_varq(gains_q16[k], 47); + let gain_adj_q16 = if gains_q16[k] != st.prev_gain_q16 { + let g = div32_varq(st.prev_gain_q16, gains_q16[k], 16); + for v in s_lpc[..MAX_LPC_ORDER].iter_mut() { + *v = smulww(g, *v); + } + g + } else { + 1 << 16 + }; + st.prev_gain_q16 = gains_q16[k]; + let lag = p.pitch_l[k]; + if voiced { + let lag_u = lag as usize; + if k == 0 { + let start_idx = (ltp_mem as isize - lag as isize - order as isize - (LTP_ORDER / 2) as isize).max(0) as usize; + lpc_analysis_filter(&mut s_ltp[start_idx..ltp_mem], &st.out_buf[start_idx..ltp_mem], a_q12, order); + inv_gain_q31 = smulwb(inv_gain_q31, ltp_scale_q14) << 2; + for i in 0..(lag_u + LTP_ORDER / 2).min(ltp_mem) { + s_ltp_q15[ltp_buf_idx - i - 1] = smulwb(inv_gain_q31, s_ltp[ltp_mem - i - 1] as i32); + } + } else if gain_adj_q16 != 1 << 16 { + for i in 0..(lag_u + LTP_ORDER / 2).min(ltp_buf_idx) { + let q = ltp_buf_idx - i - 1; + s_ltp_q15[q] = smulww(gain_adj_q16, s_ltp_q15[q]); + } + } + } + let inv_g = 16_777_216.0 / gain_q10.max(1) as f64; + for i in 0..sub { + let n = k * sub + i; + seed_v = seed_v.wrapping_mul(196_314_165).wrapping_add(907_633_515); + let ltp_q14 = if voiced { + let base = ltp_buf_idx as isize - lag as isize + (LTP_ORDER / 2) as isize; + let g = |o: isize| -> i32 { if base - o >= 0 { s_ltp_q15[(base - o) as usize] } else { 0 } }; + let mut pred = 2i32; + for o in 0..LTP_ORDER { + pred = smlawb(pred, g(o as isize), b_q14[o]); + } + pred << 1 + } else { + 0 + }; + let mut lpc_pred = (order >> 1) as i32; + for j in 0..order { + lpc_pred = smlawb(lpc_pred, s_lpc[MAX_LPC_ORDER + i - 1 - j], a_q12[j]); + } + let lpc_q14 = lpc_pred.saturating_mul(16); + // Target excitation (Q14) for this sample. + let fb: f64 = (0..order).map(|j| nf_c[j] * nf[j]).sum(); + let tgt = (x[n] as f64 + fb).clamp(-32768.0, 32767.0) * inv_g - lpc_q14 as f64 - ltp_q14 as f64; + let sgn = if seed_v < 0 { -1.0 } else { 1.0 }; + let u = sgn * tgt; + let pf = (u - off16 as f64) / 16384.0; + let p0 = if pf > 0.0 { (pf + 0.078_125).round() } else { (pf - 0.078_125).round() } as i32; + let level = |q: i32| -> i32 { + let mut e = q << 14; + if e > 0 { + e -= 80 << 4; + } else if e < 0 { + e += 80 << 4; + } + e + off16 + }; + let mut best = (f64::MAX, 0i32); + for q in [p0 - 1, p0, p0 + 1, 0] { + let q = q.clamp(-pulses::MAX_PULSE, pulses::MAX_PULSE); + let d = (level(q) as f64 - u) / 16384.0; + let cost = d * d + RD_LAMBDA * q.abs() as f64; + if cost < best.0 { + best = (cost, q); + } + } + let q = best.1; + pulses[n] = q; + let mut e = level(q); + if seed_v < 0 { + e = -e; + } + seed_v = seed_v.wrapping_add(q); + let res = e.wrapping_add(ltp_q14); + if voiced { + s_ltp_q15[ltp_buf_idx] = res << 1; + ltp_buf_idx += 1; + } + let v = res.saturating_add(lpc_q14); + s_lpc[MAX_LPC_ORDER + i] = v; + xq[n] = sat16(rshift_round(smulww(v, gain_q10), 8)) as i16; + nf.copy_within(0..MAX_LPC_ORDER - 1, 1); + nf[0] = xq[n] as f64 - x[n] as f64; + } + s_lpc.copy_within(sub..sub + MAX_LPC_ORDER, 0); + } + st.s_lpc_q14.copy_from_slice(&s_lpc[..MAX_LPC_ORDER]); + st.out_buf[..l].copy_from_slice(&xq); + (Coded { gain_ix, gains_q16, seed, pulses }, xq) + } + + /// Writes the frame's indices and pulses (`decode_indices` + `decode_pulses` order, coded + /// independently). + fn write(&self, enc: &mut RangeEncoder, p: &Params, c: &Coded) { + let ix = p.signal_type * 2 + p.quant_offset_type; + if p.vad { + enc.icdf(ix - 2, &TYPE_ACTIVE, 8); + } else { + enc.icdf(ix, &TYPE_INACTIVE, 8); + } + enc.icdf((c.gain_ix[0] >> 3) as usize, GAIN_MSB[p.signal_type], 8); + enc.icdf((c.gain_ix[0] & 7) as usize, &UNIFORM8, 8); + for k in 1..4 { + enc.icdf(c.gain_ix[k] as usize, &DELTA_GAIN, 8); + } + let wb = self.order == 16; + let i1 = p.nlsf_ix[0] as usize; + enc.icdf(i1, NLSF_STAGE1[(p.signal_type >> 1) + if wb { 2 } else { 0 }], 8); + for i in 0..self.order { + let cb = if wb { 8 + NLSF_SEL_WB[i1][i] as usize } else { NLSF_SEL_NBMB[i1][i] as usize }; + let v = p.nlsf_ix[i + 1] + 4; + if v <= 0 { + enc.icdf(0, NLSF_STAGE2[cb], 8); + enc.icdf((-v) as usize, &NLSF_EXT, 8); + } else if v >= 8 { + enc.icdf(8, NLSF_STAGE2[cb], 8); + enc.icdf((v - 8) as usize, &NLSF_EXT, 8); + } else { + enc.icdf(v as usize, NLSF_STAGE2[cb], 8); + } + } + // No NLSF interpolation. + enc.icdf(4, &NLSF_INTERP, 8); + if p.signal_type == 2 { + let half = (self.fs_khz >> 1) as i32; + enc.icdf((p.lag_index / half) as usize, &PITCH_HIGH, 8); + let low = match self.fs_khz { + 8 => PITCH_LOW[0], + 12 => PITCH_LOW[1], + _ => PITCH_LOW[2], + }; + enc.icdf((p.lag_index % half) as usize, low, 8); + enc.icdf(p.contour, if self.fs_khz == 8 { PITCH_CONTOUR[1] } else { PITCH_CONTOUR[3] }, 8); + enc.icdf(p.per_index, <P_PERIODICITY, 8); + for k in 0..4 { + enc.icdf(p.ltp_index[k], LTP_FILTER[p.per_index], 8); + } + // LTP scaling index 0 (15565 in Q14), matching `quantize`. + enc.icdf(0, <P_SCALE, 8); + } + enc.icdf(c.seed, &UNIFORM4, 8); + pulses::encode(enc, &c.pulses, p.signal_type, p.quant_offset_type); + } +} + +/// Quantises a stereo prediction weight (Q13) to the decoder's grid (§4.2.7.1): returns the +/// (UNIFORM3, UNIFORM5, joint) indices and the reconstructed value. +fn quant_pred(w: f64) -> ([usize; 3], i32) { + let mut best = (f64::MAX, [0usize; 3], 0i32); + for i in 0..15 { + let low = STEREO_WEIGHTS_Q13[i]; + let step = smulwb(STEREO_WEIGHTS_Q13[i + 1] - low, 6554); + for j in 0..5 { + let v = low + step * (2 * j as i32 + 1); + let e = (v as f64 - w).abs(); + if e < best.0 { + best = (e, [i % 3, j, i / 3], v); + } + } + } + (best.1, best.2) +} + +/// Per-frame preparation shared by every rate-loop trial. +struct Prep { + x: [Vec; 2], + params: [Params; 2], + mid_only: bool, + pred_ix: [[usize; 3]; 2], + pred_q13: [i32; 2], +} + +/// SILK encoder for one mono or stereo (mid/side) stream. +#[derive(Clone)] +pub struct SilkEncoder { + fs_khz: usize, + channels: usize, + ch: [Channel; 2], + pred_prev_q13: [i32; 2], + prev_mid_only: bool, + /// Last mid sample of the previous frame (for the stereo predictor's low-pass). + prev_mid: f32, + frame: usize, + /// Gain offset chosen for the previous frame (search starting point). + pub last_offset: f64, +} + +impl SilkEncoder { + pub fn new(fs_khz: usize, channels: usize) -> Self { + SilkEncoder { + fs_khz, + channels, + ch: [Channel::new(fs_khz), Channel::new(fs_khz)], + pred_prev_q13: [0; 2], + prev_mid_only: false, + prev_mid: 0.0, + frame: 0, + last_offset: 1.0, + } + } + + /// Samples per channel of one 20 ms frame at the internal rate. + pub fn frame_len(&self) -> usize { + 20 * self.fs_khz + } + + fn prepare(&self, input: &[Vec]) -> Prep { + let l = self.frame_len(); + if self.channels == 1 { + let x = input[0].clone(); + let p = self.ch[0].analyze(&x); + return Prep { x: [x, vec![0.0; l]], params: [p, Params::default()], mid_only: true, pred_ix: [[0; 3]; 2], pred_q13: [0; 2] }; + } + let mid: Vec = (0..l).map(|i| 0.5 * (input[0][i] + input[1][i])).collect(); + let side: Vec = (0..l).map(|i| 0.5 * (input[0][i] - input[1][i])).collect(); + let lp = |j: usize| -> f64 { + let a = if j == 0 { self.prev_mid } else { mid[j - 1] }; + let c = if j + 1 < l { mid[j + 1] } else { mid[l - 1] }; + (a as f64 + 2.0 * mid[j] as f64 + c as f64) / 4.0 + }; + // Least-squares prediction of the side from the low-passed mid and the mid. + let (mut r00, mut r01, mut r11, mut c0, mut c1, mut ss, mut mm) = (0f64, 0f64, 0f64, 0f64, 0f64, 0f64, 0f64); + for j in 0..l { + let (a, b, s) = (lp(j), mid[j] as f64, side[j] as f64); + r00 += a * a; + r01 += a * b; + r11 += b * b; + c0 += a * s; + c1 += b * s; + ss += s * s; + mm += b * b; + } + // Ridge regularisation: the low-passed mid and the mid are nearly collinear. + let lam = 0.02 * 0.5 * (r00 + r11) + 1e-9; + let (a00, a11) = (r00 + lam, r11 + lam); + let det = a00 * a11 - r01 * r01; + let (p0, p1) = ((c0 * a11 - c1 * r01) / det, (c1 * a00 - c0 * r01) / det); + let lim = 13732.0; + let quant = |p0: f64, p1: f64| { + let (ix0, q0) = quant_pred(((p0 + p1) * 8192.0).clamp(-lim, lim)); + let (ix1, q1) = quant_pred((p1 * 8192.0).clamp(-lim, lim)); + ([ix0, ix1], [q0 - q1, q1]) + }; + // Side residual with the decoder's interpolated predictor. + let fs = self.fs_khz as i32; + let denom_q16 = (1 << 16) / (8 * fs); + let interp = 8 * self.fs_khz; + let residual = |pred_q13: [i32; 2]| -> Vec { + let d0 = rshift_round((pred_q13[0] - self.pred_prev_q13[0]) as i16 as i32 * denom_q16 as i16 as i32, 16); + let d1 = rshift_round((pred_q13[1] - self.pred_prev_q13[1]) as i16 as i32 * denom_q16 as i16 as i32, 16); + let (mut pp0, mut pp1) = (self.pred_prev_q13[0] + d0, self.pred_prev_q13[1] + d1); + (0..l) + .map(|j| { + if j + 1 < interp { + pp0 += d0; + pp1 += d1; + } else { + pp0 = pred_q13[0]; + pp1 = pred_q13[1]; + } + (side[j] as f64 - (pp0 as f64 * lp(j) + pp1 as f64 * mid[j] as f64) / 8192.0) as f32 + }) + .collect() + }; + let energy = |v: &[f32]| -> f64 { v.iter().map(|&x| (x as f64).powi(2)).sum() }; + let (mut pred_ix, mut pred_q13) = quant(p0, p1); + let mut res = residual(pred_q13); + let mut res_e = energy(&res); + // Never predict worse than not predicting at all. + if res_e > ss { + (pred_ix, pred_q13) = quant(0.0, 0.0); + res = residual(pred_q13); + res_e = energy(&res); + } + let res_rms = (res_e / l as f64).sqrt(); + let mid_rms = (mm / l as f64).sqrt(); + let mid_only = res_rms < 0.01 * mid_rms + 1.0; + let pm = self.ch[0].analyze(&mid); + let ps = if mid_only { Params::default() } else { self.ch[1].analyze(&res) }; + Prep { x: [mid, res], params: [pm, ps], mid_only, pred_ix, pred_q13 } + } + + /// Codes one frame into `enc` with gain offset `offset`. + fn code(&mut self, prep: &Prep, offset: f64, enc: &mut RangeEncoder) { + let stereo = self.channels == 2; + let side_vad = stereo && !prep.mid_only && prep.params[1].vad; + enc.bit_logp(prep.params[0].vad, 1); + enc.bit_logp(false, 1); + if stereo { + enc.bit_logp(side_vad, 1); + enc.bit_logp(false, 1); + let [a, b] = prep.pred_ix; + enc.icdf(5 * a[2] + b[2], &STEREO_PRED_JOINT, 8); + enc.icdf(a[0], &UNIFORM3, 8); + enc.icdf(a[1], &UNIFORM5, 8); + enc.icdf(b[0], &UNIFORM3, 8); + enc.icdf(b[1], &UNIFORM5, 8); + if !side_vad { + enc.icdf(prep.mid_only as usize, &MID_ONLY, 8); + } + } + let seed = self.frame & 3; + let (cm, _) = self.ch[0].quantize(&prep.params[0], &prep.x[0], offset, None, seed); + self.ch[0].write(enc, &prep.params[0], &cm); + if stereo { + if !prep.mid_only { + if self.prev_mid_only { + self.ch[1].dec.reset_side(); + } + let (cs, _) = self.ch[1].quantize(&prep.params[1], &prep.x[1], offset, Some(&cm.gains_q16), seed); + self.ch[1].write(enc, &prep.params[1], &cs); + } + self.pred_prev_q13 = prep.pred_q13; + self.prev_mid_only = prep.mid_only; + } + } + + /// Encodes one 20 ms frame (`input`: one internal-rate, 16-bit-scale vector per channel) + /// into `enc`, aiming at `target_bits` total and never exceeding `max_bits`. + pub fn encode(&mut self, input: &[Vec], target_bits: f64, max_bits: i32, enc: &mut RangeEncoder) { + let prep = self.prepare(input); + let start_bits = enc.tell(); + let trial = |s: &Self, o: f64| -> i32 { + let mut t = s.clone(); + let mut e = enc.clone(); + t.code(&prep, o, &mut e); + e.tell() - start_bits + }; + // Bisection on the gain offset: the smallest offset (finest quantisation) within budget. + let target = target_bits.min(max_bits as f64); + // Bits are not strictly monotonic in the offset (very coarse quantisation feeds large + // errors back), so walk an integer grid from the previous frame's offset to find a + // bracket [infeasible, feasible], then bisect inside it. + const MIN_O: f64 = -3.0; + const MAX_O: f64 = 6.0; + let mut best_any = (i32::MAX, MAX_O); + let mut probe = |o: f64| -> bool { + let b = trial(self, o); + if b < best_any.0 { + best_any = (b, o); + } + b as f64 <= target + }; + let g0 = self.last_offset.round().clamp(MIN_O, MAX_O); + let mut bracket = None; + if probe(g0) { + let mut hi = g0; + loop { + if hi - 1.0 < MIN_O { + bracket = Some((None, hi)); + break; + } + if probe(hi - 1.0) { + hi -= 1.0; + } else { + bracket = Some((Some(hi - 1.0), hi)); + break; + } + } + } else { + let mut lo = g0; + while lo + 1.0 <= MAX_O { + if probe(lo + 1.0) { + bracket = Some((Some(lo), lo + 1.0)); + break; + } + lo += 1.0; + } + } + let mut o = match bracket { + Some((Some(mut lo), mut hi)) => { + for _ in 0..4 { + let m = 0.5 * (lo + hi); + if probe(m) { + hi = m; + } else { + lo = m; + } + } + hi + } + Some((None, hi)) => hi, + None => best_any.1, + }; + // Hard limit (hybrid packets must leave room for CELT). + while trial(self, o) > max_bits && o < 12.0 { + o += 1.0; + } + self.last_offset = o; + self.code(&prep, o, enc); + for ch in 0..self.channels { + self.ch[ch].push_history(&prep.x[ch]); + } + if self.channels == 2 { + self.prev_mid = prep.x[0][self.frame_len() - 1]; + } + self.frame += 1; + } +} diff --git a/crates/opusenc/src/silk/nlsf.rs b/crates/opusenc/src/silk/nlsf.rs new file mode 100644 index 00000000..3f1f17ac --- /dev/null +++ b/crates/opusenc/src/silk/nlsf.rs @@ -0,0 +1,128 @@ +//! NLSF quantisation (encoder side of RFC 6716 §4.2.7.5): stage-1 vector codebook preselection +//! and a backwards-predictive stage-2 scalar quantiser that reproduces the decoder's residual +//! dequantisation exactly. The search strategy is our own. + +use super::lpc::{MAX_LPC_ORDER, nlsf_decode, smlawb, sqrt_approx}; +use super::tables::*; + +/// Number of stage-1 candidates refined with stage 2. +const SURVIVORS: usize = 8; +/// Rate weight (weighted squared Q15 error per bit). +const LAMBDA: f64 = 60.0; + +fn icdf_bits(icdf: &[u8], s: usize) -> f64 { + let hi = if s == 0 { 256 } else { icdf[s - 1] as u32 }; + let p = (hi - icdf[s] as u32).max(1) as f64 / 256.0; + -p.log2() +} + +/// Bits of a stage-2 index `q` (-10..=10) with stage-2 table `tab`. +fn stage2_bits(tab: &[u8], q: i32) -> f64 { + let v = q + 4; + if v <= 0 { + icdf_bits(tab, 0) + icdf_bits(&NLSF_EXT, (-v) as usize) + } else if v >= 8 { + icdf_bits(tab, 8) + icdf_bits(&NLSF_EXT, (v - 8) as usize) + } else { + icdf_bits(tab, v as usize) + } +} + +/// Quantises target NLSFs (`target`, Q15 as f64, ascending) and returns the 1 + order indices +/// in the decoder's layout (`[stage1, stage2...]`) together with the reconstructed NLSFs. +pub fn quantize(target: &[f64], signal_type: usize, wb: bool) -> ([i32; 17], [i32; MAX_LPC_ORDER]) { + let order = target.len(); + // Laroia-style weights. + let w: Vec = (0..order) + .map(|k| { + let prev = if k == 0 { 0.0 } else { target[k - 1] }; + let next = if k + 1 == order { 32768.0 } else { target[k + 1] }; + 1.0 / (target[k] - prev).max(50.0) + 1.0 / (next - target[k]).max(50.0) + }) + .collect(); + let s1_tab = NLSF_STAGE1[(signal_type >> 1) + if wb { 2 } else { 0 }]; + let n_cb = 32; + let mut cands: Vec<(f64, usize)> = (0..n_cb) + .map(|i1| { + let cb: &[i32] = if wb { &NLSF_CB_WB[i1] } else { &NLSF_CB_NBMB[i1] }; + let d: f64 = (0..order).map(|k| w[k] * ((cb[k] << 7) as f64 - target[k]).powi(2)).sum(); + (d + LAMBDA * icdf_bits(s1_tab, i1), i1) + }) + .collect(); + cands.sort_by(|a, b| a.0.total_cmp(&b.0)); + let qstep = if wb { 9830 } else { 11796 }; + let mut best = (f64::MAX, [0i32; 17], [0i32; MAX_LPC_ORDER]); + for &(_, i1) in cands.iter().take(SURVIVORS) { + let cb1: &[i32] = if wb { &NLSF_CB_WB[i1] } else { &NLSF_CB_NBMB[i1] }; + let mut ix = [0i32; 17]; + ix[0] = i1 as i32; + let mut bits = icdf_bits(s1_tab, i1); + // Exact decoder weights per coefficient. + let w_q9: Vec = (0..order) + .map(|k| { + let prev = if k == 0 { 0 } else { cb1[k - 1] }; + let next = if k + 1 == order { 256 } else { cb1[k + 1] }; + let ww = (1024 / (cb1[k] - prev).max(1) + 1024 / (next - cb1[k]).max(1)).min(32767); + sqrt_approx(ww << 16).max(1) + }) + .collect(); + let mut out_q10 = 0i32; + for i in (0..order).rev() { + let pred = if i + 1 < order { + let sel = if wb { NLSF_PSEL_WB[i1][i] } else { NLSF_PSEL_NBMB[i1][i] } as usize; + let wp = if wb { NLSF_PRED_WB[sel][i] } else { NLSF_PRED_NBMB[sel][i] }; + (out_q10 * wp) >> 8 + } else { + 0 + }; + let cb = if wb { 8 + NLSF_SEL_WB[i1][i] as usize } else { NLSF_SEL_NBMB[i1][i] as usize }; + let tab = NLSF_STAGE2[cb]; + let want_q10 = (target[i] - (cb1[i] << 7) as f64) * w_q9[i] as f64 / 16384.0; + let step = qstep as f64 * 1024.0 / 65536.0; + let q0 = ((want_q10 - pred as f64) / step).round() as i32; + let mut bq = (f64::MAX, 0i32, 0i32); + for q in (q0 - 1).max(-10)..=(q0 + 1).min(10) { + let mut v = q << 10; + if v > 0 { + v -= 102; + } else if v < 0 { + v += 102; + } + let o = smlawb(pred, v, qstep); + let nl = (cb1[i] << 7) as f64 + ((o << 14) / w_q9[i]) as f64; + let cost = w[i] * (nl - target[i]).powi(2) + LAMBDA * stage2_bits(tab, q); + if cost < bq.0 { + bq = (cost, q, o); + } + } + ix[i + 1] = bq.1; + out_q10 = bq.2; + bits += stage2_bits(tab, bq.1); + } + let nlsf = nlsf_decode(&ix, wb); + let d: f64 = (0..order).map(|k| w[k] * (nlsf[k] as f64 - target[k]).powi(2)).sum(); + let cost = d + LAMBDA * bits; + if cost < best.0 { + best = (cost, ix, nlsf); + } + } + (best.1, best.2) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn quantised_nlsfs_track_the_target() { + for wb in [false, true] { + let order = if wb { 16 } else { 10 }; + // A smooth, plausible NLSF vector. + let target: Vec = (0..order).map(|k| 32768.0 * (k as f64 + 0.8 + 0.3 * (k as f64).sin()) / (order as f64 + 1.0)).collect(); + let (ix, nlsf) = quantize(&target, 1, wb); + assert!(ix[1..=order].iter().all(|v| (-10..=10).contains(v))); + let max_err = (0..order).map(|k| (nlsf[k] as f64 - target[k]).abs()).fold(0.0, f64::max); + assert!(max_err < 1200.0, "wb={wb} max err {max_err}: {:?} vs {target:?}", &nlsf[..order]); + } + } +} diff --git a/crates/opusenc/src/silk/pulses.rs b/crates/opusenc/src/silk/pulses.rs new file mode 100644 index 00000000..f8cda15a --- /dev/null +++ b/crates/opusenc/src/silk/pulses.rs @@ -0,0 +1,113 @@ +//! Excitation coding (encoder side of RFC 6716 §4.2.7.8): rate level, per-block pulse counts +//! with LSB escapes, the recursive shell code, LSBs and signs, written in exactly the order the +//! decoder reads them. + +use super::tables::*; +use crate::range::RangeEncoder; + +/// Largest pulse magnitude the quantiser produces (keeps every 16-sample block codable). +pub const MAX_PULSE: i32 = 500; + +pub fn icdf_bits(icdf: &[u8], s: usize) -> f32 { + let hi = if s == 0 { 256 } else { icdf[s - 1] as u32 }; + let p = (hi - icdf[s] as u32).max(1) as f32 / 256.0; + -p.log2() +} + +fn count_table(nls: usize) -> &'static [u8] { + if nls == 10 { PULSE_COUNT[10] } else { PULSE_COUNT[9] } +} + +fn shell(enc: &mut RangeEncoder, v: &[i32], count: i32) { + let n = v.len(); + if n == 1 || count == 0 { + return; + } + let tab = match n { + 16 => SHELL_16[(count - 1) as usize], + 8 => SHELL_8[(count - 1) as usize], + 4 => SHELL_4[(count - 1) as usize], + _ => SHELL_2[(count - 1) as usize], + }; + let (a, b) = v.split_at(n / 2); + let left: i32 = a.iter().sum(); + enc.icdf(left as usize, tab, 8); + shell(enc, a, left); + shell(enc, b, count - left); +} + +/// Codes the frame's pulses (`len` a multiple of 16). +pub fn encode(enc: &mut RangeEncoder, pulses: &[i32], signal_type: usize, quant_offset_type: usize) { + let nblk = pulses.len() / 16; + let mut nls = vec![0usize; nblk]; + let mut sums = vec![0i32; nblk]; + let mut hi = vec![0i32; pulses.len()]; + for b in 0..nblk { + let blk = &pulses[b * 16..(b + 1) * 16]; + let mut s = 0usize; + loop { + let sum: i32 = blk.iter().map(|p| p.abs() >> s).sum(); + if sum <= 16 || s == 10 { + sums[b] = sum.min(16); + break; + } + s += 1; + } + nls[b] = s; + for (h, p) in hi[b * 16..(b + 1) * 16].iter_mut().zip(blk) { + *h = p.abs() >> s; + } + } + // Rate level with the cheapest pulse-count symbols. + let rl_tab = RATE_LEVEL[signal_type >> 1]; + let mut best = (f32::MAX, 0usize); + for rl in 0..9 { + let mut bits = icdf_bits(rl_tab, rl); + for b in 0..nblk { + bits += if nls[b] == 0 { icdf_bits(PULSE_COUNT[rl], sums[b] as usize) } else { icdf_bits(PULSE_COUNT[rl], 17) }; + } + if bits < best.0 { + best = (bits, rl); + } + } + let rl = best.1; + enc.icdf(rl, rl_tab, 8); + for b in 0..nblk { + if nls[b] == 0 { + enc.icdf(sums[b] as usize, PULSE_COUNT[rl], 8); + } else { + enc.icdf(17, PULSE_COUNT[rl], 8); + for j in 1..nls[b] { + enc.icdf(17, count_table(j), 8); + } + enc.icdf(sums[b] as usize, count_table(nls[b]), 8); + } + } + for b in 0..nblk { + if sums[b] > 0 { + shell(enc, &hi[b * 16..(b + 1) * 16], sums[b]); + } + } + for b in 0..nblk { + if nls[b] > 0 { + for p in &pulses[b * 16..(b + 1) * 16] { + let a = p.abs(); + for j in (0..nls[b]).rev() { + enc.icdf(((a >> j) & 1) as usize, &EXC_LSB, 8); + } + } + } + } + let base = 7 * (quant_offset_type + (signal_type << 1)); + for b in 0..nblk { + let p = sums[b] | ((nls[b] as i32) << 5); + if p > 0 { + let tab = EXC_SIGN[base + ((p & 0x1F) as usize).min(6)]; + for &q in &pulses[b * 16..(b + 1) * 16] { + if q != 0 { + enc.icdf((q > 0) as usize, tab, 8); + } + } + } + } +} diff --git a/crates/opusenc/src/silk/tables.rs b/crates/opusenc/src/silk/tables.rs new file mode 100644 index 00000000..8b472726 --- /dev/null +++ b/crates/opusenc/src/silk/tables.rs @@ -0,0 +1,540 @@ +//! SILK probability tables and codebooks, transcribed from RFC 6716 §4.2 (Tables 4–53). +//! +//! PDFs are written as in the RFC (`{...}/256`) and converted at compile time to the inverse-CDF +//! form consumed by the range coder. +//! +//! Copied from FilmCraft's first-party, spec-derived `filmcraft-opus` decoder (MIT OR +//! Apache-2.0) so encoder and decoder share exactly the same values. + +/// Converts a PDF (summing to 256) into an inverse CDF (`256 - cdf[i+1]`). +pub const fn icdf(pdf: &[u8; N]) -> [u8; N] { + let mut out = [0u8; N]; + let mut acc: u32 = 0; + let mut i = 0; + while i < N { + acc += pdf[i] as u32; + out[i] = (256 - acc) as u8; + i += 1; + } + out +} + +pub const STEREO_PRED_JOINT: [u8; 25] = icdf(&[7, 2, 1, 1, 1, 10, 24, 8, 1, 1, 3, 23, 92, 23, 3, 1, 1, 8, 24, 10, 1, 1, 1, 2, 7]); +pub const UNIFORM3: [u8; 3] = icdf(&[85, 86, 85]); +pub const UNIFORM5: [u8; 5] = icdf(&[51, 51, 52, 51, 51]); +pub const STEREO_WEIGHTS_Q13: [i32; 16] = [-13732, -10050, -8266, -7526, -6500, -5000, -2950, -820, 820, 2950, 5000, 6500, 7526, 8266, 10050, 13732]; +pub const MID_ONLY: [u8; 2] = icdf(&[192, 64]); +pub const TYPE_INACTIVE: [u8; 2] = icdf(&[26, 230]); +pub const TYPE_ACTIVE: [u8; 4] = icdf(&[24, 74, 148, 10]); +pub const GAIN_MSB: [&[u8]; 3] = + [&icdf::<8>(&[32, 112, 68, 29, 12, 1, 1, 1]), &icdf::<8>(&[2, 17, 45, 60, 62, 47, 19, 4]), &icdf::<8>(&[1, 3, 26, 71, 94, 50, 9, 2])]; +pub const UNIFORM8: [u8; 8] = icdf(&[32, 32, 32, 32, 32, 32, 32, 32]); +pub const DELTA_GAIN: [u8; 41] = + icdf(&[6, 5, 11, 31, 132, 21, 8, 4, 3, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]); +pub const NLSF_STAGE1: [&[u8]; 4] = [ + &icdf::<32>(&[44, 34, 30, 19, 21, 12, 11, 3, 3, 2, 16, 2, 2, 1, 5, 2, 1, 3, 3, 1, 1, 2, 2, 2, 3, 1, 9, 9, 2, 7, 2, 1]), + &icdf::<32>(&[1, 10, 1, 8, 3, 8, 8, 14, 13, 14, 1, 14, 12, 13, 11, 11, 12, 11, 10, 10, 11, 8, 9, 8, 7, 8, 1, 1, 6, 1, 6, 5]), + &icdf::<32>(&[31, 21, 3, 17, 1, 8, 17, 4, 1, 18, 16, 4, 2, 3, 1, 10, 1, 3, 16, 11, 16, 2, 2, 3, 2, 11, 1, 4, 9, 8, 7, 3]), + &icdf::<32>(&[1, 4, 16, 5, 18, 11, 5, 14, 15, 1, 3, 12, 13, 14, 14, 6, 14, 12, 2, 6, 1, 12, 12, 11, 10, 3, 10, 5, 1, 1, 1, 3]), +]; +pub const NLSF_STAGE2: [&[u8]; 16] = [ + &icdf::<9>(&[1, 1, 1, 15, 224, 11, 1, 1, 1]), + &icdf::<9>(&[1, 1, 2, 34, 183, 32, 1, 1, 1]), + &icdf::<9>(&[1, 1, 4, 42, 149, 55, 2, 1, 1]), + &icdf::<9>(&[1, 1, 8, 52, 123, 61, 8, 1, 1]), + &icdf::<9>(&[1, 3, 16, 53, 101, 74, 6, 1, 1]), + &icdf::<9>(&[1, 3, 17, 55, 90, 73, 15, 1, 1]), + &icdf::<9>(&[1, 7, 24, 53, 74, 67, 26, 3, 1]), + &icdf::<9>(&[1, 1, 18, 63, 78, 58, 30, 6, 1]), + &icdf::<9>(&[1, 1, 1, 9, 232, 9, 1, 1, 1]), + &icdf::<9>(&[1, 1, 2, 28, 186, 35, 1, 1, 1]), + &icdf::<9>(&[1, 1, 3, 42, 152, 53, 2, 1, 1]), + &icdf::<9>(&[1, 1, 10, 49, 126, 65, 2, 1, 1]), + &icdf::<9>(&[1, 4, 19, 48, 100, 77, 5, 1, 1]), + &icdf::<9>(&[1, 1, 14, 54, 100, 72, 12, 1, 1]), + &icdf::<9>(&[1, 1, 15, 61, 87, 61, 25, 4, 1]), + &icdf::<9>(&[1, 7, 21, 50, 77, 81, 17, 1, 1]), +]; +pub const NLSF_SEL_NBMB: [[u8; 10]; 32] = [ + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [1, 3, 1, 2, 2, 1, 2, 1, 1, 1], + [2, 1, 1, 1, 1, 1, 1, 1, 1, 1], + [1, 2, 2, 2, 2, 1, 2, 1, 1, 1], + [2, 3, 3, 3, 3, 2, 2, 2, 2, 2], + [0, 5, 3, 3, 2, 2, 2, 2, 1, 1], + [0, 2, 2, 2, 2, 2, 2, 2, 2, 1], + [2, 3, 6, 4, 4, 4, 5, 4, 5, 5], + [2, 4, 5, 5, 4, 5, 4, 6, 4, 4], + [2, 4, 4, 7, 4, 5, 4, 5, 5, 4], + [4, 3, 3, 3, 2, 3, 2, 2, 2, 2], + [1, 5, 5, 6, 4, 5, 4, 5, 5, 5], + [2, 7, 4, 6, 5, 5, 5, 5, 5, 5], + [2, 7, 5, 5, 5, 5, 5, 6, 5, 4], + [3, 3, 5, 4, 4, 5, 4, 5, 4, 4], + [2, 3, 3, 5, 5, 4, 4, 4, 4, 4], + [2, 4, 4, 6, 4, 5, 4, 5, 5, 5], + [2, 5, 4, 6, 5, 5, 5, 4, 5, 4], + [2, 7, 4, 5, 4, 5, 4, 5, 5, 5], + [2, 5, 4, 6, 7, 6, 5, 6, 5, 4], + [3, 6, 7, 4, 6, 5, 5, 6, 4, 5], + [2, 7, 6, 4, 4, 4, 5, 4, 5, 5], + [4, 5, 5, 4, 6, 6, 5, 6, 5, 4], + [2, 5, 5, 6, 5, 6, 4, 6, 4, 4], + [4, 5, 5, 5, 3, 7, 4, 5, 5, 4], + [2, 3, 4, 5, 5, 6, 4, 5, 5, 4], + [2, 3, 2, 3, 3, 4, 2, 3, 3, 3], + [1, 1, 2, 2, 2, 2, 2, 3, 2, 2], + [4, 5, 5, 6, 6, 6, 5, 6, 4, 5], + [3, 5, 5, 4, 4, 4, 4, 3, 3, 2], + [2, 5, 3, 7, 5, 5, 4, 4, 5, 4], + [4, 4, 5, 4, 5, 6, 5, 6, 5, 4], +]; +pub const NLSF_SEL_WB: [[u8; 16]; 32] = [ + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [2, 3, 3, 3, 3, 3, 2, 2, 2, 2, 2, 1, 1, 1, 0, 3], + [2, 5, 5, 3, 7, 4, 4, 5, 2, 5, 4, 5, 5, 4, 3, 3], + [0, 2, 1, 2, 2, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 1], + [0, 6, 5, 4, 6, 4, 7, 5, 4, 4, 4, 5, 5, 4, 4, 3], + [0, 3, 5, 5, 4, 3, 3, 5, 3, 3, 3, 3, 3, 3, 2, 4], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 2, 6, 3, 7, 2, 5, 3, 4, 5, 5, 4, 3, 3, 2, 3], + [0, 6, 2, 6, 6, 4, 5, 4, 6, 5, 4, 4, 5, 3, 3, 3], + [2, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1], + [2, 2, 3, 4, 5, 3, 3, 3, 3, 3, 3, 3, 2, 2, 1, 3], + [2, 2, 3, 3, 4, 3, 3, 3, 3, 3, 3, 3, 3, 2, 1, 3], + [3, 4, 4, 4, 6, 4, 4, 5, 3, 5, 4, 4, 5, 4, 3, 4], + [0, 6, 4, 5, 4, 7, 5, 2, 6, 5, 7, 4, 4, 3, 5, 3], + [0, 1, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 1, 0], + [1, 6, 5, 7, 5, 4, 5, 3, 4, 5, 4, 4, 4, 3, 3, 4], + [1, 3, 3, 4, 4, 3, 3, 5, 2, 3, 3, 5, 5, 5, 3, 4], + [2, 3, 3, 2, 2, 2, 3, 2, 1, 2, 1, 2, 1, 1, 1, 4], + [0, 2, 3, 5, 3, 3, 2, 2, 2, 1, 1, 0, 0, 0, 0, 0], + [3, 4, 3, 5, 3, 3, 2, 2, 1, 1, 1, 1, 1, 2, 2, 4], + [2, 6, 3, 7, 7, 4, 5, 4, 5, 3, 5, 3, 3, 2, 3, 3], + [2, 3, 5, 6, 6, 3, 5, 3, 4, 4, 3, 3, 3, 3, 2, 4], + [1, 3, 3, 4, 4, 4, 4, 3, 5, 5, 5, 3, 1, 1, 1, 1], + [2, 5, 3, 6, 6, 4, 7, 4, 4, 5, 3, 4, 4, 3, 3, 3], + [0, 6, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 6, 6, 3, 5, 2, 5, 5, 3, 4, 4, 7, 7, 4, 4, 4], + [3, 3, 7, 3, 5, 4, 3, 3, 3, 2, 2, 3, 3, 3, 2, 3], + [0, 0, 1, 0, 0, 0, 2, 1, 2, 1, 1, 2, 2, 2, 1, 1], + [0, 3, 2, 5, 3, 3, 2, 3, 2, 1, 0, 0, 1, 0, 0, 1], + [3, 5, 5, 4, 7, 5, 3, 3, 2, 3, 2, 2, 1, 0, 1, 0], + [2, 3, 5, 3, 4, 3, 3, 3, 2, 1, 2, 6, 4, 0, 0, 0], +]; +pub const NLSF_EXT: [u8; 7] = icdf(&[156, 60, 24, 9, 4, 2, 1]); +pub const NLSF_PRED_NBMB: [[i32; 9]; 2] = [[179, 138, 140, 148, 151, 149, 153, 151, 163], [116, 67, 82, 59, 92, 72, 100, 89, 92]]; +pub const NLSF_PRED_WB: [[i32; 15]; 2] = + [[175, 148, 160, 176, 178, 173, 174, 164, 177, 174, 196, 182, 198, 192, 182], [68, 62, 66, 60, 72, 117, 85, 90, 118, 136, 151, 142, 160, 142, 155]]; +pub const NLSF_PSEL_NBMB: [[u8; 9]; 32] = [ + [0, 1, 0, 0, 0, 0, 0, 0, 0], + [1, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0], + [1, 1, 1, 0, 0, 0, 0, 1, 0], + [0, 1, 0, 0, 0, 0, 0, 0, 0], + [0, 1, 0, 0, 0, 0, 0, 0, 0], + [1, 0, 1, 1, 0, 0, 0, 1, 0], + [0, 1, 1, 0, 0, 1, 1, 0, 0], + [0, 0, 1, 1, 0, 1, 0, 1, 1], + [0, 0, 1, 1, 0, 0, 1, 1, 1], + [0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 1, 0, 1, 1, 1, 1, 1, 0], + [0, 1, 0, 1, 1, 1, 1, 1, 0], + [0, 1, 1, 1, 1, 1, 1, 1, 0], + [1, 0, 1, 1, 0, 1, 1, 1, 1], + [0, 1, 1, 1, 1, 1, 0, 1, 0], + [0, 0, 1, 1, 0, 1, 0, 1, 0], + [0, 0, 1, 1, 1, 0, 1, 1, 1], + [0, 1, 1, 0, 0, 1, 1, 1, 0], + [0, 0, 0, 1, 1, 1, 0, 1, 0], + [0, 1, 1, 0, 0, 1, 0, 1, 0], + [0, 1, 1, 0, 0, 0, 1, 1, 0], + [0, 0, 0, 0, 0, 1, 1, 1, 1], + [0, 0, 1, 1, 0, 0, 0, 1, 1], + [0, 0, 0, 1, 0, 1, 1, 1, 1], + [0, 1, 1, 1, 1, 1, 1, 1, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 1, 0, 1, 1, 0, 1, 0], + [1, 0, 0, 1, 0, 0, 0, 0, 0], + [0, 0, 0, 1, 1, 0, 1, 0, 1], + [1, 0, 1, 1, 0, 1, 1, 1, 1], +]; +pub const NLSF_PSEL_WB: [[u8; 15]; 32] = [ + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 1, 0, 0, 1, 1, 1, 0, 1, 1, 1, 1, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0], + [0, 1, 1, 0, 1, 0, 1, 1, 0, 1, 1, 1, 1, 1, 0], + [0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0], + [0, 1, 1, 0, 0, 0, 1, 0, 1, 1, 1, 0, 1, 0, 1], + [0, 1, 0, 1, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1], + [0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 1, 0, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 0], + [0, 0, 1, 0, 0, 1, 0, 1, 0, 1, 0, 0, 1, 0, 0], + [0, 0, 0, 0, 1, 1, 0, 1, 0, 1, 1, 1, 1, 0, 0], + [0, 1, 0, 0, 0, 1, 1, 0, 1, 1, 1, 0, 1, 1, 1], + [0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0], + [0, 1, 1, 0, 1, 0, 1, 1, 1, 1, 1, 0, 1, 0, 0], + [0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 1, 1, 1, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 1, 0, 1, 0, 1, 1, 0, 1, 0, 1, 0, 1, 1, 0], + [0, 0, 1, 1, 1, 1, 0, 1, 1, 0, 0, 1, 1, 0, 0], + [0, 1, 1, 0, 1, 0, 1, 0, 1, 0, 0, 0, 0, 1, 0], + [0, 0, 0, 1, 1, 0, 1, 0, 1, 1, 1, 1, 1, 1, 1], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1], + [0, 1, 1, 0, 0, 0, 1, 1, 0, 0, 1, 1, 1, 1, 1], + [0, 0, 0, 0, 0, 1, 0, 1, 1, 1, 1, 0, 1, 1, 1], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1], + [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0], + [0, 0, 1, 0, 0, 1, 1, 1, 0, 0, 1, 0, 0, 1, 0], +]; +pub const NLSF_CB_NBMB: [[i32; 10]; 32] = [ + [12, 35, 60, 83, 108, 132, 157, 180, 206, 228], + [15, 32, 55, 77, 101, 125, 151, 175, 201, 225], + [19, 42, 66, 89, 114, 137, 162, 184, 209, 230], + [12, 25, 50, 72, 97, 120, 147, 172, 200, 223], + [26, 44, 69, 90, 114, 135, 159, 180, 205, 225], + [13, 22, 53, 80, 106, 130, 156, 180, 205, 228], + [15, 25, 44, 64, 90, 115, 142, 168, 196, 222], + [19, 24, 62, 82, 100, 120, 145, 168, 190, 214], + [22, 31, 50, 79, 103, 120, 151, 170, 203, 227], + [21, 29, 45, 65, 106, 124, 150, 171, 196, 224], + [30, 49, 75, 97, 121, 142, 165, 186, 209, 229], + [19, 25, 52, 70, 93, 116, 143, 166, 192, 219], + [26, 34, 62, 75, 97, 118, 145, 167, 194, 217], + [25, 33, 56, 70, 91, 113, 143, 165, 196, 223], + [21, 34, 51, 72, 97, 117, 145, 171, 196, 222], + [20, 29, 50, 67, 90, 117, 144, 168, 197, 221], + [22, 31, 48, 66, 95, 117, 146, 168, 196, 222], + [24, 33, 51, 77, 116, 134, 158, 180, 200, 224], + [21, 28, 70, 87, 106, 124, 149, 170, 194, 217], + [26, 33, 53, 64, 83, 117, 152, 173, 204, 225], + [27, 34, 65, 95, 108, 129, 155, 174, 210, 225], + [20, 26, 72, 99, 113, 131, 154, 176, 200, 219], + [34, 43, 61, 78, 93, 114, 155, 177, 205, 229], + [23, 29, 54, 97, 124, 138, 163, 179, 209, 229], + [30, 38, 56, 89, 118, 129, 158, 178, 200, 231], + [21, 29, 49, 63, 85, 111, 142, 163, 193, 222], + [27, 48, 77, 103, 133, 158, 179, 196, 215, 232], + [29, 47, 74, 99, 124, 151, 176, 198, 220, 237], + [33, 42, 61, 76, 93, 121, 155, 174, 207, 225], + [29, 53, 87, 112, 136, 154, 170, 188, 208, 227], + [24, 30, 52, 84, 131, 150, 166, 186, 203, 229], + [37, 48, 64, 84, 104, 118, 156, 177, 201, 230], +]; +pub const NLSF_CB_WB: [[i32; 16]; 32] = [ + [7, 23, 38, 54, 69, 85, 100, 116, 131, 147, 162, 178, 193, 208, 223, 239], + [13, 25, 41, 55, 69, 83, 98, 112, 127, 142, 157, 171, 187, 203, 220, 236], + [15, 21, 34, 51, 61, 78, 92, 106, 126, 136, 152, 167, 185, 205, 225, 240], + [10, 21, 36, 50, 63, 79, 95, 110, 126, 141, 157, 173, 189, 205, 221, 237], + [17, 20, 37, 51, 59, 78, 89, 107, 123, 134, 150, 164, 184, 205, 224, 240], + [10, 15, 32, 51, 67, 81, 96, 112, 129, 142, 158, 173, 189, 204, 220, 236], + [8, 21, 37, 51, 65, 79, 98, 113, 126, 138, 155, 168, 179, 192, 209, 218], + [12, 15, 34, 55, 63, 78, 87, 108, 118, 131, 148, 167, 185, 203, 219, 236], + [16, 19, 32, 36, 56, 79, 91, 108, 118, 136, 154, 171, 186, 204, 220, 237], + [11, 28, 43, 58, 74, 89, 105, 120, 135, 150, 165, 180, 196, 211, 226, 241], + [6, 16, 33, 46, 60, 75, 92, 107, 123, 137, 156, 169, 185, 199, 214, 225], + [11, 19, 30, 44, 57, 74, 89, 105, 121, 135, 152, 169, 186, 202, 218, 234], + [12, 19, 29, 46, 57, 71, 88, 100, 120, 132, 148, 165, 182, 199, 216, 233], + [17, 23, 35, 46, 56, 77, 92, 106, 123, 134, 152, 167, 185, 204, 222, 237], + [14, 17, 45, 53, 63, 75, 89, 107, 115, 132, 151, 171, 188, 206, 221, 240], + [9, 16, 29, 40, 56, 71, 88, 103, 119, 137, 154, 171, 189, 205, 222, 237], + [16, 19, 36, 48, 57, 76, 87, 105, 118, 132, 150, 167, 185, 202, 218, 236], + [12, 17, 29, 54, 71, 81, 94, 104, 126, 136, 149, 164, 182, 201, 221, 237], + [15, 28, 47, 62, 79, 97, 115, 129, 142, 155, 168, 180, 194, 208, 223, 238], + [8, 14, 30, 45, 62, 78, 94, 111, 127, 143, 159, 175, 192, 207, 223, 239], + [17, 30, 49, 62, 79, 92, 107, 119, 132, 145, 160, 174, 190, 204, 220, 235], + [14, 19, 36, 45, 61, 76, 91, 108, 121, 138, 154, 172, 189, 205, 222, 238], + [12, 18, 31, 45, 60, 76, 91, 107, 123, 138, 154, 171, 187, 204, 221, 236], + [13, 17, 31, 43, 53, 70, 83, 103, 114, 131, 149, 167, 185, 203, 220, 237], + [17, 22, 35, 42, 58, 78, 93, 110, 125, 139, 155, 170, 188, 206, 224, 240], + [8, 15, 34, 50, 67, 83, 99, 115, 131, 146, 162, 178, 193, 209, 224, 239], + [13, 16, 41, 66, 73, 86, 95, 111, 128, 137, 150, 163, 183, 206, 225, 241], + [17, 25, 37, 52, 63, 75, 92, 102, 119, 132, 144, 160, 175, 191, 212, 231], + [19, 31, 49, 65, 83, 100, 117, 133, 147, 161, 174, 187, 200, 213, 227, 242], + [18, 31, 52, 68, 88, 103, 117, 126, 138, 149, 163, 177, 192, 207, 223, 239], + [16, 29, 47, 61, 76, 90, 106, 119, 133, 147, 161, 176, 193, 209, 224, 240], + [15, 21, 35, 50, 61, 73, 86, 97, 110, 119, 129, 141, 175, 198, 218, 237], +]; +pub const NLSF_MIN_SPACING_NBMB: [i32; 11] = [250, 3, 6, 3, 3, 3, 4, 3, 3, 3, 461]; +pub const NLSF_MIN_SPACING_WB: [i32; 17] = [100, 3, 40, 3, 3, 3, 5, 14, 14, 10, 11, 3, 8, 9, 7, 3, 347]; +pub const NLSF_INTERP: [u8; 5] = icdf(&[13, 22, 29, 11, 181]); +pub const LSF_ORDER_NBMB: [usize; 10] = [0, 9, 6, 3, 4, 5, 8, 1, 2, 7]; +pub const LSF_ORDER_WB: [usize; 16] = [0, 15, 8, 7, 4, 11, 12, 3, 2, 13, 10, 5, 6, 9, 14, 1]; +pub const COS_Q12: [i32; 129] = [ + 4096, 4095, 4091, 4085, 4076, 4065, 4052, 4036, 4017, 3997, 3973, 3948, 3920, 3889, 3857, 3822, 3784, 3745, 3703, 3659, 3613, 3564, 3513, 3461, 3406, 3349, + 3290, 3229, 3166, 3102, 3035, 2967, 2896, 2824, 2751, 2676, 2599, 2520, 2440, 2359, 2276, 2191, 2106, 2019, 1931, 1842, 1751, 1660, 1568, 1474, 1380, 1285, + 1189, 1093, 995, 897, 799, 700, 601, 501, 401, 301, 201, 101, 0, -101, -201, -301, -401, -501, -601, -700, -799, -897, -995, -1093, -1189, -1285, -1380, + -1474, -1568, -1660, -1751, -1842, -1931, -2019, -2106, -2191, -2276, -2359, -2440, -2520, -2599, -2676, -2751, -2824, -2896, -2967, -3035, -3102, -3166, + -3229, -3290, -3349, -3406, -3461, -3513, -3564, -3613, -3659, -3703, -3745, -3784, -3822, -3857, -3889, -3920, -3948, -3973, -3997, -4017, -4036, -4052, + -4065, -4076, -4085, -4091, -4095, -4096, +]; +pub const PITCH_HIGH: [u8; 32] = icdf(&[3, 3, 6, 11, 21, 30, 32, 19, 11, 10, 12, 13, 13, 12, 11, 9, 8, 7, 6, 4, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1]); +pub const PITCH_LOW: [&[u8]; 3] = [&icdf::<4>(&[64, 64, 64, 64]), &icdf::<6>(&[43, 42, 43, 43, 42, 43]), &icdf::<8>(&[32, 32, 32, 32, 32, 32, 32, 32])]; +pub const PITCH_DELTA: [u8; 21] = icdf(&[46, 2, 2, 3, 4, 6, 10, 15, 26, 38, 30, 22, 15, 10, 7, 6, 4, 4, 2, 2, 2]); +pub const PITCH_CONTOUR: [&[u8]; 4] = [ + &icdf::<3>(&[143, 50, 63]), + &icdf::<11>(&[68, 12, 21, 17, 19, 22, 30, 24, 17, 16, 10]), + &icdf::<12>(&[91, 46, 39, 19, 14, 12, 8, 7, 6, 5, 5, 4]), + &icdf::<34>(&[33, 22, 18, 16, 15, 14, 14, 13, 13, 10, 9, 9, 8, 6, 6, 6, 5, 4, 4, 4, 3, 3, 3, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1]), +]; +pub const CB_NB_10: [[i32; 2]; 3] = [[0, 0], [1, 0], [0, 1]]; +pub const CB_NB_20: [[i32; 4]; 11] = [ + [0, 0, 0, 0], + [2, 1, 0, -1], + [-1, 0, 1, 2], + [-1, 0, 0, 1], + [-1, 0, 0, 0], + [0, 0, 0, 1], + [0, 0, 1, 1], + [1, 1, 0, 0], + [1, 0, 0, 0], + [0, 0, 0, -1], + [1, 0, 0, -1], +]; +pub const CB_WB_10: [[i32; 2]; 12] = [[0, 0], [0, 1], [1, 0], [-1, 1], [1, -1], [-1, 2], [2, -1], [-2, 2], [2, -2], [-2, 3], [3, -2], [-3, 3]]; +pub const CB_WB_20: [[i32; 4]; 34] = [ + [0, 0, 0, 0], + [0, 0, 1, 1], + [1, 1, 0, 0], + [-1, 0, 0, 0], + [0, 0, 0, 1], + [1, 0, 0, 0], + [-1, 0, 0, 1], + [0, 0, 0, -1], + [-1, 0, 1, 2], + [1, 0, 0, -1], + [-2, -1, 1, 2], + [2, 1, 0, -1], + [-2, 0, 0, 2], + [-2, 0, 1, 3], + [2, 1, -1, -2], + [-3, -1, 1, 3], + [2, 0, 0, -2], + [3, 1, 0, -2], + [-3, -1, 2, 4], + [-4, -1, 1, 4], + [3, 1, -1, -3], + [-4, -1, 2, 5], + [4, 2, -1, -3], + [4, 1, -1, -4], + [-5, -1, 2, 6], + [5, 2, -1, -4], + [-6, -2, 2, 6], + [-5, -2, 2, 5], + [6, 2, -1, -5], + [-7, -2, 3, 8], + [6, 2, -2, -6], + [5, 2, -2, -5], + [8, 3, -2, -7], + [-9, -3, 3, 9], +]; +pub const LTP_PERIODICITY: [u8; 3] = icdf(&[77, 80, 99]); +pub const LTP_FILTER: [&[u8]; 3] = [ + &icdf::<8>(&[185, 15, 13, 13, 9, 9, 6, 6]), + &icdf::<16>(&[57, 34, 21, 20, 15, 13, 12, 13, 10, 10, 9, 10, 9, 8, 7, 8]), + &icdf::<32>(&[15, 16, 14, 12, 12, 12, 11, 11, 11, 10, 9, 9, 9, 9, 8, 8, 8, 8, 7, 7, 6, 6, 5, 4, 5, 4, 4, 4, 3, 4, 3, 2]), +]; +pub const LTP_TAPS_0: [[i32; 5]; 8] = [ + [4, 6, 24, 7, 5], + [0, 0, 2, 0, 0], + [12, 28, 41, 13, -4], + [-9, 15, 42, 25, 14], + [1, -2, 62, 41, -9], + [-10, 37, 65, -4, 3], + [-6, 4, 66, 7, -8], + [16, 14, 38, -3, 33], +]; +pub const LTP_TAPS_1: [[i32; 5]; 16] = [ + [13, 22, 39, 23, 12], + [-1, 36, 64, 27, -6], + [-7, 10, 55, 43, 17], + [1, 1, 8, 1, 1], + [6, -11, 74, 53, -9], + [-12, 55, 76, -12, 8], + [-3, 3, 93, 27, -4], + [26, 39, 59, 3, -8], + [2, 0, 77, 11, 9], + [-8, 22, 44, -6, 7], + [40, 9, 26, 3, 9], + [-7, 20, 101, -7, 4], + [3, -8, 42, 26, 0], + [-15, 33, 68, 2, 23], + [-2, 55, 46, -2, 15], + [3, -1, 21, 16, 41], +]; +pub const LTP_TAPS_2: [[i32; 5]; 32] = [ + [-6, 27, 61, 39, 5], + [-11, 42, 88, 4, 1], + [-2, 60, 65, 6, -4], + [-1, -5, 73, 56, 1], + [-9, 19, 94, 29, -9], + [0, 12, 99, 6, 4], + [8, -19, 102, 46, -13], + [3, 2, 13, 3, 2], + [9, -21, 84, 72, -18], + [-11, 46, 104, -22, 8], + [18, 38, 48, 23, 0], + [-16, 70, 83, -21, 11], + [5, -11, 117, 22, -8], + [-6, 23, 117, -12, 3], + [3, -8, 95, 28, 4], + [-10, 15, 77, 60, -15], + [-1, 4, 124, 2, -4], + [3, 38, 84, 24, -25], + [2, 13, 42, 13, 31], + [21, -4, 56, 46, -1], + [-1, 35, 79, -13, 19], + [-7, 65, 88, -9, -14], + [20, 4, 81, 49, -29], + [20, 0, 75, 3, -17], + [5, -9, 44, 92, -8], + [1, -3, 22, 69, 31], + [-6, 95, 41, -12, 5], + [39, 67, 16, -4, 1], + [0, -6, 120, 55, -36], + [-13, 44, 122, 4, -24], + [81, 5, 11, 3, 7], + [2, 0, 9, 10, 88], +]; +pub const LTP_SCALE: [u8; 3] = icdf(&[128, 64, 64]); +pub const UNIFORM4: [u8; 4] = icdf(&[64, 64, 64, 64]); +pub const RATE_LEVEL: [&[u8]; 2] = [&icdf::<9>(&[15, 51, 12, 46, 45, 13, 33, 27, 14]), &icdf::<9>(&[33, 30, 36, 17, 34, 49, 18, 21, 18])]; +pub const PULSE_COUNT: [&[u8]; 11] = [ + &icdf::<18>(&[131, 74, 25, 8, 3, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]), + &icdf::<18>(&[58, 93, 60, 23, 7, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]), + &icdf::<18>(&[43, 51, 46, 33, 24, 16, 11, 8, 6, 3, 3, 3, 2, 1, 1, 2, 1, 2]), + &icdf::<18>(&[17, 52, 71, 57, 31, 12, 5, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]), + &icdf::<18>(&[6, 21, 41, 53, 49, 35, 21, 11, 6, 3, 2, 2, 1, 1, 1, 1, 1, 1]), + &icdf::<18>(&[7, 14, 22, 28, 29, 28, 25, 20, 17, 13, 11, 9, 7, 5, 4, 4, 3, 10]), + &icdf::<18>(&[2, 5, 14, 29, 42, 46, 41, 31, 19, 11, 6, 3, 2, 1, 1, 1, 1, 1]), + &icdf::<18>(&[1, 2, 4, 10, 19, 29, 35, 37, 34, 28, 20, 14, 8, 5, 4, 2, 2, 2]), + &icdf::<18>(&[1, 2, 2, 5, 9, 14, 20, 24, 27, 28, 26, 23, 20, 15, 11, 8, 6, 15]), + &icdf::<18>(&[1, 1, 1, 6, 27, 58, 56, 39, 25, 14, 10, 6, 3, 3, 2, 1, 1, 2]), + &icdf::<18>(&[2, 1, 6, 27, 58, 56, 39, 25, 14, 10, 6, 3, 3, 2, 1, 1, 2, 0]), +]; +pub const SHELL_16: [&[u8]; 16] = [ + &icdf::<2>(&[126, 130]), + &icdf::<3>(&[56, 142, 58]), + &icdf::<4>(&[25, 101, 104, 26]), + &icdf::<5>(&[12, 60, 108, 64, 12]), + &icdf::<6>(&[7, 35, 84, 87, 37, 6]), + &icdf::<7>(&[4, 20, 59, 86, 63, 21, 3]), + &icdf::<8>(&[3, 12, 38, 72, 75, 42, 12, 2]), + &icdf::<9>(&[2, 8, 25, 54, 73, 59, 27, 7, 1]), + &icdf::<10>(&[2, 5, 17, 39, 63, 65, 42, 18, 4, 1]), + &icdf::<11>(&[1, 4, 12, 28, 49, 63, 54, 30, 11, 3, 1]), + &icdf::<12>(&[1, 4, 8, 20, 37, 55, 57, 41, 22, 8, 2, 1]), + &icdf::<13>(&[1, 3, 7, 15, 28, 44, 53, 48, 33, 16, 6, 1, 1]), + &icdf::<14>(&[1, 2, 6, 12, 21, 35, 47, 48, 40, 25, 12, 5, 1, 1]), + &icdf::<15>(&[1, 1, 4, 10, 17, 27, 37, 47, 43, 33, 21, 9, 4, 1, 1]), + &icdf::<16>(&[1, 1, 1, 8, 14, 22, 33, 40, 43, 38, 28, 16, 8, 1, 1, 1]), + &icdf::<17>(&[1, 1, 1, 1, 13, 18, 27, 36, 41, 41, 34, 24, 14, 1, 1, 1, 1]), +]; +pub const SHELL_8: [&[u8]; 16] = [ + &icdf::<2>(&[127, 129]), + &icdf::<3>(&[53, 149, 54]), + &icdf::<4>(&[22, 105, 106, 23]), + &icdf::<5>(&[11, 61, 111, 63, 10]), + &icdf::<6>(&[6, 35, 86, 88, 36, 5]), + &icdf::<7>(&[4, 20, 59, 87, 62, 21, 3]), + &icdf::<8>(&[3, 13, 40, 71, 73, 41, 13, 2]), + &icdf::<9>(&[3, 9, 27, 53, 70, 56, 28, 9, 1]), + &icdf::<10>(&[3, 8, 19, 37, 57, 61, 44, 20, 6, 1]), + &icdf::<11>(&[3, 7, 15, 28, 44, 54, 49, 33, 17, 5, 1]), + &icdf::<12>(&[1, 7, 13, 22, 34, 46, 48, 38, 28, 14, 4, 1]), + &icdf::<13>(&[1, 1, 11, 22, 27, 35, 42, 47, 33, 25, 10, 1, 1]), + &icdf::<14>(&[1, 1, 6, 14, 26, 37, 43, 43, 37, 26, 14, 6, 1, 1]), + &icdf::<15>(&[1, 1, 4, 10, 20, 31, 40, 42, 40, 31, 20, 10, 4, 1, 1]), + &icdf::<16>(&[1, 1, 3, 8, 16, 26, 35, 38, 38, 35, 26, 16, 8, 3, 1, 1]), + &icdf::<17>(&[1, 1, 2, 6, 12, 21, 30, 36, 38, 36, 30, 21, 12, 6, 2, 1, 1]), +]; +pub const SHELL_4: [&[u8]; 16] = [ + &icdf::<2>(&[127, 129]), + &icdf::<3>(&[49, 157, 50]), + &icdf::<4>(&[20, 107, 109, 20]), + &icdf::<5>(&[11, 60, 113, 62, 10]), + &icdf::<6>(&[7, 36, 84, 87, 36, 6]), + &icdf::<7>(&[6, 24, 57, 82, 60, 23, 4]), + &icdf::<8>(&[5, 18, 39, 64, 68, 42, 16, 4]), + &icdf::<9>(&[6, 14, 29, 47, 61, 52, 30, 14, 3]), + &icdf::<10>(&[1, 15, 23, 35, 51, 50, 40, 30, 10, 1]), + &icdf::<11>(&[1, 1, 21, 32, 42, 52, 46, 41, 18, 1, 1]), + &icdf::<12>(&[1, 6, 16, 27, 36, 42, 42, 36, 27, 16, 6, 1]), + &icdf::<13>(&[1, 5, 12, 21, 31, 38, 40, 38, 31, 21, 12, 5, 1]), + &icdf::<14>(&[1, 3, 9, 17, 26, 34, 38, 38, 34, 26, 17, 9, 3, 1]), + &icdf::<15>(&[1, 3, 7, 14, 22, 29, 34, 36, 34, 29, 22, 14, 7, 3, 1]), + &icdf::<16>(&[1, 2, 5, 11, 18, 25, 31, 35, 35, 31, 25, 18, 11, 5, 2, 1]), + &icdf::<17>(&[1, 1, 4, 9, 15, 21, 28, 32, 34, 32, 28, 21, 15, 9, 4, 1, 1]), +]; +pub const SHELL_2: [&[u8]; 16] = [ + &icdf::<2>(&[128, 128]), + &icdf::<3>(&[42, 172, 42]), + &icdf::<4>(&[21, 107, 107, 21]), + &icdf::<5>(&[12, 60, 112, 61, 11]), + &icdf::<6>(&[8, 34, 86, 86, 35, 7]), + &icdf::<7>(&[8, 23, 55, 90, 55, 20, 5]), + &icdf::<8>(&[5, 15, 38, 72, 72, 36, 15, 3]), + &icdf::<9>(&[6, 12, 27, 52, 77, 47, 20, 10, 5]), + &icdf::<10>(&[6, 19, 28, 35, 40, 40, 35, 28, 19, 6]), + &icdf::<11>(&[4, 14, 22, 31, 37, 40, 37, 31, 22, 14, 4]), + &icdf::<12>(&[3, 10, 18, 26, 33, 38, 38, 33, 26, 18, 10, 3]), + &icdf::<13>(&[2, 8, 13, 21, 29, 36, 38, 36, 29, 21, 13, 8, 2]), + &icdf::<14>(&[1, 5, 10, 17, 25, 32, 38, 38, 32, 25, 17, 10, 5, 1]), + &icdf::<15>(&[1, 4, 7, 13, 21, 29, 35, 36, 35, 29, 21, 13, 7, 4, 1]), + &icdf::<16>(&[1, 2, 5, 10, 17, 25, 32, 36, 36, 32, 25, 17, 10, 5, 2, 1]), + &icdf::<17>(&[1, 2, 4, 7, 13, 21, 28, 34, 36, 34, 28, 21, 13, 7, 4, 2, 1]), +]; +pub const EXC_LSB: [u8; 2] = icdf(&[136, 120]); +pub const EXC_SIGN: [&[u8]; 42] = [ + &icdf::<2>(&[2, 254]), + &icdf::<2>(&[207, 49]), + &icdf::<2>(&[189, 67]), + &icdf::<2>(&[179, 77]), + &icdf::<2>(&[174, 82]), + &icdf::<2>(&[163, 93]), + &icdf::<2>(&[157, 99]), + &icdf::<2>(&[58, 198]), + &icdf::<2>(&[245, 11]), + &icdf::<2>(&[238, 18]), + &icdf::<2>(&[232, 24]), + &icdf::<2>(&[225, 31]), + &icdf::<2>(&[220, 36]), + &icdf::<2>(&[211, 45]), + &icdf::<2>(&[1, 255]), + &icdf::<2>(&[210, 46]), + &icdf::<2>(&[190, 66]), + &icdf::<2>(&[178, 78]), + &icdf::<2>(&[169, 87]), + &icdf::<2>(&[162, 94]), + &icdf::<2>(&[152, 104]), + &icdf::<2>(&[48, 208]), + &icdf::<2>(&[242, 14]), + &icdf::<2>(&[235, 21]), + &icdf::<2>(&[224, 32]), + &icdf::<2>(&[214, 42]), + &icdf::<2>(&[205, 51]), + &icdf::<2>(&[190, 66]), + &icdf::<2>(&[1, 255]), + &icdf::<2>(&[162, 94]), + &icdf::<2>(&[152, 104]), + &icdf::<2>(&[147, 109]), + &icdf::<2>(&[144, 112]), + &icdf::<2>(&[141, 115]), + &icdf::<2>(&[138, 118]), + &icdf::<2>(&[8, 248]), + &icdf::<2>(&[203, 53]), + &icdf::<2>(&[187, 69]), + &icdf::<2>(&[176, 80]), + &icdf::<2>(&[168, 88]), + &icdf::<2>(&[161, 95]), + &icdf::<2>(&[154, 102]), +]; +pub const LBRR_FLAGS: [&[u8]; 2] = [&icdf::<3>(&[53, 53, 150]), &icdf::<7>(&[41, 20, 29, 41, 15, 28, 82])]; diff --git a/crates/opusenc/tests/common/mod.rs b/crates/opusenc/tests/common/mod.rs new file mode 100644 index 00000000..9b420f31 --- /dev/null +++ b/crates/opusenc/tests/common/mod.rs @@ -0,0 +1,285 @@ +//! Shared helpers for the SILK/hybrid round-trip tests: synthetic signals, encoding, decoding +//! with FilmCraft's spec-derived decoder, delay-searched SNR and an Ogg Opus writer for ffmpeg. +#![allow(dead_code)] + +use effectcraft_opusenc::OpusEncoder; + +pub const SR: f64 = 48_000.0; +pub const FRAME: usize = OpusEncoder::FRAME_SIZE; + +/// Two-pole resonator state. +struct Reson { + y1: f64, + y2: f64, +} + +impl Reson { + fn run(&mut self, x: f64, f: f64, bw: f64) -> f64 { + let r = (-std::f64::consts::PI * bw / SR).exp(); + let a1 = 2.0 * r * (2.0 * std::f64::consts::PI * f / SR).cos(); + let a2 = -r * r; + let g = 1.0 - r; + let y = g * x + a1 * self.y1 + a2 * self.y2; + self.y2 = self.y1; + self.y1 = y; + y + } +} + +/// Speech-like test signal: a glottal pulse train (100–200 Hz, gliding pitch) through three +/// formant resonators whose frequencies change per syllable, with syllable envelopes, short +/// pauses and a little aspiration noise. +pub fn speech(len: usize, seed: u32) -> Vec { + let vowels: [[f64; 3]; 5] = [[730.0, 1090.0, 2440.0], [270.0, 2290.0, 3010.0], [530.0, 1840.0, 2480.0], [570.0, 840.0, 2410.0], [300.0, 870.0, 2240.0]]; + let syl = (0.24 * SR) as usize; + let mut out = Vec::with_capacity(len); + let mut res = [Reson { y1: 0.0, y2: 0.0 }, Reson { y1: 0.0, y2: 0.0 }, Reson { y1: 0.0, y2: 0.0 }]; + let mut phase = 0.0f64; + let mut s = seed; + let mut prev_glottal = 0.0; + for n in 0..len { + let t = n as f64 / SR; + let k = n / syl; + let pos = (n % syl) as f64 / syl as f64; + // Every fourth syllable is a pause. + let pause = k % 4 == 3; + let env = if pause { 0.0 } else { (std::f64::consts::PI * pos).sin().powf(0.7) }; + let f0 = 100.0 + 50.0 * (1.0 + (2.0 * std::f64::consts::PI * 0.9 * t).sin()) + 15.0 * ((k * 7) % 5) as f64 - 30.0 * pos; + phase += f0 / SR; + if phase >= 1.0 { + phase -= 1.0; + } + // Rosenberg-like glottal flow; its derivative excites the vocal tract. + let g = if phase < 0.4 { + 0.5 * (1.0 - (std::f64::consts::PI * phase / 0.4).cos()) + } else if phase < 0.56 { + (std::f64::consts::FRAC_PI_2 * (phase - 0.4) / 0.16).cos() + } else { + 0.0 + }; + let dg = (g - prev_glottal) * 40.0; + prev_glottal = g; + s = s.wrapping_mul(1_664_525).wrapping_add(1_013_904_223); + let noise = ((s >> 8) as f64 / (1u32 << 24) as f64 - 0.5) * 0.02; + let v = vowels[(k * 3 + 1) % 5]; + let w = vowels[(k * 3 + 4) % 5]; + let mix = pos; + let mut x = dg + noise; + let mut y = 0.0; + for (i, r) in res.iter_mut().enumerate() { + let f = v[i] * (1.0 - mix) + w[i] * mix; + y += r.run(x, f, 60.0 + 40.0 * i as f64) * [1.0, 0.6, 0.3][i]; + } + x = y * env * 1.2; + out.push(x as f32); + } + let peak = out.iter().fold(0f32, |m, v| m.max(v.abs())); + let g = 0.5 / peak.max(1e-9); + out.iter().map(|v| v * g).collect() +} + +/// Music-like test signal: three-note chords of harmonic tones (six partials each), changing +/// every half second with soft attacks and releases. +pub fn music(len: usize) -> Vec { + let chords: [[f64; 3]; 4] = [[261.63, 329.63, 392.0], [220.0, 277.18, 329.63], [174.61, 220.0, 261.63], [196.0, 246.94, 293.66]]; + let seg = (0.5 * SR) as usize; + (0..len) + .map(|n| { + let t = n as f64 / SR; + let k = n / seg; + let pos = (n % seg) as f64 / seg as f64; + let env = (pos * 20.0).min(1.0) * ((1.0 - pos) * 10.0).min(1.0); + let mut x = 0.0; + for (j, &f) in chords[k % 4].iter().enumerate() { + for h in 1..=6 { + x += (2.0 * std::f64::consts::PI * f * h as f64 * t + j as f64).sin() / h as f64; + } + } + (0.12 * env * x) as f32 + }) + .collect() +} + +pub fn interleave(planar: &[Vec]) -> Vec { + let c = planar.len(); + let len = planar[0].len(); + let mut pcm = vec![0f32; len * c]; + for (ch, p) in planar.iter().enumerate() { + for (i, &v) in p.iter().enumerate() { + pcm[i * c + ch] = v; + } + } + pcm +} + +/// Encodes planar input (padding the tail so the encoder delay is flushed). +pub fn encode(mut enc: OpusEncoder, planar: &[Vec]) -> (OpusEncoder, Vec>) { + let c = planar.len(); + let len = planar[0].len(); + let total = len + enc.pre_skip() as usize + 16; + let frames = total.div_ceil(FRAME); + let mut pcm = interleave(planar); + pcm.resize(frames * FRAME * c, 0.0); + let packets = pcm.chunks(FRAME * c).map(|f| enc.encode_float(f)).collect(); + (enc, packets) +} + +/// Decodes with filmcraft-opus, drops the pre-skip and returns planar channels (full length). +pub fn decode(head: &[u8], packets: &[Vec]) -> Vec> { + let mut dec = filmcraft_opus::Decoder::new(head).expect("OpusHead"); + dec.set_trim_pre_skip(true); + let mut out: Vec> = vec![Vec::new(); dec.channels()]; + for p in packets { + let planes = dec.decode(Some(p)).expect("decode"); + for (o, p) in out.iter_mut().zip(planes) { + o.extend_from_slice(&p); + } + } + out +} + +/// Waveform SNR (dB) of `dec[i + shift]` against `orig[i]`, excluding the first and last 20 ms. +pub fn snr_at(orig: &[f32], dec: &[f32], shift: isize) -> f64 { + let (mut s, mut e) = (0f64, 0f64); + for i in FRAME..orig.len() - FRAME { + let j = i as isize + shift; + let d = if j >= 0 && (j as usize) < dec.len() { dec[j as usize] as f64 } else { 0.0 }; + s += (orig[i] as f64).powi(2); + e += (orig[i] as f64 - d).powi(2); + } + 10.0 * (s / e.max(1e-30)).log10() +} + +/// Best SNR over residual delays of ±`range` samples: (SNR dB, delay). +pub fn snr_search(orig: &[f32], dec: &[f32], range: isize) -> (f64, isize) { + (-range..=range).map(|d| (snr_at(orig, dec, d), d)).fold((f64::MIN, 0), |a, b| if b.0 > a.0 { b } else { a }) +} + +/// Segmental SNR (dB): mean over 20 ms segments holding signal (each clamped to [-10, 40]). +pub fn seg_snr(orig: &[f32], dec: &[f32], shift: isize) -> f64 { + let mut acc = 0f64; + let mut n = 0; + let mut i = FRAME; + while i + FRAME <= orig.len() - FRAME { + let (mut s, mut e) = (0f64, 0f64); + for k in i..i + FRAME { + let j = k as isize + shift; + let d = if j >= 0 && (j as usize) < dec.len() { dec[j as usize] as f64 } else { 0.0 }; + s += (orig[k] as f64).powi(2); + e += (orig[k] as f64 - d).powi(2); + } + if s / FRAME as f64 > 1e-5 { + acc += (10.0 * (s / e.max(1e-30)).log10()).clamp(-10.0, 40.0); + n += 1; + } + i += FRAME; + } + acc / n.max(1) as f64 +} + +/// Average bitrate (bits per second) of a packet sequence of 20 ms packets. +pub fn bitrate(packets: &[Vec]) -> f64 { + packets.iter().map(|p| p.len() * 8).sum::() as f64 / (packets.len() as f64 * 0.02) +} + +/// Low-pass filtered copy (FFT brick wall) used to compare band-limited modes fairly. +pub fn lowpass(x: &[f32], fc: f64) -> Vec { + let n = x.len().next_power_of_two(); + let mut re: Vec = x.iter().map(|&v| v as f64).collect(); + re.resize(n, 0.0); + let mut im = vec![0f64; n]; + fft(&mut re, &mut im, false); + for k in 0..n { + let f = (k.min(n - k)) as f64 * SR / n as f64; + if f > fc { + re[k] = 0.0; + im[k] = 0.0; + } + } + fft(&mut re, &mut im, true); + re[..x.len()].iter().map(|&v| (v / n as f64) as f32).collect() +} + +pub fn fft(re: &mut [f64], im: &mut [f64], inverse: bool) { + let n = re.len(); + let mut j = 0; + for i in 1..n { + let mut bit = n >> 1; + while j & bit != 0 { + j ^= bit; + bit >>= 1; + } + j |= bit; + if i < j { + re.swap(i, j); + im.swap(i, j); + } + } + let mut len = 2; + while len <= n { + let ang = if inverse { 2.0 } else { -2.0 } * std::f64::consts::PI / len as f64; + for s in (0..n).step_by(len) { + for k in 0..len / 2 { + let (wr, wi) = ((ang * k as f64).cos(), (ang * k as f64).sin()); + let (a, b) = (s + k, s + k + len / 2); + let tr = re[b] * wr - im[b] * wi; + let ti = re[b] * wi + im[b] * wr; + re[b] = re[a] - tr; + im[b] = im[a] - ti; + re[a] += tr; + im[a] += ti; + } + } + len <<= 1; + } +} + +// ---- Ogg Opus (RFC 3533 + RFC 7845) for the ffmpeg oracle ---- + +fn ogg_crc(data: &[u8]) -> u32 { + let mut crc = 0u32; + for &b in data { + crc ^= (b as u32) << 24; + for _ in 0..8 { + crc = if crc & 0x8000_0000 != 0 { (crc << 1) ^ 0x04C1_1DB7 } else { crc << 1 }; + } + } + crc +} + +fn ogg_page(out: &mut Vec, packet: &[u8], header_type: u8, granule: u64, seq: u32) { + let mut lacing = vec![255u8; packet.len() / 255]; + lacing.push((packet.len() % 255) as u8); + let start = out.len(); + out.extend_from_slice(b"OggS"); + out.push(0); + out.push(header_type); + out.extend_from_slice(&granule.to_le_bytes()); + out.extend_from_slice(&0x4543_4F50u32.to_le_bytes()); + out.extend_from_slice(&seq.to_le_bytes()); + out.extend_from_slice(&0u32.to_le_bytes()); + out.push(lacing.len() as u8); + out.extend_from_slice(&lacing); + out.extend_from_slice(packet); + let crc = ogg_crc(&out[start..]); + out[start + 22..start + 26].copy_from_slice(&crc.to_le_bytes()); +} + +/// An Ogg Opus file holding `packets` for `len` samples of audio. +pub fn ogg_opus(enc: &OpusEncoder, packets: &[Vec], len: usize) -> Vec { + let mut out = Vec::new(); + ogg_page(&mut out, &enc.opus_head(), 0x02, 0, 0); + let vendor = b"effectcraft-opusenc"; + let mut tags = b"OpusTags".to_vec(); + tags.extend_from_slice(&(vendor.len() as u32).to_le_bytes()); + tags.extend_from_slice(vendor); + tags.extend_from_slice(&0u32.to_le_bytes()); + ogg_page(&mut out, &tags, 0, 0, 1); + let pre_skip = enc.pre_skip() as u64; + for (i, p) in packets.iter().enumerate() { + let last = i + 1 == packets.len(); + let granule = if last { pre_skip + len as u64 } else { ((i + 1) * FRAME) as u64 }; + ogg_page(&mut out, p, if last { 0x04 } else { 0 }, granule, i as u32 + 2); + } + out +} diff --git a/crates/opusenc/tests/ffmpeg.rs b/crates/opusenc/tests/ffmpeg.rs new file mode 100644 index 00000000..672426cc --- /dev/null +++ b/crates/opusenc/tests/ffmpeg.rs @@ -0,0 +1,81 @@ +//! ffmpeg as a second, external oracle: every mode/bandwidth written as Ogg Opus must decode +//! without errors with both ffmpeg's native `opus` decoder and `libopus`, honour the pre-skip +//! and end granule, and reproduce the input. Skipped when ffmpeg is not installed. + +mod common; + +use std::process::Command; + +use common::*; +use effectcraft_opusenc::{Bandwidth, Mode, OpusEncoder}; + +const FFMPEG: &str = "/opt/homebrew/bin/ffmpeg"; + +fn ffmpeg() -> Option<&'static str> { + [FFMPEG, "ffmpeg"].into_iter().find(|exe| Command::new(exe).arg("-version").output().is_ok_and(|o| o.status.success())) +} + +/// Decodes an Ogg Opus file with ffmpeg's decoder `codec`; returns interleaved f32 output. +fn ffmpeg_decode(exe: &str, codec: &str, file: &[u8], channels: usize, tag: &str) -> Option> { + let has = Command::new(exe).args(["-hide_banner", "-decoders"]).output().ok()?; + if !String::from_utf8_lossy(&has.stdout).lines().any(|l| l.split_whitespace().nth(1) == Some(codec)) { + eprintln!("ffmpeg has no {codec} decoder; skipping it"); + return None; + } + let path = std::env::temp_dir().join(format!("effectcraft-opusenc-{}-{tag}.opus", std::process::id())); + std::fs::write(&path, file).unwrap(); + let out = Command::new(exe) + .args(["-v", "error", "-c:a", codec, "-i"]) + .arg(&path) + .args(["-f", "f32le", "-ac", &channels.to_string(), "-ar", "48000", "-"]) + .output() + .expect("run ffmpeg"); + let _ = std::fs::remove_file(&path); + let stderr = String::from_utf8_lossy(&out.stderr); + assert!(out.status.success(), "ffmpeg ({codec}) failed: {stderr}"); + // ffmpeg's native decoder also makes its raw-PCM muxer print a timestamp complaint for + // narrowband SILK streams written by libopus itself; that line is about ffmpeg's own + // timestamping, not the bitstream, so it is the one message tolerated. + let errors: Vec<&str> = stderr.lines().filter(|l| !l.trim().is_empty() && !l.contains("non monotonically increasing dts to muxer")).collect(); + assert!(errors.is_empty(), "ffmpeg ({codec}) reported errors: {errors:?}"); + Some(out.stdout.chunks_exact(4).map(|b| f32::from_le_bytes([b[0], b[1], b[2], b[3]])).collect()) +} + +#[test] +fn ffmpeg_decodes_every_mode() { + let Some(exe) = ffmpeg() else { + eprintln!("ffmpeg not installed; skipping the oracle test"); + return; + }; + let len = 48_000; + let sp = speech(len + 30_000, 3); + let left = sp[..len].to_vec(); + let right: Vec = sp[30_000..].iter().map(|v| v * 0.7).collect(); + let cases = [ + (1usize, Mode::Silk, Bandwidth::Narrowband, 8_000u32, 3_400.0, 8.0), + (1, Mode::Silk, Bandwidth::Mediumband, 11_000, 5_000.0, 10.0), + (1, Mode::Silk, Bandwidth::Wideband, 16_000, 7_000.0, 12.0), + (2, Mode::Silk, Bandwidth::Wideband, 32_000, 7_000.0, 12.0), + (1, Mode::Hybrid, Bandwidth::SuperWideband, 24_000, 24_000.0, 10.0), + (1, Mode::Hybrid, Bandwidth::Fullband, 32_000, 24_000.0, 10.0), + (2, Mode::Hybrid, Bandwidth::Fullband, 64_000, 24_000.0, 10.0), + (2, Mode::Celt, Bandwidth::Fullband, 96_000, 24_000.0, 10.0), + ]; + for (c, mode, bw, rate, fc, min_snr) in cases { + let planar: Vec> = if c == 1 { vec![left.clone()] } else { vec![left.clone(), right.clone()] }; + let (enc, packets) = encode(OpusEncoder::with_mode(c as u8, rate, mode, bw), &planar); + let file = ogg_opus(&enc, &packets, len); + for codec in ["opus", "libopus"] { + let Some(dec) = ffmpeg_decode(exe, codec, &file, c, &format!("{mode:?}{bw:?}{c}{codec}")) else { + continue; + }; + assert_eq!(dec.len(), len * c, "{codec}: pre-skip and end granule must be honoured"); + for (ch, x) in planar.iter().enumerate() { + let d: Vec = (0..len).map(|i| dec[i * c + ch]).collect(); + let (snr, delay) = snr_search(&lowpass(x, fc), &d, 6); + eprintln!("ffmpeg {codec:7} {mode:?}/{bw:?} {c} ch @ {} kb/s, channel {ch}: SNR {snr:.1} dB (residual delay {delay})", rate / 1000); + assert!(snr > min_snr, "{codec} {mode:?}/{bw:?}: SNR {snr:.1} dB"); + } + } + } +} diff --git a/crates/opusenc/tests/modes.rs b/crates/opusenc/tests/modes.rs new file mode 100644 index 00000000..843cf3d9 --- /dev/null +++ b/crates/opusenc/tests/modes.rs @@ -0,0 +1,272 @@ +//! SILK, hybrid and CELT round trips through FilmCraft's spec-derived Opus decoder: every +//! mode/bandwidth decodes, waveform quality on speech-like and music-like signals, bitrate +//! tracking, stereo, the application tables and delay alignment. + +mod common; + +use common::*; +use effectcraft_opusenc::{Application, Bandwidth, Mode, OpusEncoder}; + +struct Run { + snr: f64, + seg: f64, + delay: isize, + kbps: f64, + packets: Vec>, +} + +/// Encodes `planar`, decodes, and measures each channel against `reference` (searching a +/// residual delay of up to ±6 samples). +fn run(enc: OpusEncoder, planar: &[Vec], reference: &[Vec]) -> Vec { + let (enc, packets) = encode(enc, planar); + let dec = decode(&enc.opus_head(), &packets); + let kbps = bitrate(&packets) / 1000.0; + reference + .iter() + .zip(&dec) + .map(|(r, d)| { + assert!(d.len() >= r.len(), "decoded {} < {}", d.len(), r.len()); + assert!(d.iter().all(|v| v.is_finite())); + let (snr, delay) = snr_search(r, d, 6); + Run { snr, seg: seg_snr(r, d, delay), delay, kbps, packets: packets.clone() } + }) + .collect() +} + +fn mono(mode: Mode, bw: Bandwidth, rate: u32, x: &[f32], fc: f64) -> Run { + let reference = if fc < 20_000.0 { lowpass(x, fc) } else { x.to_vec() }; + run(OpusEncoder::with_mode(1, rate, mode, bw), &[x.to_vec()], &[reference]).remove(0) +} + +/// Upper edge of the band a SILK bandwidth keeps (for band-limited references). +fn band_edge(bw: Bandwidth) -> f64 { + match bw { + Bandwidth::Narrowband => 3_400.0, + Bandwidth::Mediumband => 5_000.0, + Bandwidth::Wideband => 7_000.0, + _ => 24_000.0, + } +} + +const LEN: usize = 96_000; + +#[test] +fn every_mode_and_bandwidth_decodes() { + let sp = speech(LEN + 40_000, 11); + let left = sp[..LEN].to_vec(); + let right: Vec = sp[40_000..].iter().map(|v| v * 0.6).collect(); + for (mode, bw, config, rate) in [ + (Mode::Silk, Bandwidth::Narrowband, 1u8, 8_000u32), + (Mode::Silk, Bandwidth::Mediumband, 5, 11_000), + (Mode::Silk, Bandwidth::Wideband, 9, 16_000), + (Mode::Hybrid, Bandwidth::SuperWideband, 13, 24_000), + (Mode::Hybrid, Bandwidth::Fullband, 15, 32_000), + (Mode::Celt, Bandwidth::Fullband, 31, 64_000), + ] { + for c in [1usize, 2] { + let planar = if c == 1 { vec![left.clone()] } else { vec![left.clone(), right.clone()] }; + let reference: Vec> = planar.iter().map(|x| lowpass(x, band_edge(bw))).collect(); + let enc = OpusEncoder::with_mode(c as u8, rate * c as u32, mode, bw); + assert_eq!((enc.mode(), enc.bandwidth()), (mode, bw)); + let res = run(enc, &planar, &reference); + for p in &res[0].packets { + assert_eq!(p[0], (config << 3) | if c == 2 { 4 } else { 0 }, "TOC of {mode:?}/{bw:?}"); + assert!(p.len() >= 3 && p.len() <= 1276); + } + for (ch, r) in res.iter().enumerate() { + eprintln!( + "{mode:?}/{bw:?} {c} ch @ {} kb/s: {:.1} kb/s, channel {ch} SNR {:.1} dB, segSNR {:.1} dB, residual delay {}", + rate * c as u32 / 1000, + r.kbps, + r.snr, + r.seg, + r.delay + ); + assert!(r.snr > 8.0, "{mode:?}/{bw:?} {c} ch channel {ch}: SNR {:.1} dB", r.snr); + assert!(r.delay.abs() <= 1, "{mode:?}/{bw:?}: residual delay {} (pre-skip mismatch)", r.delay); + } + } + } +} + +#[test] +fn silk_speech_quality() { + let x = speech(LEN, 7); + for (bw, rate, min_snr) in [ + (Bandwidth::Narrowband, 8_000u32, 10.0), + (Bandwidth::Mediumband, 11_000, 14.0), + (Bandwidth::Wideband, 12_000, 14.0), + (Bandwidth::Wideband, 16_000, 18.0), + (Bandwidth::Wideband, 24_000, 24.0), + ] { + let r = mono(Mode::Silk, bw, rate, &x, band_edge(bw)); + eprintln!("SILK {bw:?} speech @ {} kb/s: {:.1} kb/s, SNR {:.1} dB, segSNR {:.1} dB", rate / 1000, r.kbps, r.snr, r.seg); + assert!(r.snr > min_snr && r.seg > min_snr - 3.0, "SILK {bw:?} @ {rate}: SNR {:.1} / seg {:.1}", r.snr, r.seg); + } +} + +#[test] +fn silk_beats_celt_on_speech_at_low_rate() { + let x = speech(LEN, 5); + // Both measured against the full-band input. + let silk = mono(Mode::Silk, Bandwidth::Wideband, 12_000, &x, 24_000.0); + let celt = mono(Mode::Celt, Bandwidth::Fullband, 12_000, &x, 24_000.0); + eprintln!( + "12 kb/s mono speech: SILK WB {:.1} dB (seg {:.1}) at {:.1} kb/s vs CELT {:.1} dB (seg {:.1}) at {:.1} kb/s", + silk.snr, silk.seg, silk.kbps, celt.snr, celt.seg, celt.kbps + ); + assert!(silk.snr > celt.snr + 3.0, "SILK {:.1} dB vs CELT {:.1} dB", silk.snr, celt.snr); + assert!(silk.seg > celt.seg + 3.0, "SILK seg {:.1} dB vs CELT seg {:.1} dB", silk.seg, celt.seg); +} + +#[test] +fn hybrid_quality() { + let sp = speech(LEN, 9); + let mu = music(LEN); + for (name, x, bw, rate, min_snr) in [ + ("speech", &sp, Bandwidth::SuperWideband, 24_000u32, 18.0), + ("speech", &sp, Bandwidth::Fullband, 32_000, 20.0), + ("music", &mu, Bandwidth::SuperWideband, 24_000, 9.0), + ("music", &mu, Bandwidth::Fullband, 32_000, 10.0), + ] { + let r = mono(Mode::Hybrid, bw, rate, x, 24_000.0); + eprintln!("hybrid {bw:?} {name} @ {} kb/s: {:.1} kb/s, SNR {:.1} dB, segSNR {:.1} dB", rate / 1000, r.kbps, r.snr, r.seg); + assert!(r.snr > min_snr, "hybrid {bw:?} {name}: SNR {:.1} dB", r.snr); + // Constant packet size. + assert!(r.packets.iter().all(|p| p.len() == rate as usize / 400)); + } +} + +#[test] +fn silk_music_quality() { + let x = music(LEN); + for (rate, min_snr) in [(12_000u32, 5.0), (16_000, 8.0), (24_000, 13.0)] { + let r = mono(Mode::Silk, Bandwidth::Wideband, rate, &x, 7_000.0); + eprintln!("SILK WB music @ {} kb/s: {:.1} kb/s, SNR {:.1} dB", rate / 1000, r.kbps, r.snr); + assert!(r.snr > min_snr, "SILK music @ {rate}: SNR {:.1} dB", r.snr); + } +} + +#[test] +fn bitrate_tracking() { + let sp = speech(LEN, 13); + let mu = music(LEN); + for (name, x) in [("speech", &sp), ("music", &mu)] { + for (mode, bw, rate) in [ + (Mode::Silk, Bandwidth::Narrowband, 6_000u32), + (Mode::Silk, Bandwidth::Narrowband, 9_000), + (Mode::Silk, Bandwidth::Mediumband, 11_000), + (Mode::Silk, Bandwidth::Wideband, 12_000), + (Mode::Silk, Bandwidth::Wideband, 20_000), + (Mode::Silk, Bandwidth::Wideband, 32_000), + (Mode::Hybrid, Bandwidth::SuperWideband, 20_000), + (Mode::Hybrid, Bandwidth::Fullband, 36_000), + ] { + let r = mono(mode, bw, rate, x, band_edge(bw)); + let ratio = r.kbps * 1000.0 / rate as f64; + eprintln!("{name} {mode:?}/{bw:?} target {} kb/s: actual {:.2} kb/s ({:+.0} %), SNR {:.1} dB", rate / 1000, r.kbps, (ratio - 1.0) * 100.0, r.snr); + assert!((0.75..=1.25).contains(&ratio), "{name} {mode:?}/{bw:?} @ {rate}: {:.2} kb/s", r.kbps); + } + } +} + +#[test] +fn stereo_silk_and_hybrid() { + let sp = speech(LEN + 40_000, 21); + let a = sp[..LEN].to_vec(); + let b: Vec = sp[40_000..].iter().map(|v| v * 0.7).collect(); + let mu = music(LEN); + let cases: Vec<(&str, Vec, Vec)> = vec![ + ("dual mono", a.clone(), a.clone()), + ("panned", a.iter().map(|v| v * 0.9).collect(), a.iter().map(|v| v * 0.35).collect()), + ("independent", a.clone(), b), + ("speech+music", a.iter().zip(&mu).map(|(x, y)| 0.8 * x + 0.3 * y).collect(), a.iter().zip(&mu).map(|(x, y)| 0.3 * x + 0.8 * y).collect()), + ]; + for (name, l, r) in &cases { + for (mode, bw, rate, min_snr) in [(Mode::Silk, Bandwidth::Wideband, 32_000u32, 9.0), (Mode::Hybrid, Bandwidth::Fullband, 64_000, 9.0)] { + let fc = band_edge(bw); + let res = run(OpusEncoder::with_mode(2, rate, mode, bw), &[l.clone(), r.clone()], &[lowpass(l, fc), lowpass(r, fc)]); + eprintln!("stereo {name} {mode:?}/{bw:?} @ {} kb/s: {:.1} kb/s, L {:.1} dB, R {:.1} dB", rate / 1000, res[0].kbps, res[0].snr, res[1].snr); + assert!(res[0].snr > min_snr && res[1].snr > min_snr, "stereo {name} {mode:?}: L {:.1} R {:.1}", res[0].snr, res[1].snr); + } + } +} + +#[test] +fn application_tables_and_pre_skip() { + let cases = [ + (Application::Voip, 1u8, 8_000u32, Mode::Silk, Bandwidth::Narrowband), + (Application::Voip, 1, 11_000, Mode::Silk, Bandwidth::Mediumband), + (Application::Voip, 1, 16_000, Mode::Silk, Bandwidth::Wideband), + (Application::Voip, 1, 24_000, Mode::Hybrid, Bandwidth::SuperWideband), + (Application::Voip, 1, 32_000, Mode::Hybrid, Bandwidth::Fullband), + (Application::Voip, 1, 48_000, Mode::Celt, Bandwidth::Fullband), + (Application::Voip, 2, 32_000, Mode::Silk, Bandwidth::Wideband), + (Application::Audio, 1, 12_000, Mode::Silk, Bandwidth::Wideband), + (Application::Audio, 1, 24_000, Mode::Hybrid, Bandwidth::Fullband), + (Application::Audio, 2, 48_000, Mode::Hybrid, Bandwidth::Fullband), + (Application::Audio, 1, 64_000, Mode::Celt, Bandwidth::Fullband), + (Application::Audio, 2, 128_000, Mode::Celt, Bandwidth::Fullband), + ]; + for (app, c, rate, mode, bw) in cases { + let e = OpusEncoder::with_application(c, rate, app); + assert_eq!((e.mode(), e.bandwidth(), e.application()), (mode, bw, app), "{app:?} {c} ch @ {rate}"); + assert_eq!(e.pre_skip(), 120, "every mode is aligned to a 120-sample delay"); + } + assert_eq!(OpusEncoder::new(2, 128_000).mode(), Mode::Celt); + assert_eq!(OpusEncoder::new(1, 12_000).application(), Application::Audio); +} + +#[test] +#[should_panic(expected = "unsupported mode/bandwidth")] +fn invalid_mode_bandwidth_panics() { + let _ = OpusEncoder::with_mode(1, 16_000, Mode::Silk, Bandwidth::Fullband); +} + +#[test] +fn extreme_inputs_stay_decodable() { + let len = 24_000; + let mut s = 1u32; + let noise: Vec = (0..len) + .map(|_| { + s = s.wrapping_mul(1_664_525).wrapping_add(1_013_904_223); + (s >> 8) as f32 / (1u32 << 23) as f32 - 1.0 + }) + .collect(); + let square: Vec = (0..len).map(|i| if (i / 60) % 2 == 0 { 0.999 } else { -0.999 }).collect(); + let clicks: Vec = (0..len).map(|i| if i % 4_801 == 0 { 1.0 } else { 0.0 }).collect(); + let dc = vec![0.7f32; len]; + let nan: Vec = (0..len).map(|i| if i % 1000 == 0 { f32::NAN } else { 0.1 * (i as f32 * 0.01).sin() }).collect(); + for (name, x) in [("noise", &noise), ("square", &square), ("clicks", &clicks), ("dc", &dc), ("nan", &nan)] { + for (mode, bw, rate) in + [(Mode::Silk, Bandwidth::Narrowband, 6_000u32), (Mode::Silk, Bandwidth::Wideband, 40_000), (Mode::Hybrid, Bandwidth::Fullband, 24_000)] + { + for c in [1usize, 2] { + let planar: Vec> = if c == 1 { vec![x.clone()] } else { vec![x.clone(), x.iter().map(|v| -0.5 * v).collect()] }; + let (enc, packets) = encode(OpusEncoder::with_mode(c as u8, rate * c as u32, mode, bw), &planar); + assert!(packets.iter().all(|p| p.len() >= 3 && p.len() <= 1276), "{name} {mode:?}: packet sizes"); + let dec = decode(&enc.opus_head(), &packets); + let peak = dec.iter().flatten().fold(0f32, |m, v| m.max(v.abs())); + let snr = if name == "nan" { f64::NAN } else { snr_search(&planar[0], &dec[0], 2).0 }; + eprintln!("{name} {mode:?}/{bw:?} {c} ch: peak {peak:.2}, SNR {snr:.1} dB, {:.1} kb/s", bitrate(&packets) / 1000.0); + assert!(dec.iter().all(|ch| ch.iter().all(|v| v.is_finite() && v.abs() <= 4.0)), "{name} {mode:?}/{bw:?} {c} ch: bad output"); + } + } + } +} + +#[test] +fn silk_and_hybrid_silence() { + for (mode, bw, rate) in [(Mode::Silk, Bandwidth::Wideband, 16_000u32), (Mode::Hybrid, Bandwidth::Fullband, 32_000)] { + let x = vec![0f32; 48_000]; + let (enc, packets) = encode(OpusEncoder::with_mode(1, rate, mode, bw), std::slice::from_ref(&x)); + let dec = decode(&enc.opus_head(), &packets); + let peak = dec[0].iter().fold(0f32, |m, v| m.max(v.abs())); + let kbps = bitrate(&packets) / 1000.0; + eprintln!("{mode:?} silence: {kbps:.2} kb/s, decoded peak {peak:.2e}"); + assert!(peak < 1e-3, "{mode:?} silence decodes to peak {peak}"); + if mode == Mode::Silk { + assert!(kbps < 4.0, "SILK silence costs {kbps:.2} kb/s"); + } + } +} diff --git a/crates/pdf/README.md b/crates/pdf/README.md index 43452da9..b7678b09 100644 --- a/crates/pdf/README.md +++ b/crates/pdf/README.md @@ -12,17 +12,22 @@ fonts, images, patterns, transparency (blend modes, soft masks) and optional con **Technical Note #5176** (The Compact Font Format Specification) and **#5177** (The Type 2 Charstring Format) for CFF font programs; **Adobe Type 1 Font Format** (version 1.1) for Type 1 font programs; the **PostScript Language Reference, third edition** and the **Encapsulated -PostScript File Format Specification 3.0** for EPS. TrueType font programs are read through -skrifa and JPEG (DCT) images through zune-jpeg (both permissively licensed, pure Rust). No -third-party PDF/PostScript/font code was read; test files (including the CFF and Type 1 fonts) -are generated in code ([`write`](src/write.rs) and the tests). +PostScript File Format Specification 3.0** for EPS; **ITU-T T.4** (07/2003) and **ITU-T T.6** +(11/1988) for CCITT fax images. TrueType font programs are read through skrifa and JPEG (DCT) +images through zune-jpeg (both permissively licensed, pure Rust). No third-party +PDF/PostScript/font/codec code was read; test files (including the CFF and Type 1 fonts and the +CCITT data) are generated in code ([`write`](src/write.rs) and the tests). One CCITT fixture was +produced once by libtiff's `tiffcp` (an external encoder used only as a fixture generator, like +ffmpeg elsewhere) to check the code tables against an independent implementation. ## What is read - **Structure:** indirect objects found by scanning (damaged or incremental cross-reference sections do not matter), object streams, the page tree with inherited resources, MediaBox / CropBox and `/Rotate`. Filters: Flate (with PNG predictors), LZW, ASCIIHex, ASCII85, - RunLength. Encrypted files are refused. + RunLength, CCITTFax (Group 3 one- and two-dimensional, Group 4; `K`, `Columns`, `Rows`, + `EndOfLine`, `EncodedByteAlign`, `EndOfBlock`, `BlackIs1`; damaged data keeps the rows + decoded before the damage). Encrypted files are refused. - **Content:** `q`/`Q`, `cm`, line width / cap / join / miter / dash, ExtGState (`CA`, `ca`, `LW`, `LC`, `LJ`, `ML`, `D`), all path construction and painting operators (non-zero and even-odd), clipping (`W`, `W*`) as clipping groups, colour in DeviceGray / RGB / CMYK, ICCBased (by @@ -39,7 +44,15 @@ are generated in code ([`write`](src/write.rs) and the tests). construction including `arc`/`arcn` and relative operators, `fill`/`eofill`/`stroke`, `clip`/`eoclip`, `rectfill`/`rectstroke`/`rectclip`, gray / RGB / CMYK / HSB colour. DOS EPS binary headers and `%%HiResBoundingBox` / `%%BoundingBox` are honoured; `%%BeginData` / - `%%BeginBinary` / preview sections are skipped. + `%%BeginBinary` / preview sections are skipped. **Text:** `findfont` / `findresource`, + `scalefont`, `makefont`, `selectfont`, `setfont`, `currentfont`, `definefont` (re-encoded + copies made with `forall` and a new `/Encoding`), `StandardEncoding` / `ISOLatin1Encoding`, + `show`, `ashow`, `widthshow`, `awidthshow`, `xshow` / `yshow` / `xyshow`, `kshow` / `cshow` + (the procedure is not run), `glyphshow`, `charpath` (glyph outlines join the path, e.g. for + clipping) and `stringwidth`. Fonts embedded in the file — Type 1 programs (`… currentfile + eexec … cleartomark`, binary or hexadecimal) and CFF font sets (`StartData`) — are found by + name before the program runs; other fonts are drawn with the bundled fonts as for PDF. Each + `show` becomes one compound shape named "Text: …". - **Text** (§9): text objects and state (`Tc`, `Tw`, `Tz`, `TL`, `Ts`, `Tr`, `Tf`, `Td`, `TD`, `Tm`, `T*`), `Tj`, `TJ` (with kerning), `'` and `"`; render modes fill / stroke / @@ -47,8 +60,9 @@ are generated in code ([`write`](src/write.rs) and the tests). become outlines (one compound shape per show operator, named "Text: …" after the string through `ToUnicode` or the glyph names). Fonts: simple fonts with `/Widths`, base encodings (Standard, WinAnsi, MacRoman), `/Differences` and built-in encodings; composite (Type 0) fonts - with Identity-H / Identity-V and embedded CMaps (`cidrange` / `cidchar`), `/W` and `/DW` - widths and `CIDToGIDMap`. Embedded programs: TrueType (`FontFile2`, cmap (3,1) / (3,0) / + with Identity-H / Identity-V and embedded CMaps (`cidrange` / `cidchar`, `/WMode`), `/W` and + `/DW` widths, vertical metrics (`/W2`, `/DW2`: the vertical displacement and the position + vector from the horizontal to the vertical origin) and `CIDToGIDMap`. Embedded programs: TrueType (`FontFile2`, cmap (3,1) / (3,0) / (1,0) and `post` names), CFF (`FontFile3` `/Type1C`, `/CIDFontType0C`, `/OpenType`; name-keyed and CID-keyed with FDArray / FDSelect, flex, hint masks, `seac`), Type 1 (`FontFile`, eexec, `/Subrs`, flex and hint replacement through othersubrs, `seac`) and Type 3 @@ -64,29 +78,54 @@ are generated in code ([`write`](src/write.rs) and the tests). of the render tree (bilinear, box-filtered when minified). - **Transparency:** blend modes (`/BM`, all 16 PDF modes) and soft masks in the graphics state (`/SMask` with `/S /Luminosity` and `/BC` backdrops, or `/S /Alpha`) as groups of the render - tree, open until the state changes or `Q`. + tree, open until the state changes or `Q`: what is painted while a soft mask is set is + rendered to an offscreen buffer and the mask applies to that combined result. Transparency + group XObjects (§11.6.6) composite as one object: the current alpha, blend mode and soft mask + apply to the group's result, and its contents start from the initial alpha. Isolated groups + (`/I true`) composite over a transparent backdrop; non-isolated groups over the page, with + the backdrop's contribution removed again (§11.4.8) so group alpha and masks apply only to + the group; knockout groups (`/K true`) composite each child with the group's initial + backdrop. Clipping groups are not transparency groups: blend modes inside them reach the + backdrop. - **Patterns:** tiling patterns (coloured and uncoloured, `XStep` / `YStep`, the cell clipped to `/BBox`, at most 4096 tiles per fill) for fills; strokes with a tiling pattern use its colour (uncoloured) or grey. -- **Functions:** sampled, exponential, stitching and PostScript calculator (type 4) functions. +- **Shadings** (§8.7.4.5): axial and radial shadings become gradients. Function-based shadings + (type 1, `Domain` and `Matrix`) and the mesh shadings — free-form (type 4, edge flags 0–2) + and lattice-form (type 5) Gouraud-shaded triangle meshes, Coons patch meshes (type 6) and + tensor-product patch meshes (type 7, flags 0–3 sharing the previous patch's edges) with + `BitsPerCoordinate` / `BitsPerComponent` / `BitsPerFlag` / `Decode` and an optional + parametric `Function` — are rasterised into an image node in page space (up to 4 pixels per + point, at most 1024 pixels a side, 2×2 supersampled): patches are tessellated into triangles + (up to 48×48 cells), colours (or `t`) interpolated across each triangle. They paint through + `sh` (their own extent only) and as shading-pattern fills (clipped to the path). +- **Functions:** sampled (one or several inputs, multilinear interpolation), exponential, + stitching and PostScript calculator (type 4) functions. - **Pages:** any page ([`parse_page`], [`page_count`]); File ▸ Import takes `page` (from 1) and the Import dialog asks for it; footage remembers its page (`Footage::page`). ## Limitations -- Mesh shadings (types 4–7) and function-based shadings (type 1), JPX / CCITT / JBIG2 images, - predefined CJK CMaps other than Identity, vertical metrics (`/W2`; vertical text advances by - the font size) and transfer functions (`/TR`) are not read; knockout and isolated transparency - groups composite as normal groups, and a soft mask applies to each painting operation while - it is set rather than to their composite. What a document skipped is listed in `Doc::skipped`. +- JPX and JBIG2 images, predefined CJK CMaps other than Identity-H / Identity-V, and transfer + functions (`/TR`) are not read. JBIG2 is deferred. Predefined CMaps (`UniGB-UCS2-H`, + `90ms-RKSJ-H` …) need the CMap data, and there is no permissively licensed pure-Rust source + of it (Adobe's CMap resources are Adobe assets, which this project does not ship); text in + such fonts is reported in `Doc::skipped`. Mesh shadings are raster images in the render tree + (sharp to about 4× the document size). Knockout uses each child's coverage as its shape (a + child with constant opacity knocks out fully where it covers). What a document skipped is + listed in `Doc::skipped`. - Illustrator EPS files whose drawing depends on Adobe's procedure-set resources (defined in the file through constructs beyond this subset) and Level 3 `shfill` gradients in EPS draw only what the subset understands; save such artwork as PDF-compatible `.ai` or PDF instead. - EPS text is not drawn. + EPS images are skipped; Type 3 (procedure) fonts defined in EPS files draw nothing (reported). - **Create Shapes from Vector Layer:** clips that enclose the whole page become layer masks (the first clip Add, later ones Intersect). Nested clipping groups become Merge Paths: each closed shape inside them is a group with its geometry merged into one compound path, the clip paths, and Merge Paths ▸ Intersect before its fill and stroke, so fills match exactly and strokes follow the clipped outline; open stroked paths inside clips stay unclipped. Blend - modes become the groups' Blend Mode; images and soft masks are not converted (shape layers - have no images; use the footage, or Import As: Composition). + modes become the groups' Blend Mode. Images become footage layers: each is written as + ` Image .png` next to the source and imported, placed by its matrix (anchor at its + top-left corner; position, rotation and scale — a skew is reported) and parented to the shape + layer; the shapes drawn between images go to shape layers of their own (` Outlines + `), so the layer stack keeps the document's paint order. Soft masks, and the clips and + masks of images, are not converted (the command's `skipped` list says so). diff --git a/crates/pdf/src/build.rs b/crates/pdf/src/build.rs index 4eeeb53b..7023e931 100644 --- a/crates/pdf/src/build.rs +++ b/crates/pdf/src/build.rs @@ -62,6 +62,8 @@ impl Builder { pub fn clip(&mut self, path: BezPath, rule: FillRule) { let mut g = group("Clip Group"); g.clip.push((path, rule)); + // Clipping is not a transparency group: blend modes inside reach the backdrop. + g.isolated = false; self.stack.push((g, Kind::Clip)); } diff --git a/crates/pdf/src/ccitt.rs b/crates/pdf/src/ccitt.rs new file mode 100644 index 00000000..ebd4cad7 --- /dev/null +++ b/crates/pdf/src/ccitt.rs @@ -0,0 +1,798 @@ +//! `CCITTFaxDecode` (ISO 32000-1 §7.4.6): ITU-T T.4 Group 3 one-dimensional (Modified +//! Huffman) and two-dimensional (Modified READ) coding and ITU-T T.6 Group 4 (Modified Modified +//! READ), implemented from the ITU-T recommendations. The result is 1-bit rows, `0` black +//! unless `BlackIs1`. + +use std::collections::HashMap; +use std::sync::OnceLock; + +/// The decoding parameters (`/DecodeParms`). +#[derive(Clone, Debug, PartialEq)] +pub struct Params { + /// `< 0` Group 4, `0` Group 3 1-D, `> 0` Group 3 mixed 1-D / 2-D. + pub k: i64, + pub end_of_line: bool, + pub byte_align: bool, + pub columns: usize, + /// `0`: as many rows as the data holds. + pub rows: usize, + pub end_of_block: bool, + pub black_is_1: bool, +} + +impl Default for Params { + fn default() -> Self { + Params { k: 0, end_of_line: false, byte_align: false, columns: 1728, rows: 0, end_of_block: true, black_is_1: false } + } +} + +/// White terminating codes (T.4 Table 2), run lengths 0–63, as (code, length). +const WHITE_TERM: [(u16, u8); 64] = [ + (0b00110101, 8), + (0b000111, 6), + (0b0111, 4), + (0b1000, 4), + (0b1011, 4), + (0b1100, 4), + (0b1110, 4), + (0b1111, 4), + (0b10011, 5), + (0b10100, 5), + (0b00111, 5), + (0b01000, 5), + (0b001000, 6), + (0b000011, 6), + (0b110100, 6), + (0b110101, 6), + (0b101010, 6), + (0b101011, 6), + (0b0100111, 7), + (0b0001100, 7), + (0b0001000, 7), + (0b0010111, 7), + (0b0000011, 7), + (0b0000100, 7), + (0b0101000, 7), + (0b0101011, 7), + (0b0010011, 7), + (0b0100100, 7), + (0b0011000, 7), + (0b00000010, 8), + (0b00000011, 8), + (0b00011010, 8), + (0b00011011, 8), + (0b00010010, 8), + (0b00010011, 8), + (0b00010100, 8), + (0b00010101, 8), + (0b00010110, 8), + (0b00010111, 8), + (0b00101000, 8), + (0b00101001, 8), + (0b00101010, 8), + (0b00101011, 8), + (0b00101100, 8), + (0b00101101, 8), + (0b00000100, 8), + (0b00000101, 8), + (0b00001010, 8), + (0b00001011, 8), + (0b01010010, 8), + (0b01010011, 8), + (0b01010100, 8), + (0b01010101, 8), + (0b00100100, 8), + (0b00100101, 8), + (0b01011000, 8), + (0b01011001, 8), + (0b01011010, 8), + (0b01011011, 8), + (0b01001010, 8), + (0b01001011, 8), + (0b00110010, 8), + (0b00110011, 8), + (0b00110100, 8), +]; + +/// White make-up codes (T.4 Table 3), run lengths 64, 128, … 1728. +const WHITE_MAKEUP: [(u16, u8); 27] = [ + (0b11011, 5), + (0b10010, 5), + (0b010111, 6), + (0b0110111, 7), + (0b00110110, 8), + (0b00110111, 8), + (0b01100100, 8), + (0b01100101, 8), + (0b01101000, 8), + (0b01100111, 8), + (0b011001100, 9), + (0b011001101, 9), + (0b011010010, 9), + (0b011010011, 9), + (0b011010100, 9), + (0b011010101, 9), + (0b011010110, 9), + (0b011010111, 9), + (0b011011000, 9), + (0b011011001, 9), + (0b011011010, 9), + (0b011011011, 9), + (0b010011000, 9), + (0b010011001, 9), + (0b010011010, 9), + (0b011000, 6), + (0b010011011, 9), +]; + +/// Black terminating codes (T.4 Table 2), run lengths 0–63. +const BLACK_TERM: [(u16, u8); 64] = [ + (0b0000110111, 10), + (0b010, 3), + (0b11, 2), + (0b10, 2), + (0b011, 3), + (0b0011, 4), + (0b0010, 4), + (0b00011, 5), + (0b000101, 6), + (0b000100, 6), + (0b0000100, 7), + (0b0000101, 7), + (0b0000111, 7), + (0b00000100, 8), + (0b00000111, 8), + (0b000011000, 9), + (0b0000010111, 10), + (0b0000011000, 10), + (0b0000001000, 10), + (0b00001100111, 11), + (0b00001101000, 11), + (0b00001101100, 11), + (0b00000110111, 11), + (0b00000101000, 11), + (0b00000010111, 11), + (0b00000011000, 11), + (0b000011001010, 12), + (0b000011001011, 12), + (0b000011001100, 12), + (0b000011001101, 12), + (0b000001101000, 12), + (0b000001101001, 12), + (0b000001101010, 12), + (0b000001101011, 12), + (0b000011010010, 12), + (0b000011010011, 12), + (0b000011010100, 12), + (0b000011010101, 12), + (0b000011010110, 12), + (0b000011010111, 12), + (0b000001101100, 12), + (0b000001101101, 12), + (0b000011011010, 12), + (0b000011011011, 12), + (0b000001010100, 12), + (0b000001010101, 12), + (0b000001010110, 12), + (0b000001010111, 12), + (0b000001100100, 12), + (0b000001100101, 12), + (0b000001010010, 12), + (0b000001010011, 12), + (0b000000100100, 12), + (0b000000110111, 12), + (0b000000111000, 12), + (0b000000100111, 12), + (0b000000101000, 12), + (0b000001011000, 12), + (0b000001011001, 12), + (0b000000101011, 12), + (0b000000101100, 12), + (0b000001011010, 12), + (0b000001100110, 12), + (0b000001100111, 12), +]; + +/// Black make-up codes (T.4 Table 3), run lengths 64, 128, … 1728. +const BLACK_MAKEUP: [(u16, u8); 27] = [ + (0b0000001111, 10), + (0b000011001000, 12), + (0b000011001001, 12), + (0b000001011011, 12), + (0b000000110011, 12), + (0b000000110100, 12), + (0b000000110101, 12), + (0b0000001101100, 13), + (0b0000001101101, 13), + (0b0000001001010, 13), + (0b0000001001011, 13), + (0b0000001001100, 13), + (0b0000001001101, 13), + (0b0000001110010, 13), + (0b0000001110011, 13), + (0b0000001110100, 13), + (0b0000001110101, 13), + (0b0000001110110, 13), + (0b0000001110111, 13), + (0b0000001010010, 13), + (0b0000001010011, 13), + (0b0000001010100, 13), + (0b0000001010101, 13), + (0b0000001011010, 13), + (0b0000001011011, 13), + (0b0000001100100, 13), + (0b0000001100101, 13), +]; + +/// Extended make-up codes for both colours (T.4 Table 3a), run lengths 1792, 1856, … 2560. +const EXT_MAKEUP: [(u16, u8); 13] = [ + (0b00000001000, 11), + (0b00000001100, 11), + (0b00000001101, 11), + (0b000000010010, 12), + (0b000000010011, 12), + (0b000000010100, 12), + (0b000000010101, 12), + (0b000000010110, 12), + (0b000000010111, 12), + (0b000000011100, 12), + (0b000000011101, 12), + (0b000000011110, 12), + (0b000000011111, 12), +]; + +/// A colour's code table: (length, code) → run length. +type Table = HashMap<(u8, u16), u32>; + +fn table(term: &[(u16, u8); 64], makeup: &[(u16, u8); 27]) -> Table { + let mut t = HashMap::new(); + for (run, (code, len)) in term.iter().enumerate() { + t.insert((*len, *code), run as u32); + } + for (i, (code, len)) in makeup.iter().enumerate() { + t.insert((*len, *code), 64 * (i as u32 + 1)); + } + for (i, (code, len)) in EXT_MAKEUP.iter().enumerate() { + t.insert((*len, *code), 1792 + 64 * i as u32); + } + t +} + +fn tables() -> &'static (Table, Table) { + static T: OnceLock<(Table, Table)> = OnceLock::new(); + T.get_or_init(|| (table(&WHITE_TERM, &WHITE_MAKEUP), table(&BLACK_TERM, &BLACK_MAKEUP))) +} + +/// The code (code, length) for a run of `run` (≥ 0, any length) as make-up codes then a +/// terminating code (used by the test encoder). +#[cfg(test)] +pub(crate) fn run_codes(white: bool, mut run: usize) -> Vec<(u16, u8)> { + let (term, makeup) = if white { (&WHITE_TERM, &WHITE_MAKEUP) } else { (&BLACK_TERM, &BLACK_MAKEUP) }; + let mut out = vec![]; + while run >= 2560 { + out.push(EXT_MAKEUP[12]); + run -= 2560; + } + if run >= 1792 { + out.push(EXT_MAKEUP[(run - 1792) / 64]); + run -= 1792 + (run - 1792) / 64 * 64; + } else if run >= 64 { + out.push(makeup[run / 64 - 1]); + run %= 64; + } + out.push(term[run]); + out +} + +struct Reader<'a> { + data: &'a [u8], + bit: usize, +} + +impl Reader<'_> { + fn bit_at(&self, i: usize) -> u16 { + self.data.get(i / 8).map(|b| ((b >> (7 - i % 8)) & 1) as u16).unwrap_or(0) + } + + fn peek(&self, n: usize) -> u16 { + (0..n).fold(0, |v, k| (v << 1) | self.bit_at(self.bit + k)) + } + + fn eof(&self) -> bool { + self.bit >= self.data.len() * 8 + } + + fn take(&mut self, n: usize) -> u16 { + let v = self.peek(n); + self.bit += n; + v + } + + fn align(&mut self) { + self.bit = self.bit.div_ceil(8) * 8; + } + + /// An EOL (eleven or more 0 bits then 1) at the read position: consume it. + fn eol(&mut self) -> bool { + let mut zeros = 0; + while zeros < 64 && self.bit + zeros < self.data.len() * 8 && self.bit_at(self.bit + zeros) == 0 { + zeros += 1; + } + if zeros >= 11 && self.bit + zeros < self.data.len() * 8 { + self.bit += zeros + 1; + return true; + } + false + } + + /// One run (make-up codes then a terminating code), `None` on an invalid code. + fn run(&mut self, t: &Table) -> Option { + let mut total = 0u32; + loop { + if self.eof() { + return None; + } + let mut found = None; + for len in 2..=13u8 { + if let Some(r) = t.get(&(len, self.peek(len as usize))) { + found = Some((len, *r)); + break; + } + } + let (len, r) = found?; + self.bit += len as usize; + total = total.saturating_add(r); + if r < 64 { + return Some(total); + } + } + } +} + +/// 2-D mode codes (T.4 Table 4). +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum Mode { + Pass, + Horizontal, + Vertical(i64), + Eol, +} + +fn mode(r: &mut Reader) -> Option { + if r.eof() { + return None; + } + let m = if r.peek(1) == 1 { + r.bit += 1; + Mode::Vertical(0) + } else if r.peek(3) == 0b011 { + r.bit += 3; + Mode::Vertical(1) + } else if r.peek(3) == 0b010 { + r.bit += 3; + Mode::Vertical(-1) + } else if r.peek(3) == 0b001 { + r.bit += 3; + Mode::Horizontal + } else if r.peek(4) == 0b0001 { + r.bit += 4; + Mode::Pass + } else if r.peek(6) == 0b000011 { + r.bit += 6; + Mode::Vertical(2) + } else if r.peek(6) == 0b000010 { + r.bit += 6; + Mode::Vertical(-2) + } else if r.peek(7) == 0b0000011 { + r.bit += 7; + Mode::Vertical(3) + } else if r.peek(7) == 0b0000010 { + r.bit += 7; + Mode::Vertical(-3) + } else if r.eol() { + Mode::Eol + } else { + return None; + }; + Some(m) +} + +/// The first changing element of `refl` after `a0` whose colour is the opposite of `white` +/// (index parity: even indices start black runs), and the one after it. +fn b1b2(refl: &[usize], a0: i64, white: bool, columns: usize) -> (usize, usize) { + let want = if white { 0 } else { 1 }; + let mut i = 0; + while i < refl.len() && (refl[i] as i64 <= a0 || i % 2 != want) { + i += 1; + } + let b1 = refl.get(i).copied().unwrap_or(columns); + let b2 = refl.get(i + 1).copied().unwrap_or(columns); + (b1, b2) +} + +/// Decode a 1-D (Modified Huffman) row into changing elements. +fn row_1d(r: &mut Reader, columns: usize) -> Option> { + let (wt, bt) = tables(); + let mut changes = vec![]; + let mut a0 = 0usize; + let mut white = true; + while a0 < columns { + let run = r.run(if white { wt } else { bt })? as usize; + a0 = (a0 + run).min(columns); + changes.push(a0); + white = !white; + } + Some(changes) +} + +/// Decode a 2-D row against the reference row's changing elements. `Err(true)` at an EOL. +fn row_2d(r: &mut Reader, refl: &[usize], columns: usize) -> Result, bool> { + let (wt, bt) = tables(); + let mut changes: Vec = vec![]; + let mut a0: i64 = -1; + let mut white = true; + let mut guard = 0; + while a0 < columns as i64 { + guard += 1; + if guard > 4 * columns + 16 { + return Err(false); + } + let (b1, b2) = b1b2(refl, a0, white, columns); + match mode(r).ok_or(false)? { + Mode::Pass => { + // The run continues under b2 (no change in the coding line). + a0 = b2 as i64; + } + Mode::Horizontal => { + let start = a0.max(0) as usize; + let r1 = r.run(if white { wt } else { bt }).ok_or(false)? as usize; + let r2 = r.run(if white { bt } else { wt }).ok_or(false)? as usize; + let a1 = (start + r1).min(columns); + let a2 = (a1 + r2).min(columns); + changes.push(a1); + changes.push(a2); + a0 = a2 as i64; + } + Mode::Vertical(d) => { + let a1 = (b1 as i64 + d).clamp(0, columns as i64); + if a1 < a0 { + return Err(false); + } + changes.push(a1 as usize); + a0 = a1; + white = !white; + } + Mode::Eol => return Err(true), + } + } + Ok(changes) +} + +/// Changing elements → packed bits (`black` bit value for black pixels). +fn pack(changes: &[usize], columns: usize, black_is_1: bool, out: &mut Vec) { + let row_bytes = columns.div_ceil(8); + let base = out.len(); + out.resize(base + row_bytes, if black_is_1 { 0 } else { 0xFF }); + let mut x = 0usize; + let mut white = true; + for &c in changes.iter().chain(std::iter::once(&columns)) { + let c = c.min(columns); + if !white { + for p in x..c { + let byte = &mut out[base + p / 8]; + let m = 0x80u8 >> (p % 8); + if black_is_1 { *byte |= m } else { *byte &= !m } + } + } + x = x.max(c); + white = !white; + if x >= columns { + break; + } + } +} + +/// Decode CCITT fax data. Damaged data ends the image at the last good row (rows already +/// decoded are kept); `None` only when no row decodes. +pub fn decode(data: &[u8], p: &Params) -> Option> { + let columns = p.columns.clamp(1, 1 << 16); + let max_rows = if p.rows > 0 { p.rows.min(1 << 20) } else { 1 << 20 }; + let mut r = Reader { data, bit: 0 }; + let mut out = vec![]; + let mut refl: Vec = vec![]; + let mut rows = 0; + while rows < max_rows && !r.eof() { + let line: Option> = if p.k < 0 { + if p.byte_align { + r.align(); + } + // An error is EOFB (two EOLs) or damage. + row_2d(&mut r, &refl, columns).ok() + } else { + // Group 3: optional fill and EOL before each row, then a tag bit when K > 0. + let had_eol = r.eol(); + if !had_eol && p.byte_align { + r.align(); + r.eol(); + } + // RTC: six consecutive EOLs (the first already consumed). + if had_eol && p.end_of_block { + let save = r.bit; + if r.eol() { + break; + } + r.bit = save; + } + if r.eof() { + break; + } + let two_d = p.k > 0 && r.take(1) == 0; + if two_d { row_2d(&mut r, &refl, columns).ok() } else { row_1d(&mut r, columns) } + }; + let Some(line) = line else { break }; + pack(&line, columns, p.black_is_1, &mut out); + refl = line; + rows += 1; + } + // Rows missing from damaged or short data stay white. + if p.rows > 0 && rows > 0 && rows < p.rows { + let row_bytes = columns.div_ceil(8); + out.resize(p.rows.min(1 << 20) * row_bytes, if p.black_is_1 { 0 } else { 0xFF }); + } + (rows > 0).then_some(out) +} + +#[cfg(test)] +pub(crate) mod tests { + use super::*; + + /// A bit writer for the test encoder. + #[derive(Default)] + pub(crate) struct W { + pub bytes: Vec, + n: usize, + } + + impl W { + pub fn put(&mut self, code: u16, len: u8) { + for k in (0..len).rev() { + if self.n.is_multiple_of(8) { + self.bytes.push(0); + } + if (code >> k) & 1 == 1 { + *self.bytes.last_mut().unwrap() |= 0x80 >> (self.n % 8); + } + self.n += 1; + } + } + fn run(&mut self, white: bool, run: usize) { + for (c, l) in run_codes(white, run) { + self.put(c, l); + } + } + } + + /// Changing elements of a row of pixels (`true` = black). + fn changes(row: &[bool]) -> Vec { + let mut out = vec![]; + let mut cur = false; + for (i, &b) in row.iter().enumerate() { + if b != cur { + out.push(i); + cur = b; + } + } + out + } + + fn encode_1d(w: &mut W, row: &[bool]) { + let cols = row.len(); + let mut ch = changes(row); + ch.push(cols); + let mut a0 = 0; + let mut white = true; + for c in ch { + w.run(white, c - a0); + a0 = c; + white = !white; + if a0 >= cols { + break; + } + } + } + + fn encode_2d(w: &mut W, row: &[bool], refl: &[usize]) { + let cols = row.len(); + let cur = changes(row); + let mut a0: i64 = -1; + let mut white = true; + let next = |a0: i64| cur.iter().copied().find(|&c| c as i64 > a0).unwrap_or(cols); + while a0 < cols as i64 { + let a1 = next(a0); + let (b1, b2) = b1b2(refl, a0, white, cols); + if b2 < a1 { + w.put(0b0001, 4); + a0 = b2 as i64; + } else if (a1 as i64 - b1 as i64).abs() <= 3 { + let d = a1 as i64 - b1 as i64; + let (c, l) = match d { + 0 => (1, 1), + 1 => (0b011, 3), + -1 => (0b010, 3), + 2 => (0b000011, 6), + -2 => (0b000010, 6), + 3 => (0b0000011, 7), + _ => (0b0000010, 7), + }; + w.put(c, l); + a0 = a1 as i64; + white = !white; + } else { + let a2 = next(a1 as i64); + w.put(0b001, 3); + let start = a0.max(0) as usize; + w.run(white, a1 - start); + w.run(!white, a2 - a1); + a0 = a2 as i64; + } + } + } + + pub(crate) fn encode(img: &[Vec], k: i64) -> Vec { + let mut w = W::default(); + let mut refl: Vec = vec![]; + for (i, row) in img.iter().enumerate() { + match k { + k if k < 0 => encode_2d(&mut w, row, &refl), + 0 => { + w.put(1, 12); + encode_1d(&mut w, row); + } + _ => { + w.put(1, 12); + if i as i64 % k == 0 { + w.put(1, 1); + encode_1d(&mut w, row); + } else { + w.put(0, 1); + encode_2d(&mut w, row, &refl); + } + } + } + refl = changes(row); + } + if k < 0 { + w.put(1, 12); + w.put(1, 12); + } + w.bytes + } + + pub(crate) fn pattern(cols: usize, rows: usize, seed: u32) -> Vec> { + let mut s = seed; + let mut rnd = || { + s = s.wrapping_mul(1_103_515_245).wrapping_add(12345); + (s >> 16) & 0x7FFF + }; + let mut img = vec![]; + let mut prev = vec![false; cols]; + for y in 0..rows { + let mut row = prev.clone(); + // Mostly like the row above (2-D coding's case), with random edits and runs. + for _ in 0..(rnd() % 5) { + let a = rnd() as usize % cols; + let len = 1 + rnd() as usize % (cols / 3).max(1); + let v = rnd() % 2 == 0; + for p in row.iter_mut().skip(a).take(len) { + *p = v; + } + } + if y % 7 == 3 { + row.iter_mut().for_each(|p| *p = rnd() % 3 == 0); + } + img.push(row.clone()); + prev = row; + } + img + } + + fn unpack(data: &[u8], cols: usize, rows: usize) -> Vec> { + let rb = cols.div_ceil(8); + (0..rows).map(|y| (0..cols).map(|x| data[y * rb + x / 8] & (0x80 >> (x % 8)) == 0).collect()).collect() + } + + #[test] + fn code_tables_are_prefix_free() { + for (white, term, makeup) in [(true, &WHITE_TERM, &WHITE_MAKEUP), (false, &BLACK_TERM, &BLACK_MAKEUP)] { + let mut all: Vec<(u16, u8)> = term.iter().chain(makeup.iter()).chain(EXT_MAKEUP.iter()).copied().collect(); + all.push((1, 12)); // EOL + let mut kraft = 0.0; + for (i, a) in all.iter().enumerate() { + kraft += 0.5f64.powi(a.1 as i32); + for (j, b) in all.iter().enumerate() { + if i != j && a.1 <= b.1 { + assert_ne!(b.0 >> (b.1 - a.1), a.0, "white {white}: {a:?} is a prefix of {b:?}"); + } + } + } + assert!(kraft <= 1.0, "white {white}: Kraft sum {kraft}"); + } + } + + #[test] + fn round_trips_group_3_and_group_4() { + for (cols, rows, seed) in [(1, 3, 1), (8, 8, 2), (37, 29, 3), (300, 40, 4), (2600, 6, 5)] { + let img = pattern(cols, rows, seed); + for k in [-1, 0, 1, 3] { + let data = encode(&img, k); + let p = Params { k, columns: cols, rows, ..Params::default() }; + let out = decode(&data, &p).unwrap_or_else(|| panic!("{cols}×{rows} K {k}: no rows")); + assert_eq!(unpack(&out, cols, rows), img, "{cols}×{rows} K {k}"); + // Rows unknown: stop at the end of the data / EOFB. + let out = decode(&data, &Params { rows: 0, ..p.clone() }).unwrap(); + assert_eq!(unpack(&out, cols, rows), img, "{cols}×{rows} K {k}, no /Rows"); + // BlackIs1 inverts. + let inv = decode(&data, &Params { black_is_1: true, ..p }).unwrap(); + assert!(inv.iter().zip(&decode(&data, &Params { k, columns: cols, rows, ..Params::default() }).unwrap()).all(|(a, b)| a ^ b == 0xFF)); + } + } + } + + fn hex(s: &str) -> Vec { + (0..s.len()).step_by(2).map(|i| u8::from_str_radix(&s[i..i + 2], 16).unwrap()).collect() + } + + /// Data from an independent encoder (libtiff's `tiffcp -c g4 / g3:1d / g3:2d / g3:1d:fill`, + /// run once as an external fixture generator) for a 61×23 generated image (1 = black). + #[test] + fn decodes_an_independent_encoders_output() { + let raw = hex( + "0000000000000008380000000000000042d05803696223a87fc00003697fffe87f00007ffd7fffe87f00007ffd7fffe87f00007fffffffe87f00007ffffffff85cc00128608c10505cc001f860007ff0fffffffffffffff8fffffffffffdfff8fffffffffffffff8f8040003fffffff84140e4180f5002d04140e4180f5002d05ffdfc180f5000005ffdfc180f5000005ffdfc180f5000005ffdf000000ff800270ca82855a00d78ffecf82855a00c00ffecf82855a00c00", + ); + let cases = [ + ( + -1, + "52f3408da23e4747111f34c8f91d17447cc2308c4082588c447fe2223e11ac13ffff88fe3943845f08a72872acf8541f0ac0412ff11e22108e844445c82b0f8e4e6555608e223a12f17408fe58e50e51ca1c104bffffffc8e150ffc447ffffffffc21116449b29cd76088e88e8e223a3688e88e88f8455191d11d8420f88ffffc4e06ffffffc004004", + ), + ( + 0, + "0014b4002f2c8008ead0f8eb10fd31f1d3a1f850a21d0e8004710463e3a7438103a0011c75820e870207400238eb041d0e040e800471d61a43a0011c7586a0008e879f0d3a1d5f8a3c5887438008e879f0cafd028e0026a0b40026a0ac3838009a82d0009a9cd540d20023ac43ae9d67ec743a3a1f1d0e0023ac43ae9d67ec743a3a1f1d0e0023a1c28e3cfd8e87558008e870a38f3f63a1d560023a1c28e3cfd8e87558008e870a39a8126002e9ebdd0e875887568743a1f1d531d0ec004d428fb9e21d5a1d0e87c754fc004d428fb9e21d5a1d0e87c754fc", + ), + ( + 4, + "001a5a001179800c75687c75887e98f8e9d0fc28510e87400288c447fe2223c00638eb041d0e040e8005ff800c71d61a43a00171800c743cf869d0eafc51e2c43a1c005fc47888423800cd416800520ac3c0066a0b400293995540063ac43ae9d67ec743a3a1f1d0e002ffffff800c743851c79fb1d0eab0017fff0018e870a38f3f63a1d56002f84222c893001ba7af743a1d621d5a1d0e87c754c743b0010420f88ffffc4e06e003350a3ee7887568743a1f1d53f0", + ), + ( + 0, + "00014b4001796400011d5a1f1d621fa63e3a743f0a1443a1d000011c4118f8e9d0e040e800011c75820e87020740011c75820e87020740011c758690e800011c7586a000011d0f3e1a743abf1478b10e8700011d0f3e195fa051c0013505a001350561c1c0013505a0013539aa81a400011d621d74eb3f63a1d1d0f8e870011d621d74eb3f63a1d1d0f8e870011d0e1471e7ec743aac00011d0e1471e7ec743aac00011d0e1471e7ec743aac00011d0e14735024c00174f5ee8743ac43ab43a1d0f8ea98e8760001350a3ee7887568743a1f1d53f001350a3ee7887568743a1f1d53f0", + ), + ]; + for (i, (k, data)) in cases.iter().enumerate() { + let p = Params { k: *k, columns: 61, rows: 23, black_is_1: true, end_of_line: *k >= 0, ..Params::default() }; + let out = decode(&hex(data), &p).unwrap(); + assert_eq!(out, raw, "case {i} (K {k})"); + let out = decode(&hex(data), &Params { rows: 0, ..p }).unwrap(); + assert_eq!(out, raw, "case {i} (K {k}), no /Rows"); + } + } + + #[test] + fn damaged_data_keeps_the_good_rows() { + let img = pattern(64, 20, 9); + let data = encode(&img, -1); + for cut in [0, 1, data.len() / 3, data.len() - 3] { + let out = decode(&data[..cut], &Params { k: -1, columns: 64, rows: 20, ..Params::default() }); + if let Some(out) = out { + assert_eq!(out.len(), 20 * 8); + } + } + let mut garbage = data.clone(); + for (i, b) in garbage.iter_mut().enumerate() { + *b ^= (i * 37) as u8; + } + for k in [-1, 0, 2] { + let _ = decode(&garbage, &Params { k, columns: 64, rows: 20, ..Params::default() }); + let _ = decode(&garbage, &Params { k, columns: 1, rows: 0, ..Params::default() }); + } + } +} diff --git a/crates/pdf/src/cff.rs b/crates/pdf/src/cff.rs index 7d647e7c..03493d81 100644 --- a/crates/pdf/src/cff.rs +++ b/crates/pdf/src/cff.rs @@ -366,6 +366,14 @@ pub struct Cff { pub encoding: [u16; 256], } +/// The first font name in a CFF program's Name INDEX. +pub fn font_name(data: &[u8]) -> Option { + let hdr = *data.get(2)? as usize; + let (names, _) = index(data, hdr)?; + let r = names.first()?.clone(); + Some(String::from_utf8_lossy(data.get(r)?).into_owned()) +} + impl Cff { pub fn parse(data: &[u8]) -> Option { let d = data; diff --git a/crates/pdf/src/color.rs b/crates/pdf/src/color.rs index 256f5d2f..6b4e4ae7 100644 --- a/crates/pdf/src/color.rs +++ b/crates/pdf/src/color.rs @@ -167,6 +167,16 @@ pub enum Func { decode: Vec, samples: Vec, }, + /// Type 0 with several inputs (function-based shadings): multilinear interpolation, the + /// first input varying fastest in the sample table. + SampledN { + domain: Vec, + size: Vec, + outputs: usize, + encode: Vec, + decode: Vec, + samples: Vec, + }, /// An array of one-output functions. Array(Vec), /// Type 4: a PostScript calculator program (§7.10.5). @@ -385,20 +395,23 @@ impl Func { 0 => { let Obj::Stream(sd, raw) = o else { return Func::Unsupported }; let Some(data) = decode_stream(file, sd, raw) else { return Func::Unsupported }; - let size = file.get(d, "Size").map(|x| file.nums(x)).and_then(|v| v.first().copied()).unwrap_or(0.0) as usize; + let sizes: Vec = file.get(d, "Size").map(|x| file.nums(x)).unwrap_or_default().iter().map(|v| v.max(0.0) as usize).collect(); + let size = sizes.first().copied().unwrap_or(0); let bps = file.get_num(d, "BitsPerSample").unwrap_or(8.0) as usize; let range = file.get(d, "Range").map(|x| file.nums(x)).unwrap_or_default(); let outputs = range.len() / 2; if size == 0 || outputs == 0 || !matches!(bps, 1 | 2 | 4 | 8 | 12 | 16 | 24 | 32) { return Func::Unsupported; } + let total = sizes.iter().try_fold(1usize, |a, b| a.checked_mul(*b)).filter(|t| *t > 0 && *t <= 1 << 22); + let Some(total) = total else { return Func::Unsupported }; let enc = file.get(d, "Encode").map(|x| file.nums(x)).unwrap_or_default(); let encode = [enc.first().copied().unwrap_or(0.0), enc.get(1).copied().unwrap_or(size as f64 - 1.0)]; let decode = file.get(d, "Decode").map(|x| file.nums(x)).filter(|v| v.len() >= outputs * 2).unwrap_or_else(|| range.clone()); let max = ((1u64 << bps) - 1) as f64; let mut samples = Vec::with_capacity(size * outputs); let mut bit = 0usize; - for _ in 0..size * outputs { + for _ in 0..total * outputs { let mut v = 0u64; for _ in 0..bps { let byte = data.get(bit / 8).copied().unwrap_or(0); @@ -407,6 +420,13 @@ impl Func { } samples.push(v as f64 / max); } + if sizes.len() > 1 { + let m = sizes.len(); + let encode = + (0..m).flat_map(|i| [enc.get(2 * i).copied().unwrap_or(0.0), enc.get(2 * i + 1).copied().unwrap_or(sizes[i] as f64 - 1.0)]).collect(); + let domain = (0..m).flat_map(|i| [dom.get(2 * i).copied().unwrap_or(0.0), dom.get(2 * i + 1).copied().unwrap_or(1.0)]).collect(); + return Func::SampledN { domain, size: sizes, outputs, encode, decode, samples }; + } Func::Sampled { domain, size, outputs, encode, decode, samples } } 4 => { @@ -441,8 +461,50 @@ impl Func { let k = st.len().saturating_sub(n); return st[k..].iter().enumerate().map(|(i, v)| v.clamp(range[2 * i], range[2 * i + 1])).collect(); } - let t = input.first().copied().unwrap_or(0.0); - self.eval1(t) + match self { + Func::SampledN { domain, size, outputs, encode, decode, samples } => { + let m = size.len(); + // Each input → a sample-table coordinate, then the 2^m surrounding samples. + let mut base = vec![0usize; m]; + let mut frac = vec![0.0f64; m]; + for i in 0..m { + let x = input.get(i).copied().unwrap_or(0.0); + let (d0, d1) = (domain[2 * i], domain[2 * i + 1]); + let x = x.clamp(d0.min(d1), d0.max(d1)); + let e = if d1 != d0 { encode[2 * i] + (x - d0) / (d1 - d0) * (encode[2 * i + 1] - encode[2 * i]) } else { encode[2 * i] }; + let e = if e.is_finite() { e.clamp(0.0, size[i] as f64 - 1.0) } else { 0.0 }; + base[i] = e.floor() as usize; + frac[i] = e - e.floor(); + } + let corners = if m <= 6 { 1usize << m } else { 1 }; + let mut out = vec![0.0; *outputs]; + for c in 0..corners { + let mut w = 1.0; + let mut idx = 0usize; + let mut stride = 1usize; + for i in 0..m { + let up = m <= 6 && (c >> i) & 1 == 1; + w *= if up { frac[i] } else { 1.0 - frac[i] }; + let k = if up { (base[i] + 1).min(size[i] - 1) } else { base[i] }; + idx += k * stride; + stride *= size[i]; + } + if w == 0.0 { + continue; + } + for (o, v) in out.iter_mut().enumerate() { + *v += w * samples.get(idx * outputs + o).copied().unwrap_or(0.0); + } + } + for (o, v) in out.iter_mut().enumerate() { + let (d0, d1) = (decode.get(2 * o).copied().unwrap_or(0.0), decode.get(2 * o + 1).copied().unwrap_or(1.0)); + *v = d0 + *v * (d1 - d0); + } + out + } + Func::Array(fs) if input.len() > 1 => fs.iter().map(|f| f.eval(input).first().copied().unwrap_or(0.0)).collect(), + _ => self.eval1(input.first().copied().unwrap_or(0.0)), + } } pub fn eval1(&self, t: f64) -> Vec { @@ -481,7 +543,7 @@ impl Func { .collect() } Func::Array(fs) => fs.iter().map(|f| f.eval1(t).first().copied().unwrap_or(0.0)).collect(), - Func::Calc { .. } => self.eval(&[t]), + Func::Calc { .. } | Func::SampledN { .. } => self.eval(&[t]), Func::Unsupported => vec![0.5], } } diff --git a/crates/pdf/src/font.rs b/crates/pdf/src/font.rs index 480873b0..e2ef1693 100644 --- a/crates/pdf/src/font.rs +++ b/crates/pdf/src/font.rs @@ -57,6 +57,10 @@ pub(crate) struct Font { codespace: Vec<(usize, u32, u32)>, cid_ranges: Vec<(u32, u32, u32)>, pub vertical: bool, + /// Vertical metrics (`/W2`, in text space): CID → (w1y, vx, vy). + cid_vmetrics: HashMap, + /// `/DW2`: default (vy, w1y). + dw2: (f64, f64), cache: RefCell>>>, /// Program glyph advances (font units → text space) for fonts without `/Widths`. has_widths: bool, @@ -194,6 +198,8 @@ struct CMap { codespace: Vec<(usize, u32, u32)>, unicode: HashMap, cids: Vec<(u32, u32, u32)>, + /// `/WMode 1`: vertical writing. + wmode: bool, } fn be(b: &[u8]) -> u32 { @@ -285,6 +291,14 @@ fn parse_cmap(data: &[u8]) -> CMap { } mode = ""; } + "def" if mode.is_empty() => { + if let [.., Obj::Name(k), Obj::Num(v)] = args.as_slice() + && k == "WMode" + { + m.wmode = *v == 1.0; + } + args.clear(); + } _ => { if mode.is_empty() { args.clear(); @@ -297,11 +311,9 @@ fn parse_cmap(data: &[u8]) -> CMap { impl Font { /// Read a font dictionary. `skipped` collects what could not be read. - pub fn load(file: &File, d: &Dict, skipped: &mut Vec) -> Font { - let subtype = file.get(d, "Subtype").and_then(Obj::name).unwrap_or("Type1").to_string(); - let base_name = file.get(d, "BaseFont").and_then(Obj::name).unwrap_or("").to_string(); - let mut f = Font { - composite: subtype == "Type0", + fn blank(base_name: &str, composite: bool) -> Font { + Font { + composite, program: Program::None, matrix: Affine::scale(0.001), first_char: 0, @@ -315,10 +327,48 @@ impl Font { codespace: vec![], cid_ranges: vec![], vertical: false, + cid_vmetrics: HashMap::new(), + dw2: (0.88, -1.0), cache: RefCell::new(HashMap::new()), has_widths: false, - base_name: base_name.clone(), + base_name: base_name.to_string(), + } + } + + /// A simple font outside a PDF (EPS text): `program` is an embedded font program (CFF when + /// the flag is set, else Type 1), otherwise the bundled stand-in for `name` is used; + /// `names` is the encoding (code → glyph name), else the program's built-in encoding or + /// StandardEncoding. Advances are the program's. + pub fn standalone(name: &str, program: Option<(&[u8], bool)>, names: Option>>) -> Font { + let mut f = Font::blank(name, false); + f.program = match program { + Some((data, true)) => Cff::parse(data).map(|c| Program::Cff(Box::new(c))), + Some((data, false)) => Type1::parse(data).map(|t| Program::Type1(Box::new(t))), + None => None, + } + .unwrap_or_else(|| Program::Fallback(fallback(name))); + match &f.program { + Program::Cff(c) => f.matrix = Affine::new(c.font_matrix), + Program::Type1(t) => f.matrix = t.font_matrix.map(Affine::new).unwrap_or(f.matrix), + _ => {} + } + let symbol = matches!(f.program, Program::Fallback(_)) && (name.contains("Symbol") || name.contains("Dingbats")); + f.names = match names { + // Unencoded codes draw .notdef (not the built-in encoding). + Some(n) => (0..256).map(|c| Some(n.get(c).cloned().flatten().unwrap_or_else(|| ".notdef".into()))).collect(), + None => match &f.program { + Program::Type1(t) if !t.encoding.is_empty() => (0..256).map(|c| t.encoding.get(&(c as u8)).cloned()).collect(), + _ if symbol => vec![None; 256], + _ => (0..256).map(|c| Base::Standard.name(c as u8)).collect(), + }, }; + f + } + + pub fn load(file: &File, d: &Dict, skipped: &mut Vec) -> Font { + let subtype = file.get(d, "Subtype").and_then(Obj::name).unwrap_or("Type1").to_string(); + let base_name = file.get(d, "BaseFont").and_then(Obj::name).unwrap_or("").to_string(); + let mut f = Font::blank(&base_name, subtype == "Type0"); if let Some(Obj::Stream(sd, raw)) = file.get(d, "ToUnicode") && let Some(data) = decode_stream(file, sd, raw) { @@ -366,6 +416,37 @@ impl Font { } } } + // Vertical metrics (§9.7.4.3): /DW2 [vy w1y] and /W2 entries + // `c [w1y vx vy …]` or `cfirst clast w1y vx vy`. + let dw2 = file.get(&desc_dict, "DW2").map(|x| file.nums(x)).unwrap_or_default(); + if dw2.len() == 2 { + f.dw2 = (dw2[0] / 1000.0, dw2[1] / 1000.0); + } + if let Some(w) = file.get(&desc_dict, "W2").and_then(Obj::array) { + let num = |i: usize| w.get(i).and_then(|x| file.resolve(x).num()); + let mut i = 0; + while i < w.len() { + let Some(first) = num(i) else { break }; + let first = first.max(0.0) as u32; + match w.get(i + 1).map(|x| file.resolve(x)) { + Some(Obj::Array(list)) => { + let v: Vec = list.iter().filter_map(|x| file.resolve(x).num()).collect(); + for (k, t) in v.chunks_exact(3).enumerate() { + f.cid_vmetrics.insert(first + k as u32, (t[0] / 1000.0, t[1] / 1000.0, t[2] / 1000.0)); + } + i += 2; + } + Some(Obj::Num(last)) => { + let (Some(w1), Some(vx), Some(vy)) = (num(i + 2), num(i + 3), num(i + 4)) else { break }; + for c in first..=(last.max(0.0) as u32).min(first + 65535) { + f.cid_vmetrics.insert(c, (w1 / 1000.0, vx / 1000.0, vy / 1000.0)); + } + i += 5; + } + _ => break, + } + } + } f.has_widths = true; // Encoding CMap. match file.get(d, "Encoding") { @@ -378,6 +459,7 @@ impl Font { Some(Obj::Stream(sd, raw)) => { if let Some(data) = decode_stream(file, sd, raw) { let cm = parse_cmap(&data); + f.vertical = cm.wmode || file.get_num(sd, "WMode") == Some(1.0); f.codespace = cm.codespace; f.cid_ranges = cm.cids; } @@ -530,6 +612,17 @@ impl Font { self.cid_ranges.iter().find(|(lo, hi, _)| code >= *lo && code <= *hi).map(|(lo, _, c)| c + (code - lo)).unwrap_or(0) } + /// Vertical metrics of a code in text space (before the font size): the vertical + /// displacement `w1y` (negative: downwards) and the position vector `(vx, vy)` from the + /// horizontal origin to the vertical origin (§9.7.4.3; defaults from `/DW2`, `vx` half the + /// horizontal advance). + pub fn vmetrics(&self, code: u32) -> (f64, f64, f64) { + match self.cid_vmetrics.get(&self.cid(code)) { + Some(m) => *m, + None => (self.dw2.1, self.width(code) / 2.0, self.dw2.0), + } + } + /// The advance of a code in text space (before the font size). pub fn width(&self, code: u32) -> f64 { if self.composite { diff --git a/crates/pdf/src/image.rs b/crates/pdf/src/image.rs index 0f0a481d..5e6c49a5 100644 --- a/crates/pdf/src/image.rs +++ b/crates/pdf/src/image.rs @@ -100,7 +100,7 @@ fn samples(file: &File, d: &Dict, raw: &[u8]) -> Result<(Vec, Option) let jpeg = decode_stream(file, &d2, raw).ok_or("image (filters)")?; jpeg_decode(&jpeg).map(|(b, n)| (b, Some(n))) } - Some(f @ ("JPXDecode" | "CCITTFaxDecode" | "JBIG2Decode" | "CCF")) => Err(format!("image ({f})")), + Some(f @ ("JPXDecode" | "JBIG2Decode")) => Err(format!("image ({f})")), _ => decode_stream(file, d, raw).map(|b| (b, None)).ok_or_else(|| "image (filters)".to_string()), } } diff --git a/crates/pdf/src/lib.rs b/crates/pdf/src/lib.rs index db443a41..3db854fc 100644 --- a/crates/pdf/src/lib.rs +++ b/crates/pdf/src/lib.rs @@ -8,6 +8,7 @@ //! (Illustrator layers), with content outside them gathered into groups of its own. mod build; +mod ccitt; mod cff; mod color; mod encoding; @@ -16,6 +17,7 @@ mod image; mod object; mod page; mod ps; +mod shading; mod type1; pub mod write; @@ -192,3 +194,9 @@ pub fn layer_doc(doc: &Doc, index: usize) -> Doc { #[cfg(test)] mod tests; +#[cfg(test)] +mod tests_groups; +#[cfg(test)] +mod tests_import; +#[cfg(test)] +mod tests_shading; diff --git a/crates/pdf/src/object.rs b/crates/pdf/src/object.rs index e24f6ec8..ffd9f9bc 100644 --- a/crates/pdf/src/object.rs +++ b/crates/pdf/src/object.rs @@ -328,6 +328,7 @@ pub fn decode_stream(file: &File, d: &Dict, raw: &[u8]) -> Option> { lzw(&data, parms.get(i).and_then(|p| p.as_ref()).and_then(|p| p.get("EarlyChange")).and_then(Obj::num).unwrap_or(1.0) != 0.0) } "RunLengthDecode" | "RL" => run_length(&data), + "CCITTFaxDecode" | "CCF" => crate::ccitt::decode(&data, &ccitt_params(file, parms.get(i).and_then(|p| p.as_ref())))?, _ => return None, }; if let Some(Some(p)) = parms.get(i) @@ -339,6 +340,24 @@ pub fn decode_stream(file: &File, d: &Dict, raw: &[u8]) -> Option> { Some(data) } +/// `/DecodeParms` of a `CCITTFaxDecode` filter. +fn ccitt_params(file: &File, d: Option<&Dict>) -> crate::ccitt::Params { + let mut p = crate::ccitt::Params::default(); + let Some(d) = d else { return p }; + let flag = |k: &str, default: bool| match file.get(d, k) { + Some(Obj::Bool(b)) => *b, + _ => default, + }; + p.k = file.get_num(d, "K").unwrap_or(0.0) as i64; + p.columns = file.get_num(d, "Columns").map(|c| c.max(1.0) as usize).unwrap_or(1728); + p.rows = file.get_num(d, "Rows").map(|r| r.max(0.0) as usize).unwrap_or(0); + p.end_of_line = flag("EndOfLine", false); + p.byte_align = flag("EncodedByteAlign", false); + p.end_of_block = flag("EndOfBlock", true); + p.black_is_1 = flag("BlackIs1", false); + p +} + pub fn inflate(data: &[u8]) -> Option> { miniz_oxide::inflate::decompress_to_vec_zlib(data).ok().or_else(|| miniz_oxide::inflate::decompress_to_vec(data).ok()).or_else(|| { // Truncated streams: keep what inflates. @@ -569,7 +588,7 @@ impl File { Obj::Dict(d) => { let save = lx.pos; lx.skip_ws(); - if data[lx.pos..].starts_with(b"stream") { + if data.get(lx.pos..).is_some_and(|d| d.starts_with(b"stream")) { let mut s = lx.pos + 6; if data.get(s) == Some(&b'\r') { s += 1; @@ -578,8 +597,9 @@ impl File { s += 1; } let declared = d.get("Length").and_then(Obj::num).map(|l| l as usize); + let s = s.min(data.len()); let end = match declared.filter(|l| { - s + l <= data.len() && { + s.checked_add(*l).is_some_and(|e| e <= data.len()) && { let mut k = s + l; while k < data.len() && is_white(data[k]) { k += 1; @@ -599,6 +619,7 @@ impl File { e } }; + let end = end.max(s); i = end; Obj::Stream(d, data[s..end].to_vec()) } else { diff --git a/crates/pdf/src/page.rs b/crates/pdf/src/page.rs index cbfaad01..c754c1dd 100644 --- a/crates/pdf/src/page.rs +++ b/crates/pdf/src/page.rs @@ -90,11 +90,20 @@ pub(crate) struct Tiling { uncolored: Option<[f64; 3]>, } +/// A shading pattern without a gradient equivalent (function-based or mesh shadings), +/// rasterised when it fills a path. +pub(crate) struct ShadingFill { + sh: Obj, + /// Shading space → page. + space: Affine, +} + #[derive(Clone)] enum Fill { Color([f64; 3]), Paint(Paint), Tiling(Rc), + Shading(Rc), None, } @@ -650,6 +659,10 @@ impl<'a> Interp<'a> { self.b.skip("tiling pattern stroke"); Some(Paint::Color(t.uncolored.unwrap_or([0.5; 3]))) } + Fill::Shading(_) => { + self.b.skip("shading pattern stroke"); + Some(Paint::Color([0.5; 3])) + } Fill::None => None, } } @@ -681,6 +694,21 @@ impl<'a> Interp<'a> { self.tile(&t, area, depth); self.close_to(d); } + Fill::Shading(sf) => { + let page = ctm * path.clone(); + let d = self.b.depth(); + let mut sk = vec![]; + let img = crate::shading::shading_image(self.file, &sf.sh, sf.space, self.page_box, &mut sk); + for s in sk { + self.b.skip(&s); + } + if let Some(mut img) = img { + img.opacity = self.gs.fill_alpha; + self.b.clip(page, rule); + self.b.push(Node::Image(img)); + self.close_to(d); + } + } f => fill_paint = self.paint_of(&f).map(|p| (p, self.gs.fill_alpha, rule)), } } @@ -811,7 +839,11 @@ impl<'a> Interp<'a> { let m = f.get(p, "Matrix").map(|m| affine(&f.nums(m))).unwrap_or(Affine::IDENTITY); match f.get_num(p, "PatternType").unwrap_or(0.0) as i32 { 2 => { - let Some(sh) = f.get(p, "Shading").and_then(Obj::dict) else { return Fill::None }; + let Some(sh_obj) = f.get(p, "Shading") else { return Fill::None }; + let Some(sh) = sh_obj.dict() else { return Fill::None }; + if !matches!(f.get_num(sh, "ShadingType").unwrap_or(0.0) as i32, 2 | 3) { + return Fill::Shading(Rc::new(ShadingFill { sh: sh_obj.clone(), space: base * m })); + } let sh = sh.clone(); match self.shading_gradient(&sh) { // Gradient space → shape user space (shapes carry the CTM). @@ -848,7 +880,21 @@ impl<'a> Interp<'a> { fn shade(&mut self, res: &Dict, name: &str) { let f = self.file; - let Some(sh) = f.get_dict(res, "Shading").and_then(|s| f.get_dict(s, name)).cloned() else { return }; + let Some(sh_obj) = f.get_dict(res, "Shading").and_then(|s| f.get(s, name)) else { return }; + let Some(sh) = sh_obj.dict().cloned() else { return }; + if !matches!(f.get_num(&sh, "ShadingType").unwrap_or(0.0) as i32, 2 | 3) { + // Function-based and mesh shadings paint only their own extent. + let mut sk = vec![]; + let img = crate::shading::shading_image(f, sh_obj, self.gs.ctm, self.page_box, &mut sk); + for s in sk { + self.b.skip(&s); + } + if let Some(mut img) = img { + img.opacity = self.gs.fill_alpha; + self.b.push(Node::Image(img)); + } + return; + } let Some(g) = self.shading_gradient(&sh) else { return }; // Fill everything (the current clip limits it): the page box in user space. let inv = self.gs.ctm.inverse(); @@ -876,12 +922,33 @@ impl<'a> Interp<'a> { let m = f.get(d, "Matrix").map(|m| affine(&f.nums(m))).unwrap_or(Affine::IDENTITY); let form_res = f.get_dict(d, "Resources").cloned().unwrap_or_else(|| res.clone()); self.saved.push((self.gs.clone(), self.b.depth())); - self.gs.ctm *= m; + // A transparency group XObject (§11.6.6) composites as one object: the current alpha + // (and the blend-mode and soft-mask groups already open around it) apply to its + // result, and its contents start from the initial alpha. let bb = f.get(d, "BBox").map(|b| f.nums(b)).unwrap_or_default(); - if bb.len() == 4 { - let r = kurbo::Rect::new(bb[0], bb[1], bb[2], bb[3]); - self.b.clip(self.gs.ctm * r.to_path(0.1), FillRule::NonZero); + let bbox_path = |ctm: Affine| (bb.len() == 4).then(|| ctm * m * kurbo::Rect::new(bb[0], bb[1], bb[2], bb[3]).to_path(0.1)); + let mut clipped = false; + if let Some(tg) = f.get_dict(d, "Group").filter(|g| f.get(g, "S").and_then(Obj::name) == Some("Transparency")) { + let mut grp = Group::new("Transparency Group"); + // The bounding box clips the group itself (its children stay its direct children, + // as knockout needs). + if let Some(p) = bbox_path(self.gs.ctm) { + grp.clip.push((p, FillRule::NonZero)); + clipped = true; + } + grp.opacity = self.gs.fill_alpha; + grp.isolated = matches!(f.get(tg, "I"), Some(Obj::Bool(true))); + grp.knockout = matches!(f.get(tg, "K"), Some(Obj::Bool(true))); + self.b.open_group(grp); + self.gs.fill_alpha = 1.0; + self.gs.stroke_alpha = 1.0; + self.gs.blend_depth = None; + self.gs.mask_depth = None; } + if !clipped && let Some(p) = bbox_path(self.gs.ctm) { + self.b.clip(p, FillRule::NonZero); + } + self.gs.ctm *= m; let base = self.gs.ctm; let path = std::mem::take(&mut self.path); let (tm, tlm) = (self.tm, self.tlm); @@ -980,7 +1047,8 @@ impl<'a> Interp<'a> { for (code, nbytes) in font.codes(s) { let w0 = font.width(code); let trm = self.tm * Affine::new([fs * th, 0.0, 0.0, fs, 0.0, rise]); - let trm = if font.vertical { trm * Affine::translate((-w0 / 2.0, -0.88)) } else { trm }; + let (w1y, vx, vy) = if font.vertical { font.vmetrics(code) } else { (0.0, 0.0, 0.0) }; + let trm = if font.vertical { trm * Affine::translate((-vx, -vy)) } else { trm }; if let Some((proc_, t3res)) = font.type3_proc(code) { let proc_ = proc_.to_vec(); let r = t3res.cloned().unwrap_or_else(|| res.clone()); @@ -993,7 +1061,7 @@ impl<'a> Interp<'a> { text.push_str(&font.unicode(code)); let tw = if nbytes == 1 && code == 32 { self.gs.word_spacing } else { 0.0 }; if font.vertical { - self.tm *= Affine::translate((0.0, -(fs + self.gs.char_spacing + tw))); + self.tm *= Affine::translate((0.0, w1y * fs - self.gs.char_spacing - tw)); } else { self.tm *= Affine::translate(((w0 * fs + self.gs.char_spacing + tw) * th, 0.0)); } diff --git a/crates/pdf/src/ps.rs b/crates/pdf/src/ps.rs index b9b0bff4..22d2e231 100644 --- a/crates/pdf/src/ps.rs +++ b/crates/pdf/src/ps.rs @@ -2,7 +2,9 @@ //! edition, and the EPSF 3.0 specification) covering what exported vector EPS files use: the //! operand and dictionary stacks, procedures and `def`, arithmetic, control flow, arrays and //! dictionaries, the graphics state, path construction (including arcs) and painting, clipping -//! and device colours. Text is skipped, as are images (their inline data is stepped over) and +//! and device colours, and text (`findfont` / `selectfont`, `show` and its variants, `charpath`, +//! `stringwidth`, re-encoded fonts) with embedded Type 1 and CFF programs or the bundled +//! stand-in fonts. Images are skipped (their inline data is stepped over), as is //! Level 3 smooth shading (`shfill`). use std::cell::RefCell; @@ -13,6 +15,7 @@ use effectcraft_svg::{Affine, BezPath, Cap, FillRule, Join, Paint, Stroke}; use kurbo::{PathEl, Point}; use crate::build::Builder; +use crate::font::Font; use crate::object::{ascii85, is_white, parse_num}; type DictRef = Rc>>; @@ -79,6 +82,8 @@ struct GState { cur: Option, start: Option, depth: usize, + /// The current font dictionary. + font: Option, } /// Source tokenizer (the data source for `currentfile`). @@ -309,6 +314,14 @@ pub(crate) struct Interp { gs: GState, saved: Vec, budget: usize, + /// Nested procedure calls (recursion is cut off at [`MAX_CALL_DEPTH`]). + calls: usize, + /// Fonts by name: `definefont`d dictionaries. + font_dir: HashMap, + /// Embedded font programs found in the file (name → (program, CFF)). + embedded: HashMap>, bool)>, + /// Fonts built for drawing, by base font and encoding. + font_cache: HashMap>, page: [f64; 4], pub unknown: Vec, } @@ -330,6 +343,9 @@ fn matrix_val(m: Affine) -> V { array(m.as_coeffs().iter().map(|c| V::Num(*c)).collect()) } +/// The deepest procedure nesting run (deeper recursion stops the program). +const MAX_CALL_DEPTH: usize = 256; + impl Interp { pub fn new(page: [f64; 4]) -> Interp { Interp { @@ -348,15 +364,21 @@ impl Interp { cur: None, start: None, depth: 1, + font: None, }, saved: vec![], budget: 8_000_000, + calls: 0, + font_dir: HashMap::new(), + embedded: HashMap::new(), + font_cache: HashMap::new(), page, unknown: vec![], } } pub fn run(&mut self, data: &[u8]) { + self.embedded = embedded_fonts(data); let mut src = Src { d: data, pos: 0 }; while let Some(t) = src.token(0) { if let Err(Flow::Quit) = self.exec_token(t, &mut src) { @@ -396,13 +418,25 @@ impl Interp { fn exec_obj(&mut self, v: V, src: &mut Src) -> Result<(), Flow> { match v { V::Proc(p) => { + if self.calls >= MAX_CALL_DEPTH { + self.b.skip("PostScript (recursion limit)"); + return Err(Flow::Quit); + } + self.calls += 1; + let mut r = Ok(()); for x in p.iter() { match x { - V::Name(n) => self.exec_name(n, src)?, + V::Name(n) => { + r = self.exec_name(n, src); + if r.is_err() { + break; + } + } other => self.push(other.clone()), } } - Ok(()) + self.calls -= 1; + r } V::Name(n) => self.exec_name(&n, src), other => { @@ -906,16 +940,24 @@ impl Interp { "countdictstack" => self.push(V::Num(self.dicts.len() as f64)), "cleardictstack" => self.dicts.truncate(1), "findresource" | "findfont" | "findencoding" => { - if n == "findresource" { - self.pop(); + let category = if n == "findresource" { self.pop().key() } else { String::new() }; + let key = self.pop().key(); + match (n, category.as_str()) { + ("findfont", _) | (_, "Font") => { + let f = self.find_font(&key); + self.push(V::Dict(f)); + } + ("findencoding", _) | (_, "Encoding") => self.push(encoding_array(&key)), + _ => self.push(V::Dict(new_dict())), } - self.pop(); - self.push(V::Dict(new_dict())); } "defineresource" => { - self.pop(); + let category = self.pop().key(); let v = self.pop(); - self.pop(); + let key = self.pop().key(); + if let (V::Dict(d), "Font") = (&v, category.as_str()) { + self.font_dir.insert(key, d.clone()); + } self.push(v); } "resourcestatus" => { @@ -1337,55 +1379,10 @@ impl Interp { self.pop(); self.b.skip("smooth shading (shfill)"); } - // Text (skipped). - "scalefont" | "makefont" | "selectfont" | "setfont" | "show" | "charpath" | "glyphshow" | "rootfont" | "setcachedevice" | "setcharwidth" => { - match n { - "scalefont" | "makefont" => { - self.pop(); - } - "selectfont" => { - self.pop(); - self.pop(); - } - "charpath" => { - self.pop(); - self.pop(); - } - "setcachedevice" => { - for _ in 0..6 { - self.pop(); - } - } - "setcharwidth" => { - self.pop(); - self.pop(); - } - "rootfont" => self.push(V::Dict(new_dict())), - _ => { - self.pop(); - } - } - if matches!(n, "show" | "charpath" | "glyphshow") { - self.b.skip("text"); - } - } - "currentfont" => self.push(V::Dict(new_dict())), - "ashow" | "widthshow" | "awidthshow" | "xshow" | "yshow" | "xyshow" | "kshow" | "cshow" => { - let k = match n { - "ashow" | "xshow" | "yshow" | "xyshow" | "kshow" | "cshow" => 3, - "widthshow" => 4, - _ => 6, - }; - for _ in 0..k.min(self.st.len()) { - self.pop(); - } - self.b.skip("text"); - } - "stringwidth" => { - self.pop(); - self.push(V::Num(0.0)); - self.push(V::Num(0.0)); - } + // Text (see `text_op`). + "scalefont" | "makefont" | "selectfont" | "setfont" | "show" | "charpath" | "glyphshow" | "rootfont" | "currentfont" | "setcachedevice" + | "setcharwidth" | "ashow" | "widthshow" | "awidthshow" | "xshow" | "yshow" | "xyshow" | "kshow" | "cshow" | "stringwidth" | "definefont" + | "StandardEncoding" | "ISOLatin1Encoding" | "eexec" | "StartData" | "undefinefont" => self.text_op(n, src)?, // Images: operands popped, inline data skipped. "image" | "imagemask" | "colorimage" => { let mut reads_file = false; @@ -1460,3 +1457,369 @@ pub fn eps_parts(bytes: &[u8]) -> Option<(&[u8], [f64; 4])> { let bbox = find_box(b"%%HiResBoundingBox:").or_else(|| find_box(b"%%BoundingBox:"))?; Some((ps, bbox)) } + +// ------------------------------------------------------------------ text + +/// An encoding array of glyph names (`StandardEncoding`; `ISOLatin1Encoding` and others are +/// approximated by WinAnsi, which agrees with ISO Latin-1 on the printable codes). +fn encoding_array(name: &str) -> V { + let base = if name == "StandardEncoding" { crate::encoding::Base::Standard } else { crate::encoding::Base::WinAnsi }; + array((0..256).map(|c| V::Lit(base.name(c as u8).unwrap_or_else(|| ".notdef".into()).into())).collect()) +} + +fn find_from(hay: &[u8], needle: &[u8], from: usize) -> Option { + hay.get(from..)?.windows(needle.len()).position(|w| w == needle).map(|p| p + from) +} + +fn rfind_before(hay: &[u8], needle: &[u8], to: usize) -> Option { + hay.get(..to)?.windows(needle.len()).rposition(|w| w == needle) +} + +/// The name after `key` (`/FontName /Name`). +fn name_after(b: &[u8], key: &[u8]) -> Option { + let p = find_from(b, key, 0)? + key.len(); + let rest = &b[p..]; + let s = rest.iter().position(|c| *c == b'/')? + 1; + let e = rest[s..].iter().position(|c| is_white(*c) || matches!(c, b'/' | b'[' | b'(' | b'{')).map_or(rest.len(), |q| s + q); + (e > s).then(|| String::from_utf8_lossy(&rest[s..e]).into_owned()) +} + +/// Embedded font programs of an EPS file: Type 1 fonts (`… /FontName /X … currentfile eexec +/// … cleartomark`, hexadecimal or binary) and CFF font sets (`… StartData `). +fn embedded_fonts(ps: &[u8]) -> HashMap>, bool)> { + let mut out = HashMap::new(); + let mut from = 0; + while let Some(ee) = find_from(ps, b"eexec", from) { + from = ee + 5; + let start = [b"%!PS-AdobeFont".as_slice(), b"%!FontType1"] + .iter() + .filter_map(|h| rfind_before(ps, h, ee)) + .max() + .or_else(|| rfind_before(ps, b"/FontName", ee).map(|p| p.saturating_sub(512))); + let Some(start) = start else { continue }; + let end = find_from(ps, b"cleartomark", ee).map_or(ps.len(), |p| p + 11); + if let Some(name) = name_after(&ps[start..ee], b"/FontName") { + out.insert(name, (Rc::new(ps[start..end].to_vec()), false)); + } + from = end; + } + let mut from = 0; + while let Some(sd) = find_from(ps, b"StartData", from) { + from = sd + 9; + let head = String::from_utf8_lossy(&ps[sd.saturating_sub(64)..sd]).into_owned(); + let Some(len) = head.split_whitespace().last().and_then(|t| t.parse::().ok()) else { continue }; + let s = sd + 10; + let Some(data) = ps.get(s..s.saturating_add(len)) else { continue }; + if let Some(name) = crate::cff::font_name(data) { + out.insert(name, (Rc::new(data.to_vec()), true)); + } + from = s + len; + } + out +} + +fn dict_matrix(d: &DictRef, key: &str) -> Affine { + d.borrow().get(key).and_then(matrix_of).unwrap_or(Affine::IDENTITY) +} + +/// A copy of a font dictionary with `m` applied after its matrix (`scalefont`, `makefont`). +fn transformed(font: &DictRef, m: Affine) -> DictRef { + let copy: HashMap = font.borrow().clone(); + let d = Rc::new(RefCell::new(copy)); + let old = dict_matrix(&d, "__m"); + d.borrow_mut().insert("__m".into(), matrix_val(m * old)); + d +} + +impl Interp { + /// A font dictionary by name: one `definefont` made, else a new dictionary for an + /// embedded program or a bundled stand-in. + fn find_font(&mut self, key: &str) -> DictRef { + if let Some(d) = self.font_dir.get(key) { + return d.clone(); + } + let d = new_dict(); + { + let mut m = d.borrow_mut(); + m.insert("FontName".into(), V::Lit(key.into())); + m.insert("FontType".into(), V::Num(1.0)); + m.insert("FontMatrix".into(), matrix_val(Affine::scale(0.001))); + m.insert("FID".into(), V::Null); + m.insert("__base".into(), V::Lit(key.into())); + m.insert("__m".into(), matrix_val(Affine::IDENTITY)); + } + self.font_dir.insert(key.to_string(), d.clone()); + d + } + + /// The drawable font of a font dictionary (its base program and encoding) and the matrix + /// from text space (font size 1) to user space. + fn font_of(&mut self, d: &DictRef) -> Option<(Rc, Affine)> { + if matches!(d.borrow().get("FontType"), Some(V::Num(t)) if *t == 3.0) { + // Type 3 fonts (glyph procedures) are not run. + self.b.skip("EPS Type 3 font"); + return None; + } + let (base, names) = { + let m = d.borrow(); + let base = m.get("__base").or_else(|| m.get("FontName")).map(V::key)?; + let names: Option>> = m.get("Encoding").and_then(V::items).map(|a| { + a.iter() + .map(|v| match v { + V::Lit(n) | V::Name(n) if &**n != ".notdef" => Some(n.to_string()), + _ => None, + }) + .collect() + }); + (base, names) + }; + let key = match &names { + Some(n) => format!("{base}\u{0}{}", n.iter().map(|x| x.as_deref().unwrap_or("")).collect::>().join(" ")), + None => base.clone(), + }; + let font = match self.font_cache.get(&key) { + Some(f) => f.clone(), + None => { + // Not embedded: a bundled stand-in, as for PDF. + let prog = self.embedded.get(&base).cloned(); + let f = Rc::new(Font::standalone(&base, prog.as_ref().map(|(d, cff)| (d.as_slice(), *cff)), names)); + self.font_cache.insert(key, f.clone()); + f + } + }; + Some((font, dict_matrix(d, "__m"))) + } + + /// A string's glyph outlines (device space) from the current point, with an extra + /// displacement `extra(index, code)` (user space) after each glyph; moves the current + /// point. + fn text_path(&mut self, bytes: &[u8], mut extra: impl FnMut(usize, u32) -> (f64, f64)) -> Option<(BezPath, String)> { + let d = self.gs.font.clone()?; + let (font, m) = self.font_of(&d)?; + let mut p = self.current_user(); + let mut path = BezPath::new(); + let mut text = String::new(); + let ctm = self.gs.ctm; + for (i, &code) in bytes.iter().enumerate() { + let code = code as u32; + if let Some(g) = font.glyph(code) { + path.extend(ctm * Affine::translate(p.to_vec2()) * m * (*g).clone()); + } + text.push_str(&font.unicode(code)); + let adv = m * Point::new(font.width(code), 0.0) - m * Point::ZERO; + let (ex, ey) = extra(i, code); + p += adv + kurbo::Vec2::new(ex, ey); + } + self.move_to(p); + Some((path, text)) + } + + fn fill_text(&mut self, path: BezPath, text: &str) { + let label: String = text.chars().filter(|c| !c.is_control()).take(40).collect(); + let name = if label.trim().is_empty() { "Text".to_string() } else { format!("Text: {}", label.trim()) }; + self.b.fill_stroke_named(&name, path, Affine::IDENTITY, Some((Paint::Color(self.gs.color), 1.0, FillRule::NonZero)), None); + } + + fn pop_string(&mut self) -> Vec { + match self.pop() { + V::Str(s) => s.borrow().clone(), + _ => vec![], + } + } + + fn show_with(&mut self, s: &[u8], extra: impl FnMut(usize, u32) -> (f64, f64)) { + match self.text_path(s, extra) { + Some((path, text)) => self.fill_text(path, &text), + None => self.b.skip("text (no font)"), + } + } + + /// Text and font operators (PostScript Language Reference §5.1, §8.2). + fn text_op(&mut self, n: &str, src: &mut Src) -> Result<(), Flow> { + match n { + "scalefont" | "makefont" => { + let m = if n == "scalefont" { + Affine::scale(self.popn()) + } else { + let v = self.pop(); + matrix_of(&v).unwrap_or(Affine::IDENTITY) + }; + let font = match self.pop() { + V::Dict(d) => d, + _ => self.find_font("Helvetica"), + }; + self.push(V::Dict(transformed(&font, m))); + } + "selectfont" => { + let size = self.pop(); + let key = self.pop().key(); + let font = self.find_font(&key); + let m = match &size { + V::Num(s) => Affine::scale(*s), + other => matrix_of(other).unwrap_or(Affine::IDENTITY), + }; + self.gs.font = Some(transformed(&font, m)); + } + "setfont" => { + if let V::Dict(d) = self.pop() { + self.gs.font = Some(d); + } + } + "currentfont" | "rootfont" => { + let d = self.gs.font.clone().unwrap_or_else(new_dict); + self.push(V::Dict(d)); + } + "definefont" => { + let v = self.pop(); + let key = self.pop().key(); + if let V::Dict(d) = &v { + if !d.borrow().contains_key("__base") { + // A font the file defines itself: its embedded program, by name. + d.borrow_mut().insert("__base".into(), V::Lit(key.as_str().into())); + d.borrow_mut().insert("__m".into(), matrix_val(Affine::IDENTITY)); + } + self.font_dir.insert(key, d.clone()); + } + self.push(v); + } + "undefinefont" => { + let key = self.pop().key(); + self.font_dir.remove(&key); + } + "StandardEncoding" | "ISOLatin1Encoding" => self.push(encoding_array(n)), + "show" => { + let s = self.pop_string(); + self.show_with(&s, |_, _| (0.0, 0.0)); + } + "ashow" => { + let s = self.pop_string(); + let ay = self.popn(); + let ax = self.popn(); + self.show_with(&s, |_, _| (ax, ay)); + } + "widthshow" => { + let s = self.pop_string(); + let ch = self.popn() as u32; + let cy = self.popn(); + let cx = self.popn(); + self.show_with(&s, |_, c| if c == ch { (cx, cy) } else { (0.0, 0.0) }); + } + "awidthshow" => { + let s = self.pop_string(); + let ay = self.popn(); + let ax = self.popn(); + let ch = self.popn() as u32; + let cy = self.popn(); + let cx = self.popn(); + self.show_with(&s, |_, c| if c == ch { (cx + ax, cy + ay) } else { (ax, ay) }); + } + "kshow" | "cshow" => { + // The procedure between characters is not run. + let s = self.pop_string(); + self.pop(); + self.show_with(&s, |_, _| (0.0, 0.0)); + } + "xshow" | "yshow" | "xyshow" => { + let disp: Vec = self.pop().items().unwrap_or_default().iter().filter_map(V::num).collect(); + let s = self.pop_string(); + self.explicit_show(&s, &disp, n); + } + "glyphshow" => { + let name = self.pop().key(); + let Some(d) = self.gs.font.clone() else { return Ok(()) }; + // Drawn through a one-glyph encoding. + let mut e: Vec = (0..256).map(|_| V::Lit(".notdef".into())).collect(); + e[65] = V::Lit(name.as_str().into()); + let tmp = transformed(&d, Affine::IDENTITY); + tmp.borrow_mut().insert("Encoding".into(), array(e)); + self.gs.font = Some(tmp); + self.show_with(b"A", |_, _| (0.0, 0.0)); + self.gs.font = Some(d); + } + "charpath" => { + self.pop(); + let s = self.pop_string(); + let saved = std::mem::take(&mut self.gs.path); + let start = self.gs.start; + match self.text_path(&s, |_, _| (0.0, 0.0)) { + Some((glyphs, _)) => { + let mut p = saved; + p.extend(glyphs); + if let Some(c) = self.gs.cur { + p.move_to(c); + } + self.gs.path = p; + self.gs.start = start; + } + None => self.gs.path = saved, + } + } + "stringwidth" => { + let s = self.pop_string(); + let (mut w, mut h) = (0.0, 0.0); + if let Some(d) = self.gs.font.clone() + && let Some((font, m)) = self.font_of(&d) + { + for &c in &s { + let v = m * Point::new(font.width(c as u32), 0.0) - m * Point::ZERO; + w += v.x; + h += v.y; + } + } + self.push(V::Num(w)); + self.push(V::Num(h)); + } + "setcachedevice" => { + for _ in 0..6 { + self.pop(); + } + } + "setcharwidth" => { + self.pop(); + self.pop(); + } + // An embedded Type 1 program's encrypted part (read by `embedded_fonts`). + "eexec" => { + if matches!(self.pop(), V::File) { + let rest = &src.d[src.pos.min(src.d.len())..]; + src.pos += rest.windows(11).position(|w| w == b"cleartomark").map_or(rest.len(), |p| p + 11); + } + } + // A CFF font set's binary data (read by `embedded_fonts`). + "StartData" => { + let len = self.popn().max(0.0) as usize; + self.pop(); + src.pos = src.pos.saturating_add(1).saturating_add(len).min(src.d.len()); + } + _ => {} + } + Ok(()) + } + + /// `xshow` / `yshow` / `xyshow`: glyph displacements from a number array instead of the + /// advances. + fn explicit_show(&mut self, s: &[u8], disp: &[f64], n: &str) { + let step = if n == "xyshow" { 2 } else { 1 }; + let d: Vec<(f64, f64)> = (0..s.len()) + .map(|k| { + let i = k * step; + let v = |j: usize| disp.get(j).copied().unwrap_or(0.0); + match n { + "xshow" => (v(i), 0.0), + "yshow" => (0.0, v(i)), + _ => (v(i), v(i + 1)), + } + }) + .collect(); + let Some(fd) = self.gs.font.clone() else { + self.b.skip("text (no font)"); + return; + }; + let Some((font, m)) = self.font_of(&fd) else { return }; + self.show_with(s, move |k, c| { + let adv = m * Point::new(font.width(c), 0.0) - m * Point::ZERO; + let (dx, dy) = d.get(k).copied().unwrap_or((0.0, 0.0)); + (dx - adv.x, dy - adv.y) + }); + } +} diff --git a/crates/pdf/src/shading.rs b/crates/pdf/src/shading.rs new file mode 100644 index 00000000..df5e5f9c --- /dev/null +++ b/crates/pdf/src/shading.rs @@ -0,0 +1,580 @@ +//! Shadings the render tree has no gradient for (ISO 32000-1 §8.7.4.5): function-based +//! shadings (type 1) and the mesh shadings — free-form and lattice-form Gouraud-shaded triangle +//! meshes (types 4 and 5), Coons patch meshes (type 6) and tensor-product patch meshes (type 7). +//! Patches are tessellated into triangles; triangles are rasterised with their colours (or the +//! parametric `t` of a shading `Function`) interpolated across them, into an image node in page +//! space (2×2 supersampled, so the mesh's outer edges are anti-aliased without seams between +//! triangles). + +use effectcraft_svg::{Affine, Image as SvgImage}; +use kurbo::Point; + +use crate::color::{Cs, Func, color_space}; +use crate::object::{Dict, File, Obj, decode_stream}; + +/// The longest side of a rasterised shading, in image pixels. +const MAX_SIDE: f64 = 1024.0; +/// The most image pixels per PDF point. +const MAX_SCALE: f64 = 4.0; +/// The most triangles one shading draws. +const MAX_TRIANGLES: usize = 2_000_000; + +/// A mesh vertex: page-space position and its value (RGB, or `t` in `[0]` with a function). +#[derive(Clone, Copy, Debug, PartialEq)] +struct V { + p: Point, + c: [f64; 3], +} + +/// How vertex values become colours. +struct Colors { + cs: Cs, + /// With a function: `t` (domain `[t0, t1]`) → RGB through a 1024-entry table. + lut: Option<(f64, f64, Vec<[u8; 3]>)>, +} + +impl Colors { + fn new(file: &File, sh: &Dict) -> (Colors, usize) { + let cs = file.get(sh, "ColorSpace").map(|c| color_space(file, c, None)).unwrap_or(Cs::Rgb); + let n = cs.components().max(1); + match file.get(sh, "Function") { + Some(fo) => { + let func = Func::read(file, fo); + let dom = file.get(sh, "Domain").map(|d| file.nums(d)).filter(|d| d.len() == 2).unwrap_or_else(|| vec![0.0, 1.0]); + let (t0, t1) = (dom[0], dom[1]); + let lut = (0..1024) + .map(|i| { + let t = t0 + (t1 - t0) * i as f64 / 1023.0; + to8(cs.to_rgb(&func.eval(&[t]))) + }) + .collect(); + (Colors { cs, lut: Some((t0, t1, lut)) }, 1) + } + None => (Colors { cs, lut: None }, n), + } + } + + /// A vertex value from its components (RGB, or `t`). + fn value(&self, comps: &[f64]) -> [f64; 3] { + match self.lut { + Some(_) => [comps.first().copied().unwrap_or(0.0), 0.0, 0.0], + None => self.cs.to_rgb(comps), + } + } + + /// An interpolated value → RGB8. + fn rgb(&self, v: [f64; 3]) -> [u8; 3] { + match &self.lut { + Some((t0, t1, lut)) => { + let k = if t1 != t0 { (v[0] - t0) / (t1 - t0) } else { 0.0 }; + let k = if k.is_finite() { k.clamp(0.0, 1.0) } else { 0.0 }; + lut[(k * 1023.0).round() as usize] + } + None => to8(v), + } + } +} + +fn to8(c: [f64; 3]) -> [u8; 3] { + c.map(|v| (v.clamp(0.0, 1.0) * 255.0).round() as u8) +} + +/// Big-endian bit reader over mesh data. +struct Bits<'a> { + data: &'a [u8], + bit: usize, +} + +impl Bits<'_> { + fn left(&self) -> usize { + (self.data.len() * 8).saturating_sub(self.bit) + } + + fn read(&mut self, n: usize) -> Option { + if n == 0 || n > 32 || self.left() < n { + return None; + } + let mut v = 0u64; + for _ in 0..n { + let byte = self.data[self.bit / 8]; + v = (v << 1) | ((byte >> (7 - self.bit % 8)) & 1) as u64; + self.bit += 1; + } + Some(v) + } + + fn align(&mut self) { + self.bit = self.bit.div_ceil(8) * 8; + } +} + +/// The layout of a mesh's vertex data (§8.7.4.5.5 Table 83 and following). +struct Layout { + bpc: usize, + bpcomp: usize, + bpf: usize, + /// Value components per vertex (1 with a function). + n: usize, + decode: Vec, +} + +impl Layout { + fn read(file: &File, sh: &Dict, n: usize) -> Option { + let bpc = file.get_num(sh, "BitsPerCoordinate")? as usize; + let bpcomp = file.get_num(sh, "BitsPerComponent")? as usize; + let bpf = file.get_num(sh, "BitsPerFlag").unwrap_or(8.0) as usize; + let decode = file.get(sh, "Decode").map(|d| file.nums(d)).unwrap_or_default(); + let ok = |b: usize| matches!(b, 1 | 2 | 4 | 8 | 12 | 16 | 24 | 32); + if !ok(bpc) || !ok(bpcomp) || !matches!(bpf, 2 | 4 | 8) || decode.len() < 4 + 2 * n { + return None; + } + Some(Layout { bpc, bpcomp, bpf, n, decode }) + } + + fn scale(raw: u64, bits: usize, lo: f64, hi: f64) -> f64 { + let max = ((1u64 << bits) - 1) as f64; + lo + raw as f64 * (hi - lo) / max + } + + fn point(&self, r: &mut Bits) -> Option { + let x = r.read(self.bpc)?; + let y = r.read(self.bpc)?; + let d = &self.decode; + Some(Point::new(Self::scale(x, self.bpc, d[0], d[1]), Self::scale(y, self.bpc, d[2], d[3]))) + } + + fn comps(&self, r: &mut Bits) -> Option> { + (0..self.n) + .map(|i| { + let v = r.read(self.bpcomp)?; + Some(Self::scale(v, self.bpcomp, self.decode[4 + 2 * i], self.decode[5 + 2 * i])) + }) + .collect() + } +} + +/// A shading rasterised into an image node in page space, for `sh` or a shading-pattern fill. +/// `sh` is the shading dictionary or stream, `to_page` maps shading space to page space and +/// `page_box` limits the image (page space). `None` with a note in `skip` when nothing draws. +pub(crate) fn shading_image(file: &File, sh: &Obj, to_page: Affine, page_box: [f64; 4], skip: &mut Vec) -> Option { + let sh_obj = file.resolve(sh); + let d = sh_obj.dict()?; + let ty = file.get_num(d, "ShadingType").unwrap_or(0.0) as i32; + if !to_page.as_coeffs().iter().all(|v| v.is_finite()) || to_page.determinant().abs() < 1e-12 { + return None; + } + let tris = match ty { + 1 => return function_shading(file, d, to_page, page_box, skip), + 4..=7 => { + let Obj::Stream(_, raw) = sh_obj else { + skip.push(format!("shading type {ty} (no data)")); + return None; + }; + let Some(data) = decode_stream(file, d, raw) else { + skip.push(format!("shading type {ty} (filters)")); + return None; + }; + let (colors, n) = Colors::new(file, d); + let Some(layout) = Layout::read(file, d, n) else { + skip.push(format!("shading type {ty} (layout)")); + return None; + }; + let mut r = Bits { data: &data, bit: 0 }; + let tris = match ty { + 4 => free_form(&mut r, &layout, &colors, to_page), + 5 => { + let per_row = file.get_num(d, "VerticesPerRow").unwrap_or(0.0) as usize; + lattice(&mut r, &layout, &colors, to_page, per_row) + } + _ => patches(&mut r, &layout, &colors, to_page, ty == 7, page_box), + }; + (tris, colors) + } + _ => { + skip.push(format!("shading type {ty}")); + return None; + } + }; + let (tris, colors) = tris; + if tris.is_empty() { + return None; + } + rasterize(&tris, &colors, page_box) +} + +/// Type 4: free-form triangles with edge flags (0 starts a triangle; 1 and 2 share an edge with +/// the previous one). +fn free_form(r: &mut Bits, l: &Layout, colors: &Colors, m: Affine) -> Vec<[V; 3]> { + let mut out = vec![]; + let mut pending: Vec = vec![]; + let mut last: Option<[V; 3]> = None; + while out.len() < MAX_TRIANGLES { + let Some(flag) = r.read(l.bpf) else { break }; + let (Some(p), Some(c)) = (l.point(r), l.comps(r)) else { break }; + r.align(); + let v = V { p: m * p, c: colors.value(&c) }; + // A triangle being started takes the next two vertices whatever their flags. + if !pending.is_empty() || flag == 0 { + pending.push(v); + if pending.len() == 3 { + let t = [pending[0], pending[1], pending[2]]; + out.push(t); + last = Some(t); + pending.clear(); + } + continue; + } + let Some([a, b, c]) = last else { continue }; + let t = if flag == 1 { [b, c, v] } else { [a, c, v] }; + out.push(t); + last = Some(t); + } + out +} + +/// Type 5: a lattice of `per_row` vertices per row, two triangles per cell. +fn lattice(r: &mut Bits, l: &Layout, colors: &Colors, m: Affine, per_row: usize) -> Vec<[V; 3]> { + if per_row < 2 { + return vec![]; + } + let mut rows: Vec> = vec![]; + let mut row = vec![]; + while let (Some(p), Some(c)) = (l.point(r), l.comps(r)) { + r.align(); + row.push(V { p: m * p, c: colors.value(&c) }); + if row.len() == per_row { + rows.push(std::mem::take(&mut row)); + if rows.len() * per_row > MAX_TRIANGLES { + break; + } + } + } + let mut out = vec![]; + for w in rows.windows(2) { + for i in 0..per_row - 1 { + let (a, b, c, d) = (w[0][i], w[0][i + 1], w[1][i], w[1][i + 1]); + out.push([a, b, c]); + out.push([b, d, c]); + } + } + out +} + +fn bez(p: [Point; 4], t: f64) -> Point { + let s = 1.0 - t; + let (a, b, c, d) = (s * s * s, 3.0 * s * s * t, 3.0 * s * t * t, t * t * t); + Point::new(a * p[0].x + b * p[1].x + c * p[2].x + d * p[3].x, a * p[0].y + b * p[1].y + c * p[2].y + d * p[3].y) +} + +/// Types 6 and 7: Coons and tensor-product patches. The 12 boundary points run around the +/// patch from its first corner (`p00 p01 p02 p03 p13 p23 p33 p32 p31 p30 p20 p10`); tensor +/// patches add `p11 p12 p22 p21`. Corner colours belong to `p00`, `p03`, `p33` and `p30`. +/// Flags 1–3 reuse the previous patch's second, third or fourth side as the first. +fn patches(r: &mut Bits, l: &Layout, colors: &Colors, m: Affine, tensor: bool, page_box: [f64; 4]) -> Vec<[V; 3]> { + let mut out = vec![]; + let mut prev: Option<([Point; 12], [[f64; 3]; 4])> = None; + while out.len() < MAX_TRIANGLES { + let Some(flag) = r.read(l.bpf) else { break }; + let mut lp = [Point::ZERO; 12]; + let mut cs = [[0.0; 3]; 4]; + let first = match (flag, &prev) { + (0, _) => 0, + (1..=3, Some((pp, pc))) => { + let (k, ci) = match flag { + 1 => (3, 1), + 2 => (6, 2), + _ => (9, 3), + }; + for i in 0..4 { + lp[i] = pp[(k + i) % 12]; + } + cs[0] = pc[ci]; + cs[1] = pc[(ci + 1) % 4]; + 4 + } + _ => break, + }; + let mut ok = true; + for p in lp.iter_mut().skip(first) { + match l.point(r) { + Some(q) => *p = q, + None => ok = false, + } + } + let mut inner = [Point::ZERO; 4]; + if tensor { + for p in &mut inner { + match l.point(r) { + Some(q) => *p = q, + None => ok = false, + } + } + } + for c in cs.iter_mut().skip(if first == 0 { 0 } else { 2 }) { + match l.comps(r) { + Some(v) => *c = colors.value(&v), + None => ok = false, + } + } + r.align(); + if !ok { + break; + } + // Shading space → page space before tessellating (the rasteriser works in page space). + let lp_page = lp.map(|p| m * p); + let inner_page = inner.map(|p| m * p); + // The 4×4 grid g[i][j]: row i = 0 is p00 … p03. + let mut g = [[Point::ZERO; 4]; 4]; + g[0] = [lp_page[0], lp_page[1], lp_page[2], lp_page[3]]; + g[1][3] = lp_page[4]; + g[2][3] = lp_page[5]; + g[3] = [lp_page[9], lp_page[8], lp_page[7], lp_page[6]]; + g[2][0] = lp_page[10]; + g[1][0] = lp_page[11]; + g[1][1] = inner_page[0]; + g[1][2] = inner_page[1]; + g[2][2] = inner_page[2]; + g[2][1] = inner_page[3]; + tessellate(&g, tensor, cs, page_box, &mut out); + prev = Some((lp, cs)); + } + out +} + +/// A patch → `n`×`n` cells of two triangles; `u` runs along `p00 → p03`, `v` along `p00 → p30`. +fn tessellate(g: &[[Point; 4]; 4], tensor: bool, cs: [[f64; 3]; 4], page_box: [f64; 4], out: &mut Vec<[V; 3]>) { + let (mut lo, mut hi) = (Point::new(f64::MAX, f64::MAX), Point::new(f64::MIN, f64::MIN)); + for p in g.iter().flatten() { + lo = Point::new(lo.x.min(p.x), lo.y.min(p.y)); + hi = Point::new(hi.x.max(p.x), hi.y.max(p.y)); + } + if !(lo.x.is_finite() && hi.x.is_finite() && lo.y.is_finite() && hi.y.is_finite()) { + return; + } + // Patches entirely off the page draw nothing. + if hi.x < page_box[0] || lo.x > page_box[2] || hi.y < page_box[1] || lo.y > page_box[3] { + return; + } + let size = (hi.x - lo.x).max(hi.y - lo.y).min(page_box[2] - page_box[0] + page_box[3] - page_box[1]); + let n = ((size * MAX_SCALE / 6.0).ceil() as usize).clamp(2, 48); + let eval = |u: f64, v: f64| -> Point { + if tensor { + let rows = g.map(|row| bez(row, u)); + bez(rows, v) + } else { + let cb = bez(g[0], u); + let ct = bez(g[3], u); + let cl = bez([g[0][0], g[1][0], g[2][0], g[3][0]], v); + let cr = bez([g[0][3], g[1][3], g[2][3], g[3][3]], v); + let corner = |a: Point, w: f64| a.to_vec2() * w; + let s = cb.to_vec2() * (1.0 - v) + ct.to_vec2() * v + cl.to_vec2() * (1.0 - u) + cr.to_vec2() * u + - corner(g[0][0], (1.0 - u) * (1.0 - v)) + - corner(g[0][3], u * (1.0 - v)) + - corner(g[3][0], (1.0 - u) * v) + - corner(g[3][3], u * v); + s.to_point() + } + }; + let color = |u: f64, v: f64| -> [f64; 3] { + // c0 (0,0), c1 (1,0), c2 (1,1), c3 (0,1). + let mut c = [0.0; 3]; + for (k, ck) in c.iter_mut().enumerate() { + *ck = cs[0][k] * (1.0 - u) * (1.0 - v) + cs[1][k] * u * (1.0 - v) + cs[2][k] * u * v + cs[3][k] * (1.0 - u) * v; + } + c + }; + let mut grid = Vec::with_capacity((n + 1) * (n + 1)); + for j in 0..=n { + for i in 0..=n { + let (u, v) = (i as f64 / n as f64, j as f64 / n as f64); + grid.push(V { p: eval(u, v), c: color(u, v) }); + } + } + for j in 0..n { + for i in 0..n { + let a = grid[j * (n + 1) + i]; + let b = grid[j * (n + 1) + i + 1]; + let c = grid[(j + 1) * (n + 1) + i]; + let d = grid[(j + 1) * (n + 1) + i + 1]; + out.push([a, b, c]); + out.push([b, d, c]); + } + } +} + +/// The page-space raster frame for content covering `lo..hi`: origin (top-left, page space), +/// scale (pixels per point) and size, or `None` when it misses the page. +fn frame(lo: Point, hi: Point, page_box: [f64; 4]) -> Option<(Point, f64, usize, usize)> { + let x0 = lo.x.max(page_box[0]); + let y0 = lo.y.max(page_box[1]); + let x1 = hi.x.min(page_box[2]); + let y1 = hi.y.min(page_box[3]); + let finite = x0.is_finite() && y0.is_finite() && x1.is_finite() && y1.is_finite(); + if !finite || x1 <= x0 || y1 <= y0 { + return None; + } + let s = (MAX_SIDE / (x1 - x0).max(y1 - y0)).min(MAX_SCALE); + let w = ((x1 - x0) * s).ceil().max(1.0) as usize; + let h = ((y1 - y0) * s).ceil().max(1.0) as usize; + Some((Point::new(x0, y1), s, w, h)) +} + +/// The image node for an RGBA8 raster with origin `o` (page space, top-left) and `s` px/pt. +fn image_node(rgba: Vec, w: usize, h: usize, o: Point, s: f64) -> SvgImage { + SvgImage { + name: "Shading".into(), + transform: Affine::new([1.0 / s, 0.0, 0.0, -1.0 / s, o.x, o.y]), + width: w as u32, + height: h as u32, + rgba: std::sync::Arc::new(rgba), + opacity: 1.0, + } +} + +/// Gouraud-shade the triangles (page space) into an image: 2×2 samples per pixel, later +/// triangles over earlier ones. +fn rasterize(tris: &[[V; 3]], colors: &Colors, page_box: [f64; 4]) -> Option { + let (mut lo, mut hi) = (Point::new(f64::MAX, f64::MAX), Point::new(f64::MIN, f64::MIN)); + for v in tris.iter().flatten() { + if v.p.x.is_finite() && v.p.y.is_finite() { + lo = Point::new(lo.x.min(v.p.x), lo.y.min(v.p.y)); + hi = Point::new(hi.x.max(v.p.x), hi.y.max(v.p.y)); + } + } + let (o, s, w, h) = frame(lo, hi, page_box)?; + // Supersampled buffer: value + covered. + let (sw, sh) = (w * 2, h * 2); + let ss = s * 2.0; + let mut val = vec![[0.0f32; 3]; sw * sh]; + let mut cov = vec![false; sw * sh]; + for t in tris { + let q = t.map(|v| Point::new((v.p.x - o.x) * ss, (o.y - v.p.y) * ss)); + let area = (q[1].x - q[0].x) * (q[2].y - q[0].y) - (q[2].x - q[0].x) * (q[1].y - q[0].y); + if area.abs() < 1e-12 || !area.is_finite() { + continue; + } + let bx0 = q.iter().map(|p| p.x).fold(f64::MAX, f64::min).floor().max(0.0) as usize; + let by0 = q.iter().map(|p| p.y).fold(f64::MAX, f64::min).floor().max(0.0) as usize; + let bx1 = (q.iter().map(|p| p.x).fold(f64::MIN, f64::max).ceil().max(0.0) as usize).min(sw); + let by1 = (q.iter().map(|p| p.y).fold(f64::MIN, f64::max).ceil().max(0.0) as usize).min(sh); + let eps = 1e-9 * area.abs(); + for y in by0..by1 { + for x in bx0..bx1 { + let p = Point::new(x as f64 + 0.5, y as f64 + 0.5); + let w0 = ((q[1].x - p.x) * (q[2].y - p.y) - (q[2].x - p.x) * (q[1].y - p.y)) / area; + let w1 = ((q[2].x - p.x) * (q[0].y - p.y) - (q[0].x - p.x) * (q[2].y - p.y)) / area; + let w2 = 1.0 - w0 - w1; + if w0 < -eps || w1 < -eps || w2 < -eps { + continue; + } + let i = y * sw + x; + for k in 0..3 { + val[i][k] = (t[0].c[k] * w0 + t[1].c[k] * w1 + t[2].c[k] * w2) as f32; + } + cov[i] = true; + } + } + } + // Resolve: average the covered samples' colours, alpha = covered fraction. + let mut rgba = vec![0u8; w * h * 4]; + for y in 0..h { + for x in 0..w { + let mut acc = [0.0f64; 3]; + let mut n = 0; + for (dx, dy) in [(0, 0), (1, 0), (0, 1), (1, 1)] { + let i = (2 * y + dy) * sw + 2 * x + dx; + if cov[i] { + let c = colors.rgb(val[i].map(f64::from)); + for k in 0..3 { + acc[k] += c[k] as f64; + } + n += 1; + } + } + if n > 0 { + let o = (y * w + x) * 4; + for k in 0..3 { + rgba[o + k] = (acc[k] / n as f64).round() as u8; + } + rgba[o + 3] = (n * 255 / 4) as u8; + } + } + } + Some(image_node(rgba, w, h, o, s)) +} + +/// Type 1: `Function(x, y)` over `Domain` `[x0 x1 y0 y1]`, mapped by `Matrix`. +fn function_shading(file: &File, d: &Dict, to_page: Affine, page_box: [f64; 4], skip: &mut Vec) -> Option { + let Some(fo) = file.get(d, "Function") else { + skip.push("shading type 1 (no function)".into()); + return None; + }; + let func = Func::read(file, fo); + if func == Func::Unsupported { + skip.push("shading function".into()); + return None; + } + let cs = file.get(d, "ColorSpace").map(|c| color_space(file, c, None)).unwrap_or(Cs::Rgb); + let dom = file.get(d, "Domain").map(|x| file.nums(x)).filter(|v| v.len() == 4).unwrap_or_else(|| vec![0.0, 1.0, 0.0, 1.0]); + let mtx = file.get(d, "Matrix").map(|x| file.nums(x)).filter(|v| v.len() == 6).map(|v| Affine::new([v[0], v[1], v[2], v[3], v[4], v[5]])); + let full = to_page * mtx.unwrap_or(Affine::IDENTITY); + if full.determinant().abs() < 1e-12 || !full.as_coeffs().iter().all(|v| v.is_finite()) { + return None; + } + let corners = [(dom[0], dom[2]), (dom[1], dom[2]), (dom[1], dom[3]), (dom[0], dom[3])].map(|(x, y)| full * Point::new(x, y)); + let lo = Point::new(corners.iter().map(|p| p.x).fold(f64::MAX, f64::min), corners.iter().map(|p| p.y).fold(f64::MAX, f64::min)); + let hi = Point::new(corners.iter().map(|p| p.x).fold(f64::MIN, f64::max), corners.iter().map(|p| p.y).fold(f64::MIN, f64::max)); + let (o, s, w, h) = frame(lo, hi, page_box)?; + let inv = full.inverse(); + let (xa, xb) = (dom[0].min(dom[1]), dom[0].max(dom[1])); + let (ya, yb) = (dom[2].min(dom[3]), dom[2].max(dom[3])); + let mut rgba = vec![0u8; w * h * 4]; + for y in 0..h { + for x in 0..w { + // 2×2 samples: anti-aliased domain edges. + let mut acc = [0.0f64; 3]; + let mut n = 0; + for (dx, dy) in [(0.25, 0.25), (0.75, 0.25), (0.25, 0.75), (0.75, 0.75)] { + let page = Point::new(o.x + (x as f64 + dx) / s, o.y - (y as f64 + dy) / s); + let q = inv * page; + if q.x < xa - 1e-9 || q.x > xb + 1e-9 || q.y < ya - 1e-9 || q.y > yb + 1e-9 { + continue; + } + let c = cs.to_rgb(&func.eval(&[q.x, q.y])); + for k in 0..3 { + acc[k] += c[k]; + } + n += 1; + } + if n > 0 { + let i = (y * w + x) * 4; + let c = to8(acc.map(|v| v / n as f64)); + rgba[i..i + 3].copy_from_slice(&c); + rgba[i + 3] = (n * 255 / 4) as u8; + } + } + } + Some(image_node(rgba, w, h, o, s)) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn bits_reader_and_alignment() { + let mut r = Bits { data: &[0b1010_0000, 0xFF], bit: 0 }; + assert_eq!(r.read(3), Some(0b101)); + r.align(); + assert_eq!(r.read(8), Some(0xFF)); + assert_eq!(r.read(1), None); + assert_eq!(r.read(33), None); + } + + #[test] + fn bezier_ends() { + let p = [Point::new(0.0, 0.0), Point::new(1.0, 2.0), Point::new(2.0, 2.0), Point::new(3.0, 0.0)]; + assert_eq!(bez(p, 0.0), p[0]); + assert_eq!(bez(p, 1.0), p[3]); + } +} diff --git a/crates/pdf/src/tests.rs b/crates/pdf/src/tests.rs index 39c82b6e..dac34bee 100644 --- a/crates/pdf/src/tests.rs +++ b/crates/pdf/src/tests.rs @@ -119,7 +119,8 @@ showpage\n%%EOF\n"; fn eps_postscript_subset() { let doc = parse(EPS.as_bytes()).unwrap(); assert_eq!((doc.width, doc.height), (100.0, 100.0)); - assert!(doc.skipped.contains(&"text".to_string())); + assert!(doc.skipped.is_empty(), "{:?}", doc.skipped); + assert!(shape_names(&doc).contains(&"Text: text".to_string()), "EPS text is drawn: {:?}", shape_names(&doc)); let at = |x, y| px(&doc, x, y); assert!(at(25, 75)[0] > 0.99 && at(25, 75)[3] > 0.99, "red box {:?}", at(25, 75)); assert!(at(70, 50)[2] > 0.99, "blue disc {:?}", at(70, 50)); @@ -152,11 +153,11 @@ fn filters_decode() { // ------------------------------------------------------------------ M13.6: text, images, // patterns, soft masks, blend modes, pages -type Objs = Vec<(u32, String, Option>)>; +pub(crate) type Objs = Vec<(u32, String, Option>)>; /// A one-page 200×100 PDF: `resources` is the page's resource dictionary body, `extra` more /// objects (numbered from 10). -fn page_pdf(content: &str, resources: &str, extra: Objs) -> Vec { +pub(crate) fn page_pdf(content: &str, resources: &str, extra: Objs) -> Vec { let mut objs: Objs = vec![ (1, "<< /Type /Catalog /Pages 2 0 R >>".into(), None), (2, "<< /Type /Pages /Kids [3 0 R] /Count 1 >>".into(), None), @@ -167,12 +168,12 @@ fn page_pdf(content: &str, resources: &str, extra: Objs) -> Vec { pdf(&objs, 1) } -fn render(doc: &Doc) -> effectcraft_raster::Image { +pub(crate) fn render(doc: &Doc) -> effectcraft_raster::Image { let (w, h) = doc.pixel_size(); effectcraft_svg::rasterize(doc, w, h, 1.0) } -fn coverage(img: &effectcraft_raster::Image, x0: i64, y0: i64, x1: i64, y1: i64) -> f32 { +pub(crate) fn coverage(img: &effectcraft_raster::Image, x0: i64, y0: i64, x1: i64, y1: i64) -> f32 { let mut s = 0.0; for y in y0..y1 { for x in x0..x1 { @@ -182,7 +183,7 @@ fn coverage(img: &effectcraft_raster::Image, x0: i64, y0: i64, x1: i64, y1: i64) s } -fn shape_names(doc: &Doc) -> Vec { +pub(crate) fn shape_names(doc: &Doc) -> Vec { fn walk(g: &effectcraft_svg::Group, out: &mut Vec) { for c in &g.children { match c { @@ -223,7 +224,7 @@ fn square_cff() -> Vec { const SQUARE_FONT: &str = "<< /Type /Font /Subtype /Type1 /BaseFont /Square /FirstChar 65 /LastChar 65 /Widths [600] /Encoding << /Differences [65 /square] >> /FontDescriptor 11 0 R >>"; -fn square_font_objs() -> Objs { +pub(crate) fn square_font_objs() -> Objs { vec![ (10, SQUARE_FONT.into(), None), (11, "<< /Type /FontDescriptor /FontName /Square /Flags 32 /FontFile3 12 0 R >>".into(), None), diff --git a/crates/pdf/src/tests_groups.rs b/crates/pdf/src/tests_groups.rs new file mode 100644 index 00000000..ce928139 --- /dev/null +++ b/crates/pdf/src/tests_groups.rs @@ -0,0 +1,153 @@ +//! M13.12: transparency groups (isolated, knockout, group alpha) and soft masks on a group's +//! combined result. + +use super::*; +use crate::tests::{page_pdf, render}; + +fn near(a: [f32; 4], b: [f32; 4]) -> bool { + (0..4).all(|i| (a[i] - b[i]).abs() < 0.02) +} + +/// A page painting form `Fm` with graphics state `GS` set; the form has `group` as its +/// `/Group` entry body and `content`. +fn group_page(page: &str, gs: &str, group: &str, content: &str) -> Doc { + let bytes = page_pdf( + page, + &format!("/ExtGState << /GS << {gs} >> /M << /BM /Multiply >> /H << /ca 0.5 >> >> /XObject << /Fm 10 0 R >>"), + vec![( + 10, + format!( + "<< /Type /XObject /Subtype /Form /BBox [0 0 200 100] {group} /Resources << /ExtGState << /M << /BM /Multiply >> /H << /ca 0.5 >> >> >> >>" + ), + Some(content.as_bytes().to_vec()), + )], + ); + let doc = parse(&bytes).unwrap(); + assert!(doc.skipped.is_empty(), "{:?}", doc.skipped); + doc +} + +#[test] +fn group_alpha_applies_to_the_combined_result() { + let content = "1 0 0 rg 0 0 100 100 re f 0 0 1 rg 50 0 100 100 re f"; + // A transparency group: the overlap is blue at half opacity (no red shows through). + let doc = group_page("/GS gs /Fm Do", "/ca 0.5", "/Group << /S /Transparency >>", content); + let img = render(&doc); + assert!(near(img.get(75, 50), [0.0, 0.0, 0.5, 0.5]), "{:?}", img.get(75, 50)); + assert!(near(img.get(25, 50), [0.5, 0.0, 0.0, 0.5]), "{:?}", img.get(25, 50)); + // A plain form: each rectangle at half opacity. + let doc = group_page("/GS gs /Fm Do", "/ca 0.5", "", content); + let img = render(&doc); + assert!(near(img.get(75, 50), [0.25, 0.0, 0.5, 0.75]), "{:?}", img.get(75, 50)); +} + +#[test] +fn knockout_groups_composite_with_the_initial_backdrop() { + // Red, then half-opaque blue over it: knocked out, the overlap shows only the blue. + let content = "1 0 0 rg 0 0 100 100 re f /H gs 0 0 1 rg 50 0 100 100 re f"; + let ko = group_page("/Fm Do", "", "/Group << /S /Transparency /I true /K true >>", content); + let img = render(&ko); + assert!(near(img.get(75, 50), [0.0, 0.0, 0.5, 0.5]), "knockout {:?}", img.get(75, 50)); + assert!(near(img.get(25, 50), [1.0, 0.0, 0.0, 1.0]), "{:?}", img.get(25, 50)); + let plain = group_page("/Fm Do", "", "/Group << /S /Transparency /I true >>", content); + let img = render(&plain); + assert!(near(img.get(75, 50), [0.5, 0.0, 0.5, 1.0]), "plain {:?}", img.get(75, 50)); + // Non-isolated knockout over a white page: the overlap is half blue over the page. + let ko_bd = group_page("1 g 0 0 200 100 re f /Fm Do", "", "/Group << /S /Transparency /K true >>", content); + let img = render(&ko_bd); + assert!(near(img.get(75, 50), [0.5, 0.5, 1.0, 1.0]), "non-isolated knockout {:?}", img.get(75, 50)); +} + +#[test] +fn isolated_and_non_isolated_groups_with_blend_modes() { + // Yellow page; the group multiplies cyan over it at half group opacity. + let content = "/M gs 0 1 1 rg 0 0 200 100 re f"; + let non = group_page("1 1 0 rg 0 0 200 100 re f /GS gs /Fm Do", "/ca 0.5", "/Group << /S /Transparency >>", content); + let img = render(&non); + // yellow × cyan = green, half over yellow. + assert!(near(img.get(100, 50), [0.5, 1.0, 0.0, 1.0]), "non-isolated {:?}", img.get(100, 50)); + let iso = group_page("1 1 0 rg 0 0 200 100 re f /GS gs /Fm Do", "/ca 0.5", "/Group << /S /Transparency /I true >>", content); + let img = render(&iso); + // Isolated: multiply against nothing = cyan, half over yellow. + assert!(near(img.get(100, 50), [0.5, 1.0, 0.5, 1.0]), "isolated {:?}", img.get(100, 50)); +} + +#[test] +fn blend_modes_inside_clips_reach_the_backdrop() { + let bytes = page_pdf("1 1 0 rg 0 0 200 100 re f q 0 0 100 100 re W n /M gs 0 1 1 rg 0 0 200 100 re f Q", "/ExtGState << /M << /BM /Multiply >> >>", vec![]); + let img = render(&parse(&bytes).unwrap()); + assert!(near(img.get(50, 50), [0.0, 1.0, 0.0, 1.0]), "{:?}", img.get(50, 50)); + assert!(near(img.get(150, 50), [1.0, 1.0, 0.0, 1.0]), "{:?}", img.get(150, 50)); +} + +#[test] +fn soft_mask_applies_to_the_masked_group_result() { + // Two overlapping opaque rectangles under a 50 % grey luminosity mask: the overlap is + // the top rectangle at half opacity (the mask applies to their composite). + let bytes = page_pdf( + "q /GS1 gs 1 0 0 rg 0 0 100 100 re f 0 0 1 rg 50 0 100 100 re f Q", + "/ExtGState << /GS1 << /SMask << /S /Luminosity /G 10 0 R >> >> >>", + vec![( + 10, + "<< /Type /XObject /Subtype /Form /BBox [0 0 200 100] /Group << /S /Transparency /CS /DeviceGray >> >>".into(), + Some(b"0.5 g 0 0 200 100 re f".to_vec()), + )], + ); + let img = render(&parse(&bytes).unwrap()); + let p = img.get(75, 50); + assert!((p[3] - 0.5).abs() < 0.02 && p[0] < 0.02 && (p[2] - 0.5).abs() < 0.02, "{p:?}"); +} + +/// The ink bounding box `(x0, y0, x1, y1)` of a render (pixels with alpha > 0.5). +fn ink_box(img: &effectcraft_raster::Image) -> (i64, i64, i64, i64) { + let (mut x0, mut y0, mut x1, mut y1) = (i64::MAX, i64::MAX, i64::MIN, i64::MIN); + for y in 0..img.height as i64 { + for x in 0..img.width as i64 { + if img.get(x, y)[3] > 0.5 { + (x0, y0, x1, y1) = (x0.min(x), y0.min(y), x1.max(x), y1.max(y)); + } + } + } + (x0, y0, x1, y1) +} + +#[test] +fn vertical_metrics_w2_and_dw2() { + use skrifa::MetadataProvider; + let inter = effectcraft_text::fonts::INTER_REGULAR.to_vec(); + let font = skrifa::FontRef::new(&inter).unwrap(); + let h = font.charmap().map('H').unwrap().to_u32(); + let doc = |desc_extra: &str| { + let bytes = page_pdf( + &format!("BT /F1 20 Tf 100 95 Td <{h:04X}{h:04X}> Tj ET"), + "/Font << /F1 10 0 R >>", + vec![ + (10, "<< /Type /Font /Subtype /Type0 /BaseFont /Inter /Encoding /Identity-V /DescendantFonts [13 0 R] >>".into(), None), + ( + 13, + format!( + "<< /Type /Font /Subtype /CIDFontType2 /BaseFont /Inter /CIDToGIDMap /Identity /W [{h} [700]] {desc_extra} /FontDescriptor 11 0 R >>" + ), + None, + ), + (11, "<< /Type /FontDescriptor /Flags 32 /FontFile2 12 0 R >>".into(), None), + (12, "<< >>".into(), Some(inter.clone())), + ], + ); + let d = parse(&bytes).unwrap(); + assert!(d.skipped.is_empty(), "{:?}", d.skipped); + ink_box(&render(&d)) + }; + // Defaults: an advance of 1 em down; the vertical origin (350, 880) from the horizontal one. + let base = doc(""); + // /W2: a 2 em advance and vx 250. + let w2 = doc(&format!("/W2 [{h} [-2000 250 880]]")); + assert_eq!(w2.1, base.1, "the first glyph's top stays: {base:?} {w2:?}"); + assert!(((w2.3 - base.3) - 20).abs() <= 1, "second glyph 20 pt lower: {base:?} {w2:?}"); + assert!(((w2.0 - base.0) - 2).abs() <= 1, "vx 250 vs 350 → 2 pt right: {base:?} {w2:?}"); + // The range form gives the same; /DW2 with vy 780 draws the glyphs 2 pt higher. + let range = doc(&format!("/W2 [{h} {h} -2000 250 880]")); + assert_eq!(range, w2); + let dw2 = doc("/DW2 [780 -1000]"); + assert!(((base.1 - dw2.1) - 2).abs() <= 1, "vy 780 → 2 pt higher: {base:?} {dw2:?}"); +} diff --git a/crates/pdf/src/tests_import.rs b/crates/pdf/src/tests_import.rs new file mode 100644 index 00000000..b6b96b80 --- /dev/null +++ b/crates/pdf/src/tests_import.rs @@ -0,0 +1,297 @@ +//! M13.12: CCITT images in PDF pages, EPS text and parser robustness over truncated and +//! corrupt inputs (generated fixtures). + +use super::*; +use crate::tests::{coverage, page_pdf, render, shape_names}; + +fn eps(body: &[u8]) -> Doc { + let mut b = b"%!PS-Adobe-3.0 EPSF-3.0\n%%BoundingBox: 0 0 200 100\n".to_vec(); + b.extend_from_slice(body); + let doc = parse(&b).unwrap(); + assert!(doc.skipped.is_empty(), "{:?}", doc.skipped); + doc +} + +/// The ink columns `(x0, x1)` of a render (alpha > 0.5). +fn ink_x(img: &effectcraft_raster::Image) -> (i64, i64) { + let (mut x0, mut x1) = (i64::MAX, i64::MIN); + for y in 0..img.height as i64 { + for x in 0..img.width as i64 { + if img.get(x, y)[3] > 0.5 { + (x0, x1) = (x0.min(x), x1.max(x)); + } + } + } + (x0, x1) +} + +#[test] +fn eps_text_with_bundled_fonts_and_show_variants() { + // Not embedded: drawn with the bundled stand-in, like the same text in a PDF. + let doc = eps(b"/Helvetica findfont 40 scalefont setfont 10 30 moveto (Hi) show\n"); + assert_eq!(shape_names(&doc), vec!["Text: Hi"]); + let pdf = parse(&page_pdf("BT /F1 40 Tf 10 30 Td (Hi) Tj ET", "/Font << /F1 << /Type /Font /Subtype /Type1 /BaseFont /Helvetica >> >>", vec![])).unwrap(); + let (a, b) = (render(&doc), render(&pdf)); + let (ia, ib) = (coverage(&a, 0, 0, 200, 100), coverage(&b, 0, 0, 200, 100)); + assert!(ia > 100.0 && (ia - ib).abs() < 1.0, "eps {ia} vs pdf {ib}"); + assert_eq!(ink_x(&a), ink_x(&b)); + // ashow spreads the letters; widthshow widens spaces only; selectfont; stringwidth centres. + let plain = ink_x(&render(&eps(b"/Helvetica 30 selectfont 10 30 moveto (HHH) show\n"))); + let spread = ink_x(&render(&eps(b"/Helvetica 30 selectfont 10 30 moveto 10 0 (HHH) ashow\n"))); + assert_eq!(spread.0, plain.0); + assert!(((spread.1 - plain.1) - 20).abs() <= 1, "{plain:?} {spread:?}"); + let ws = ink_x(&render(&eps(b"/Helvetica 30 selectfont 10 30 moveto 15 0 72 (HHH) widthshow\n"))); + assert!(((ws.1 - plain.1) - 30).abs() <= 1, "{plain:?} {ws:?}"); + let centred = ink_x(&render(&eps(b"/Helvetica 30 selectfont 100 (HHH) stringwidth pop 2 div sub 30 moveto (HHH) show\n"))); + let mid = (centred.0 + centred.1) as f64 / 2.0; + assert!((mid - 100.0).abs() < 3.0, "{centred:?}"); + // xshow: explicit advances. + let xs = ink_x(&render(&eps(b"/Helvetica 30 selectfont 10 30 moveto (HH) [50 0] xshow\n"))); + let one = ink_x(&render(&eps(b"/Helvetica 30 selectfont 10 30 moveto (H) show\n"))); + assert!(((xs.1 - one.1) - 50).abs() <= 1, "{one:?} {xs:?}"); + // makefont mirrors; charpath clips; colour applies. + let doc = eps(b"1 0 0 setrgbcolor /Helvetica findfont [40 0 0 40 0 0] makefont setfont 10 30 moveto (I) false charpath clip 0 0 200 100 rectfill\n"); + let img = render(&doc); + assert!(coverage(&img, 0, 0, 200, 100) > 50.0); + assert!(coverage(&img, 60, 0, 200, 100) < 1.0, "only the glyph is painted"); +} + +#[test] +fn eps_text_with_an_embedded_type1_font_and_reencoding() { + // hsbw 50 600; 0 0 rmoveto; 500 hlineto 500 vlineto -500 hlineto closepath endchar + let cs = vec![50 + 139, 248, 236, 13, 139, 139, 21, 248, 136, 6, 248, 136, 7, 252, 136, 6, 9, 14]; + let raw = crate::type1::write_test_type1(&[(".notdef", vec![139, 139, 13, 14]), ("box", cs)], &[(66, "box")]); + // Give the program a name. + let mut font = vec![]; + let nl = raw.iter().position(|c| *c == b'\n').unwrap() + 1; + font.extend_from_slice(&raw[..nl]); + font.extend_from_slice(b"12 dict begin /FontName /BoxFont def /FontType 1 def\n"); + font.extend_from_slice(&raw[nl..]); + let mut body = font.clone(); + body.extend_from_slice(b"\n/BoxFont findfont 100 scalefont setfont 0 0 moveto (BB) show\n"); + let doc = eps(&body); + let img = render(&doc); + // Boxes 50..550 / 650..1150 font units at size 100: x 5..55 and 65..115, y 0..50. + for (x, on) in [(30, true), (60, false), (90, true), (120, false)] { + assert_eq!(img.get(x, 75)[3] > 0.99, on, "x = {x}"); + } + // Re-encoded copy (`forall` / `definefont`): code 65 → box. + let mut body = font; + body.extend_from_slice( + b"\n/BoxFont findfont dup length dict begin { 1 index /FID ne { def } { pop pop } ifelse } forall \ +/Encoding 256 array def 0 1 255 { Encoding exch /.notdef put } for Encoding 65 /box put currentdict end \ +/BoxA exch definefont pop /BoxA findfont 100 scalefont setfont 0 0 moveto (AB) show\n", + ); + let img = render(&eps(&body)); + assert!(img.get(30, 75)[3] > 0.99, "A is the box now"); + assert!(img.get(90, 75)[3] < 0.01, "B is unencoded"); +} + +/// An image XObject of `img` (black = true) CCITT-encoded with `k`, drawn over the page. +fn ccitt_page(img: &[Vec], k: i64, mask: bool) -> Doc { + let (w, h) = (img[0].len(), img.len()); + let data = crate::ccitt::tests::encode(img, k); + let kind = if mask { "/ImageMask true" } else { "/ColorSpace /DeviceGray /BitsPerComponent 1" }; + let bytes = page_pdf( + "0 0 1 rg q 200 0 0 100 0 0 cm /Im Do Q", + "/XObject << /Im 10 0 R >>", + vec![( + 10, + format!( + "<< /Type /XObject /Subtype /Image /Width {w} /Height {h} {kind} /Filter /CCITTFaxDecode /DecodeParms << /K {k} /Columns {w} /Rows {h} /EndOfLine {} >> >>", + k >= 0 + ), + Some(data), + )], + ); + let doc = parse(&bytes).unwrap(); + assert!(doc.skipped.is_empty(), "{:?}", doc.skipped); + doc +} + +#[test] +fn ccitt_images_and_stencil_masks() { + // Left half black, a white bar on rows 4..6 (20×10 pixels → 10 PDF units per pixel). + let img: Vec> = (0..10).map(|y| (0..20).map(|x| x < 10 && !(4..6).contains(&y)).collect()).collect(); + for k in [-1, 0, 2] { + let doc = ccitt_page(&img, k, false); + let r = render(&doc); + let px = |x: usize, y: usize| r.get(x as i64 * 10 + 5, y as i64 * 10 + 5); + assert!(px(2, 2)[0] < 0.01 && px(2, 2)[3] > 0.99, "K {k}: black {:?}", px(2, 2)); + assert!(px(15, 2)[0] > 0.99, "K {k}: white {:?}", px(15, 2)); + assert!(px(2, 4)[0] > 0.99, "K {k}: the white bar {:?}", px(2, 4)); + // Stencil mask: black (sample 0) pixels paint the fill colour, the rest is clear. + let doc = ccitt_page(&img, k, true); + let r = render(&doc); + let px = |x: usize, y: usize| r.get(x as i64 * 10 + 5, y as i64 * 10 + 5); + assert!(px(2, 2)[2] > 0.99 && px(2, 2)[3] > 0.99, "K {k}: mask {:?}", px(2, 2)); + assert_eq!(px(15, 2)[3], 0.0, "K {k}"); + } +} + +/// Byte-level mutations of `bytes`: truncations, flipped bytes, deleted and duplicated +/// spans (deterministic). +fn mutations(bytes: &[u8]) -> Vec> { + let mut out = vec![]; + let n = bytes.len(); + for k in 1..24 { + out.push(bytes[..n * k / 24].to_vec()); + } + let mut s = 0x9E37_79B9u32; + let mut rnd = move || { + s ^= s << 13; + s ^= s >> 17; + s ^= s << 5; + s + }; + for _ in 0..60 { + let mut b = bytes.to_vec(); + for _ in 0..1 + rnd() % 6 { + let i = rnd() as usize % n; + b[i] = match rnd() % 4 { + 0 => b[i] ^ 0xFF, + 1 => b'0' + (rnd() % 10) as u8, + 2 => b"[]<>()/{}% \n"[(rnd() % 12) as usize], + _ => rnd() as u8, + }; + } + out.push(b); + } + for _ in 0..30 { + let mut b = bytes.to_vec(); + let i = rnd() as usize % n; + let len = (rnd() as usize % 64).min(n - i); + if rnd() % 2 == 0 { + b.drain(i..i + len); + } else { + let span = b[i..i + len].to_vec(); + b.splice(i..i, span); + } + out.push(b); + } + out +} + +/// Parse and render every page of every mutation: errors are fine, panics are not. +fn survive(name: &str, bytes: &[u8]) { + for (i, m) in mutations(bytes).into_iter().enumerate() { + let r = std::panic::catch_unwind(|| { + let pages = page_count(&m); + for p in 0..pages.clamp(1, 3) { + if let Ok(doc) = parse_page(&m, p) { + let (w, h) = doc.pixel_size(); + let s = 64.0 / (w.max(h).max(1) as f64); + let _ = effectcraft_svg::rasterize(&doc, 64, 64, s); + let _ = layer_names(&doc); + } + } + }); + assert!(r.is_ok(), "{name}: mutation {i} panicked"); + } +} + +#[test] +fn truncated_and_corrupt_pdfs_never_panic() { + survive("sample", &crate::tests::sample_pdf(false)); + survive("object streams", &crate::tests::sample_pdf(true)); + let mut fonts = crate::tests::square_font_objs(); + fonts.push((13, "<< /Type /XObject /Subtype /Image /Width 2 /Height 2 /ColorSpace /DeviceRGB /BitsPerComponent 8 >>".into(), Some(vec![255; 12]))); + survive( + "text and images", + &page_pdf("BT /F1 40 Tf 10 10 Td (AAA) Tj ET q 50 0 0 50 0 0 cm /Im Do Q", "/Font << /F1 10 0 R >> /XObject << /Im 13 0 R >>", fonts), + ); + let img: Vec> = (0..8).map(|y| (0..16).map(|x| (x + y) % 3 == 0).collect()).collect(); + let data = crate::ccitt::tests::encode(&img, -1); + survive( + "ccitt", + &page_pdf( + "q 100 0 0 50 0 0 cm /Im Do Q", + "/XObject << /Im 10 0 R >>", + vec![( + 10, + "<< /Type /XObject /Subtype /Image /Width 16 /Height 8 /ImageMask true /Filter /CCITTFaxDecode /DecodeParms << /K -1 /Columns 16 >> >>".into(), + Some(data), + )], + ), + ); +} + +#[test] +fn hostile_structures_return_errors_or_draw_nothing() { + // Not a PDF / EPS at all, empty, and headers alone. + for b in [&b""[..], b"%PDF-", b"%PDF-1.7\n%%EOF", b"%!PS-Adobe-3.0 EPSF-3.0\n", b"\xC5\xD0\xD3\xC6", b"hello"] { + let _ = parse(b); + let _ = page_count(b); + } + assert_eq!(parse(b"hello"), Err(Error::NotVector)); + assert_eq!(parse(b"%PDF-1.7\n%%EOF"), Err(Error::NoPages)); + // A page tree that refers to itself, a content stream that is a reference loop, deep + // form recursion and absurd numbers. + let objs: crate::tests::Objs = vec![ + (1, "<< /Type /Catalog /Pages 2 0 R >>".into(), None), + (2, "<< /Type /Pages /Kids [2 0 R 3 0 R] /Count 1 >>".into(), None), + (3, "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 1e300 -1e300] /Contents 5 0 R /Resources << /XObject << /F 4 0 R >> >> >>".into(), None), + ( + 4, + "<< /Type /XObject /Subtype /Form /BBox [0 0 1 1] /Resources << /XObject << /F 4 0 R >> >> >>".into(), + Some(b"/F Do 1e308 1e308 m 1e308 0 l f".to_vec()), + ), + (5, "<< >>".into(), Some(b"/F Do q q q q Q Q Q Q Q Q 99999999999 w [1 0 0 1 0 0] cm".to_vec())), + ]; + let bytes = crate::write::pdf(&objs, 1); + if let Ok(doc) = parse(&bytes) { + let _ = effectcraft_svg::rasterize(&doc, 32, 32, 1e-300); + } + // Images with absurd sizes and filters. + let bytes = page_pdf( + "/A Do /B Do /C Do /D Do", + "/XObject << /A 10 0 R /B 11 0 R /C 12 0 R /D 13 0 R >>", + vec![ + (10, "<< /Subtype /Image /Width 100000 /Height 100000 /BitsPerComponent 8 /ColorSpace /DeviceRGB >>".into(), Some(vec![0; 4])), + (11, "<< /Subtype /Image /Width 4 /Height 4 /BitsPerComponent 7 /ColorSpace /DeviceRGB >>".into(), Some(vec![0; 4])), + ( + 12, + "<< /Subtype /Image /Width 4 /Height 4 /BitsPerComponent 1 /ImageMask true /Filter /CCITTFaxDecode /DecodeParms << /K -1 /Columns 0 >> >>" + .into(), + Some(vec![0xFF; 4]), + ), + (13, "<< /Subtype /Image /Width 4 /Height 4 /BitsPerComponent 8 /ColorSpace [/Indexed /DeviceRGB 300 <00>] >>".into(), Some(vec![9; 16])), + ], + ); + let doc = parse(&bytes).unwrap(); + let _ = render(&doc); + assert!(!doc.skipped.is_empty()); +} + +#[test] +fn truncated_and_corrupt_eps_never_panic() { + let eps = b"%!PS-Adobe-3.0 EPSF-3.0\n%%BoundingBox: 0 0 100 100\n\ +/sq { newpath 0 0 moveto 10 0 lineto 10 10 lineto closepath } bind def\n\ +1 0 0 setrgbcolor 0 1 9 { gsave dup 10 mul 0 translate sq fill grestore } for\n\ +/Helvetica findfont 12 scalefont setfont 10 50 moveto (Hello) show\n\ +[1 2] 0 setdash 0 0 moveto 100 100 lineto stroke\n\ +1 1 1 { pop } repeat { exit } loop 0 0 50 0 360 arc closepath eofill\n%%EOF\n"; + survive("eps", eps); + // Stack underflow, unbalanced procedures, runaway loops and huge repeats. + for prog in [ + &b"pop pop pop add mul def"[..], + b"{ { { { {", + b"} } ] >> )", + b"{ } loop", + b"0 1 1e9 { pop } for", + b"1e9 { 1 } repeat", + b"/a { a } def a", + b"(unterminated", + b"); + +impl Mesh { + fn flag(&mut self, f: u8) -> &mut Self { + self.0.push(f); + self + } + fn pt(&mut self, x: u16, y: u16) -> &mut Self { + self.0.extend_from_slice(&x.to_be_bytes()); + self.0.extend_from_slice(&y.to_be_bytes()); + self + } + fn c(&mut self, c: &[u8]) -> &mut Self { + self.0.extend_from_slice(c); + self + } +} + +const RGB_DECODE: &str = "/Decode [0 65535 0 65535 0 1 0 1 0 1]"; + +fn close(a: [f32; 4], b: [f32; 3], tol: f32) -> bool { + a[3] > 0.99 && (0..3).all(|i| (a[i] - b[i]).abs() < tol) +} + +fn sh_page(shading: &str, data: Vec) -> Doc { + let bytes = page_pdf("/Sh sh", "/Shading << /Sh 10 0 R >>", vec![(10, shading.into(), Some(data))]); + let doc = parse(&bytes).unwrap(); + assert!(doc.skipped.is_empty(), "{:?}", doc.skipped); + doc +} + +#[test] +fn free_form_triangle_mesh_with_edge_flags() { + let mut m = Mesh::default(); + // Triangle (20,20) red, (80,20) green, (20,80) blue; then flag 1: (80,20) (20,80) (80,80) white. + m.flag(0).pt(20, 20).c(&[255, 0, 0]); + m.flag(0).pt(80, 20).c(&[0, 255, 0]); + m.flag(0).pt(20, 80).c(&[0, 0, 255]); + m.flag(1).pt(80, 80).c(&[255, 255, 255]); + // Flag 2 off the shared edge (a, c): (80,20) (80,80) → (140,20) black. + m.flag(2).pt(140, 20).c(&[0, 0, 0]); + let doc = sh_page(&format!("<< /ShadingType 4 /ColorSpace /DeviceRGB /BitsPerCoordinate 16 /BitsPerComponent 8 /BitsPerFlag 8 {RGB_DECODE} >>"), m.0); + let img = render(&doc); + let at = |x: i64, y: i64| img.get(x, 99 - y); + assert!(close(at(21, 21), [1.0, 0.0, 0.0], 0.06), "{:?}", at(21, 21)); + // Centroid of the first triangle: the average of its colours. + assert!(close(at(40, 40), [1.0 / 3.0, 1.0 / 3.0, 1.0 / 3.0], 0.04), "{:?}", at(40, 40)); + assert!(close(at(79, 79), [1.0, 1.0, 1.0], 0.06), "{:?}", at(79, 79)); + // Third triangle (a = (80,20) green, c = (80,80) white, (140,20) black). + assert!(close(at(135, 22), [0.0, 0.0, 0.0], 0.1), "{:?}", at(135, 22)); + // Outside the mesh: nothing. + assert_eq!(at(10, 50)[3], 0.0); + assert_eq!(at(150, 90)[3], 0.0); + // The shading is an image node (no Gouraud primitive in the render tree). + assert!(crate::tests::shape_names(&doc).contains(&"Shading".to_string())); +} + +#[test] +fn lattice_mesh_with_a_function() { + // Two rows of three vertices; t = 0 at the left, 1 at the right; grey ramp function. + let mut m = Mesh::default(); + for y in [10u16, 90] { + for (x, t) in [(10u16, 0u8), (100, 128), (190, 255)] { + m.pt(x, y).c(&[t]); + } + } + let doc = sh_page( + "<< /ShadingType 5 /ColorSpace /DeviceRGB /BitsPerCoordinate 16 /BitsPerComponent 8 /VerticesPerRow 3 /Decode [0 65535 0 65535 0 1] \ + /Function << /FunctionType 2 /Domain [0 1] /C0 [0 0 0] /C1 [1 1 1] /N 1 >> >>", + m.0, + ); + let img = render(&doc); + let at = |x: i64, y: i64| img.get(x, 99 - y); + for x in [20i64, 60, 100, 140, 180] { + let want = (x as f32 + 0.5 - 10.0) / 180.0; + let p = at(x, 50); + assert!(p[3] > 0.99 && (p[0] - want).abs() < 0.03 && (p[1] - p[0]).abs() < 0.01, "x {x}: {p:?} want {want}"); + } + assert_eq!(at(5, 50)[3], 0.0); +} + +/// A straight-sided rectangle as the 12 boundary points from `(x0, y0)` up the left side, +/// along the top, down the right side and back along the bottom. +fn rect_loop(x0: u16, y0: u16, x1: u16, y1: u16) -> Vec<(u16, u16)> { + let lerp = |a: u16, b: u16, k: u16| ((a as u32 * (3 - k as u32) + b as u32 * k as u32) / 3) as u16; + let mut v = vec![]; + for k in 0..3 { + v.push((x0, lerp(y0, y1, k))); + } + for k in 0..3 { + v.push((lerp(x0, x1, k), y1)); + } + for k in 0..3 { + v.push((x1, lerp(y1, y0, k))); + } + for k in 0..3 { + v.push((lerp(x1, x0, k), y0)); + } + v +} + +fn patch_doc(tensor: bool) -> Doc { + let mut m = Mesh::default(); + // Patch 1 over x 100..140, y 20..80: corners (100,20) red, (100,80) green, (140,80) blue, + // (140,20) black. + m.flag(0); + let l1 = rect_loop(100, 20, 140, 80); + for (x, y) in &l1 { + m.pt(*x, *y); + } + if tensor { + // Interior points p11 p12 p22 p21 at the thirds. + for (x, y) in [(113, 40), (113, 60), (127, 60), (127, 40)] { + m.pt(x, y); + } + } + m.c(&[255, 0, 0]).c(&[0, 255, 0]).c(&[0, 0, 255]).c(&[0, 0, 0]); + // Patch 2 (flag 2): shares patch 1's third side (140,80) → (140,20); then (180,20) white, + // (180,80) red. + m.flag(2); + // The 8 new points: from (140,20) along the bottom to (180,20), up to (180,80), back + // along the top towards (140,80). + let new_pts = [(153, 20), (167, 20), (180, 20), (180, 40), (180, 60), (180, 80), (167, 80), (153, 80)]; + for (x, y) in new_pts { + m.pt(x, y); + } + if tensor { + for (x, y) in [(153, 60), (153, 40), (167, 40), (167, 60)] { + m.pt(x, y); + } + } + m.c(&[255, 255, 255]).c(&[255, 0, 0]); + let ty = if tensor { 7 } else { 6 }; + sh_page(&format!("<< /ShadingType {ty} /ColorSpace /DeviceRGB /BitsPerCoordinate 16 /BitsPerComponent 8 /BitsPerFlag 8 {RGB_DECODE} >>"), m.0) +} + +#[test] +fn coons_and_tensor_patch_meshes() { + for tensor in [false, true] { + let doc = patch_doc(tensor); + let img = render(&doc); + let at = |x: i64, y: i64| img.get(x, 99 - y); + let k = if tensor { "tensor" } else { "coons" }; + assert!(close(at(100, 20), [1.0, 0.0, 0.0], 0.08), "{k} {:?}", at(100, 20)); + assert!(close(at(100, 79), [0.0, 1.0, 0.0], 0.08), "{k} {:?}", at(100, 79)); + assert!(close(at(139, 79), [0.0, 0.0, 1.0], 0.08), "{k} {:?}", at(139, 79)); + // Patch centre: the average of the corners. + assert!(close(at(120, 50), [0.25, 0.25, 0.25], 0.04), "{k} {:?}", at(120, 50)); + // Patch 2 inherits blue / black on its shared side. + assert!(close(at(179, 20), [1.0, 1.0, 1.0], 0.08), "{k} {:?}", at(179, 20)); + assert!(close(at(179, 79), [1.0, 0.0, 0.0], 0.08), "{k} {:?}", at(179, 79)); + assert!(close(at(141, 22), [0.0, 0.0, 0.0], 0.1), "{k} {:?}", at(141, 22)); + assert_eq!(at(90, 50)[3], 0.0); + assert_eq!(at(190, 50)[3], 0.0); + } +} + +#[test] +fn curved_coons_patch_follows_its_boundary() { + // A patch whose top side bulges up to y = 95 between x 40 and 60. + let mut m = Mesh::default(); + m.flag(0); + let pts = [(20, 20), (20, 40), (20, 60), (20, 80), (40, 107), (60, 107), (80, 80), (80, 60), (80, 40), (80, 20), (60, 20), (40, 20)]; + for (x, y) in pts { + m.pt(x, y); + } + m.c(&[255, 0, 0]).c(&[255, 0, 0]).c(&[255, 0, 0]).c(&[255, 0, 0]); + let doc = sh_page(&format!("<< /ShadingType 6 /ColorSpace /DeviceRGB /BitsPerCoordinate 16 /BitsPerComponent 8 /BitsPerFlag 8 {RGB_DECODE} >>"), m.0); + let img = render(&doc); + let at = |x: i64, y: i64| img.get(x, 99 - y); + // The top curve peaks at 80 + 0.75·27 ≈ 100 at x = 50. + assert!(at(50, 95)[3] > 0.99, "{:?}", at(50, 95)); + assert_eq!(at(22, 95)[3], 0.0); +} + +#[test] +fn function_based_shadings_calculator_and_sampled() { + let bytes = page_pdf( + "/A sh /B sh", + "/Shading << /A 10 0 R /B 11 0 R >>", + vec![ + (10, "<< /ShadingType 1 /ColorSpace /DeviceRGB /Matrix [100 0 0 50 0 0] /Function 12 0 R >>".into(), None), + (11, "<< /ShadingType 1 /ColorSpace /DeviceRGB /Domain [0 1 0 1] /Matrix [100 0 0 50 100 50] /Function 13 0 R >>".into(), None), + (12, "<< /FunctionType 4 /Domain [0 1 0 1] /Range [0 1 0 1 0 1] >>".into(), Some(b"{ 0 }".to_vec())), + ( + 13, + "<< /FunctionType 0 /Domain [0 1 0 1] /Range [0 1 0 1 0 1] /Size [2 2] /BitsPerSample 8 >>".into(), + Some(vec![255, 0, 0, 0, 255, 0, 0, 0, 255, 255, 255, 255]), + ), + ], + ); + let doc = parse(&bytes).unwrap(); + assert!(doc.skipped.is_empty(), "{:?}", doc.skipped); + let img = render(&doc); + let at = |x: i64, y: i64| img.get(x, 99 - y); + // Calculator: (x, y) → (x, y, 0). + let p = at(90, 10); + assert!(close(p, [0.905, 0.21, 0.0], 0.03), "{p:?}"); + // Only the domain is painted. + assert_eq!(at(50, 75)[3], 0.0); + // Sampled 2-in function, bilinear: red (0,0), green (1,0), blue (0,1), white (1,1). + assert!(close(at(150, 75), [0.5, 0.5, 0.5], 0.03), "{:?}", at(150, 75)); + assert!(close(at(100, 50), [1.0, 0.0, 0.0], 0.04), "{:?}", at(100, 50)); + assert!(close(at(199, 99), [1.0, 1.0, 1.0], 0.04), "{:?}", at(199, 99)); +} + +#[test] +fn mesh_shading_pattern_fills_a_path() { + let mut m = Mesh::default(); + m.flag(0).pt(0, 0).c(&[255, 0, 0]); + m.flag(0).pt(200, 0).c(&[255, 0, 0]); + m.flag(0).pt(0, 200).c(&[255, 0, 0]); + let objs: Objs = vec![ + (10, "<< /PatternType 2 /Shading 11 0 R >>".into(), None), + (11, format!("<< /ShadingType 4 /ColorSpace /DeviceRGB /BitsPerCoordinate 16 /BitsPerComponent 8 /BitsPerFlag 8 {RGB_DECODE} >>"), Some(m.0)), + ]; + let bytes = page_pdf("/Pattern cs /P scn 10 10 30 30 re f", "/Pattern << /P 10 0 R >>", objs); + let doc = parse(&bytes).unwrap(); + assert!(doc.skipped.is_empty(), "{:?}", doc.skipped); + let img = render(&doc); + let at = |x: i64, y: i64| img.get(x, 99 - y); + assert!(close(at(20, 20), [1.0, 0.0, 0.0], 0.02), "{:?}", at(20, 20)); + // Clipped to the path. + assert_eq!(at(50, 20)[3], 0.0); + assert_eq!(at(5, 5)[3], 0.0); +} + +#[test] +fn corrupt_mesh_data_draws_what_parses() { + // Truncated and garbage vertex data never panic. + for ty in [4, 5, 6, 7] { + for data in [vec![], vec![0u8; 3], vec![0xFF; 7], (0..=255u8).collect::>(), vec![3u8; 200]] { + let bytes = page_pdf( + "/Sh sh /Pattern cs /P scn 0 0 50 50 re f", + "/Shading << /Sh 10 0 R >> /Pattern << /P << /PatternType 2 /Shading 10 0 R >> >>", + vec![( + 10, + format!( + "<< /ShadingType {ty} /ColorSpace /DeviceRGB /BitsPerCoordinate 16 /BitsPerComponent 8 /BitsPerFlag 8 /VerticesPerRow 2 {RGB_DECODE} >>" + ), + Some(data), + )], + ); + let doc = parse(&bytes).unwrap(); + let _ = render(&doc); + } + } + // Bad layouts are reported. + let bytes = page_pdf( + "/Sh sh", + "/Shading << /Sh 10 0 R >>", + vec![(10, "<< /ShadingType 4 /ColorSpace /DeviceRGB /BitsPerCoordinate 7 >>".into(), Some(vec![0; 16]))], + ); + let doc = parse(&bytes).unwrap(); + assert!(doc.skipped.iter().any(|s| s.contains("layout")), "{:?}", doc.skipped); +} diff --git a/crates/plugin/src/tests.rs b/crates/plugin/src/tests.rs index 9363ab3e..f68fde00 100644 --- a/crates/plugin/src/tests.rs +++ b/crates/plugin/src/tests.rs @@ -12,11 +12,20 @@ fn load_err(r: Result<&'static effectcraft_effects::EffectSpec, String>) -> Stri } } +/// The gain plug-in's manifest. +fn gain_manifest(id: &str) -> String { + format!( + r#"{{"api":1,"id":"{id}","name":"Gain {id}","category":"Test Plug-ins","version":"1.0","params":[{{"id":"gain","name":"Gain","type":"slider","default":0.5,"min":0,"max":4}},{{"id":"on","name":"Lift","type":"checkbox","default":false}},{{"id":"tint","name":"Tint","type":"color","default":[0,0,1,1]}}]}}"# + ) +} + /// A plug-in in WebAssembly text: multiplies RGB by `gain`, adds `lift` × `tint` when `on`. fn gain_module(id: &str, render_body: &str) -> String { - let manifest = format!( - r#"{{"api":1,"id":"{id}","name":"Gain {id}","category":"Test Plug-ins","version":"1.0","params":[{{"id":"gain","name":"Gain","type":"slider","default":0.5,"min":0,"max":4}},{{"id":"on","name":"Lift","type":"checkbox","default":false}},{{"id":"tint","name":"Tint","type":"color","default":[0,0,1,1]}}]}}"# - ); + module_with(&gain_manifest(id), render_body) +} + +/// The gain module with its own manifest JSON. +fn module_with(manifest: &str, render_body: &str) -> String { let len = manifest.len(); let escaped = manifest.replace('\\', "\\\\").replace('"', "\\\""); format!( @@ -162,3 +171,149 @@ fn wasm_plugins_are_sandboxed() { let reserved = gain_module("ec.blur.mine", ""); assert!(load_err(load_wasm(reserved.as_bytes(), "reserved.wat")).contains("reserved")); } + +/// `base` with `from` replaced by `to` (which must occur). +fn edit(base: &str, from: &str, to: &str) -> String { + assert!(base.contains(from), "{from}"); + base.replace(from, to) +} + +#[test] +fn abi_violations_fail_to_load_with_reasons() { + let base = gain_module("org.test.abi", ""); + // Missing exports. + for export in ["ec_render", "ec_alloc", "ec_manifest_ptr", "ec_manifest_len", "ec_api_version"] { + let m = edit(&base, &format!("(export \"{export}\")"), ""); + let e = load_err(load_wasm(m.as_bytes(), "x.wat")); + assert!(e.contains(export), "{export}: {e}"); + } + let m = edit(&base, "(memory (export \"memory\") 2)", "(memory 2)"); + assert!(load_err(load_wasm(m.as_bytes(), "x.wat")).contains("memory")); + // Wrong signatures. + let m = edit(&base, "(func (export \"ec_api_version\") (result i32) i32.const 1)", "(func (export \"ec_api_version\") (result f64) f64.const 1)"); + assert!(load_err(load_wasm(m.as_bytes(), "x.wat")).contains("ec_api_version")); + let m = edit(&base, "(param $scale f64) (result i32)", "(param $scale f32) (result i32)"); + assert!(load_err(load_wasm(m.as_bytes(), "x.wat")).contains("ec_render")); + // API versions other than 1. + for v in ["0", "-1", "2147483647"] { + let m = edit( + &base, + "(func (export \"ec_api_version\") (result i32) i32.const 1)", + &format!("(func (export \"ec_api_version\") (result i32) i32.const {v})"), + ); + assert!(load_err(load_wasm(m.as_bytes(), "x.wat")).contains(&format!("API {v}")), "{v}"); + } + // Manifest out of bounds, negative or absurd lengths, not JSON. + let ptr = "(func (export \"ec_manifest_ptr\") (result i32) i32.const 16)"; + let m = edit(&base, ptr, "(func (export \"ec_manifest_ptr\") (result i32) i32.const 200000)"); + assert!(load_err(load_wasm(m.as_bytes(), "x.wat")).contains("out of bounds")); + let len = gain_manifest("org.test.abi").len(); + for bad in ["-5", "2000000000"] { + let m = edit(&base, &format!("(result i32) i32.const {len})"), &format!("(result i32) i32.const {bad})")); + assert!(load_err(load_wasm(m.as_bytes(), "x.wat")).contains("manifest length"), "{bad}"); + } + let m = edit(&base, ptr, "(func (export \"ec_manifest_ptr\") (result i32) i32.const 4000)"); + assert!(load_err(load_wasm(m.as_bytes(), "x.wat")).contains("manifest JSON")); + // Traps and runaway code while loading. + let version = "(func (export \"ec_api_version\") (result i32) i32.const 1)"; + let m = edit(&base, version, "(func (export \"ec_api_version\") (result i32) unreachable)"); + assert!(load_wasm(m.as_bytes(), "x.wat").is_err()); + let m = edit(&base, version, "(func (export \"ec_api_version\") (result i32) (loop $l br $l) i32.const 1)"); + assert!(load_wasm(m.as_bytes(), "x.wat").is_err(), "fuel runs out in ec_api_version"); + let m = edit(&base, "(memory (export \"memory\") 2)", "(memory (export \"memory\") 2) (func $s (loop $l br $l)) (start $s)"); + assert!(load_err(load_wasm(m.as_bytes(), "x.wat")).contains("instantiation"), "fuel runs out in the start function"); + // Memory beyond the sandbox's limit. + let m = edit(&base, "(memory (export \"memory\") 2)", "(memory (export \"memory\") 20000)"); + assert!(load_err(load_wasm(m.as_bytes(), "x.wat")).contains("instantiation")); + // Not a module at all. + for junk in [&b""[..], b"\0asm", b"\0asm\x01\0\0\0\xff\xff", b"(module", b"(module (func (export \"x\") unknown_op))"] { + assert!(load_wasm(junk, "junk").is_err()); + } +} + +#[test] +fn manifest_parameter_schemas_are_validated() { + let p = |params: &str| format!(r#"{{"api":1,"id":"org.test.schema","name":"Schema","category":"Test Plug-ins","version":"1","params":[{params}]}}"#); + let bad = [ + (r#"{"id":"a","name":"A","type":"checkbox"},{"id":"a","name":"B","type":"checkbox"}"#, "duplicate"), + (r#"{"id":"a b","name":"A","type":"checkbox"}"#, "parameter id"), + (r#"{"id":"","name":"A","type":"checkbox"}"#, "parameter id"), + (r#"{"id":"p","name":"P","type":"popup","options":[],"default":0}"#, "popup"), + (r#"{"id":"p","name":"P","type":"popup","options":["x"],"default":3}"#, "popup"), + (r#"{"id":"s","name":"S","type":"slider","default":1,"min":5,"max":0}"#, "slider"), + (r#"{"id":"s","name":"S","type":"slider","default":9,"min":0,"max":4}"#, "slider"), + (r#"{"id":"s","name":"S","type":"slider","default":1,"min":0,"max":4,"sliderMin":3,"sliderMax":2}"#, "slider"), + (r#"{"id":"s","name":"S","type":"wobble"}"#, "manifest JSON"), + (r#"{"id":"s","name":"S","type":"slider"}"#, "manifest JSON"), + ]; + for (params, want) in bad { + let m = module_with(&p(params), ""); + let e = load_err(load_wasm(m.as_bytes(), "schema.wat")); + assert!(e.contains(want), "{params}: {e}"); + } + let many: Vec = (0..300).map(|i| format!(r#"{{"id":"c{i}","name":"C","type":"checkbox"}}"#)).collect(); + let e = load_err(load_wasm(module_with(&p(&many.join(",")), "").as_bytes(), "many.wat")); + assert!(e.contains("at most"), "{e}"); + // Missing name, another API in the manifest. + let e = load_err(load_wasm(module_with(r#"{"api":1,"id":"org.test.noname","name":" ","category":"X","version":"1","params":[]}"#, "").as_bytes(), "x.wat")); + assert!(e.contains("name"), "{e}"); + let e = load_err(load_wasm(module_with(r#"{"api":3,"id":"org.test.api3","name":"N","category":"X","version":"1","params":[]}"#, "").as_bytes(), "x.wat")); + assert!(e.contains("API 3"), "{e}"); + // A valid schema loads. + let ok = p( + r#"{"id":"s","name":"S","type":"slider","default":2,"min":0,"max":4,"sliderMin":1,"sliderMax":3},{"id":"p","name":"P","type":"popup","options":["x","y"],"default":1}"#, + ); + let spec = load_wasm(module_with(&ok, "").as_bytes(), "ok.wat").unwrap(); + assert_eq!(spec.params.len(), 2); +} + +#[test] +fn render_failures_leave_the_frame_and_recover() { + let orig = [0.8, 0.6, 0.2, 1.0]; + // ec_render traps (when time > 0.5), returns an error code, or ec_alloc hands back memory + // out of bounds: the frame passes through unchanged, and the plug-in keeps working. + let trap = load_wasm(gain_module("org.test.trap", "local.get $t f64.const 0.5 f64.gt if unreachable end").as_bytes(), "trap.wat").unwrap(); + let mut next = 1; + let g = effectcraft_effects::instantiate(trap, &mut Ids(&mut next), trap.name, [4.0, 2.0]); + let params = effectcraft_effects::flatten_params(&g, &mut |p| p.value.clone()); + let at = |time: f64| { + let ctx = EffectCtx { params: ¶ms, time, layer_size: [4.0, 2.0], seed: 1, adjustment: false, env: EffectEnv::default() }; + let buf = Buf { img: Image::filled(4, 2, orig), offset: [0.0; 2], scale: 1.0 }; + (trap.render)(&ctx, buf).img + }; + assert_eq!(at(1.0).get(0, 0), orig, "trapped"); + assert_eq!(at(0.0).get(0, 0), [0.4, 0.3, 0.1, 1.0], "a fresh instance after the trap"); + assert_eq!(at(1.0).get(0, 0), orig); + assert_eq!(at(0.0).get(1, 1), [0.4, 0.3, 0.1, 1.0]); + let code = load_wasm(gain_module("org.test.code", "i32.const 7 return").as_bytes(), "code.wat").unwrap(); + assert_eq!(render(code, 0.5, false).get(0, 0), orig); + let oob = edit(&gain_module("org.test.oob", ""), " i32.const 65536)", " i32.const -16)"); + let oob = load_wasm(oob.as_bytes(), "oob.wat").unwrap(); + assert_eq!(render(oob, 0.5, false).get(0, 0), orig); + // Runaway rendering: out of fuel every time, never hangs. + let spin = load_wasm(gain_module("org.test.spin2", "(loop $spin br $spin)").as_bytes(), "spin2.wat").unwrap(); + for _ in 0..3 { + assert_eq!(render(spin, 0.5, false).get(0, 0), orig); + } +} + +#[test] +fn wasm_rendering_is_deterministic() { + // Float edge cases (NaN from sqrt(−x), infinities from x / 0) come out bit-identical + // across instances, threads and repeated runs. + let body = "local.get $px local.get $px f32.load f32.const -1 f32.mul f32.sqrt f32.store \ + local.get $px local.get $px f32.load offset=4 f32.const 0 f32.div f32.store offset=4"; + let spec = load_wasm(gain_module("org.test.det", body).as_bytes(), "det.wat").unwrap(); + let bits = |img: &Image| -> Vec { (0..2).flat_map(|y| (0..4).map(move |x| (x, y))).flat_map(|(x, y)| img.get(x, y).map(f32::to_bits)).collect() }; + let first = bits(&render(spec, 0.37, true)); + let runs: Vec> = std::thread::scope(|sc| { + let hs: Vec<_> = (0..6).map(|_| sc.spawn(|| bits(&render(spec, 0.37, true)))).collect(); + hs.into_iter().map(|h| h.join().unwrap()).collect() + }); + for r in runs { + assert_eq!(r, first); + } + // The same module loaded under another id renders the same bits. + let twin = load_wasm(gain_module("org.test.det2", body).as_bytes(), "det2.wat").unwrap(); + assert_eq!(bits(&render(twin, 0.37, true)), first); +} diff --git a/crates/plugin/src/wasm.rs b/crates/plugin/src/wasm.rs index 8015f7ee..71f1f136 100644 --- a/crates/plugin/src/wasm.rs +++ b/crates/plugin/src/wasm.rs @@ -2,7 +2,7 @@ use std::sync::Mutex; -use wasmi::{Config, Engine, Instance, Linker, Memory, Module, Store, TypedFunc, WasmParams, WasmResults}; +use wasmi::{Config, Engine, Instance, Linker, Memory, Module, Store, StoreLimits, StoreLimitsBuilder, TypedFunc, WasmParams, WasmResults}; use crate::{EffectPlugin, PLUGIN_API_VERSION, PluginFrame, PluginManifest, PluginParams}; @@ -10,12 +10,16 @@ use crate::{EffectPlugin, PLUGIN_API_VERSION, PluginFrame, PluginManifest, Plugi /// work, but a runaway loop stops instead of hanging the render. const FUEL_BASE: u64 = 50_000_000; const FUEL_PER_PIXEL: u64 = 20_000; +/// The most linear memory a plug-in instance may have (a 4K float frame is ~130 MB). +const MAX_MEMORY: usize = 1 << 30; +/// The longest manifest read. +const MAX_MANIFEST: i32 = 1 << 20; type RenderFn = TypedFunc<(i32, i32, i32, i32, i32, f64, f64), i32>; /// One instantiated module (renders running on several threads each take their own). struct Inst { - store: Store<()>, + store: Store, instance: Instance, memory: Memory, alloc: TypedFunc, @@ -37,9 +41,10 @@ fn err(e: impl std::fmt::Display) -> String { } fn instantiate(engine: &Engine, module: &Module) -> Result { - let mut store = Store::new(engine, ()); + let mut store = Store::new(engine, StoreLimitsBuilder::new().memory_size(MAX_MEMORY).instances(1).build()); + store.limiter(|l| l); store.set_fuel(FUEL_BASE).map_err(err)?; - let linker = Linker::<()>::new(engine); + let linker = Linker::::new(engine); let instance = linker.instantiate_and_start(&mut store, module).map_err(|e| format!("instantiation failed: {e}"))?; let memory = instance.get_memory(&store, "memory").ok_or("the module exports no `memory`")?; let alloc = instance.get_typed_func::(&store, "ec_alloc").map_err(|_| missing("ec_alloc"))?; @@ -74,7 +79,10 @@ impl WasmPlugin { } let ptr = inst.func::<(), i32>("ec_manifest_ptr")?.call(&mut inst.store, ()).map_err(err)?; let len = inst.func::<(), i32>("ec_manifest_len")?.call(&mut inst.store, ()).map_err(err)?; - let mut text = vec![0u8; len.max(0) as usize]; + if !(0..=MAX_MANIFEST).contains(&len) { + return Err(format!("bad manifest length {len}")); + } + let mut text = vec![0u8; len as usize]; inst.memory.read(&inst.store, ptr as u32 as usize, &mut text).map_err(|e| format!("manifest out of bounds: {e}"))?; let manifest: PluginManifest = serde_json::from_slice(&text).map_err(|e| format!("bad manifest JSON: {e}"))?; manifest.validate()?; diff --git a/crates/project/src/essential.rs b/crates/project/src/essential.rs index 71a38a0c..fcf2cfb3 100644 --- a/crates/project/src/essential.rs +++ b/crates/project/src/essential.rs @@ -47,8 +47,18 @@ pub struct EgControl { #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] #[serde(tag = "type")] pub enum EgKind { - /// A property of a layer of the comp. - Property { layer: LayerId, prop: Uid }, + /// A property of a layer of the comp. `links`: further properties the control drives + /// (they follow the main property in the comp and take the instance's value in every + /// instance). + Property { + layer: LayerId, + prop: Uid, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + links: Vec, + }, + /// A mirror: the property of control `of` shown again (its own name and place in the + /// panel; one value, editing either edits both, in the comp and in every instance). + Mirror { of: u64 }, /// Media Replacement: the footage of a footage layer can be swapped per instance. Media { layer: LayerId }, /// A text note shown in the panel. @@ -60,6 +70,13 @@ pub enum EgKind { }, } +/// A further property driven by a property control. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct EgLink { + pub layer: LayerId, + pub prop: Uid, +} + /// The kind of control a property becomes. #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "lowercase")] @@ -215,16 +232,49 @@ impl EssentialGraphics { } /// The control exposing property `prop` of `layer`, if any. pub fn control_for(&self, layer: LayerId, prop: Uid) -> Option<&EgControl> { - self.flat().into_iter().find(|c| matches!(c.kind, EgKind::Property { layer: l, prop: p } if l == layer && p == prop)) + self.flat().into_iter().find(|c| matches!(c.kind, EgKind::Property { layer: l, prop: p, .. } if l == layer && p == prop)) } /// The control exposing property `prop` of `layer` as `as_type` (`None` = its natural /// control): a property can have one control of each kind (Source Text as Text and Font). pub fn control_for_as(&self, layer: LayerId, prop: Uid, as_type: Option) -> Option<&EgControl> { - self.flat().into_iter().find(|c| c.as_type == as_type && matches!(c.kind, EgKind::Property { layer: l, prop: p } if l == layer && p == prop)) + self.flat().into_iter().find(|c| c.as_type == as_type && matches!(c.kind, EgKind::Property { layer: l, prop: p, .. } if l == layer && p == prop)) } - /// Controls an instance can override (properties and media; not comments or groups). + /// The property control a property drives: its main property or one of its links. + pub fn driver_of(&self, layer: LayerId, prop: Uid) -> Option<&EgControl> { + self.flat().into_iter().find(|c| match &c.kind { + EgKind::Property { layer: l, prop: p, links } => (*l == layer && *p == prop) || links.iter().any(|k| k.layer == layer && k.prop == prop), + _ => false, + }) + } + /// The control a mirror shows (following mirrors of mirrors); the control itself otherwise. + pub fn resolve(&self, id: u64) -> Option<&EgControl> { + let mut c = self.find(id)?; + for _ in 0..8 { + match c.kind { + EgKind::Mirror { of } => c = self.find(of)?, + _ => return Some(c), + } + } + None + } + /// Mirrors of control `id`. + pub fn mirrors_of(&self, id: u64) -> Vec { + self.flat().into_iter().filter(|c| matches!(c.kind, EgKind::Mirror { .. }) && self.resolve(c.id).is_some_and(|m| m.id == id)).map(|c| c.id).collect() + } + /// Controls an instance can override (properties, mirrors and media; not comments or + /// groups). pub fn has_instance_controls(&self) -> bool { - self.flat().iter().any(|c| matches!(c.kind, EgKind::Property { .. } | EgKind::Media { .. })) + self.flat().iter().any(|c| matches!(c.kind, EgKind::Property { .. } | EgKind::Mirror { .. } | EgKind::Media { .. })) + } +} + +impl EgKind { + /// Every property a property control drives: the main one, then the links. + pub fn targets(&self) -> Vec<(LayerId, Uid)> { + match self { + EgKind::Property { layer, prop, links } => std::iter::once((*layer, *prop)).chain(links.iter().map(|k| (k.layer, k.prop))).collect(), + _ => vec![], + } } } @@ -251,13 +301,93 @@ pub fn overridden(layer: &Layer) -> Vec { } } -/// The source property a control exposes, in `project`. +/// The source property a control exposes, in `project` (a mirror's: its master's main +/// property). pub fn source_prop<'a>(project: &'a Project, comp: ItemId, c: &EgControl) -> Option<(&'a Layer, &'a Property)> { - let EgKind::Property { layer, prop } = c.kind else { return None }; + let (layer, prop) = match c.kind { + EgKind::Property { layer, prop, .. } => (layer, prop), + EgKind::Mirror { .. } => match project.comp(comp)?.essential.as_ref()?.resolve(c.id)?.kind { + EgKind::Property { layer, prop, .. } => (layer, prop), + _ => return None, + }, + _ => return None, + }; let l = project.comp(comp)?.layer(layer)?; Some((l, l.props.find(prop)?)) } +/// The linked properties of a property control that exist in `project`: (layer, property). +pub fn linked_props<'a>(project: &'a Project, comp: ItemId, c: &EgControl) -> Vec<(&'a Layer, &'a Property)> { + let Some(cm) = project.comp(comp) else { return vec![] }; + match &c.kind { + EgKind::Property { links, .. } => links.iter().filter_map(|k| cm.layer(k.layer).and_then(|l| l.props.find(k.prop).map(|p| (l, p)))).collect(), + _ => vec![], + } +} + +/// Whether property `b` can be driven by a control showing `a` (the same kind of value). +pub fn linkable(a: &Property, b: &Property) -> bool { + control_type(a).is_some() && control_type(a) == control_type(b) && std::mem::discriminant(&a.value) == std::mem::discriminant(&b.value) +} + +/// The properties a control drives stay equal: when this edit (`before` → `p`) changed one of +/// them, its value and keyframes (moved to each layer's time) go to all the others; otherwise +/// the main property's do. +fn sync_links(before: &Project, p: &mut Project) { + let ids: Vec = p.comps().map(|(i, _)| *i).collect(); + for cid in ids { + let Some(comp) = p.comp(cid) else { continue }; + let Some(eg) = &comp.essential else { continue }; + let old = before.comp(cid); + let mut writes: Vec<(LayerId, Uid, Value, Vec)> = vec![]; + for c in eg.flat() { + let targets = c.kind.targets(); + // Font controls take only the font: they never drive whole values. + if targets.len() < 2 || c.as_type == Some(ControlType::Font) { + continue; + } + let get = |cm: &crate::Comp, (l, u): (LayerId, Uid)| cm.layer(l).and_then(|l| l.props.find(u).cloned()); + let changed = |t: (LayerId, Uid)| match (get(comp, t), old.and_then(|o| get(o, t))) { + (Some(a), Some(b)) => a.value != b.value || a.keys != b.keys, + _ => false, + }; + // The edited property leads (the main one when several changed). + let lead = targets.iter().copied().find(|t| changed(*t)).filter(|t| !changed(targets[0]) || *t == targets[0]).unwrap_or(targets[0]); + let Some(src_l) = comp.layer(lead.0) else { continue }; + let Some(src) = src_l.props.find(lead.1) else { continue }; + for k in targets.iter().filter(|t| **t != lead) { + let Some(l) = comp.layer(k.0) else { continue }; + let Some(dst) = l.props.find(k.1) else { continue }; + if !linkable(src, dst) { + continue; + } + let keys: Vec<_> = src + .keys + .iter() + .map(|kf| { + let mut kf = kf.clone(); + kf.time = l.layer_time(src_l.comp_time(kf.time)); + kf + }) + .collect(); + if dst.value != src.value || dst.keys != keys { + writes.push((k.0, k.1, src.value.clone(), keys)); + } + } + } + if writes.is_empty() { + continue; + } + let Some(c) = p.comp_mut(cid) else { continue }; + for (lid, uid, v, keys) in writes { + if let Some(pr) = c.layer_mut(lid).and_then(|l| l.props.find_mut(uid)) { + pr.value = v; + pr.keys = keys; + } + } + } +} + /// The footage item a media control shows by default (the layer's own source). pub fn source_media(project: &Project, comp: ItemId, c: &EgControl) -> Option { let EgKind::Media { layer } = c.kind else { return None }; @@ -304,7 +434,7 @@ fn build_group(project: &Project, comp: ItemId, eg: &EssentialGraphics, old: Opt g.children = build(project, comp, children, old, over, alloc, keep); out.push(Node::Group(g)); } - EgKind::Property { .. } => { + EgKind::Property { .. } | EgKind::Mirror { .. } => { let Some((_, src)) = source_prop(project, comp, c) else { continue }; let prev = prev.and_then(Node::as_prop); let uid = prev.map(|p| p.uid).unwrap_or_else(&mut *alloc); @@ -339,10 +469,84 @@ fn build_group(project: &Project, comp: ItemId, eg: &EssentialGraphics, old: Opt } let uid = old.map(|g| g.uid).unwrap_or_else(&mut alloc); let mut keep = vec![]; - let children = build(project, comp, &eg.controls, old, &over, &mut alloc, &mut keep); + let mut children = build(project, comp, &eg.controls, old, &over, &mut alloc, &mut keep); + // Mirrors show their master's instance value (and override state). + for c in eg.flat() { + if !matches!(c.kind, EgKind::Mirror { .. }) { + continue; + } + let Some(master) = eg.resolve(c.id) else { continue }; + let Some(src) = find_prop(&children, &match_id(master.id)).cloned() else { continue }; + if let Some(dst) = find_prop_mut(&mut children, &match_id(c.id)) { + dst.value = src.value.clone(); + dst.keys = src.keys.clone(); + dst.expr = src.expr.clone(); + keep.retain(|u| *u != dst.uid); + if keep.contains(&src.uid) { + keep.push(dst.uid); + } + } + } PropGroup { uid, match_id: GROUP.into(), name: "Essential Properties".into(), kind: GroupKind::Essential { overridden: keep }, enabled: true, children } } +fn find_prop<'a>(nodes: &'a [Node], m: &str) -> Option<&'a Property> { + nodes.iter().find_map(|n| match n { + Node::Prop(p) if p.match_id == m => Some(p), + Node::Group(g) => find_prop(&g.children, m), + _ => None, + }) +} + +fn find_prop_mut<'a>(nodes: &'a mut [Node], m: &str) -> Option<&'a mut Property> { + nodes.iter_mut().find_map(|n| match n { + Node::Prop(p) if p.match_id == m => Some(p), + Node::Group(g) => find_prop_mut(&mut g.children, m), + _ => None, + }) +} + +/// An instance's mirror property edited in this edit passes its value to its master (the +/// group build then copies the master back to every mirror). +fn propagate_mirrors(before: &Project, after: &mut Project) { + let ids: Vec = after.comps().map(|(i, _)| *i).collect(); + for cid in ids { + let (Some(comp), Some(old_comp)) = (after.comp(cid), before.comp(cid)) else { continue }; + let mut writes: Vec<(usize, String, Property)> = vec![]; + for (i, l) in comp.layers.iter().enumerate() { + let LayerSource::Comp { item } = l.source else { continue }; + let Some(eg) = after.comp(item).and_then(|c| c.essential.as_ref()) else { continue }; + let (Some(g), Some(og)) = (group(l), old_comp.layer(l.id).and_then(group)) else { continue }; + for c in eg.flat() { + if !matches!(c.kind, EgKind::Mirror { .. }) { + continue; + } + let Some(master) = eg.resolve(c.id) else { continue }; + let (mm, cm) = (match_id(master.id), match_id(c.id)); + let (Some(mp), Some(cp)) = (find_prop(&g.children, &mm), find_prop(&g.children, &cm)) else { continue }; + let (Some(omp), Some(ocp)) = (find_prop(&og.children, &mm), find_prop(&og.children, &cm)) else { continue }; + let changed = |a: &Property, b: &Property| a.value != b.value || a.keys != b.keys || a.expr != b.expr; + if changed(cp, ocp) && !changed(mp, omp) { + writes.push((i, mm, cp.clone())); + } + } + } + if writes.is_empty() { + continue; + } + let Some(c) = after.comp_mut(cid) else { continue }; + for (i, mm, src) in writes { + if let Some(g) = c.layers[i].props.sub_mut(GROUP) + && let Some(dst) = find_prop_mut(&mut g.children, &mm) + { + dst.value = src.value; + dst.keys = src.keys; + dst.expr = src.expr; + } + } + } +} + /// Master properties the user changed in this edit (`before` → `after`) become overridden. fn mark_overrides(before: &Project, after: &mut Project) { let ids: Vec = after.comps().map(|(i, _)| *i).collect(); @@ -390,6 +594,8 @@ fn mark_overrides(before: &Project, after: &mut Project) { /// After an edit: mark changed master properties as overridden, then add, update or remove the /// Essential Properties groups of every precomp layer so they match their comp's controls. pub fn sync_project(before: &Project, after: &mut Project) { + sync_links(before, after); + propagate_mirrors(before, after); mark_overrides(before, after); let ids: Vec = after.comps().map(|(i, _)| *i).collect(); let mut next = after.next_id; @@ -449,26 +655,30 @@ pub fn with_overrides(project: &Project, comp: ItemId, overrides: &[Override]) - let c = p.comp_mut(comp)?; let mut any = false; // Whole values first, then Font controls (which change only the font of whatever text the - // instance shows). - let is_font = |o: &&Override| eg.find(o.control).is_some_and(|c| c.as_type == Some(ControlType::Font)); + // instance shows). A mirror acts as its master control. + let is_font = |o: &&Override| eg.resolve(o.control).is_some_and(|c| c.as_type == Some(ControlType::Font)); let ordered = overrides.iter().filter(|o| !is_font(o)).chain(overrides.iter().filter(is_font)); for o in ordered { - let Some(ctl) = eg.find(o.control) else { continue }; - match ctl.kind { - EgKind::Property { layer, prop } if ctl.as_type == Some(ControlType::Font) => { - if let Some(pr) = c.layer_mut(layer).and_then(|l| l.props.find_mut(prop)) { - merge_font(&mut pr.value, &o.value); - for k in &mut pr.keys { - merge_font(&mut k.value, &o.value); + let Some(ctl) = eg.resolve(o.control) else { continue }; + match &ctl.kind { + EgKind::Property { .. } if ctl.as_type == Some(ControlType::Font) => { + for (layer, prop) in ctl.kind.targets() { + if let Some(pr) = c.layer_mut(layer).and_then(|l| l.props.find_mut(prop)) { + merge_font(&mut pr.value, &o.value); + for k in &mut pr.keys { + merge_font(&mut k.value, &o.value); + } + any = true; } - any = true; } } - EgKind::Property { layer, prop } => { - if let Some(pr) = c.layer_mut(layer).and_then(|l| l.props.find_mut(prop)) { - pr.keys.clear(); - pr.value = o.value.clone(); - any = true; + EgKind::Property { .. } => { + for (layer, prop) in ctl.kind.targets() { + if let Some(pr) = c.layer_mut(layer).and_then(|l| l.props.find_mut(prop)) { + pr.keys.clear(); + pr.value = o.value.clone(); + any = true; + } } } EgKind::Media { layer } => { @@ -477,7 +687,7 @@ pub fn with_overrides(project: &Project, comp: ItemId, overrides: &[Override]) - if !matches!(project.item(item).map(|i| &i.kind), Some(ItemKind::Footage(_))) { continue; } - if let Some(l) = c.layer_mut(layer) { + if let Some(l) = c.layer_mut(*layer) { l.source = LayerSource::Footage { item }; any = true; } @@ -508,10 +718,15 @@ pub fn offset_ids(p: &mut Project, offset: u64) { for c in v { c.id += o; match &mut c.kind { - EgKind::Property { layer, prop } => { + EgKind::Property { layer, prop, links } => { *layer = LayerId(layer.0 + o); *prop += o; + for k in links { + k.layer = LayerId(k.layer.0 + o); + k.prop += o; + } } + EgKind::Mirror { of } => *of += o, EgKind::Media { layer } => *layer = LayerId(layer.0 + o), EgKind::Group { children } => go(children, o), EgKind::Comment { .. } => {} diff --git a/crates/project/src/render_queue.rs b/crates/project/src/render_queue.rs index 35aed0be..c3a5503e 100644 --- a/crates/project/src/render_queue.rs +++ b/crates/project/src/render_queue.rs @@ -515,11 +515,18 @@ pub enum OutputFormat { Wav, /// Audio only: AIFF (16-bit PCM). Aiff, + /// HEVC (H.265) video (+ AAC audio) in MP4 (`hvc1`). + Hevc, + /// AV1 video (+ AAC audio) in MP4 (`av01`). WebM with AV1: [`OutputFormat::WebM`] with + /// [`WebmVideoCodec::Av1`]. + Av1, } impl OutputFormat { - pub const ALL: [OutputFormat; 10] = [ + pub const ALL: [OutputFormat; 12] = [ OutputFormat::H264, + OutputFormat::Hevc, + OutputFormat::Av1, OutputFormat::ProRes, OutputFormat::WebM, OutputFormat::PngSequence, @@ -543,6 +550,8 @@ impl OutputFormat { OutputFormat::WebM => "WebM (VP9 + Opus)", OutputFormat::Wav => "WAV", OutputFormat::Aiff => "AIFF", + OutputFormat::Hevc => "HEVC (H.265)", + OutputFormat::Av1 => "AV1", } } pub fn extension(self) -> &'static str { @@ -557,29 +566,34 @@ impl OutputFormat { OutputFormat::WebM => "webm", OutputFormat::Wav => "wav", OutputFormat::Aiff => "aif", + OutputFormat::Hevc | OutputFormat::Av1 => "mp4", } } pub fn is_sequence(self) -> bool { matches!(self, OutputFormat::PngSequence | OutputFormat::JpegSequence | OutputFormat::TiffSequence | OutputFormat::ExrSequence) } pub fn is_movie(self) -> bool { - matches!(self, OutputFormat::H264 | OutputFormat::ProRes | OutputFormat::WebM) + matches!(self, OutputFormat::H264 | OutputFormat::ProRes | OutputFormat::WebM | OutputFormat::Hevc | OutputFormat::Av1) + } + /// Formats whose video codec options live in [`OutputModule::codec`] (HEVC, AV1). + pub fn has_codec_options(self) -> bool { + matches!(self, OutputFormat::Hevc | OutputFormat::Av1) } /// Audio-only outputs (no video is rendered). pub fn is_audio_only(self) -> bool { matches!(self, OutputFormat::Wav | OutputFormat::Aiff) } pub fn supports_alpha(self) -> bool { - !matches!(self, OutputFormat::H264 | OutputFormat::JpegSequence | OutputFormat::Wav | OutputFormat::Aiff) + !matches!(self, OutputFormat::H264 | OutputFormat::Hevc | OutputFormat::Av1 | OutputFormat::JpegSequence | OutputFormat::Wav | OutputFormat::Aiff) } pub fn supports_audio(self) -> bool { self.is_movie() || self.is_audio_only() } - /// The written frame size for a rendered size: H.264 needs even dimensions (rounded down, + /// The written frame size for a rendered size: H.264 and HEVC need even dimensions (rounded down, /// at least 2). pub fn coded_size(self, w: u32, h: u32) -> (u32, u32) { let even = |v: u32| if v < 2 { 2 } else { v & !1 }; - if self == OutputFormat::H264 { (even(w), even(h)) } else { (w, h) } + if matches!(self, OutputFormat::H264 | OutputFormat::Hevc) { (even(w), even(h)) } else { (w, h) } } /// Parse a format name: `h264`, `mp4`, `prores`, `mov`, `png`, `jpeg`/`jpg`, `tiff`/`tif`, /// `exr`, `gif`, or a label. @@ -587,6 +601,8 @@ impl OutputFormat { let n = s.trim().to_ascii_lowercase().replace([' ', '.', '-', '_'], ""); Some(match n.as_str() { "h264" | "mp4" | "avc" => OutputFormat::H264, + "hevc" | "h265" | "hevch265" | "hvc1" | "x265" => OutputFormat::Hevc, + "av1" | "av01" | "mp4av1" => OutputFormat::Av1, "prores" | "mov" | "quicktime" | "quicktimeprores" => OutputFormat::ProRes, "png" | "pngsequence" => OutputFormat::PngSequence, "jpg" | "jpeg" | "jpegsequence" => OutputFormat::JpegSequence, @@ -800,6 +816,155 @@ impl ProResProfile { } } +/// HEVC / AV1 profile: 8-bit (HEVC Main, AV1 Main 8-bit) or 10-bit (HEVC Main 10, AV1 Main 10-bit). +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum CodecProfile { + #[default] + Main, + Main10, +} + +impl CodecProfile { + pub const ALL: [CodecProfile; 2] = [CodecProfile::Main, CodecProfile::Main10]; + pub fn label(self) -> &'static str { + match self { + CodecProfile::Main => "Main", + CodecProfile::Main10 => "Main 10", + } + } + pub fn bit_depth(self) -> u8 { + match self { + CodecProfile::Main => 8, + CodecProfile::Main10 => 10, + } + } + pub fn from_name(s: &str) -> Option { + match s.to_ascii_lowercase().replace([' ', '-', '_'], "").as_str() { + "main" | "main8" | "8" | "8bit" => Some(CodecProfile::Main), + "main10" | "10" | "10bit" => Some(CodecProfile::Main10), + _ => None, + } + } +} + +/// HEVC / AV1 rate control: a target bitrate ([`OutputModule::bitrate_kbps`]) or constant +/// quality ([`VideoCodecOptions::quality`], CRF-like). +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum RateControlMode { + #[default] + Bitrate, + Quality, +} + +impl RateControlMode { + pub fn label(self) -> &'static str { + match self { + RateControlMode::Bitrate => "Target Bitrate", + RateControlMode::Quality => "Constant Quality", + } + } + pub fn from_name(s: &str) -> Option { + match s.to_ascii_lowercase().replace([' ', '-', '_'], "").as_str() { + "bitrate" | "vbr" | "abr" | "targetbitrate" => Some(RateControlMode::Bitrate), + "quality" | "crf" | "cq" | "constantquality" | "qp" => Some(RateControlMode::Quality), + _ => None, + } + } +} + +/// Video codec options of the HEVC and AV1 formats (and AV1 in WebM). +#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(default)] +pub struct VideoCodecOptions { + pub profile: CodecProfile, + /// Level × 10 (`41` = level 4.1); `None` picks the lowest level that fits the frame size and rate. + pub level: Option, + pub rate_control: RateControlMode, + /// Constant-quality setting 1–100 (100 ≈ visually lossless), used with [`RateControlMode::Quality`]. + pub quality: u8, +} + +impl Default for VideoCodecOptions { + fn default() -> Self { + VideoCodecOptions { profile: CodecProfile::Main, level: None, rate_control: RateControlMode::Bitrate, quality: 70 } + } +} + +impl VideoCodecOptions { + /// HEVC levels (× 10) offered in the UI (Main tier). + pub const HEVC_LEVELS: [u8; 13] = [10, 20, 21, 30, 31, 40, 41, 50, 51, 52, 60, 61, 62]; + /// AV1 levels (× 10) offered in the UI. + pub const AV1_LEVELS: [u8; 14] = [20, 21, 30, 31, 40, 41, 50, 51, 52, 53, 60, 61, 62, 63]; + /// HEVC QP (0–51) for the constant-quality setting. + pub fn hevc_qp(&self) -> u8 { + (4.0 + (100 - self.quality.clamp(1, 100)) as f64 * 0.45).round() as u8 + } + /// AV1 `base_q_idx` (1–255) for the constant-quality setting. + pub fn av1_qindex(&self) -> u8 { + (4.0 + (100 - self.quality.clamp(1, 100)) as f64 * 2.4).round() as u8 + } + /// `general_level_idc` (30 × level). + pub fn hevc_level_idc(&self) -> Option { + self.level.map(|l| l.saturating_mul(3)) + } + /// AV1 `seq_level_idx`: (major − 2) × 4 + minor. + pub fn av1_level_idx(&self) -> Option { + self.level.map(|l| ((l / 10).clamp(2, 7) - 2) * 4 + (l % 10).min(3)) + } + pub fn level_label(&self) -> String { + match self.level { + None => "Auto".into(), + Some(l) => format!("{}.{}", l / 10, l % 10), + } + } + /// Parse `"auto"`, `"4.1"`, `"41"` (level × 10). `None` when unparsable; `Some(None)` = auto. + pub fn parse_level(s: &str) -> Option> { + let s = s.trim().to_ascii_lowercase(); + if s == "auto" || s.is_empty() { + return Some(None); + } + let v: f64 = s.trim_start_matches('l').parse().ok()?; + let l = if v >= 10.0 { v.round() } else { (v * 10.0).round() }; + (10.0..=73.0).contains(&l).then_some(Some(l as u8)) + } +} + +/// The video codec of a WebM output. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum WebmVideoCodec { + #[default] + Vp9, + /// AV1 (`V_AV1`) with the [`OutputModule::codec`] options; no alpha. + Av1, +} + +impl WebmVideoCodec { + pub fn label(self) -> &'static str { + match self { + WebmVideoCodec::Vp9 => "VP9", + WebmVideoCodec::Av1 => "AV1", + } + } +} + +/// Opus coding application: general audio (music and mixed content) or voice (speech-tuned +/// SILK / hybrid coding at low bitrates). +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum OpusApplication { + #[default] + Audio, + Voip, +} + +impl OpusApplication { + pub fn label(self) -> &'static str { + match self { + OpusApplication::Audio => "Audio", + OpusApplication::Voip => "Voice", + } + } +} + #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] #[serde(default)] pub struct OutputModule { @@ -814,7 +979,7 @@ pub struct OutputModule { pub output: String, /// JPEG / WebM quality 1–100. pub quality: u8, - /// H.264 target bitrate (and WebM's when `webm_bitrate`). + /// H.264 target bitrate (and WebM's when `webm_bitrate`; HEVC / AV1 with [`RateControlMode::Bitrate`]). pub bitrate_kbps: u32, /// WebM: rate control by `bitrate_kbps` instead of `quality`. pub webm_bitrate: bool, @@ -830,6 +995,13 @@ pub struct OutputModule { pub audio_format: AudioFormat, /// GIF: loop forever. pub gif_loop: bool, + /// HEVC / AV1 options. + pub codec: VideoCodecOptions, + /// WebM video codec. + pub webm_codec: WebmVideoCodec, + /// WebM Opus audio bitrate (stereo total), kbit/s. + pub opus_bitrate_kbps: u32, + pub opus_application: OpusApplication, /// Include Project Link: kept for parity; EffectCraft's writers have no place to store a /// link back to the project, so it changes nothing in the file. pub include_project_link: bool, @@ -855,6 +1027,10 @@ impl Default for OutputModule { audio_channels: 2, audio_format: AudioFormat::S16, gif_loop: true, + codec: VideoCodecOptions::default(), + webm_codec: WebmVideoCodec::Vp9, + opus_bitrate_kbps: 192, + opus_application: OpusApplication::Audio, include_project_link: true, } } @@ -941,7 +1117,7 @@ impl OutputModule { pub fn set_format(&mut self, format: OutputFormat) { let old = self.format; self.format = format; - if !format.supports_alpha() && self.channels == Channels::Rgba { + if !self.supports_alpha() && self.channels == Channels::Rgba { self.channels = Channels::Rgb; } if self.output.contains("[fileExtension]") { @@ -961,6 +1137,24 @@ impl OutputModule { } self.output = format!("{stem}.{}", format.extension()); } + /// Whether this module can write alpha (the format can, and WebM uses VP9). + pub fn supports_alpha(&self) -> bool { + self.format.supports_alpha() && !(self.format == OutputFormat::WebM && self.webm_codec == WebmVideoCodec::Av1) + } + /// The key-frame interval in frames at `fps` ([`OutputModule::keyframe_interval`]; automatic: 2 s). + pub fn keyint(&self, fps: f64) -> u32 { + if self.keyframe_interval > 0 { self.keyframe_interval } else { (fps * 2.0).round().max(1.0) as u32 } + } + /// Codec options summary: `Main · 10000 kbps · Level Auto · Key every auto`. + fn codec_summary(&self) -> String { + let c = &self.codec; + let rate = match c.rate_control { + RateControlMode::Bitrate => format!("{} kbps", self.bitrate_kbps), + RateControlMode::Quality => format!("Quality {}", c.quality), + }; + let gop = if self.keyframe_interval == 0 { "auto".to_string() } else { self.keyframe_interval.to_string() }; + format!("{} · {rate} · Level {} · Key every {gop}", c.profile.label(), c.level_label()) + } /// One-line summary shown next to "Output Module:". pub fn summary(&self) -> String { if !self.name.is_empty() { @@ -973,6 +1167,10 @@ impl OutputModule { "{} · {ch}", if self.channels == Channels::Rgba && !self.prores_profile.is_4444() { "Apple ProRes 4444" } else { self.prores_profile.label() } ), + OutputFormat::Hevc | OutputFormat::Av1 => format!("{} · {}", self.format.label(), self.codec_summary()), + OutputFormat::WebM if self.webm_codec == WebmVideoCodec::Av1 => { + format!("WebM (AV1 + Opus {} kbps) · {}", self.opus_bitrate_kbps, self.codec_summary()) + } f => format!("{} · {ch}", f.label()), } } @@ -1353,4 +1551,42 @@ mod tests { assert_eq!(s.first_frame(&c), 30); assert!(s.frame_time(&c, 0) >= Tick::from_seconds_f64(1.0)); } + + #[test] + fn hevc_av1_codec_options() { + assert_eq!(OutputFormat::from_name("H.265"), Some(OutputFormat::Hevc)); + assert_eq!(OutputFormat::from_name("hevc"), Some(OutputFormat::Hevc)); + assert_eq!(OutputFormat::from_name("AV1"), Some(OutputFormat::Av1)); + assert_eq!(OutputFormat::Hevc.coded_size(33, 17), (32, 16)); + assert_eq!(OutputFormat::Av1.coded_size(33, 17), (33, 17)); + assert!(!OutputFormat::Hevc.supports_alpha() && OutputFormat::Av1.is_movie() && OutputFormat::Av1.supports_audio()); + assert_eq!(VideoCodecOptions::parse_level("4.1"), Some(Some(41))); + assert_eq!(VideoCodecOptions::parse_level("51"), Some(Some(51))); + assert_eq!(VideoCodecOptions::parse_level("auto"), Some(None)); + assert_eq!(VideoCodecOptions::parse_level("9"), None); + let mut o = VideoCodecOptions { level: Some(41), ..Default::default() }; + assert_eq!((o.hevc_level_idc(), o.av1_level_idx()), (Some(123), Some(9))); + o.level = Some(20); + assert_eq!(o.av1_level_idx(), Some(0)); + o.quality = 100; + assert_eq!((o.hevc_qp(), o.av1_qindex()), (4, 4)); + o.quality = 1; + assert!(o.hevc_qp() <= 51 && o.av1_qindex() >= 240); + // WebM with AV1 has no alpha; switching drops RGB + Alpha. + let mut m = OutputModule::for_format(OutputFormat::WebM); + m.channels = Channels::Rgba; + assert!(m.supports_alpha()); + m.webm_codec = WebmVideoCodec::Av1; + assert!(!m.supports_alpha()); + assert!(m.summary().starts_with("WebM (AV1 + Opus 192 kbps)"), "{}", m.summary()); + let mut h = OutputModule::for_format(OutputFormat::Hevc); + assert_eq!(h.keyint(29.97), 60); + h.keyframe_interval = 1; + assert_eq!(h.keyint(29.97), 1); + h.keyframe_interval = 0; + assert_eq!(h.summary(), "HEVC (H.265) · Main · 10000 kbps · Level Auto · Key every auto"); + // Older projects (no codec fields) load with the defaults. + let old: OutputModule = serde_json::from_str(r#"{"format":"H264","bitrate_kbps":5000}"#).unwrap(); + assert_eq!((old.codec, old.webm_codec, old.opus_bitrate_kbps), (VideoCodecOptions::default(), WebmVideoCodec::Vp9, 192)); + } } diff --git a/crates/render/src/lib.rs b/crates/render/src/lib.rs index de6ff8df..e76afb71 100644 --- a/crates/render/src/lib.rs +++ b/crates/render/src/lib.rs @@ -278,6 +278,12 @@ pub trait Accelerator: Send + Sync { fn raster_3d(&self, _scene: &three_d::adv::Scene) -> Option { None } + /// Render a whole prepared Advanced 3D run (every motion-blur sub-sample rasterised and + /// resolved, averaged, depth of field, encoding) with [`three_d::adv::render_prepared`]'s + /// semantics. `None` = not handled (the CPU path runs, using [`Self::raster_3d`]). + fn render_3d(&self, _run: &three_d::adv::Prepared) -> Option { + None + } /// A particle simulation backend (GPU particles) for the stepped particle effects, with /// the CPU simulation's semantics. `None` = they simulate on the CPU. fn particles(&self) -> Option<&dyn effectcraft_effects::psim::ParticleSim> { @@ -592,22 +598,8 @@ impl<'a> Renderer<'a> { /// Draw a Wireframe-quality layer: its bounds as a one-pixel outline. fn draw_wireframe(&self, ctx: &EvalCtx, layer: &Layer, canvas: &mut Image) { - let Some(b) = content_bounds(ctx, layer) else { return }; - let s = self.opts.scale; - let (l2c, _) = ctx.layer_to_comp(layer); - let m = Mat3::scale(vec2(s, s)) * self.outer.map_or(l2c, |o| o * l2c); - let c = [(b[0], b[1]), (b[2], b[1]), (b[2], b[3]), (b[0], b[3])].map(|(x, y)| m.apply(vec2(x, y))); - let (w, h) = (canvas.width as i64, canvas.height as i64); - for i in 0..4 { - let (p, q) = (c[i], c[(i + 1) % 4]); - let n = ((q.x - p.x).abs().max((q.y - p.y).abs()) * 2.0).ceil().clamp(1.0, 1.0e5) as usize; - for j in 0..=n { - let t = j as f64 / n as f64; - let (x, y) = ((p.x + (q.x - p.x) * t).floor() as i64, (p.y + (q.y - p.y) * t).floor() as i64); - if x >= 0 && y >= 0 && x < w && y < h { - canvas.set(x as u32, y as u32, [1.0, 1.0, 1.0, 1.0]); - } - } + for (x, y) in self.wireframe_pixels(ctx, layer, (canvas.width, canvas.height)) { + canvas.set(x, y, [1.0, 1.0, 1.0, 1.0]); } } @@ -1571,6 +1563,39 @@ impl<'a> Renderer<'a> { three_d::compose::gpu_run(self, ctx, run, out) } + /// A run of consecutive 3D layers of an Advanced 3D comp prepared for an accelerator's + /// compositor ([`three_d::adv::Prepared`]): rendered as one scene and composited (Normal, + /// premultiplied over) onto the canvas. `None` = not an Advanced 3D comp, or a layer needs the + /// 2D compositing path (blend mode, track matte, Preserve Transparency): draw it with + /// [`Self::draw_3d_run`]. + pub fn prepare_adv_run<'p>(&'p self, ctx: &'p EvalCtx<'a>, run: &'p [&'p Layer], out: (u32, u32)) -> Option> { + three_d::adv::prepare_run(self, ctx, run, out) + } + + /// The pixels a Wireframe-quality layer sets (white, opaque) on a `size` canvas: its + /// bounds' outline, one pixel wide. Empty when the layer has no bounds. + pub fn wireframe_pixels(&self, ctx: &EvalCtx, layer: &Layer, size: (u32, u32)) -> Vec<(u32, u32)> { + let Some(b) = content_bounds(ctx, layer) else { return vec![] }; + let s = self.opts.scale; + let (l2c, _) = ctx.layer_to_comp(layer); + let m = Mat3::scale(vec2(s, s)) * self.outer.map_or(l2c, |o| o * l2c); + let c = [(b[0], b[1]), (b[2], b[1]), (b[2], b[3]), (b[0], b[3])].map(|(x, y)| m.apply(vec2(x, y))); + let (w, h) = (size.0 as i64, size.1 as i64); + let mut out = vec![]; + for i in 0..4 { + let (p, q) = (c[i], c[(i + 1) % 4]); + let n = ((q.x - p.x).abs().max((q.y - p.y).abs()) * 2.0).ceil().clamp(1.0, 1.0e5) as usize; + for j in 0..=n { + let t = j as f64 / n as f64; + let (x, y) = ((p.x + (q.x - p.x) * t).floor() as i64, (p.y + (q.y - p.y) * t).floor() as i64); + if x >= 0 && y >= 0 && x < w && y < h { + out.push((x as u32, y as u32)); + } + } + } + out + } + /// Run the layer's effect stack on `target` (an accelerator's resident image): GPU-capable /// runs through [`FxTarget::gpu`], the others through [`FxTarget::cpu`], with the CPU's /// parameters and environment. `adjustment`: the stack runs on the comp below (adjustment diff --git a/crates/render/src/text.rs b/crates/render/src/text.rs index ca37a951..3bfb5593 100644 --- a/crates/render/src/text.rs +++ b/crates/render/src/text.rs @@ -568,8 +568,17 @@ pub fn text_geom(ctx: &EvalCtx, layer: &Layer) -> Option { (lay, xfs) }; - // Tracking (before / after each character) and Line Anchor, per line. + // Variable Font Axes: the advance at the animated design-space position (later characters + // on the line move with it). let n = lay.glyphs.len(); + let vadv: Vec = (0..n) + .map(|gi| match (subs[gi], xfs[gi].axis_deltas()) { + (None, d) if !d.is_empty() => effectcraft_text::variable::char_advance_delta(&lay.glyphs[gi], &d).unwrap_or(0.0), + _ => 0.0, + }) + .collect(); + let advance = |gi: usize| lay.glyphs[gi].advance + vadv[gi]; + // Tracking (before / after each character) and Line Anchor, per line. let mut shift = vec![0.0f64; n]; let mut li_start = 0; while li_start < n { @@ -585,7 +594,7 @@ pub fn text_geom(ctx: &EvalCtx, layer: &Layer) -> Option { let em = lay.styles.get(lay.glyphs[gi].run).map_or(doc.size, |s| s.size) / 1000.0; pen += x.track_before * em; shift[gi] = pen; - pen += x.track_after * em; + pen += x.track_after * em + vadv[gi]; if x.line_anchor_k > la_k { la = x.line_anchor; la_k = x.line_anchor_k; @@ -622,7 +631,7 @@ pub fn text_geom(ctx: &EvalCtx, layer: &Layer) -> Option { let o = origin(gi); let e = extents.entry(group_key(g)).or_insert([f64::INFINITY, f64::INFINITY, f64::NEG_INFINITY, f64::NEG_INFINITY]); e[0] = e[0].min(o[0]); - e[2] = e[2].max(o[0] + g.advance); + e[2] = e[2].max(o[0] + advance(gi)); e[1] = e[1].min(o[1]); e[3] = e[3].max(o[1]); } @@ -645,7 +654,7 @@ pub fn text_geom(ctx: &EvalCtx, layer: &Layer) -> Option { while j < n && lay.glyphs[j].line_index == line { j += 1; } - let xs: Vec = (i..j).map(|gi| origin(gi)[0] + lay.glyphs[gi].advance / 2.0).collect(); + let xs: Vec = (i..j).map(|gi| origin(gi)[0] + advance(gi) / 2.0).collect(); let lb = lay.line_boxes.get(line).copied().unwrap_or([0.0; 4]); let pos = path_text::arc_positions(&xs, [lb[0], lb[0] + lb[2]], align, pm.length(), *fm, *lm, *force); arc[i..j].copy_from_slice(&pos); @@ -684,7 +693,7 @@ pub fn text_geom(ctx: &EvalCtx, layer: &Layer) -> Option { } // Variable Font Axes: the outline redrawn at the animated design-space position. None => match x.axis_deltas() { - d if !d.is_empty() => (effectcraft_text::variable::char_outline_varied(g, &d).unwrap_or_else(|| g.path.clone()), g.advance), + d if !d.is_empty() => (effectcraft_text::variable::char_outline_varied(g, &d).unwrap_or_else(|| g.path.clone()), advance(gi)), _ => (g.path.clone(), g.advance), }, }; diff --git a/crates/render/src/three_d/adv/mod.rs b/crates/render/src/three_d/adv/mod.rs index 4bd3fd04..0fae1be7 100644 --- a/crates/render/src/three_d/adv/mod.rs +++ b/crates/render/src/three_d/adv/mod.rs @@ -99,32 +99,81 @@ fn mb_samples(r: &Renderer, ctx: &EvalCtx, layers: &[&Layer]) -> usize { if r.opts.draft { 4 } else { ctx.comp.motion_blur_samples.clamp(2, 64) as usize } } -/// Render `layers` as one Advanced 3D scene: premultiplied pixels in the canvas's encoding and -/// the camera depth of each pixel (∞ where nothing was drawn). With motion blur the scene is -/// rendered at sub-samples spread over the comp's shutter (angle and phase) and averaged; the -/// depth is the nearest over the samples. -pub(crate) fn render_layers(r: &Renderer, ctx: &EvalCtx, layers: &[&Layer], out: (u32, u32)) -> Option<(Image, Vec)> { - let n = mb_samples(r, ctx, layers); - let (mut img, depth, linear) = if n <= 1 { - let s = scene::build(r, ctx, layers, out); +/// An Advanced 3D run prepared for an accelerator ([`crate::Accelerator::render_3d`], the GPU +/// compositor's walk): the motion-blur sub-sample scenes (built on demand, one at a time), the +/// camera's depth of field and the output encoding. The accelerator rasterises every +/// sub-sample, resolves the supersampling, averages the sub-samples (nearest depth), applies +/// the depth of field and encodes, as [`render_prepared`] does on the CPU. +pub struct Prepared<'p, 'a> { + r: &'p Renderer<'a>, + ctx: &'p EvalCtx<'a>, + layers: &'p [&'p Layer], + /// Output size (pixels). + pub out: (u32, u32), + /// Motion-blur sub-samples (1 = none). + pub samples: usize, + /// Depth of field (the camera at the frame time; `None` = off, Draft 3D or draft renders). + pub dof: Option, + /// Render scale (circles of confusion are in comp pixels). + pub scale: f64, + /// The scene is in the working encoding already (linear projects): no sRGB encode. + pub linear: bool, +} + +impl<'p, 'a> Prepared<'p, 'a> { + pub fn new(r: &'p Renderer<'a>, ctx: &'p EvalCtx<'a>, layers: &'p [&'p Layer], out: (u32, u32)) -> Self { + let cam = camera_for(r, ctx); + Prepared { + r, + ctx, + layers, + out, + samples: mb_samples(r, ctx, layers), + dof: cam.dof.filter(|_| !ctx.comp.draft_3d && !r.opts.draft), + scale: r.opts.scale, + linear: r.pipe.linear || r.pipe.linear_blend, + } + } + + /// The scene of sub-sample `i` (`0..samples`), spread over the comp's shutter (angle and + /// phase). + pub fn scene(&self, i: usize) -> Scene { + let n = self.samples; + if n <= 1 { + return scene::build(self.r, self.ctx, self.layers, self.out); + } + let ctx = self.ctx; + let fd = ctx.comp.frame_duration().seconds(); + let (angle, phase) = (ctx.comp.shutter_angle / 360.0, ctx.comp.shutter_phase / 360.0); + let f = phase + angle * i as f64 / (n - 1) as f64; + let sub = ctx.at(ctx.time + effectcraft_time::Tick::from_seconds_f64(f * fd)); + scene::build_at(self.r, ctx, Some(&sub), self.layers, self.out) + } +} + +/// What an accelerator returns for a [`Prepared`] run: `None` when nothing was drawn, else the +/// premultiplied image in the canvas's encoding and the camera depth of each pixel (∞ where +/// nothing was drawn; the nearest over the motion-blur sub-samples). +pub type Rendered = Option<(Image, Vec)>; + +/// Render a prepared run on the CPU compositor (each scene rasterised by the accelerator's +/// [`crate::Accelerator::raster_3d`] when there is one): the reference for +/// [`crate::Accelerator::render_3d`]. +pub fn render_prepared(p: &Prepared) -> Rendered { + let (r, n, out) = (p.r, p.samples, p.out); + let (mut img, depth) = if n <= 1 { + let s = p.scene(0); if s.indices.is_empty() { return None; } - let (img, depth) = raster_resolved(r, &s); - (img, depth, s.linear_io) + raster_resolved(r, &s) } else { - let fd = ctx.comp.frame_duration().seconds(); - let (angle, phase) = (ctx.comp.shutter_angle / 360.0, ctx.comp.shutter_phase / 360.0); let mut acc = Image::new(out.0, out.1); let mut depth = vec![f32::INFINITY; (out.0 * out.1) as usize]; - let mut linear = false; let mut any = false; let k = 1.0 / n as f32; for i in 0..n { - let f = phase + angle * i as f64 / (n - 1) as f64; - let sub = ctx.at(ctx.time + effectcraft_time::Tick::from_seconds_f64(f * fd)); - let s = scene::build_at(r, ctx, Some(&sub), layers, out); - linear = s.linear_io; + let s = p.scene(i); if s.indices.is_empty() { continue; } @@ -140,18 +189,43 @@ pub(crate) fn render_layers(r: &Renderer, ctx: &EvalCtx, layers: &[&Layer], out: if !any { return None; } - (acc, depth, linear) + (acc, depth) }; - let cam = camera_for(r, ctx); - if let Some(dof) = cam.dof.filter(|_| !ctx.comp.draft_3d && !r.opts.draft) { - img = depth_of_field(&img, &depth, &dof, r.opts.scale); + if let Some(dof) = &p.dof { + img = depth_of_field(&img, &depth, dof, p.scale); } - if !linear { + if !p.linear { encode(&mut img); } Some((img, depth)) } +/// Render `layers` as one Advanced 3D scene: premultiplied pixels in the canvas's encoding and +/// the camera depth of each pixel (∞ where nothing was drawn). With motion blur the scene is +/// rendered at sub-samples spread over the comp's shutter (angle and phase) and averaged; the +/// depth is the nearest over the samples. The whole run goes to the accelerator when it +/// implements [`crate::Accelerator::render_3d`]. +pub(crate) fn render_layers(r: &Renderer, ctx: &EvalCtx, layers: &[&Layer], out: (u32, u32)) -> Option<(Image, Vec)> { + let p = Prepared::new(r, ctx, layers, out); + if let Some(a) = r.active_accel() + && let Some(res) = a.render_3d(&p) + { + return res; + } + render_prepared(&p) +} + +/// The run as one prepared scene for an accelerator's compositor when it can be composited +/// directly over the canvas (Normal-mode layers without track mattes or Preserve Transparency; +/// those go through [`draw_run`]'s 2D path; environment backgrounds draw their sky first). `None` +/// when the comp isn't Advanced 3D. +pub(crate) fn prepare_run<'p, 'a>(r: &'p Renderer<'a>, ctx: &'p EvalCtx<'a>, run: &'p [&'p Layer], out: (u32, u32)) -> Option> { + if !active(r, ctx) || run.iter().any(|l| l.environment_background || needs_2d_composite(ctx, l)) { + return None; + } + Some(Prepared::new(r, ctx, run, out)) +} + /// Layers whose blend mode, track matte or Preserve Transparency needs the 2D compositing path. fn needs_2d_composite(ctx: &EvalCtx, l: &Layer) -> bool { l.blend_mode != effectcraft_color::BlendMode::Normal diff --git a/crates/render/src/three_d/adv/scene.rs b/crates/render/src/three_d/adv/scene.rs index 5ec3e8c2..1570c2c6 100644 --- a/crates/render/src/three_d/adv/scene.rs +++ b/crates/render/src/three_d/adv/scene.rs @@ -311,11 +311,16 @@ impl Builder { let wn = n.apply_vec(vec3(nn[0] as f64, nn[1] as f64, nn[2] as f64)).normalize(); let t = p.tangents.get(k).copied().unwrap_or([1.0, 0.0, 0.0, 1.0]); let wt = m.apply_vec(vec3(t[0] as f64, t[1] as f64, t[2] as f64)).normalize(); + // Degenerate normals and tangents (zero length: NaN once normalised) get the shader's + // fallbacks here, so every rasteriser shades them alike (GPU compilers may not keep + // NaN comparisons). + let finite = |v: Vec3, or: [f32; 3]| if v.x.is_finite() && v.y.is_finite() && v.z.is_finite() { [v.x as f32, v.y as f32, v.z as f32] } else { or }; + let [tx, ty, tz] = finite(wt, [1.0, 0.0, 0.0]); self.s.vertices.push(Vertex { pos: [w.x as f32, w.y as f32, w.z as f32], - normal: [wn.x as f32, wn.y as f32, wn.z as f32], + normal: finite(wn, [0.0, 0.0, -1.0]), uv: p.uvs.get(k).copied().unwrap_or([0.0, 0.0]), - tangent: [wt.x as f32, wt.y as f32, wt.z as f32, if det < 0.0 { -t[3] } else { t[3] }], + tangent: [tx, ty, tz, if det < 0.0 { -t[3] } else { t[3] }], material: mat, }); } diff --git a/crates/script/src/prelude.js b/crates/script/src/prelude.js index 897beeaa..c06bc9bf 100644 --- a/crates/script/src/prelude.js +++ b/crates/script/src/prelude.js @@ -1439,7 +1439,7 @@ CompItem.prototype.constructor = CompItem; var out = []; (function walk(list) { for (var i = 0; i < list.length; i++) { - if (list[i].kind === "property" || list[i].kind === "media") out.push(list[i]); + if (list[i].kind === "property" || list[i].kind === "mirror" || list[i].kind === "media") out.push(list[i]); if (list[i].children) walk(list[i].children); } })(this.__eg().controls); diff --git a/crates/script/src/scriptui.js b/crates/script/src/scriptui.js index 57e0f445..8c1c09ed 100644 --- a/crates/script/src/scriptui.js +++ b/crates/script/src/scriptui.js @@ -143,6 +143,12 @@ function __uiParseRes(src) { ws(); if (s.charAt(i) === ",") { i++; + // A trailing comma, as JavaScript allows. + ws(); + if (s.charAt(i) === "]") { + i++; + return a; + } continue; } if (s.charAt(i) === "]") { @@ -281,9 +287,34 @@ function DropDownList() {} function ListBox() {} function TabbedPanel() {} function Tab() {} -function ScriptUIImage() {} +// ScriptUIImage: an image file (`src`) or embedded image bytes (`data`, a binary string as in +// resource-string images: one character per byte). +function ScriptUIImage(src, data, name) { + this.__img = true; + this.src = src || null; + this.data = data || null; + this.name = name || (src ? String(src).replace(/^.*[\\/]/, "") : ""); + this.pathname = src || ""; + this.format = data ? (data.charCodeAt(0) === 0x89 ? "png" : "jpeg") : String(src || "").replace(/^.*\./, "").toLowerCase(); +} +ScriptUIImage.prototype.toString = function () { return "[object ScriptUIImage]"; }; function ListItem() {} +// Anything ScriptUI accepts as an image: a ScriptUIImage, a File, a path (or resource name), or +// a binary string holding PNG / JPEG bytes. +function __uiImage(v) { + if (v === undefined || v === null || typeof v === "number" || typeof v === "boolean") return null; + if (v.__img) return v; + if (v.fsName !== undefined) return new ScriptUIImage(String(v.fsName), null, String(v.name || "")); + var s = String(v); + if ((s.charCodeAt(0) === 0x89 && s.substr(1, 3) === "PNG") || (s.charCodeAt(0) === 0xff && s.charCodeAt(1) === 0xd8)) return new ScriptUIImage(null, s, ""); + return s ? new ScriptUIImage(s, null, "") : null; +} +function __uiImageJson(v) { + var img = __uiImage(v); + return img ? { src: img.src, data: img.data, name: img.name } : null; +} + var ScriptUI = { version: "6.2.2", frameworkName: "EffectCraft", @@ -293,7 +324,7 @@ var ScriptUI = { PenType: { SOLID_COLOR: 0, THEME_COLOR: 1 }, Alignment: { LEFT: "left", RIGHT: "right", TOP: "top", BOTTOM: "bottom", CENTER: "center", FILL: "fill" }, newFont: function (name, style, size) { return { name: name, style: style, size: size }; }, - newImage: function () { return {}; }, + newImage: function (normal) { return __uiImage(normal); }, getResourceText: function (t) { return t; }, events: { createEvent: function (t) { return { type: t }; } } }; @@ -347,7 +378,7 @@ function __uiGraphics() { this.__push({ op: "text", text: String(text), color: pen ? pen.color : null, x: __num(x || 0), y: __num(y || 0), size: __uiFontSize(f, this), style: String(f && f.style !== undefined ? f.style : "") }); }, drawOSControl: function () { this.__push({ op: "os" }); }, - drawImage: function (img, x, y, w, h) { this.__push({ op: "image", x: __num(x || 0), y: __num(y || 0), w: w === undefined ? 0 : __num(w), h: h === undefined ? 0 : __num(h) }); }, + drawImage: function (img, x, y, w, h) { this.__push({ op: "image", image: __uiImageJson(img), x: __num(x || 0), y: __num(y || 0), w: w === undefined ? 0 : __num(w), h: h === undefined ? 0 : __num(h) }); }, drawFocusRing: function () {}, measureString: function (s, font, boundaryWidth) { var size = __uiFontSize(font, this); @@ -397,6 +428,11 @@ function __uiInit(o, type, win, parent, bounds, text, props) { o.__wid = __uiNewId(); } o.properties = props || {}; + // Image controls and icon buttons take an image where other controls take their text. + if ((type === "image" || type === "iconbutton") && __uiImage(text)) { + o.image = __uiImage(text); + text = ""; + } o.text = text === undefined || text === null ? "" : String(text); o.enabled = true; o.visible = true; @@ -459,7 +495,8 @@ function __uiProto() { if (!ctor || t === "window") throw __err("add(): unknown control type " + type); var c = Object.create(__ecGlobal[ctor].prototype); // Value arguments after `text`: slider (value, min, max), progressbar (value, max), lists (items). - __uiInit(c, t, this.window, this, bounds, typeof text === "object" && text !== null && !(text instanceof Array) ? "" : text, props); + var arg = t === "image" || t === "iconbutton" ? text : typeof text === "object" && text !== null && !(text instanceof Array) ? "" : text; + __uiInit(c, t, this.window, this, bounds, arg, props); if ((t === "dropdownlist" || t === "listbox") && text instanceof Array) { for (var i = 0; i < text.length; i++) c.add("item", text[i]); c.text = ""; @@ -718,6 +755,7 @@ function __uiJson(c) { } if (c.type === "tabbedpanel") o.activeTab = c.__sel.length ? c.__sel[0] : 0; if (c.__draw && typeof c.onDraw === "function") o.draw = c.__draw; + if ((c.type === "image" || c.type === "iconbutton") && c.image) o.image = __uiImageJson(c.image); if (c.type === "edittext") { o.multiline = !!c.__multiline; o.readOnly = !!c.__readonly; diff --git a/crates/script/src/socket.rs b/crates/script/src/socket.rs index 8b1074d4..6ac0d41d 100644 --- a/crates/script/src/socket.rs +++ b/crates/script/src/socket.rs @@ -144,7 +144,10 @@ mod imp { err(format!("cannot connect to {host}: {last}")) } "listen" => { - let port = p.get("port").and_then(J::as_u64).unwrap_or(0) as u16; + let port = match p.get("port").and_then(J::as_f64).unwrap_or(0.0) { + v if v.fract() == 0.0 && (0.0..=65535.0).contains(&v) => v as u16, + v => return err(format!("bad port {v} (0 to 65535)")), + }; match TcpListener::bind(("127.0.0.1", port)).and_then(|l| l.set_nonblocking(true).map(|_| l)) { Ok(l) => { let port = l.local_addr().map(|a| a.port()).unwrap_or(port); diff --git a/crates/script/src/tests.rs b/crates/script/src/tests.rs index 458f2b41..151ab57f 100644 --- a/crates/script/src/tests.rs +++ b/crates/script/src/tests.rs @@ -446,3 +446,28 @@ fn layer_switches_timing_and_comp_settings() { let o = ok(&mut s, "var n = app.project.numItems; app.project.item(1).remove(); n - app.project.numItems"); assert_eq!(o.result, json!(1)); } + +#[test] +fn motion_graphics_template_hooks_add_mirrors_and_controllers() { + let mut s = session(); + let o = ok( + &mut s, + r#" + var comp = app.project.items.addComp("Card", 64, 64, 1, 2, 30); + var bg = comp.layers.addSolid([1, 1, 1], "BG", 64, 64, 1); + var op = bg.transform.opacity; + var r = [op.canAddToMotionGraphicsTemplate(comp), op.addToMotionGraphicsTemplateAs(comp, "Fade")]; + // Again: a mirror of the same property (a controller of its own). + r.push(op.addToMotionGraphicsTemplate(comp)); + r.push(comp.motionGraphicsTemplateControllerCount); + r.push(comp.getMotionGraphicsTemplateControllerName(1), comp.getMotionGraphicsTemplateControllerName(2)); + comp.setMotionGraphicsControllerName(2, "Fade Again"); + r.push(comp.getMotionGraphicsTemplateControllerName(2)); + r + "#, + ); + assert_eq!(o.result, json!([true, true, true, 2, "Fade", "Fade", "Fade Again"])); + let (_, c) = comp_named(&s, "Card"); + let eg = c.essential.as_ref().unwrap(); + assert!(matches!(eg.controls[1].kind, effectcraft_engine::project::essential::EgKind::Mirror { of } if of == eg.controls[0].id)); +} diff --git a/crates/script/src/tests_ui_more.rs b/crates/script/src/tests_ui_more.rs index 2c24329a..69abb61d 100644 --- a/crates/script/src/tests_ui_more.rs +++ b/crates/script/src/tests_ui_more.rs @@ -240,3 +240,137 @@ fn sockets_talk_tcp_behind_the_network_preference() { let o = run_code(&mut s, "var l = new Socket(); l.listen(0); var r = [l.port > 0, l.poll() === null].join('|'); l.close(); r", "net.jsx"); assert_eq!(o.result, json!("true|true")); } + +/// `__uiParseRes(src)` as JSON, or the error message. +fn parse_res(s: &mut Session, src: &str) -> Result { + let code = format!("JSON.stringify(__uiParseRes({}))", serde_json::to_string(src).unwrap()); + let o = run_code(s, &code, "res.jsx"); + match o.error { + Some(e) => Err(e.message), + None => Ok(serde_json::from_str(o.result.as_str().unwrap_or("null")).unwrap_or_default()), + } +} + +#[test] +fn resource_string_parser_edge_cases() { + let mut s = session(); + // Accepted: trailing commas, quoted keys, number forms, escapes, comments, empty bodies, + // null / undefined, bare words, nested arrays and object literals. + let ok = parse_res( + &mut s, + "group { 'quoted key': 1, \"dq\": -.5, n: +3, e: 1e2, arr: [[1, [2]], 'x', ], \ + esc: 'it\\'s\\n\\\\', /* block */ nul: null, u: undefined, w: fill, // line\n \ + obj: { a: { b: true } }, kid: Button {}, }", + ) + .unwrap(); + assert_eq!(ok["type"], "group"); + let list = ok["list"].as_array().unwrap(); + let get = |k: &str| list.iter().find(|e| e[0] == k).map(|e| e[1].clone()).unwrap(); + assert_eq!(get("quoted key"), json!(1)); + assert_eq!(get("dq"), json!(-0.5)); + assert_eq!(get("n"), json!(3)); + assert_eq!(get("e"), json!(100)); + assert_eq!(get("arr"), json!([[1, [2]], "x"])); + assert_eq!(get("esc"), json!("it's\n\\")); + assert_eq!(get("nul"), json!(null)); + assert_eq!(get("w"), json!("fill")); + assert_eq!(get("kid"), json!({"__element": "Button", "list": []})); + // Rejected with a located message (never a crash). + for bad in [ + "", + " ", + "dialog", + "dialog {", + "dialog { text: 'unterminated }", + "dialog { text 'x' }", + "dialog { a: [1, 2 }", + "dialog { a: 1 } trailing", + "dialog { /* unterminated comment", + "dialog { a: 'x\\", + "{ }", + "123 { }", + "dialog { a: 1 b: 2 }", + "dialog { : 1 }", + "dialog { a: }", + "dialog { a: Button { }", + ] { + let e = parse_res(&mut s, bad).expect_err(bad); + assert!(e.contains("resource string"), "{bad:?}: {e}"); + assert!(e.contains("offset"), "{bad:?}: {e}"); + } + // Deep nesting builds; absurd nesting fails cleanly (the script engine's recursion limit). + let deep = |n: usize| format!("group {{ {}{} }}", "g: Group { ".repeat(n), "}".repeat(n)); + let d = parse_res(&mut s, &deep(40)).unwrap(); + assert_eq!(d["list"][0][1]["__element"], "Group"); + let _ = parse_res(&mut s, &deep(5000)); + // A window from a deep resource. + let o = run_code( + &mut s, + &format!( + "var w = new Window({}); w.children.length", + serde_json::to_string(&format!("palette {{ {}{} }}", "g: Group { ".repeat(20), "}".repeat(20))).unwrap() + ), + "deep.jsx", + ); + assert_eq!(o.result, json!(1), "{:?}", o.error); +} + +#[test] +fn socket_edge_cases() { + let mut s = session(); + s.prefs.scripting.allow_scripts_write_files = true; + // Bad addresses and ports: false with an error, never a throw or crash. + for host in ["", "nohostport", "127.0.0.1", "127.0.0.1:99999", "[::1", "a:b:c", "127.0.0.1:-5"] { + let code = format!( + "var s = new Socket(); s.timeout = 1; var ok = s.open({}); [ok, s.error.length > 0, s.connected, s.eof].join('|')", + serde_json::to_string(host).unwrap() + ); + let o = run_code(&mut s, &code, "net.jsx"); + assert_eq!(o.result, json!("false|true|false|true"), "{host:?}: {:?}", o.error); + } + for port in ["70000", "-1", "1.5"] { + let o = run_code(&mut s, &format!("var l = new Socket(); [l.listen({port}), l.error.length > 0].join('|')"), "net.jsx"); + assert_eq!(o.result, json!("false|true"), "{port}: {:?}", o.error); + } + // Never opened or closed: reads are empty, writes fail, close is idempotent. + let o = run_code( + &mut s, + "var s = new Socket(); var r = [s.read(), s.read(5), s.readln(), s.write('x'), s.connected, s.eof, s.poll(), s.close(), s.close()]; r.join('|')", + "net.jsx", + ); + assert_eq!(o.result, json!("|||false|false|true||true|true"), "{:?}", o.error); + // Binary bytes round trip in BINARY encoding; invalid UTF-8 is replaced in UTF-8; partial + // and zero-length reads; read stops at the timeout without data. + let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap(); + let port = listener.local_addr().unwrap().port(); + let server = std::thread::spawn(move || { + use std::io::Write; + for _ in 0..2 { + let (mut c, _) = listener.accept().unwrap(); + let bytes: Vec = (0..=255u8).collect(); + c.write_all(&bytes).unwrap(); + } + }); + let code = format!( + "var s = new Socket(); s.open('127.0.0.1:{port}', 'BINARY'); var z = s.read(0); var a = s.read(10); var b = s.read(); s.close(); \ + var ok = true; for (var i = 0; i < 10; i++) ok = ok && a.charCodeAt(i) === i; for (var j = 0; j < 246; j++) ok = ok && b.charCodeAt(j) === j + 10; \ + [z.length, a.length, b.length, ok].join('|')" + ); + let o = run_code(&mut s, &code, "net.jsx"); + assert_eq!(o.result, json!("0|10|246|true"), "{:?}", o.error); + let code = format!( + "var s = new Socket(); s.open('127.0.0.1:{port}', 'UTF-8'); var d = s.read(); s.close(); [d.charCodeAt(0), d.charCodeAt(127), d.indexOf('\\ufffd') > 0].join('|')" + ); + let o = run_code(&mut s, &code, "net.jsx"); + assert_eq!(o.result, json!("0|127|true"), "{:?}", o.error); + server.join().unwrap(); + // A listener with nobody connecting: a short read timeout returns quickly. + let t = std::time::Instant::now(); + let o = run_code( + &mut s, + "var l = new Socket(); l.listen(0); l.timeout = 0; var c = new Socket(); c.timeout = 0.2; c.open(l.host); var x = c.read(); c.close(); l.close(); x.length", + "net.jsx", + ); + assert_eq!(o.result, json!(0), "{:?}", o.error); + assert!(t.elapsed().as_secs_f64() < 5.0); +} diff --git a/crates/script/src/ui.rs b/crates/script/src/ui.rs index 66574ce0..609b767b 100644 --- a/crates/script/src/ui.rs +++ b/crates/script/src/ui.rs @@ -127,10 +127,24 @@ pub(crate) fn native_modal(_: &JsValue, args: &[JsValue], ctx: &mut Context) -> // ---------------------------------------------------------------- script-host threads pub(crate) enum ToHost { + /// Run the script (script-host threads exist only off the web: the browser runs scripts + /// inline). + #[cfg(not(target_arch = "wasm32"))] Start(Session), Event(Session, ScriptUiEvent), } +impl ToHost { + /// The session a message carries (given back when the host has gone). + fn into_session(self) -> Session { + match self { + #[cfg(not(target_arch = "wasm32"))] + ToHost::Start(s) => s, + ToHost::Event(s, _) => s, + } + } +} + pub(crate) struct FromHost { session: Session, outcome: crate::Outcome, @@ -191,6 +205,7 @@ fn yield_wait(outcome: crate::Outcome, windows: Vec, done: bool) - } } +#[cfg(not(target_arch = "wasm32"))] fn host_main(name: String, code: String, link: Link) { let Ok(ToHost::Start(session)) = link.rx.recv() else { return }; let mut a = Active::new(session, &name); @@ -248,7 +263,9 @@ fn receive(session: &mut Session, host: u32, end: HostEnd) -> crate::Outcome { } } -/// Run a script file on a new script-host thread (see the module docs). +/// Run a script file on a new script-host thread (see the module docs). Not on the web, where +/// scripts run inline. +#[cfg(not(target_arch = "wasm32"))] pub(crate) fn run_threaded(session: &mut Session, req: &effectcraft_engine::ScriptRequest) -> crate::Outcome { let host = new_host_id(); let (to_tx, to_rx) = std::sync::mpsc::channel(); @@ -259,8 +276,8 @@ pub(crate) fn run_threaded(session: &mut Session, req: &effectcraft_engine::Scri if spawned.is_err() { return crate::runtime::run_inline(session, req); } - if let Err(std::sync::mpsc::SendError(ToHost::Start(s) | ToHost::Event(s, _))) = to_tx.send(ToHost::Start(std::mem::take(session))) { - *session = s; + if let Err(std::sync::mpsc::SendError(msg)) = to_tx.send(ToHost::Start(std::mem::take(session))) { + *session = msg.into_session(); return crate::Outcome { error: Some(ScriptError { message: "the script host did not start".into(), ..Default::default() }), ..Default::default() }; } receive(session, host, HostEnd { tx: to_tx, rx: from_rx }) @@ -302,8 +319,8 @@ pub fn dispatch_ui(session: &mut Session, ev: &ScriptUiEvent) -> Result>, + /// Composited on its own (a transparent backdrop, as SVG groups and PDF isolated + /// transparency groups are) rather than over what is below it (PDF non-isolated groups and + /// clipping groups: blend modes inside reach the backdrop). Matters only when the group is + /// drawn offscreen (clip, mask, blend mode, opacity or knockout). + pub isolated: bool, + /// A PDF knockout group: each child composites with the group's initial backdrop instead + /// of with the children below it. + pub knockout: bool, } impl Group { /// An empty, opaque, unclipped group. pub fn new(name: &str) -> Group { - Group { name: name.to_string(), transform: Affine::IDENTITY, opacity: 1.0, children: vec![], clip: vec![], blend: BlendMode::Normal, mask: None } + Group { + name: name.to_string(), + transform: Affine::IDENTITY, + opacity: 1.0, + children: vec![], + clip: vec![], + blend: BlendMode::Normal, + mask: None, + isolated: true, + knockout: false, + } } } @@ -753,7 +771,17 @@ impl<'a, 'i> Parser<'a, 'i> { for c in n.children() { self.node(c, &st, &mut children, depth + 1); } - out.push(Node::Group(Group { name: Self::element_name(n), transform, opacity, children, clip: vec![], blend: BlendMode::Normal, mask: None })); + out.push(Node::Group(Group { + name: Self::element_name(n), + transform, + opacity, + children, + clip: vec![], + blend: BlendMode::Normal, + mask: None, + isolated: true, + knockout: false, + })); } "svg" => { // Nested viewport. @@ -780,6 +808,8 @@ impl<'a, 'i> Parser<'a, 'i> { clip: vec![], blend: BlendMode::Normal, mask: None, + isolated: true, + knockout: false, })); } "use" => { @@ -809,11 +839,23 @@ impl<'a, 'i> Parser<'a, 'i> { clip: vec![], blend: BlendMode::Normal, mask: None, + isolated: true, + knockout: false, })); } else { self.node(target, &st, &mut children, depth + 1); } - out.push(Node::Group(Group { name: Self::element_name(n), transform, opacity, children, clip: vec![], blend: BlendMode::Normal, mask: None })); + out.push(Node::Group(Group { + name: Self::element_name(n), + transform, + opacity, + children, + clip: vec![], + blend: BlendMode::Normal, + mask: None, + isolated: true, + knockout: false, + })); } "path" | "rect" | "circle" | "ellipse" | "line" | "polyline" | "polygon" => { if !st.visible { @@ -861,6 +903,8 @@ impl<'a, 'i> Parser<'a, 'i> { clip: vec![], blend: BlendMode::Normal, mask: None, + isolated: true, + knockout: false, })); } } @@ -1056,7 +1100,7 @@ pub fn parse(bytes: &[u8]) -> Result { Ok(Doc { width: dw, height: dh, - root: Group { name: "svg".into(), transform, opacity, children, clip: vec![], blend: BlendMode::Normal, mask: None }, + root: Group { name: "svg".into(), transform, opacity, children, clip: vec![], blend: BlendMode::Normal, mask: None, isolated: true, knockout: false }, skipped: std::mem::take(&mut p.skipped), }) } diff --git a/crates/svg/src/raster.rs b/crates/svg/src/raster.rs index 975adc5a..b1b9d93b 100644 --- a/crates/svg/src/raster.rs +++ b/crates/svg/src/raster.rs @@ -31,26 +31,62 @@ fn draw_group(g: &Group, m: Affine, opacity: f64, dst: &mut Image) { if o <= 0.0 { return; } - let isolated = !g.clip.is_empty() || g.mask.is_some() || g.blend != BlendMode::Normal || (g.opacity < 1.0 && g.children.len() > 1); - if !isolated { + let offscreen = !g.clip.is_empty() + || g.mask.is_some() + || g.blend != BlendMode::Normal + || (g.opacity < 1.0 && !matches!(g.children.as_slice(), [Node::Shape(_) | Node::Image(_)])) + || g.knockout; + if !offscreen { for c in &g.children { draw_node(c, m, o, dst); } return; } - // Draw the children on their own, keep what every clip path covers and the soft mask - // lets through, then composite with the group's opacity and blend mode. - let mut tmp = Image::new(dst.width, dst.height); - for c in &g.children { - draw_node(c, m, 1.0, &mut tmp); + let only_clip = g.mask.is_none() && g.blend == BlendMode::Normal && o >= 1.0 && !g.knockout; + // Draw the children on their own (isolated) or over a copy of the backdrop, keep what + // every clip path covers and the soft mask lets through, then composite with the group's + // opacity and blend mode. + let backdrop = if g.isolated { None } else { Some(dst.clone()) }; + let mut tmp = match &backdrop { + Some(b) => b.clone(), + None => Image::new(dst.width, dst.height), + }; + draw_children(g, m, &mut tmp, backdrop.as_ref()); + if let Some(b) = &backdrop { + if only_clip { + // A non-isolated clipping group: the backdrop where the clip excludes the children. + let cov = clip_coverage(g, m, dst.width, dst.height); + dst.data.par_iter_mut().zip(tmp.data.par_iter()).zip(cov.par_iter()).for_each(|((d, t), c)| { + for i in 0..4 { + d[i] = d[i] * (1.0 - c) + t[i] * c; + } + }); + return; + } + // Remove the backdrop's contribution (ISO 32000-1 §11.4.8): with the group's own + // alpha αg (its children alone), C = Cn + (Cn − C0)·(α0/αg − α0). + let mut solo = Image::new(dst.width, dst.height); + draw_children(g, m, &mut solo, None); + tmp.data.par_iter_mut().zip(b.data.par_iter()).zip(solo.data.par_iter()).for_each(|((t, b0), s)| { + let ag = s[3]; + if ag <= 0.0 { + *t = [0.0; 4]; + return; + } + let un = |c: &[f32; 4]| if c[3] > 0.0 { [c[0] / c[3], c[1] / c[3], c[2] / c[3]] } else { [0.0; 3] }; + let (cn, c0, a0) = (un(t), un(b0), b0[3]); + let k = a0 / ag - a0; + let mut out = [0.0; 4]; + for i in 0..3 { + out[i] = (cn[i] + (cn[i] - c0[i]) * k).clamp(0.0, 1.0) * ag; + } + out[3] = ag; + *t = out; + }); } - for (path, rule) in &g.clip { - let rule = match rule { - FillRule::NonZero => effectcraft_path::FillRule::NonZero, - FillRule::EvenOdd => effectcraft_path::FillRule::EvenOdd, - }; - let cov = effectcraft_path::fill_coverage(std::slice::from_ref(path), &mat3(m), dst.width, dst.height, rule); - tmp.data.par_iter_mut().zip(cov.data.par_iter()).for_each(|(p, c)| { + if !g.clip.is_empty() { + let cov = clip_coverage(g, m, dst.width, dst.height); + tmp.data.par_iter_mut().zip(cov.par_iter()).for_each(|(p, c)| { for v in p.iter_mut() { *v *= c; } @@ -89,6 +125,76 @@ fn draw_group(g: &Group, m: Affine, opacity: f64, dst: &mut Image) { } } +/// The product of a group's clip-path coverages. +fn clip_coverage(g: &Group, m: Affine, w: u32, h: u32) -> Vec { + let mut out = vec![1.0f32; w as usize * h as usize]; + for (path, rule) in &g.clip { + let rule = match rule { + FillRule::NonZero => effectcraft_path::FillRule::NonZero, + FillRule::EvenOdd => effectcraft_path::FillRule::EvenOdd, + }; + let cov = effectcraft_path::fill_coverage(std::slice::from_ref(path), &mat3(m), w, h, rule); + out.par_iter_mut().zip(cov.data.par_iter()).for_each(|(o, c)| *o *= c); + } + out +} + +/// Draw a group's children into `dst`. Knockout groups composite each child with the +/// group's initial backdrop (`backdrop`, or transparent) instead of with the children below: +/// where the child has shape `f`, `dst = dst·(1 − f) + child + backdrop·(f − αchild)`. +fn draw_children(g: &Group, m: Affine, dst: &mut Image, backdrop: Option<&Image>) { + if !g.knockout { + for c in &g.children { + draw_node(c, m, 1.0, dst); + } + return; + } + for c in &g.children { + let mut ci = Image::new(dst.width, dst.height); + draw_node(c, m, 1.0, &mut ci); + // Shape: the child drawn fully opaque (constant opacities to 1). + let mut si = Image::new(dst.width, dst.height); + draw_node(&opaque(c), m, 1.0, &mut si); + let zero = [0.0f32; 4]; + dst.data.par_iter_mut().enumerate().for_each(|(i, d)| { + let (cp, f) = (ci.data[i], si.data[i][3].max(ci.data[i][3])); + if f <= 0.0 { + return; + } + let b = backdrop.map(|b| b.data[i]).unwrap_or(zero); + for k in 0..4 { + d[k] = d[k] * (1.0 - f) + cp[k] + b[k] * (f - cp[3]); + } + }); + } +} + +/// A node with its constant opacities set to 1 (its shape, for knockout groups). +fn opaque(n: &Node) -> Node { + match n { + Node::Group(g) => { + let mut g = g.clone(); + g.opacity = 1.0; + g.children = g.children.iter().map(opaque).collect(); + Node::Group(g) + } + Node::Shape(s) => { + let mut s = s.clone(); + s.opacity = 1.0; + s.fill_opacity = 1.0; + if let Some(st) = &mut s.stroke { + st.opacity = 1.0; + } + Node::Shape(s) + } + Node::Image(i) => { + let mut i = i.clone(); + i.opacity = 1.0; + Node::Image(i) + } + } +} + /// `src` composited onto `dst` with a blend mode (both premultiplied RGBA): /// `co = cs·(1 − αb) + cb·(1 − αs) + αs·αb·B(cb, cs)` (ISO 32000-1 §11.3.6). pub fn blend_pixel(mode: BlendMode, dst: [f32; 4], src: [f32; 4]) -> [f32; 4] { diff --git a/crates/text/src/variable.rs b/crates/text/src/variable.rs index 2072c294..6594e2dc 100644 --- a/crates/text/src/variable.rs +++ b/crates/text/src/variable.rs @@ -123,6 +123,37 @@ pub fn char_outline_varied(g: &CharGlyph, deltas: &[(String, f32)]) -> Option Option { + let f = fonts::face(face); + let font = f.font()?; + let loc = font.axes().location(coords.iter().map(|(t, v)| (tag_of(t), *v))); + font.glyph_metrics(Size::unscaled(), LocationRef::from(&loc)).advance_width(GlyphId::new(gid)) +} + +/// The change of a laid-out character's advance (layout units) when its face's axes move by +/// `deltas` (as [`char_outline_varied`]): Variable Font Axes animators re-space the text. +pub fn char_advance_delta(g: &CharGlyph, deltas: &[(String, f32)]) -> Option { + let axes = font_axes(g.face); + let base = |t: &str, d: f32| g.variations.iter().find(|(x, _)| x.trim_end() == t.trim_end()).map(|(_, v)| *v).unwrap_or(d); + let mut coords: Vec<(String, f32)> = + deltas.iter().filter_map(|(t, d)| axes.iter().find(|a| a.tag == *t).map(|a| (t.clone(), (base(&a.tag, a.default) + d).clamp(a.min, a.max)))).collect(); + if coords.is_empty() { + return None; + } + for (t, v) in &g.variations { + if !coords.iter().any(|(c, _)| c == t) { + coords.push((t.clone(), *v)); + } + } + let varied = advance_units_at(g.face, g.gid, &coords)?; + let own = advance_units_at(g.face, g.gid, &g.variations)?; + // Font units → layout units: the outline transform's horizontal scale. + let sx = g.outline_xf.as_coeffs()[0]; + Some((varied - own) as f64 * sx) +} + /// The first installed face with variation axes (system fonts are scanned), for tests and /// demos: (face, its axes). pub fn find_variable_face() -> Option<(FaceId, Vec)> { diff --git a/crates/ui-egui/Cargo.toml b/crates/ui-egui/Cargo.toml index 31cd782c..beaae6be 100644 --- a/crates/ui-egui/Cargo.toml +++ b/crates/ui-egui/Cargo.toml @@ -28,9 +28,9 @@ log = { workspace = true } rayon = { workspace = true } web-time = { workspace = true } kurbo = { workspace = true } +image = { workspace = true } [dev-dependencies] egui_kittest = { workspace = true, features = ["eframe", "wgpu"] } -image = { workspace = true } pollster = "0.4" effectcraft-host = { workspace = true } diff --git a/crates/ui-egui/src/panels/essential.rs b/crates/ui-egui/src/panels/essential.rs index 437181a3..b761985e 100644 --- a/crates/ui-egui/src/panels/essential.rs +++ b/crates/ui-egui/src/panels/essential.rs @@ -8,6 +8,10 @@ //! control, Scale as one uniform slider); rename by double-clicking the name; //! reorder with ▲▼, remove with ✕; groups and comments from Add Formatting; Media //! Replacement for the selected footage layer. +//! - **Mirrors and links**: adding a property that is already in the panel adds a *mirror* +//! (same value, its own name and place); a control's ⋯ menu adds a mirror, links the selected +//! timeline property (the control then drives both) or unlinks one. Mirrors and linked +//! controls are badged (`essential.control..badge`, `.menu`). //! - **Export Template…** writes an open `.ectemplate`. //! - With a precomp layer that has Essential Properties selected, its instance values: edit them //! (overrides), Revert or Push to Comp. @@ -192,13 +196,37 @@ fn controls(app: &mut EffectcraftApp, ui: &mut egui::Ui, cid: ItemId, list: &[Eg let shown = EgControl { name: text.clone(), ..c.clone() }; name_label(app, ui, cid, &shown, RichText::new(text).italics().color(t.text_dim), actions); } - EgKind::Property { .. } => { + EgKind::Property { .. } | EgKind::Mirror { .. } => { name_label(app, ui, cid, c, RichText::new(&c.name), actions); + // Mirrors and linked properties are marked (hover: what they share). + let eg = app.session.project.comp(cid).and_then(|x| x.essential.clone()).unwrap_or_default(); + let badge = match &c.kind { + EgKind::Mirror { of } => Some(("mirror".to_string(), format!("Mirror of {}", eg.find(*of).map(|m| m.name.as_str()).unwrap_or("?")))), + EgKind::Property { links, .. } if !links.is_empty() => { + let names: Vec = essential::linked_props(&app.session.project, cid, c) + .into_iter() + .map(|(l, p)| format!("{} › {}", l.name, l.props.name_path_of(p.uid).unwrap_or_else(|| p.name.clone()))) + .collect(); + Some((format!("+{} linked", links.len()), format!("Also drives:\n{}", names.join("\n")))) + } + _ => None, + }; + if let Some((text, tip)) = badge { + let r = ui.label(RichText::new(text).color(t.accent).size(10.0)).on_hover_text(&tip); + app.auto.add(&format!("essential.control.{}.badge", c.id), r.rect, &tip); + } let src = essential::source_prop(&app.session.project, cid, c).map(|(l, p)| (l.id.0, l.layer_time(app.session.time()), p.clone())); match src { Some((layer, lt, p)) => { let v = p.value_at(lt); - if let Some(nv) = value_widget(app, ui, &format!("essential.control.{}.value", c.id), essential::effective_type(c, &p), &p, &v) { + if let Some(nv) = value_widget( + app, + ui, + &format!("essential.control.{}.value", c.id), + essential::effective_type(eg.resolve(c.id).unwrap_or(c), &p), + &p, + &v, + ) { actions.push(( "prop.set".into(), json!({"comp": cid.0, "layer": layer, "prop": p.uid, "value": nv, "merge": format!("eg-{}", c.id)}), @@ -209,6 +237,7 @@ fn controls(app: &mut EffectcraftApp, ui: &mut egui::Ui, cid: ItemId, list: &[Eg ui.label(RichText::new("(missing)").color(t.danger)); } } + link_menu(app, ui, cid, c, group, actions); } EgKind::Media { .. } => { name_label(app, ui, cid, c, RichText::new(&c.name), actions); @@ -230,6 +259,35 @@ fn controls(app: &mut EffectcraftApp, ui: &mut egui::Ui, cid: ItemId, list: &[Eg } } +/// A property control's "⋯" menu: Add Mirror, Link Selected Property, Unlink. +fn link_menu(app: &mut EffectcraftApp, ui: &mut egui::Ui, cid: ItemId, c: &EgControl, group: Option, actions: &mut Actions) { + let links: Vec<(u64, u64, String)> = match &c.kind { + EgKind::Property { .. } => essential::linked_props(&app.session.project, cid, c) + .into_iter() + .map(|(l, p)| (l.id.0, p.uid, format!("{} › {}", l.name, l.props.name_path_of(p.uid).unwrap_or_else(|| p.name.clone())))) + .collect(), + _ => vec![], + }; + let has_sel = !app.session.state.selected_props.is_empty(); + let r = ui.menu_button(RichText::new("⋯").size(10.0), |ui| { + if ui.button("Add Mirror").clicked() { + actions.push(("essential.addMirror".into(), json!({"comp": cid.0, "control": c.id, "group": group}))); + ui.close(); + } + if ui.add_enabled(has_sel, egui::Button::new("Link Selected Property")).clicked() { + actions.push(("essential.linkProperty".into(), json!({"comp": cid.0, "control": c.id}))); + ui.close(); + } + for (layer, prop, name) in &links { + if ui.button(format!("Unlink {name}")).clicked() { + actions.push(("essential.unlinkProperty".into(), json!({"comp": cid.0, "control": c.id, "layer": layer, "prop": prop}))); + ui.close(); + } + } + }); + app.auto.add(&format!("essential.control.{}.menu", c.id), r.response.rect, "Mirror and link"); +} + /// A control's name; double-click to rename (comments: edit the text). fn name_label(app: &mut EffectcraftApp, ui: &mut egui::Ui, cid: ItemId, c: &EgControl, text: RichText, actions: &mut Actions) { let edit_id = egui::Id::new(("eg-rename", c.id)); @@ -445,7 +503,7 @@ fn instance(app: &mut EffectcraftApp, ui: &mut egui::Ui, actions: &mut Actions) } _ => { let v = p.value_at(lt); - if let Some(nv) = value_widget(app, ui, &auto, essential::effective_type(c, &p), &p, &v) { + if let Some(nv) = value_widget(app, ui, &auto, essential::effective_type(eg.resolve(c.id).unwrap_or(c), &p), &p, &v) { actions.push(( "essential.set".into(), json!({"layer": layer.id.0, "control": c.id, "value": nv, "merge": format!("egi-{}-{}", layer.id.0, c.id)}), diff --git a/crates/ui-egui/src/panels/render_queue.rs b/crates/ui-egui/src/panels/render_queue.rs index ec0853a1..cf6c0c71 100644 --- a/crates/ui-egui/src/panels/render_queue.rs +++ b/crates/ui-egui/src/panels/render_queue.rs @@ -12,8 +12,8 @@ //! key frame interval, Make Template) open a form that runs the same command. use effectcraft_engine::project::render_queue::{ - AlphaMode, AudioFormat, AudioOutput, Channels, ColorDepth, CurrentOrOff, EffectsMode, FieldRender, OutputFormat, PostRenderAction, ProResProfile, Pulldown, - RESIZE_PRESETS, RenderLog, RenderQuality, RenderQueueItem, RenderStatus, ResizeQuality, SwitchOverride, TimeSpan, + AlphaMode, AudioFormat, AudioOutput, Channels, ColorDepth, CurrentOrOff, EffectsMode, FieldRender, OpusApplication, OutputFormat, PostRenderAction, + ProResProfile, Pulldown, RESIZE_PRESETS, RenderLog, RenderQuality, RenderQueueItem, RenderStatus, ResizeQuality, SwitchOverride, TimeSpan, WebmVideoCodec, }; use effectcraft_engine::project::render_templates::RenderTemplates; use egui::{Align2, Color32, Rect, Sense, Stroke, StrokeKind, pos2, vec2}; @@ -144,7 +144,7 @@ fn output_menu(it: &RenderQueueItem, available: &[OutputFormat], templates: &Ren } v.push(("-".into(), Value::Null)); v.push((mark(o.channels == Channels::Rgb, "Channels: RGB"), json!({"channels": "rgb"}))); - if o.format.supports_alpha() { + if o.supports_alpha() { v.push((mark(o.channels == Channels::Rgba, "Channels: RGB + Alpha"), json!({"channels": "rgba"}))); } if !o.format.is_audio_only() { @@ -167,15 +167,35 @@ fn output_menu(it: &RenderQueueItem, available: &[OutputFormat], templates: &Ren v.push((mark(o.bitrate_kbps == kbps, &format!("Bitrate: {} Mbps", kbps / 1000)), json!({"bitrate": kbps}))); } } - OutputFormat::JpegSequence | OutputFormat::WebM => { + OutputFormat::Hevc | OutputFormat::Av1 => codec_menu(o, &mut v), + OutputFormat::WebM => { v.push(("-".into(), Value::Null)); - for q in [60u8, 80, 90, 100] { - v.push((mark(o.quality == q && !o.webm_bitrate, &format!("Quality: {q}")), json!({"quality": q, "webmBitrate": false}))); + for c in [WebmVideoCodec::Vp9, WebmVideoCodec::Av1] { + v.push((mark(o.webm_codec == c, &format!("Video Codec: {}", c.label())), json!({"webmCodec": c.label().to_ascii_lowercase()}))); } - if o.format == OutputFormat::WebM { + if o.webm_codec == WebmVideoCodec::Av1 { + codec_menu(o, &mut v); + } else { + v.push(("-".into(), Value::Null)); + for q in [60u8, 80, 90, 100] { + v.push((mark(o.quality == q && !o.webm_bitrate, &format!("Quality: {q}")), json!({"quality": q, "webmBitrate": false}))); + } v.push((mark(o.webm_bitrate, &format!("Target Bitrate ({} kbps)…", o.bitrate_kbps)), json!({"form": "bitrate"}))); v.push((mark(o.keyframe_interval > 0, "Key Frame Interval…"), json!({"form": "keyframes"}))); } + v.push(("-".into(), Value::Null)); + for kbps in [24u32, 32, 64, 96, 128, 192, 256] { + v.push((mark(o.opus_bitrate_kbps == kbps, &format!("Opus Bitrate: {kbps} kbps")), json!({"audioBitrate": kbps}))); + } + for (a, k) in [(OpusApplication::Audio, "audio"), (OpusApplication::Voip, "voice")] { + v.push((mark(o.opus_application == a, &format!("Opus Tuning: {}", a.label())), json!({"opusApplication": k}))); + } + } + OutputFormat::JpegSequence => { + v.push(("-".into(), Value::Null)); + for q in [60u8, 80, 90, 100] { + v.push((mark(o.quality == q, &format!("Quality: {q}")), json!({"quality": q}))); + } } OutputFormat::Gif => { v.push(("-".into(), Value::Null)); @@ -236,6 +256,49 @@ fn output_menu(it: &RenderQueueItem, available: &[OutputFormat], templates: &Ren v } +/// HEVC / AV1 options: profile, level, rate control (bitrate or constant quality), key frames. +fn codec_menu(o: &effectcraft_engine::project::render_queue::OutputModule, v: &mut Vec<(String, Value)>) { + use effectcraft_engine::project::render_queue::{CodecProfile, RateControlMode, VideoCodecOptions}; + let c = &o.codec; + v.push(("-".into(), Value::Null)); + for p in CodecProfile::ALL { + v.push((mark(c.profile == p, &format!("Profile: {}", p.label())), json!({"profile": format!("{p:?}").to_ascii_lowercase()}))); + } + v.push(("-".into(), Value::Null)); + v.push((mark(c.level.is_none(), "Level: Auto"), json!({"level": "auto"}))); + // The common HEVC / AV1 levels (every level is reachable over the control channel). + for l in [30u8, 31, 40, 41, 50, 51, 52] { + debug_assert!(VideoCodecOptions::HEVC_LEVELS.contains(&l) && VideoCodecOptions::AV1_LEVELS.contains(&l)); + let name = format!("{}.{}", l / 10, l % 10); + v.push((mark(c.level == Some(l), &format!("Level: {name}")), json!({"level": name}))); + } + v.push(("-".into(), Value::Null)); + for r in [RateControlMode::Bitrate, RateControlMode::Quality] { + v.push((mark(c.rate_control == r, &format!("Rate Control: {}", r.label())), json!({"rateControl": format!("{r:?}").to_ascii_lowercase()}))); + } + if c.rate_control == RateControlMode::Bitrate { + for kbps in [1_000u32, 2_000, 5_000, 10_000, 20_000, 40_000] { + let l = if kbps < 2_000 { format!("Bitrate: {kbps} kbps") } else { format!("Bitrate: {} Mbps", kbps / 1000) }; + v.push((mark(o.bitrate_kbps == kbps, &l), json!({"bitrate": kbps}))); + } + v.push((mark(false, "Bitrate: Custom…"), json!({"form": "codecBitrate"}))); + } else { + for q in [40u8, 60, 70, 80, 90, 100] { + v.push((mark(c.quality == q, &format!("Quality: {q}")), json!({"quality": q}))); + } + } + v.push(("-".into(), Value::Null)); + for k in [0u32, 1, 30, 60, 120, 250] { + let l = match k { + 0 => "Key Frames: Auto (2 s)".to_string(), + 1 => "Key Frames: Every Frame (all intra)".to_string(), + k => format!("Key Frames: Every {k} Frames"), + }; + v.push((mark(o.keyframe_interval == k, &l), json!({"keyframeInterval": k}))); + } + v.push((mark(false, "Key Frame Interval…"), json!({"form": "keyframes"}))); +} + /// Open the form behind a `{"form": …}` menu entry. fn open_menu_form(app: &mut EffectcraftApp, it: &RenderQueueItem, form: &str) { use crate::panels::forms::{Field, form as open}; @@ -297,6 +360,13 @@ fn open_menu_form(app: &mut EffectcraftApp, it: &RenderQueueItem, form: &str) { json!({"item": id, "webmBitrate": true}), vec![Field::num("bitrate", "Bitrate (kbps)", o.bitrate_kbps as f64)], ), + "codecBitrate" => open( + app, + "Target Bitrate", + "renderQueue.setOutputModule", + json!({"item": id, "rateControl": "bitrate"}), + vec![Field::num("bitrate", "Bitrate (kbps)", o.bitrate_kbps as f64)], + ), "keyframes" => open( app, "Key Frame Interval", diff --git a/crates/ui-egui/src/panels/scriptui_view.rs b/crates/ui-egui/src/panels/scriptui_view.rs index ddf3d35e..c3543cb6 100644 --- a/crates/ui-egui/src/panels/scriptui_view.rs +++ b/crates/ui-egui/src/panels/scriptui_view.rs @@ -6,7 +6,8 @@ //! named it). Controls with an `onDraw` handler are painted from their draw list; edit texts and //! sliders send live `changing` updates (onChanging) while typing / dragging. -use effectcraft_engine::scriptui::{DrawOp, PathSeg, ScriptWindow, Widget, WidgetKind, WindowKind, layout}; +use effectcraft_engine::Services; +use effectcraft_engine::scriptui::{DrawOp, ImageRef, PathSeg, ScriptWindow, Widget, WidgetKind, WindowKind, layout}; use egui::{Align2, Color32, Rect, Sense, Stroke, StrokeKind, UiBuilder, pos2, vec2}; use serde_json::{Value, json}; @@ -77,6 +78,7 @@ fn draw_children(app: &mut EffectcraftApp, ui: &mut egui::Ui, origin: egui::Pos2 fn draw_widget(app: &mut EffectcraftApp, ui: &mut egui::Ui, origin: egui::Pos2, win: u32, w: &Widget, acts: &mut Vec) { let t = app.tokens; + let services = app.session.services.clone(); let rect = rect_of(origin, w.bounds); register(app, win, w, rect); let id = egui::Id::new(("sui", win, w.id)); @@ -87,20 +89,20 @@ fn draw_widget(app: &mut EffectcraftApp, ui: &mut egui::Ui, origin: egui::Pos2, let os = w.draw.iter().any(|d| matches!(d, DrawOp::Os)); if custom && !os { if w.kind.is_container() { - paint_draw(ui, rect, &w.draw, &t); + paint_draw(ui, rect, &w.draw, &t, &*services); draw_children(app, ui, origin, win, w, acts); return; } let clickable = matches!(w.kind, WidgetKind::Button | WidgetKind::IconButton | WidgetKind::Checkbox | WidgetKind::RadioButton); let r = ui.interact(rect, id, if clickable && w.enabled { Sense::click() } else { Sense::hover() }); - paint_draw(ui, rect, &w.draw, &t); + paint_draw(ui, rect, &w.draw, &t, &*services); if tip(r).clicked() && clickable { acts.push(Act::Click(win, w.id)); } return; } if custom && w.kind.is_container() { - paint_draw(ui, rect, &w.draw, &t); + paint_draw(ui, rect, &w.draw, &t, &*services); } match w.kind { WidgetKind::Group | WidgetKind::Tab | WidgetKind::Window | WidgetKind::TabbedPanel => draw_children(app, ui, origin, win, w, acts), @@ -117,6 +119,9 @@ fn draw_widget(app: &mut EffectcraftApp, ui: &mut egui::Ui, origin: egui::Pos2, } WidgetKind::Button | WidgetKind::IconButton => { let r = ui.add_enabled_ui(w.enabled, |ui| ui.put(rect, egui::Button::new(&w.text))).inner; + if let Some(img) = &w.image { + paint_image(ui, rect.shrink(3.0), img, &*services); + } if tip(r).clicked() { acts.push(Act::Click(win, w.id)); } @@ -236,16 +241,18 @@ fn draw_widget(app: &mut EffectcraftApp, ui: &mut egui::Ui, origin: egui::Pos2, } } WidgetKind::Image | WidgetKind::Unknown => { - ui.painter().rect_stroke(rect, 2.0, Stroke::new(1.0, t.field_border), StrokeKind::Inside); + if !w.image.as_ref().is_some_and(|img| paint_image(ui, rect, img, &*services)) { + ui.painter().rect_stroke(rect, 2.0, Stroke::new(1.0, t.field_border), StrokeKind::Inside); + } } } if custom && !w.kind.is_container() { - paint_draw(ui, rect, &w.draw, &t); + paint_draw(ui, rect, &w.draw, &t, &*services); } } /// Paint an onDraw draw list in `rect` (control coordinates → screen). -fn paint_draw(ui: &egui::Ui, rect: Rect, ops: &[DrawOp], t: &Tokens) { +fn paint_draw(ui: &egui::Ui, rect: Rect, ops: &[DrawOp], t: &Tokens, services: &dyn Services) { let p = ui.painter().with_clip_rect(rect.intersect(ui.clip_rect())); let col = |c: &Option<[f32; 4]>, theme: Color32| match c { Some(c) => egui::Rgba::from_rgba_unmultiplied(c[0], c[1], c[2], c[3]).into(), @@ -255,10 +262,19 @@ fn paint_draw(ui: &egui::Ui, rect: Rect, ops: &[DrawOp], t: &Tokens) { for op in ops { match op { DrawOp::Fill { color, path } => { + // Any path shape (concave, self-intersecting, with holes): non-zero winding + // triangles, not egui's convex-only polygon fill. let c = col(color, t.field_bg); - for (pts, _) in PathSeg::polylines(path, 48) { - let pts: Vec = pts.iter().map(|q| at(q[0], q[1])).collect(); - p.add(egui::Shape::convex_polygon(pts, c, Stroke::NONE)); + let mut mesh = egui::Mesh::default(); + for tri in PathSeg::fill_triangles(path, 48, false) { + let base = mesh.vertices.len() as u32; + for q in tri { + mesh.colored_vertex(at(q[0], q[1]), c); + } + mesh.add_triangle(base, base + 1, base + 2); + } + if !mesh.is_empty() { + p.add(egui::Shape::mesh(mesh)); } } DrawOp::Stroke { color, width, path } => { @@ -271,15 +287,53 @@ fn paint_draw(ui: &egui::Ui, rect: Rect, ops: &[DrawOp], t: &Tokens) { DrawOp::Text { text, color, x, y, size, .. } => { p.text(at(*x, *y), Align2::LEFT_TOP, text, Tokens::ui(*size as f32), col(color, t.text)); } - DrawOp::Image { x, y, w, h } => { - let r = Rect::from_min_size(at(*x, *y), vec2(*w as f32, *h as f32)); - p.rect_stroke(r, 0.0, Stroke::new(1.0, t.field_border), StrokeKind::Inside); + DrawOp::Image { image, x, y, w, h } => { + let tex = image.as_ref().and_then(|i| image_texture(ui.ctx(), i, services)); + let size = match &tex { + Some(tx) if *w <= 0.0 || *h <= 0.0 => tx.size_vec2(), + _ => vec2(*w as f32, *h as f32), + }; + let r = Rect::from_min_size(at(*x, *y), size); + match tex { + Some(tx) => { + p.image(tx.id(), r, Rect::from_min_max(pos2(0.0, 0.0), pos2(1.0, 1.0)), Color32::WHITE); + } + None => { + p.rect_stroke(r, 0.0, Stroke::new(1.0, t.field_border), StrokeKind::Inside); + } + } } DrawOp::Os => {} } } } +/// A ScriptUIImage as a texture (decoded once and kept; `None` when it can't be read or +/// decoded). +fn image_texture(ctx: &egui::Context, img: &ImageRef, services: &dyn Services) -> Option { + let id = egui::Id::new(("scriptui-image", img)); + if let Some(cached) = ctx.data(|d| d.get_temp::>(id)) { + return cached; + } + let tex = img.bytes(services).and_then(|b| image::load_from_memory(&b).ok()).map(|d| { + let rgba = d.to_rgba8(); + let ci = egui::ColorImage::from_rgba_unmultiplied([rgba.width() as usize, rgba.height() as usize], rgba.as_raw()); + ctx.load_texture(format!("scriptui-image-{}", img.name), ci, egui::TextureOptions::LINEAR) + }); + ctx.data_mut(|d| d.insert_temp(id, tex.clone())); + tex +} + +/// An image control's or icon button's image, fitted (aspect kept) and centred in `r`. +fn paint_image(ui: &egui::Ui, r: Rect, img: &ImageRef, services: &dyn Services) -> bool { + let Some(tex) = image_texture(ui.ctx(), img, services) else { return false }; + let s = tex.size_vec2(); + let k = (r.width() / s.x.max(1.0)).min(r.height() / s.y.max(1.0)).min(1.0); + let dst = Rect::from_center_size(r.center(), s * k); + ui.painter().with_clip_rect(r.intersect(ui.clip_rect())).image(tex.id(), dst, Rect::from_min_max(pos2(0.0, 0.0), pos2(1.0, 1.0)), Color32::WHITE); + true +} + /// Checkboxes and radio buttons sit at the left of their (possibly filled) bounds. fn snug(r: Rect, text: &str) -> Rect { let w = (text.chars().count() as f32 * 7.0 + 26.0).min(r.width()); diff --git a/crates/ui-egui/tests/ui_scriptui.rs b/crates/ui-egui/tests/ui_scriptui.rs index bbfd9e56..9ec9c5ee 100644 --- a/crates/ui-egui/tests/ui_scriptui.rs +++ b/crates/ui-egui/tests/ui_scriptui.rs @@ -111,3 +111,58 @@ fn history_panel_jumps_between_branches() { click(&mut h, &format!("history.state.{c}")); assert_eq!(layer_names(&h), ["C", "A"]); } + +/// onDraw fills of concave paths and real images (drawImage, image controls), rendered. +#[test] +fn on_draw_concave_fills_and_images_render() { + let dir = std::env::temp_dir().join(format!("effectcraft-scriptui-img-{}", std::process::id())); + std::fs::create_dir_all(&dir).unwrap(); + let png = dir.join("green.png"); + image::RgbaImage::from_pixel(8, 8, image::Rgba([0, 255, 0, 255])).save(&png).unwrap(); + let png = png.to_string_lossy().replace('\\', "/"); + let mut s = effectcraft_host::session(); + s.execute("comp.new", json!({"name": "Main", "width": 320, "height": 180, "duration": 4})).unwrap(); + let mut h = Harness::builder().with_size(egui::vec2(1400.0, 900.0)).with_pixels_per_point(1.0).wgpu().build_eframe(|_| EffectcraftApp::new(s)); + h.run_steps(3); + let code = format!( + r#" + var w = new Window("palette", "Paint"); + var c = w.add("group"); + c.preferredSize = [120, 80]; + c.onDraw = function () {{ + var g = this.graphics; + g.newPath(); + // An L: the top-right quarter is a notch (a convex-only fill would cover it). + g.moveTo(0, 0); g.lineTo(60, 0); g.lineTo(60, 40); g.lineTo(120, 40); g.lineTo(120, 80); g.lineTo(0, 80); g.closePath(); + g.fillPath(g.newBrush(g.BrushType.SOLID_COLOR, [1, 0, 0, 1])); + g.drawImage(ScriptUI.newImage(File("{png}")), 90, 5, 20, 20); + }}; + var im = w.add("image", undefined, File("{png}")); + im.preferredSize = [40, 40]; + w.show(); + "# + ); + let r = h.state_mut().session.execute("script.run", json!({"code": code, "name": "paint.jsx"})).unwrap(); + assert_eq!(r["ok"], true, "{r}"); + h.run_steps(4); + let win = h.state().session.script_ui.windows[0].clone(); + let g = &win.root.children[0]; + assert!(matches!(&g.draw[1], effectcraft_engine::scriptui::DrawOp::Image { image: Some(i), .. } if i.src.as_deref() == Some(png.as_str()))); + assert_eq!(win.root.children[1].image.as_ref().and_then(|i| i.src.clone()).as_deref(), Some(png.as_str())); + let img = h.render().expect("render"); + if let Ok(d) = std::env::var("EC_SNAPSHOT_DIR") { + img.save(format!("{d}/scriptui_paint.png")).unwrap(); + } + let px = |p: egui::Pos2| img.get_pixel(p.x as u32, p.y as u32).0; + let gr = rect(&h, &format!("scriptui.{}.{}", win.id, g.id)); + let red = |c: [u8; 4]| c[0] > 200 && c[1] < 60 && c[2] < 60; + let green = |c: [u8; 4]| c[1] > 200 && c[0] < 60 && c[2] < 60; + assert!(red(px(gr.min + vec2(20.0, 20.0))), "inside the L: {:?}", px(gr.min + vec2(20.0, 20.0))); + assert!(red(px(gr.min + vec2(100.0, 60.0))), "the L's foot"); + assert!(!red(px(gr.min + vec2(75.0, 30.0))), "the notch stays empty: {:?}", px(gr.min + vec2(75.0, 30.0))); + assert!(green(px(gr.min + vec2(100.0, 15.0))), "drawImage: {:?}", px(gr.min + vec2(100.0, 15.0))); + let ir = rect(&h, &format!("scriptui.{}.{}", win.id, win.root.children[1].id)); + assert!(green(px(ir.center())), "image control: {:?}", px(ir.center())); + h.state_mut().session.execute("scriptui.close", json!({})).unwrap(); + let _ = std::fs::remove_dir_all(&dir); +} diff --git a/docs/architecture.md b/docs/architecture.md index d0c9267e..ff210e5c 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -17,7 +17,9 @@ cpal or muda. Everything in L0 to L4, the egui UI and the web app also build for | L0 | `geom` | Vectors, matrices, quaternions, the layer transform (anchor, position, scale, orientation, rotation) | | L0 | `color` | sRGB and linear, HSL/HSV, luminance, the 38 blend modes, label colors | | L0 | `vp9enc` | VP9 intra-frame encoder (profile 0, 8-bit 4:2:0; lossless at quality 100) for WebM export, from the VP9 bitstream specification | -| L0 | `opusenc` | Opus (CELT) encoder: RFC 6716 CELT-only fullband 48 kHz 20 ms packets, mono/stereo, and the RFC 7845 `OpusHead`, for WebM export audio | +| L0 | `opusenc` | Opus encoder: RFC 6716 SILK (NB/MB/WB), hybrid (SWB/FB) and CELT (FB) modes chosen by bitrate and application (audio / voice), 48 kHz 20 ms packets, mono/stereo, and the RFC 7845 `OpusHead`, for WebM export audio | +| L0 | `hevcenc` | HEVC (H.265) encoder from ITU-T H.265: Main / Main 10 4:2:0, IDR + P slices (quarter-pel motion, merge/AMVP), CABAC, deblocking, bitrate or constant-QP rate control, for MP4 (`hvc1`) export | +| L0 | `av1enc` | AV1 encoder from the AV1 bitstream specification: Main profile 8/10-bit 4:2:0, key + inter frames (quarter-pel motion), deblocking, for MP4 (`av01`) and WebM export | | L1 | `raster` | Premultiplied float images, sampling, affine and projective warps, blurs, compositing (parallel with rayon) | | L1 | `keyframe` | Animated values, keyframes with temporal ease and spatial Bezier, roving, hold, velocity | | L1 | `psd` | Photoshop PSD/PSB reader (layers, groups, masks, blend modes, text, layer effects, adjustment layers; 8/16/32-bit; RGB/CMYK/Gray/Lab) and a minimal writer, from Adobe's published format specification | @@ -32,7 +34,7 @@ cpal or muda. Everything in L0 to L4, the egui UI and the web app also build for | L3 | `render` | Evaluation and compositing: sources, masks, effects, transforms, 3D, motion blur, mattes, blending, layer cache, audio mixdown | | L3 | `media` | Footage decoding (FilmCraft's pure-Rust codecs), image sequences, Photoshop and SVG stills, frame cache | | L3 | `expr` | The expression engine (JavaScript via boa) with the After Effects object model | -| L3 | `export` | Render queue encoding: H.264, ProRes, WebM (VP9 + alpha, Opus), PNG/JPEG/TIFF/EXR sequences, GIF, WAV/AIFF | +| L3 | `export` | Render queue encoding: H.264, HEVC and AV1 MP4, ProRes, WebM (VP9 + alpha or AV1, Opus), PNG/JPEG/TIFF/EXR sequences, GIF, WAV/AIFF | | L3 | `gpu` | The GPU compositor and GPU effects on wgpu compute shaders (Metal, Vulkan, Direct3D 12, WebGPU), checked against the CPU renderer | | L3 | `lottie` | Lottie JSON / dotLottie import and export (layers, precomps, eased and spatial keyframes, shapes, masks, mattes) with a warnings list for what Lottie cannot express | | L3 | `plugin` | Effect plug-ins: loads sandboxed WebAssembly effect modules (plug-in API v1; the wasmi interpreter behind the `wasm` feature, on for native hosts) into the effect registry ([plugins.md](plugins.md)) | @@ -173,7 +175,15 @@ fragment sort (intersecting planes, coplanar stack order), Blinn-Phong lighting, against the caster planes (Shadow Diffusion, Light Transmission) and blending. Adjustment layers run their effect stacks on the GPU-resident comp (`Renderer::run_effects_on` with an `FxTarget`): runs of GPU effects stay on the device and only non-GPU effects read back and upload. Advanced 3D -compositing and wireframes still run on the CPU between GPU steps (read back, draw, upload). GPU +runs render on the GPU end to end (`gpu::adv3d`, `Renderer::prepare_adv_run` / +`Accelerator::render_3d`): each motion-blur sub-sample's scene is rasterised (`advanced3d.wgsl`), +then compute kernels (`adv3d.wgsl`) resolve the 2×2 supersampling, average the sub-samples +(nearest depth), apply the depth-based iris depth of field with the Classic 3D bokeh spans +(`three_d::bokeh::kernel_spans`, highlight boost, progressive blur levels) and composite over the +GPU canvas; only the depth of field reads back its 8-byte radius range. Advanced 3D layers with +blend modes, track mattes or Preserve Transparency and environment backgrounds take the CPU's +2D compositing path (their scenes still render through `render_3d`). Wireframe outlines draw on +the GPU from the CPU's pixel list (`Renderer::wireframe_pixels`). GPU effects (`effects::GPU_EFFECTS`, 152 of them: blurs, colour correction, keying incl. Key Light, mattes, channel, stylize, distortion, transitions, generators, noise, grain and time; see [effects.md](effects.md)) repeat the CPU effect's steps (padding, box radii, parameters, hashes) diff --git a/docs/parity.md b/docs/parity.md index 94603950..4eac5a3c 100644 --- a/docs/parity.md +++ b/docs/parity.md @@ -35,8 +35,8 @@ Tracking 96%, Effects 85%, Web 65%. What is left, in priority order: the web app's depth (threads, storage, audio, non-blocking renders); stroke taper/wave and multi-segment dashes; variable mask feather points; camera iris/bokeh and focus-link commands; -approximated effects (Key Cleaner) and Liquify's viewer brush; mesh shadings and JPX images in -imported PDF/AI files and Illustrator procset EPS; and more codecs. Face tracking and a true Subspace Warp +approximated effects (Key Cleaner) and Liquify's viewer brush; JPX / JBIG2 images in +imported PDF/AI files (mesh shadings and CCITT landed in M13.12) and Illustrator procset EPS; and codec depth (B-frames, SAO/CDEF; HEVC and AV1 export and Opus SILK/hybrid landed in M13.4). Face tracking and a true Subspace Warp landed in M13.3. ScriptUI (dialogs that block `show()`, palettes, dockable ScriptUI panels, File ▸ Scripts install and sample scripts), puppet pin recording and the remaining "better than After Effects" items — a versioned effect plug-in API with sandboxed WebAssembly plug-ins ([plugins.md](plugins.md)), @@ -126,21 +126,21 @@ the disk cache in the formats wave; the 3D Camera Tracker in M12.6.) | Area | Weighted parity | Remaining (agent-hours) | Biggest gaps | |---|---|---|---| | Layers | 88% | 4.8 | frame blending, collapse transformations, slip edit | -| Output | 97% | 0.8 | Render Settings complete (field render + 3:2 pulldown, effects/solo/guide/depth/blending/blur overrides, time sampling, storage overflow), Output Module crop/ROI/resize, alpha modes, post-render actions, PCM formats, templates with defaults, render logs, Notify (M10.2); WebM VP9 key + inter frames with motion search, loop filter and rate control. Left: Opus is CELT-only, Photoshop sequence output, overflow for movies only checks at file creation | +| Output | 97% | 0.8 | Render Settings complete (field render + 3:2 pulldown, effects/solo/guide/depth/blending/blur overrides, time sampling, storage overflow), Output Module crop/ROI/resize, alpha modes, post-render actions, PCM formats, templates with defaults, render logs, Notify (M10.2); WebM VP9 key + inter frames with motion search, loop filter and rate control; M13.4: HEVC export (MP4 `hvc1`, `effectcraft-hevcenc`: Main / Main 10, IDR + P slices, quarter-pel motion, deblocking, bit-exact with ffmpeg) and AV1 export (MP4 `av01` and WebM, `effectcraft-av1enc`: 8/10-bit key + inter frames, quarter-pel motion, loop filter, bit-exact with libdav1d) with profile, level, bitrate / constant-quality and key-frame interval options; Opus SILK (NB/MB/WB, mid/side stereo) and hybrid (SWB/FB) modes chosen by bitrate and Audio/Voice tuning, with the Opus bitrate in the Output Module. HEVC/AV1 import decodes through FilmCraft (MP4/MOV/MKV/WebM). Left: HEVC/AV1 have no B-frames, multi-reference, SAO/CDEF/restoration or alpha (compression below mature encoders); Opus has no FEC/DTX and only 20 ms frames; Photoshop sequence output, overflow for movies only checks at file creation | | Audio | 85% | 0.5 | audio to keyframes | -| Import | 96% | 0.6 | mesh shadings (types 4–7), JPX / CCITT / JBIG2 images and non-Identity CJK CMaps inside PDF/AI files, Illustrator EPS relying on Adobe procsets, PSD 3D layers (M13.6: PDF/AI text with embedded TrueType / CFF / Type 1 / Type 3 fonts and standard-14 fallback to the bundled fonts, Flate / DCT / inline / stencil images with soft masks, Indexed and ICC alternates, luminosity / alpha soft masks, the 16 blend modes, tiling patterns, calculator functions, any page via `file.import page` and the Import dialog, clip groups kept by Create Shapes from Vector Layer as layer masks / Merge Paths; PSD smart-object perspective quads and placed-layer warps — named styles and custom quilt meshes — baked as placed; PDF / PDF-compatible AI / EPS vector footage with Continuously Rasterize, layered composition import and Create Shapes from Vector Layer, and PSD smart objects with embedded files landed in M13.2; PSD as footage/composition/retain layer sizes, SVG footage earlier) | -| Automation | ≈ 99% | 0.1 | `.jsxbin` (AUT-2: ScriptUI resource strings, `onDraw`/ScriptUIGraphics, live `onChanging`, `Socket`, Essential Graphics hooks — `addToMotionGraphicsTemplate(As)`, `canAddToMotionGraphicsTemplate`, `exportAsMotionGraphicsTemplate`, `motionGraphicsTemplateName`, controller count/names — and Watch Folder landed in M13.7; the core object model landed in M14.4; ScriptUI windows/dialogs/dockable panels with `scriptui.*` agent commands, File ▸ Scripts install + sample scripts, and the effect plug-in API (EFF-6, WebAssembly) landed in M13.1) | +| Import | 97% | 0.5 | JPX / JBIG2 images and predefined non-Identity CJK CMaps inside PDF/AI files, Illustrator EPS relying on Adobe procsets, PSD 3D layers (M13.12: mesh shadings — free-form / lattice Gouraud triangles, Coons and tensor patches — and function-based shadings, CCITT G3 / G4 images, knockout / isolated transparency groups with group alpha and soft masks on the group's result, `/W2` vertical metrics, EPS text with embedded Type 1 / CFF or bundled fonts, and Create Shapes from Vector Layer keeping images as parented footage layers in paint order; M13.6: PDF/AI text with embedded TrueType / CFF / Type 1 / Type 3 fonts and standard-14 fallback to the bundled fonts, Flate / DCT / inline / stencil images with soft masks, Indexed and ICC alternates, luminosity / alpha soft masks, the 16 blend modes, tiling patterns, calculator functions, any page via `file.import page` and the Import dialog, clip groups kept by Create Shapes from Vector Layer as layer masks / Merge Paths; PSD smart-object perspective quads and placed-layer warps — named styles and custom quilt meshes — baked as placed; PDF / PDF-compatible AI / EPS vector footage with Continuously Rasterize, layered composition import and Create Shapes from Vector Layer, and PSD smart objects with embedded files landed in M13.2; PSD as footage/composition/retain layer sizes, SVG footage earlier) | +| Automation | ≈ 99% | 0.1 | `.jsxbin` (AUT-2: concave / self-intersecting `onDraw` fills, real images in `drawImage`, image controls and icon buttons landed in M13.11; ScriptUI resource strings, `onDraw`/ScriptUIGraphics, live `onChanging`, `Socket`, Essential Graphics hooks — `addToMotionGraphicsTemplate(As)`, `canAddToMotionGraphicsTemplate`, `exportAsMotionGraphicsTemplate`, `motionGraphicsTemplateName`, controller count/names — and Watch Folder landed in M13.7; the core object model landed in M14.4; ScriptUI windows/dialogs/dockable panels with `scriptui.*` agent commands, File ▸ Scripts install + sample scripts, and the effect plug-in API (EFF-6, WebAssembly) landed in M13.1) | | Shapes | ≈ 80% | 1.5 | Lottie can't carry stroke taper/wave (stroke Taper and Wave, Dash 2/Gap 2/Dash 3/Gap 3 and radial-gradient Highlight Length/Angle landed in M13.5; pen tool for shape paths and vertex editing in M6.5) | -| Compositions | ≈ 80% | 3.5 | Mocha-style planar tracks for templates, Essential Graphics' mirrored properties (CMP-7: Font and uniform Scale controls, Composition ▸ Open in Essential Graphics, Save Frame As ▸ Photoshop Layers / ProEXR and the VR Comp Editor landed in M13.7; the marker dialog, Composition Flowchart, Essential Graphics with master properties, `.ectemplate` templates and Responsive Design — Time landed: CMP-6, CMP-7) | +| Compositions | ≈ 81% | 3.3 | Mocha-style planar tracks for templates (CMP-7: Essential Graphics mirrored and linked properties landed in M13.11; Font and uniform Scale controls, Composition ▸ Open in Essential Graphics, Save Frame As ▸ Photoshop Layers / ProEXR and the VR Comp Editor landed in M13.7; the marker dialog, Composition Flowchart, Essential Graphics with master properties, `.ectemplate` templates and Responsive Design — Time landed: CMP-6, CMP-7) | | Animation | 70% | 9.0 | puppet depth beyond pins and recording (puppet pin recording with Record Options landed in M13.1), Wiggler/Smoother/Motion Sketch (motion-path handles and the graph editor transform box landed in M5.8; keyframe colour labels and Select Keyframe Label Group, Graph Editor snapping to markers / layer ends in M13.5) | -| Text | ≈ 95% | 0.4 | no extruded strokes, the Variable Font Axes animator changes outlines but not advances (M13.6: variable font axes in the character style — `layer.setText variations`, the Character panel's Variable Font Axes fields — shape with HVAR / gvar advances and draw at that design-space position; OpenType features — stylistic sets, discretionary ligatures, contextual / stylistic alternates, swash, titling, ordinals, fractions, figure styles, true small caps / all small caps and superior / inferior glyphs with faux fallback — per character with the Character panel's OpenType popup and `text.fontFeatures` landed in M13.2; vertical Roman / Tate-Chu-Yoko, forced LTR paragraphs, caret on animated and path text, Variable Font Axes and Lottie style runs landed in M13.5; extruded, bevelled text in M7.6; per-character styles, paragraph settings, on-canvas editing and the `sourceText` style API in M9.9–M9.10) | +| Text | ≈ 96% | 0.3 | no extruded strokes (M13.12: the Variable Font Axes animator re-spaces the text — advances follow the animated axes; M13.6: variable font axes in the character style — `layer.setText variations`, the Character panel's Variable Font Axes fields — shape with HVAR / gvar advances and draw at that design-space position; OpenType features — stylistic sets, discretionary ligatures, contextual / stylistic alternates, swash, titling, ordinals, fractions, figure styles, true small caps / all small caps and superior / inferior glyphs with faux fallback — per character with the Character panel's OpenType popup and `text.fontFeatures` landed in M13.2; vertical Roman / Tate-Chu-Yoko, forced LTR paragraphs, caret on animated and path text, Variable Font Axes and Lottie style runs landed in M13.5; extruded, bevelled text in M7.6; per-character styles, paragraph settings, on-canvas editing and the `sourceText` style API in M9.9–M9.10) | | Web | 93% | 0.5 | GPU effects in the browser (WebGPU compositing only on the page), no disk cache, no shared-memory threads inside one engine instance (browser storage, Web Audio, Web Worker renders/analyses, WebGPU viewer and offline install landed in M15.2; viewer frames in frame workers fed by project diffs, Roto Brush propagation in a worker, non-blocking `wait: true` jobs and a browser Media Browser (File System Access folders, browser storage) in M13.10) | -| 3D | 88% | 5.5 | multi-view layouts, the Extended Viewer for Advanced 3D comps (Classic 3D Extended Viewer landed in M13.5 UI completion); collapsed precomps of another size seen through the parent's camera render (fixed in M13.2); stereo rigs, orbit nulls, lights controlled by the camera, cameras/lights from glTF models, environment backgrounds, Advanced 3D motion blur, blend modes and track mattes landed in M7.7; Classic 3D iris-shaped bokeh with highlights, progressive depth of field on tilted layers and the focus-link commands landed in M13.5; Advanced 3D depth of field with the iris and highlight options, collapsed precomps as real Advanced 3D geometry and extruded text/shape strokes landed in M13.8; Advanced 3D (glTF/OBJ models, primitives, extruded text and shapes, PBR, image-based light, shadow maps, GPU rasteriser) in M7.4–M7.6 | +| 3D | 88% | 5.5 | multi-view layouts, the Extended Viewer for Advanced 3D comps (Classic 3D Extended Viewer landed in M13.5 UI completion); collapsed precomps of another size seen through the parent's camera render (fixed in M13.2); stereo rigs, orbit nulls, lights controlled by the camera, cameras/lights from glTF models, environment backgrounds, Advanced 3D motion blur, blend modes and track mattes landed in M7.7; Classic 3D iris-shaped bokeh with highlights, progressive depth of field on tilted layers and the focus-link commands landed in M13.5; Advanced 3D depth of field with the iris and highlight options, collapsed precomps as real Advanced 3D geometry and extruded text/shape strokes landed in M13.8; Advanced 3D (glTF/OBJ models, primitives, extruded text and shapes, PBR, image-based light, shadow maps, GPU rasteriser) in M7.4–M7.6; Advanced 3D end to end on the GPU (motion blur, iris depth of field, compositing) in M13.11 | | Effects | ≈ 85% | 3.0 | GPU versions of the remaining effects (152 run on the GPU since M13.9, EFF-5: keying, mattes, channel, colour correction, stylize, distort, noise, blurs and time effects joined the 58 of M12.7; Warp, Bezier Warp, Reshape, Smear, Cartoon, CC Page Turn and others still render on the CPU) and the missing controls listed as partial in [effects.md](effects.md) (every After Effects effect exists since M9.11, M12.5 and M12.6; parameter names, order, twirl-downs, popups, units and defaults were aligned in M9.12) | | Interface | 74% | 5.0 | more Learn tutorials and pixel-level fidelity of dialogs (the Home ▸ Learn tab with interactive tutorials and a UI fidelity pass landed in M13.5 UI completion; visual editors for Lumetri RGB / hue-saturation curves, Colorama's output cycle wheel, Glow's colour map and Reshape's correspondence points (viewer handles), a scrolling Preview panel and AE-style Composition / Timeline tabs (close, label swatch, viewer lock) landed in M13.10; Timeline outline and Project panel columns scroll horizontally, the Layer Style dialog, ROI resize handles, Pan Behind snapping and 3D Reference Axes landed in M13.5; native macOS menu bar, Timeline columns/search/reveal-add, Home screen with recent projects and all AE workspaces landed; viewer rulers/snapping/channels/snapshots landed in M0.13) | | Project | ≈ 68% | 5.5 | auto-save, folder moves, OCIO displays beyond the built-in tone map (Color Engine with OCIO/ACES working spaces, HDR compand/tone mapping, Rec. 2100 PQ/HLG output, Feet + Frames, display colour management, Simulate Output and the locked viewer landed in M7.7; proxies and Interpret Footage fields / pixel aspect / alpha guess landed: PRJ-8, PRJ-3) | | Masks & roto | 74% | 5.0 | Roto Brush's learned (3.0) segmentation model (variable-width mask feather points with the Mask Feather tool landed in M13.5; mask tracking and Mask Interpolation landed in M6.6; Roto Brush & Refine Edge with graph-cut segmentation, flow propagation, edge matting, decontamination and Freeze in M6.7) | -| Preview | 74% | 3.2 | wireframes and Advanced 3D compositing on the GPU (Classic 3D bokeh depth of field — iris shapes, highlights, fringe, progressive blur on tilted planes — runs in WGSL since M13.6: 3D Showcase GPU warm 205 → 54 ms/frame at full size on an M4 Pro; the Preview panel's five shortcuts with their own Include / Loop / Cache Before Playback / Range / Play From / Frame Rate / Skip / Resolution / Full Screen / stop options and `playback.settings.get/set` landed in M13.8; Classic 3D runs and adjustment layers composite on the GPU since M12.7; persistent disk cache with the blue cache bar landed; region of interest, snapshots, exposure and Fast Previews landed in M0.13) | +| Preview | 77% | 2.8 | GPU compositing of Advanced 3D layers with blend modes / track mattes and of environment backgrounds (Advanced 3D runs — raster, motion blur, iris depth of field, compositing — and wireframes run on the GPU since M13.11: CPU warm 3394 → GPU warm 290 ms/frame at 1920×1080 (11.7×; Half 968 → 85 ms), GPU ≠ CPU on 0.005 % of pixels, on an M4 Pro under load; Classic 3D bokeh depth of field — iris shapes, highlights, fringe, progressive blur on tilted planes — runs in WGSL since M13.6: 3D Showcase GPU warm 205 → 54 ms/frame at full size on an M4 Pro; the Preview panel's five shortcuts with their own Include / Loop / Cache Before Playback / Range / Play From / Frame Rate / Skip / Resolution / Full Screen / stop options and `playback.settings.get/set` landed in M13.8; Classic 3D runs and adjustment layers composite on the GPU since M12.7; persistent disk cache with the blue cache bar landed; region of interest, snapshots, exposure and Fast Previews landed in M0.13) | | Tracking | ≈ 96% | 0.3 | face tracking is a classical (skin model + feature components + shape model) fitter, not a learned detector: profile views and occluded faces are weak; Rolling Shutter Ripple is approximated by Subspace Warp's mesh density (face tracking (Outline Only / Detailed Features with Face Track Points and Extract & Copy Face Measurements), Subspace Warp's content-preserving mesh warp on subspace-smoothed trajectories, and radial lens distortion (k1, k2) in the camera bundle adjustment with Undistort Footage landed in M13.3; Rolling Shutter Repair in M9.11; point tracker, mask tracking, Warp Stabilizer and the 3D Camera Tracker in M6.x / M12.5 / M12.6) | | Paint | 0% | 4.0 | Brush, Clone Stamp, Eraser | @@ -180,8 +180,8 @@ Tate-Chu-Yoko, forced LTR paragraphs, the editing caret on animated / path text style runs; the Layer Style dialog (LYR-9); Graph Editor snapping to markers and layer ends (ANM-4; the reference graph existed); viewer ROI resize handles, Pan Behind snapping and 3D Reference Axes; Animate Text ▸ Variable Font Axes. Remaining in these rows: Lottie can't carry -taper/wave and the Variable Font Axes animator doesn't change advances (the Character panel's -axes do since M13.6; Advanced 3D's DOF has iris shapes since M13.8). +taper/wave (the Variable Font Axes animator changes advances since M13.12, the Character panel's +axes since M13.6; Advanced 3D's DOF has iris shapes since M13.8). ## Update: M13.8 Preview panel, Align panel, Advanced 3D completion @@ -204,6 +204,41 @@ axes do since M13.6; Advanced 3D's DOF has iris shapes since M13.8). as real geometry lit by the parent (2D collapsed precomps draw nested 3D layers with the parent's renderer); text and shape strokes extrude as bevelled meshes in paint order. +## Update: M13.11 Advanced 3D on the GPU, Essential Graphics mirrors, ScriptUI paint + +- **Advanced 3D on the GPU (PRV-3, 3D-3)**: a whole Advanced 3D run renders on the device + (`gpu::adv3d`, `Renderer::prepare_adv_run`, `Accelerator::render_3d`): every motion-blur + sub-sample's scene is rasterised (depth buffer, PBR, image-based light, shadow maps), then + compute kernels resolve the 2×2 supersampling, average the sub-samples (nearest depth), apply + the depth-based iris depth of field (the Classic 3D bokeh row spans and prefix-sum gather, + highlight boost, progressive levels) and composite over the GPU canvas without a readback. + Meshes from extruded text and shapes, cards, glTF models and collapsed precomps all go through + it. Layers with blend modes, track mattes or Preserve Transparency (and environment + backgrounds) keep the CPU's 2D compositing path, their scenes still rendered by + `render_3d`. Wireframe-quality outlines draw on the GPU (exactly the CPU's pixels). The CPU + stays the oracle: GPU-vs-CPU tests on lit scenes with shadows, environment light, DOF (hexagon + with fringe and highlights, Fast Rectangle, radii at the 48 px cap), motion blur, extrusions + and collapsed precomps agree within 1/255 on ≥ 99 % of pixels (measured: ≤ 0.01 %; the rest + are silhouette pixels the two rasterisers cover differently), and the GPU post kernels match + the CPU post-processing of the same GPU raster on ≥ 99.9 % (measured: all pixels). Fixed on + the way: NaN environment lookups for straight up/down normals on Metal (`atan2(0, 0)`). + `bench --gpu --adv3d` (1920×1080 Advanced 3D comp: PBR primitives, bevelled extruded text, a + shadow-casting spot, point and environment lights, hexagonal-iris DOF with highlights, + 8-sample motion blur): CPU warm 3394 → GPU warm 290 ms/frame at 1920×1080 (11.7×; Half 968 → 85 ms), GPU ≠ CPU on 0.005 % of pixels, on an M4 Pro under load. +- **Essential Graphics mirrors (CMP-7)**: adding a property that is already in the panel adds a + *mirror* (its own name and place; one value in the comp and in every instance — editing, + reverting or pushing either acts on both; Font / Scale `as` controls mirror per kind); + `essential.addMirror`. A property control can also drive further properties of the comp + (`essential.linkProperty` / `unlinkProperty`): links follow the main property both ways and + take each instance's value. Panel badges and a ⋯ menu (Add Mirror, Link Selected Property, + Unlink); `addToMotionGraphicsTemplate` on a property already present adds a mirror, and + mirrors count as controllers. +- **ScriptUI (AUT-2)**: `fillPath` fills any path (non-zero winding trapezoid tessellation: + concave, self-intersecting, holes) instead of egui's convex-only polygons; + `ScriptUI.newImage` / ScriptUIImage hold an image file or embedded PNG/JPEG bytes, drawn by + `drawImage`, image controls and icon buttons; script-host threads are compiled out of the + web build (no dead code warnings). + ## Update: M13.5 UI completion & fidelity **Extended Viewer** (Settings ▸ 3D, the viewer's Extended Viewer button, `view.extendedViewer`): @@ -241,6 +276,32 @@ menu entries. Not yet: creating a reference frame by editing a still (After Effe Photoshop), Content-Aware Fill's learned model (ours is PatchMatch + flow propagation), and Media Browser in the web app (landed in M13.10: browser storage and File System Access folders). +## Update: M13.12 import and text leftovers, test hardening + +- **PDF / AI**: mesh shadings — free-form and lattice-form Gouraud triangle meshes (types 4, 5), + Coons and tensor-product patch meshes (6, 7, tessellated to triangles) — and function-based + shadings (type 1, with multi-input sampled functions), for `sh` and shading-pattern fills, + rasterised into image nodes; CCITT Group 3 (1-D and 2-D) and Group 4 images from ITU-T T.4 / + T.6 (checked against an independent encoder's output); knockout and isolated transparency + groups, the current alpha / blend mode / soft mask applied to a transparency group XObject's + combined result, and blend modes inside clipping groups reaching the backdrop; `/W2` / `/DW2` + vertical metrics and CMap `WMode`. Not done: JBIG2 images (deferred) and predefined CJK CMaps (no permissively licensed + pure-Rust source of the CMap data; Adobe's CMap resources are Adobe assets). +- **EPS**: text through the PostScript interpreter — `findfont` / `scalefont` / `makefont` / + `selectfont` / `setfont`, `show`, `ashow`, `widthshow`, `awidthshow`, `xshow` / `yshow` / + `xyshow`, `kshow`, `glyphshow`, `charpath`, `stringwidth`, re-encoded fonts (`definefont` + copies with a new `/Encoding`) — with embedded Type 1 and CFF programs or the bundled fonts. +- **Create Shapes from Vector Layer**: images become PNG footage layers parented to the shape + layer, and the shapes between images are split into layers of their own so the stack keeps + the document's paint order; soft masks and clipped images are reported. +- **Text**: the Variable Font Axes animator re-spaces the text (advances at the animated + design-space position), not only the outlines. +- **Hardening**: PDF / EPS parsers survive truncated and corrupted files (fixed an + out-of-range stream slice and unbounded PostScript recursion); WebAssembly plug-ins get a + memory cap, a manifest length limit and slider schema checks, with tests for missing exports, + wrong signatures and versions, fuel exhaustion, traps and bit-identical output; script + `Socket` validates ports, and ScriptUI resource strings accept trailing array commas. + ## Highest-value gaps, in order 1. ~~On-canvas text editing and per-character styles~~ (landed: M9.9–M9.10). diff --git a/docs/plugins.md b/docs/plugins.md index 5d5b39c0..47ce5caa 100644 --- a/docs/plugins.md +++ b/docs/plugins.md @@ -12,7 +12,7 @@ There are two ways to write one: |---|---|---| | What | a `.wasm` (or `.wat`) module | a type implementing `effectcraft_effects::plugin::EffectPlugin` | | Loaded by | Effect ▸ Load Effect Plug-in…, `effect.plugins.load {path \| folder}`, or the `Plug-ins` folder next to the settings at start-up | `register_plugin(Arc::new(MyEffect))` in an app that links EffectCraft's crates | -| Sandbox | yes: no imports (no files, clock or network), fuel-limited, deterministic floats | none (it is your code) | +| Sandbox | yes: no imports (no files, clock or network), fuel-limited, at most 1 GiB of memory, deterministic floats | none (it is your code) | | Platforms | desktop and CLI (the wasmi interpreter, crate feature `effectcraft-plugin/wasm`); not the web build | everywhere | `effect.plugins.list` lists what is loaded: `{api, wasm, plugins: [{id, name, category, version, @@ -60,7 +60,9 @@ Every plug-in describes itself with a JSON manifest: * Parameter types: `slider` (`default`, `min`, `max`, optional `sliderMin`, `sliderMax`, `decimals`), `angle` (`default` degrees), `checkbox` (`default` bool), `popup` (`options`, `default` index), `point` (`default` as fractions of the layer size, `[0.5, 0.5]` = centre), - `color` (`default` RGBA 0–1). Parameter ids are letters, digits and `_`. + `color` (`default` RGBA 0–1). Parameter ids are letters, digits and `_`, unique; at most 256 + parameters; a slider needs `min ≤ default ≤ max` (and `sliderMin ≤ sliderMax`); the manifest + is at most 1 MiB. ## WebAssembly ABI (v1) diff --git a/xtask/src/main.rs b/xtask/src/main.rs index 01cb44b4..681aee1e 100644 --- a/xtask/src/main.rs +++ b/xtask/src/main.rs @@ -26,6 +26,8 @@ const LAYERS: &[(&str, u8)] = &[ ("vp9enc", 0), ("testkit", 0), ("opusenc", 0), + ("hevcenc", 0), + ("av1enc", 0), ("raster", 1), ("keyframe", 1), ("path", 1),