diff --git a/Cargo.lock b/Cargo.lock index 07348727..d0b99c47 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", @@ -1181,7 +1188,7 @@ dependencies = [ [[package]] name = "effectcraft-color" -version = "0.1.0" +version = "0.1.1" dependencies = [ "proptest", "serde", @@ -1189,7 +1196,7 @@ dependencies = [ [[package]] name = "effectcraft-effects" -version = "0.1.0" +version = "0.1.1" dependencies = [ "effectcraft-color", "effectcraft-geom", @@ -1205,7 +1212,7 @@ dependencies = [ [[package]] name = "effectcraft-engine" -version = "0.1.0" +version = "0.1.1" dependencies = [ "effectcraft-color", "effectcraft-effects", @@ -1236,9 +1243,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", @@ -1263,7 +1272,7 @@ dependencies = [ [[package]] name = "effectcraft-expr" -version = "0.1.0" +version = "0.1.1" dependencies = [ "boa_engine", "effectcraft-color", @@ -1280,7 +1289,7 @@ dependencies = [ [[package]] name = "effectcraft-geom" -version = "0.1.0" +version = "0.1.1" dependencies = [ "proptest", "serde", @@ -1288,7 +1297,7 @@ dependencies = [ [[package]] name = "effectcraft-gpu" -version = "0.1.0" +version = "0.1.1" dependencies = [ "bytemuck", "effectcraft-color", @@ -1305,9 +1314,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", @@ -1327,7 +1343,7 @@ dependencies = [ [[package]] name = "effectcraft-keyframe" -version = "0.1.0" +version = "0.1.1" dependencies = [ "effectcraft-color", "effectcraft-geom", @@ -1339,7 +1355,7 @@ dependencies = [ [[package]] name = "effectcraft-lottie" -version = "0.1.0" +version = "0.1.1" dependencies = [ "effectcraft-color", "effectcraft-effects", @@ -1356,7 +1372,7 @@ dependencies = [ [[package]] name = "effectcraft-media" -version = "0.1.0" +version = "0.1.1" dependencies = [ "effectcraft-model", "effectcraft-pdf", @@ -1381,7 +1397,7 @@ dependencies = [ [[package]] name = "effectcraft-model" -version = "0.1.0" +version = "0.1.1" dependencies = [ "effectcraft-geom", "image", @@ -1392,11 +1408,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", @@ -1408,7 +1427,7 @@ dependencies = [ [[package]] name = "effectcraft-pdf" -version = "0.1.0" +version = "0.1.1" dependencies = [ "effectcraft-raster", "effectcraft-svg", @@ -1422,7 +1441,7 @@ dependencies = [ [[package]] name = "effectcraft-plugin" -version = "0.1.0" +version = "0.1.1" dependencies = [ "effectcraft-effects", "effectcraft-keyframe", @@ -1434,7 +1453,7 @@ dependencies = [ [[package]] name = "effectcraft-project" -version = "0.1.0" +version = "0.1.1" dependencies = [ "effectcraft-color", "effectcraft-geom", @@ -1447,14 +1466,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", @@ -1463,7 +1482,7 @@ dependencies = [ [[package]] name = "effectcraft-render" -version = "0.1.0" +version = "0.1.1" dependencies = [ "effectcraft-color", "effectcraft-effects", @@ -1485,7 +1504,7 @@ dependencies = [ [[package]] name = "effectcraft-script" -version = "0.1.0" +version = "0.1.1" dependencies = [ "boa_engine", "effectcraft-engine", @@ -1500,7 +1519,7 @@ dependencies = [ [[package]] name = "effectcraft-svg" -version = "0.1.0" +version = "0.1.1" dependencies = [ "effectcraft-geom", "effectcraft-path", @@ -1512,7 +1531,7 @@ dependencies = [ [[package]] name = "effectcraft-text" -version = "0.1.0" +version = "0.1.1" dependencies = [ "effectcraft-keyframe", "harfrust", @@ -1524,7 +1543,7 @@ dependencies = [ [[package]] name = "effectcraft-time" -version = "0.1.0" +version = "0.1.1" dependencies = [ "proptest", "serde", @@ -1532,7 +1551,7 @@ dependencies = [ [[package]] name = "effectcraft-track" -version = "0.1.0" +version = "0.1.1" dependencies = [ "effectcraft-raster", "rayon", @@ -1543,7 +1562,7 @@ dependencies = [ [[package]] name = "effectcraft-ui-egui" -version = "0.1.0" +version = "0.1.1" dependencies = [ "effectcraft-color", "effectcraft-engine", @@ -1571,11 +1590,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", @@ -6189,7 +6208,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 ca904233..27a118de 100644 --- a/apps/effectcraft-cli/src/main.rs +++ b/apps/effectcraft-cli/src/main.rs @@ -9,11 +9,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 script FILE.jsx [F.ecproj] | --eval CODE run an After Effects-style script //! effectcraft-cli mcp [--bridge PORT] MCP server on stdio @@ -44,8 +46,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})); @@ -786,6 +816,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; @@ -796,14 +929,17 @@ 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(); project.settings.gpu_acceleration = true; 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 } }; @@ -864,7 +1000,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/apps/effectcraft-web/js/host.js b/apps/effectcraft-web/js/host.js index 7b6200eb..1fcdc40b 100644 --- a/apps/effectcraft-web/js/host.js +++ b/apps/effectcraft-web/js/host.js @@ -333,3 +333,158 @@ export function workerCancel(id) { export function workerStats() { return { running: running.size, idle: idle.length }; } + +// ------------------------------------------------------------------ frame workers + +// Long-lived workers that render viewer frames from a project replica (src/frames.rs). Unlike job +// workers they are never terminated between requests; the page sends one frame at a time each. +const frameWorkers = []; + +/// Start a frame worker; returns its index. `onMessage(index, json, bytes, error)` receives each +/// `frameReply` (JSON, pixels as a Uint8Array) or, if the worker dies, the error. +export function frameWorkerStart(base, onMessage) { + const index = frameWorkers.length; + const worker = new Worker(new URL("worker.js", base), { type: "module", name: `effectcraft-frames-${index}` }); + worker.onmessage = (e) => { + const m = e.data; + if (m && m.type === "frameReply") onMessage(index, m.json, m.bytes ? new Uint8Array(m.bytes) : null, undefined); + }; + worker.onerror = (e) => { + onMessage(index, undefined, undefined, String(e.message || "frame worker failed")); + worker.terminate(); + }; + worker.postMessage({ type: "init", module: self.effectcraftModule ?? null, base }); + frameWorkers.push(worker); + return index; +} + +/// Post a message ({type: "frame", json} or {type: "file", path, bytes}) to frame worker `index`. +export function frameWorkerPost(index, msg, transfer) { + const w = frameWorkers[index]; + if (w) w.postMessage(msg, transfer ?? []); +} + +// ------------------------------------------------------------------ media browser + +// Folders opened with the File System Access API (src/browse.rs): name → {handle, files: Map of +// "name/sub/file" → FileSystemFileHandle}. Their handles are remembered in IndexedDB so the +// folders come back on the next visit (once the browser grants access again). +const folders = new Map(); +const HANDLES_DB = "effectcraft-handles"; +const MAX_ENTRIES = 20000; + +async function handlesDb() { + const open = indexedDB.open(HANDLES_DB, 1); + open.onupgradeneeded = () => open.result.createObjectStore("folders", { keyPath: "name" }); + return req(open); +} + +async function enumerate(dir, prefix, out, files, depth) { + for await (const [name, h] of dir.entries()) { + if (name.startsWith(".") || out.length >= MAX_ENTRIES) continue; + const rel = `${prefix}/${name}`; + if (h.kind === "directory") { + out.push({ path: rel, dir: true, size: 0, modified: 0 }); + if (depth < 8) await enumerate(h, rel, out, files, depth + 1); + } else { + const f = await h.getFile(); + out.push({ path: rel, dir: false, size: f.size, modified: Math.floor(f.lastModified / 1000) }); + files.set(rel, h); + } + } +} + +async function openFolder(handle, remember) { + const name = handle.name; + const entries = []; + const files = new Map(); + await enumerate(handle, name, entries, files, 0); + folders.set(name, { handle, files }); + if (remember) { + try { + const db = await handlesDb(); + await req(db.transaction("folders", "readwrite").objectStore("folders").put({ name, handle })); + } catch (e) { console.info("media browser: cannot remember the folder", e); } + } + return { name, entries }; +} + +/// Whether the browser can open folders (File System Access API). +export function browseCanPickFolder() { + return typeof self.showDirectoryPicker === "function"; +} + +/// Ask for a folder; resolves with {name, entries: [{path, dir, size, modified}]}. +export async function browsePickFolder() { + const handle = await self.showDirectoryPicker({ id: "effectcraft-media", mode: "read" }); + return openFolder(handle, true); +} + +/// Folders opened on earlier visits: [{name, granted, entries?}] (entries when access is granted). +export async function browseRestore() { + if (!self.indexedDB || !browseCanPickFolder()) return []; + let all = []; + try { + const db = await handlesDb(); + all = await req(db.transaction("folders", "readonly").objectStore("folders").getAll()); + } catch { return []; } + const out = []; + for (const { name, handle } of all) { + let granted = false; + try { granted = (await handle.queryPermission({ mode: "read" })) === "granted"; } catch {} + if (granted) { + try { out.push({ granted, ...(await openFolder(handle, false)) }); continue; } catch {} + } + out.push({ name, granted: false }); + } + return out; +} + +/// Ask for access to a remembered folder again (needs a user gesture); resolves like +/// browsePickFolder. +export async function browseReconnect(name) { + const db = await handlesDb(); + const rec = await req(db.transaction("folders", "readonly").objectStore("folders").get(name)); + if (!rec) throw new Error(`${name} is not remembered`); + if ((await rec.handle.requestPermission({ mode: "read" })) !== "granted") throw new Error(`access to ${name} was not granted`); + return openFolder(rec.handle, false); +} + +/// Forget a remembered folder. +export async function browseForget(name) { + folders.delete(name); + try { + const db = await handlesDb(); + await req(db.transaction("folders", "readwrite").objectStore("folders").delete(name)); + } catch {} +} + +/// A file of an opened folder ("/Folders/name/sub/file"): its bytes. +export async function browseRead(path) { + const rel = path.replace(/^\/Folders\//, ""); + const folder = folders.get(rel.split("/")[0]); + const h = folder && folder.files.get(rel); + if (!h) throw new Error(`${path}: its folder is not open (reconnect it)`); + return new Uint8Array(await (await h.getFile()).arrayBuffer()); +} + +/// The fallback where folders can't be opened: pick files (or a whole folder, `directory`) with a +/// plain file input; resolves with [{path, bytes}] (path relative to the picked folder). +export function browsePickFiles(directory) { + return new Promise((resolve, reject) => { + const input = document.createElement("input"); + input.type = "file"; + input.multiple = true; + if (directory) input.webkitdirectory = true; + input.onchange = async () => { + try { + const out = []; + for (const f of input.files) { + out.push({ path: (directory && f.webkitRelativePath) || f.name, bytes: new Uint8Array(await f.arrayBuffer()) }); + } + resolve(out); + } catch (e) { reject(e); } + }; + input.click(); + }); +} diff --git a/apps/effectcraft-web/src/app.rs b/apps/effectcraft-web/src/app.rs index 0076cd4f..e6712e3d 100644 --- a/apps/effectcraft-web/src/app.rs +++ b/apps/effectcraft-web/src/app.rs @@ -6,7 +6,7 @@ use std::sync::atomic::{AtomicBool, Ordering}; use effectcraft_engine::Session; -use crate::{api, audio, files, persist, worker}; +use crate::{api, audio, browse, files, frames, persist, worker}; use effectcraft_ui_egui::EffectcraftApp; use serde_json::json; use wasm_bindgen::prelude::*; @@ -103,6 +103,7 @@ impl eframe::App for WebApp { persist::snapshot(&self.app.session, ctx.input(|i| i.time), 1.0); } api::set_info("gpu", json!({"compositor": self.app.gpu_adapter(), "viewerOnGpu": self.app.viewer_on_gpu()})); + api::set_info("frameWorkers", frames::stats()); } fn ui(&mut self, ui: &mut egui::Ui, frame: &mut eframe::Frame) { @@ -137,6 +138,7 @@ pub fn session() -> Session { exporter: Some(Arc::new(effectcraft_host::FileExporter { sink: Some(files::export_sink()) })), config: Some(Arc::new(persist::config())), offload: (!query_flag("noworkers")).then(|| Arc::new(worker::WorkerOffload) as Arc), + browser: Some(Arc::new(browse::WebBrowser)), ..Default::default() } } @@ -183,6 +185,7 @@ pub async fn start(canvas_id: String) -> Result<(), JsValue> { }; api::set_info("storage", json!({"backend": persist::backend(), "entries": stored, "loadMs": js_sys::Date::now() - t0})); audio::install_unlock(); + browse::restore(); let runner = eframe::WebRunner::new(); runner .start( @@ -206,6 +209,12 @@ pub async fn start(canvas_id: String) -> Result<(), JsValue> { app.offer_recovery(r); } install_hooks(&mut app); + // Viewer frames render in frame workers (`?frameworkers=N`, 0 or `?noworkers`: + // on the page's thread). + let n = if query_flag("noworkers") { 0 } else { query_value("frameworkers").and_then(|v| v.parse().ok()).unwrap_or(2usize).min(8) }; + if n > 0 { + app.frames.set_remote(Some(Arc::new(frames::WebFrames::start(n)))); + } let (tx, rx) = std::sync::mpsc::channel(); api::set_sender(tx); app = app.with_control(rx); diff --git a/apps/effectcraft-web/src/browse.rs b/apps/effectcraft-web/src/browse.rs new file mode 100644 index 00000000..aba2eb22 --- /dev/null +++ b/apps/effectcraft-web/src/browse.rs @@ -0,0 +1,277 @@ +//! The Media Browser in the browser ([`effectcraft_engine::media_browser::Browser`]). +//! +//! It browses a virtual tree: +//! +//! - **Browser Storage** (`/Browser Storage/…`): every file in the persistent file table (OPFS / +//! IndexedDB): imported media, uploaded files and folders, saved projects. Files added with +//! **Add Files…** / **Upload Folder…** (plain ``, so every browser) are copied +//! there and stay across visits: the recent list. +//! - **Folders** (`/Folders//…`): folders opened with **Open Folder…** through the File +//! System Access API (`showDirectoryPicker`, Chromium browsers). Only their listing is read; +//! a file's bytes are read when it is imported ([`Browser::fetch`]) and then kept in the file +//! table. The folder handles are remembered (IndexedDB), so after a reload the folders come +//! back once the browser grants access again (**Reconnect **). + +use std::sync::{LazyLock, Mutex}; + +use effectcraft_engine::media_browser::{Browser, Entry, VirtualTree}; +use serde_json::{Value, json}; +use wasm_bindgen::JsCast; +use wasm_bindgen::prelude::*; + +#[wasm_bindgen(module = "/js/host.js")] +extern "C" { + #[wasm_bindgen(js_name = browseCanPickFolder)] + fn can_pick_folder() -> bool; + #[wasm_bindgen(js_name = browsePickFolder)] + fn pick_folder() -> js_sys::Promise; + #[wasm_bindgen(js_name = browseRestore)] + fn restore_folders() -> js_sys::Promise; + #[wasm_bindgen(js_name = browseReconnect)] + fn reconnect(name: &str) -> js_sys::Promise; + #[wasm_bindgen(js_name = browseForget)] + fn forget(name: &str) -> js_sys::Promise; + #[wasm_bindgen(js_name = browseRead)] + fn read(path: &str) -> js_sys::Promise; + #[wasm_bindgen(js_name = browsePickFiles)] + fn pick_files(directory: bool) -> js_sys::Promise; +} + +const STORAGE: &str = "/Browser Storage"; +const FOLDERS: &str = "/Folders"; + +#[derive(Default)] +struct State { + /// Opened folders' files (`/Folders/…`). + folders: VirtualTree, + /// Opened folder names, and remembered ones waiting for access. + open: Vec, + disconnected: Vec, +} + +static STATE: LazyLock> = LazyLock::new(Mutex::default); + +fn state() -> std::sync::MutexGuard<'static, State> { + STATE.lock().unwrap_or_else(|e| e.into_inner()) +} + +/// The web app's Media Browser. +pub struct WebBrowser; + +/// The stored files as a tree under `/Browser Storage` (rebuilt per listing: storage changes). +fn storage_tree() -> VirtualTree { + let mut t = VirtualTree::default(); + t.add_dir(STORAGE); + for (path, size, modified, persisted) in crate::files::STORE.files() { + if !persisted || path.starts_with(FOLDERS) { + continue; + } + t.add_file(&format!("{STORAGE}{path}"), &path, size as u64, Some((modified / 1000.0) as u64)); + } + t +} + +/// Add an opened folder's listing (`[{path, dir, size, modified}]`, paths `name/…`). +fn add_folder(v: &Value) -> Option { + let name = v["name"].as_str()?.to_string(); + let mut st = state(); + st.folders.remove(&format!("{FOLDERS}/{name}")); + st.folders.add_dir(&format!("{FOLDERS}/{name}")); + for e in v["entries"].as_array().into_iter().flatten() { + let Some(rel) = e["path"].as_str() else { continue }; + let path = format!("{FOLDERS}/{rel}"); + if e["dir"].as_bool() == Some(true) { + st.folders.add_dir(&path); + } else { + st.folders.add_file(&path, &path, e["size"].as_u64().unwrap_or(0), e["modified"].as_u64()); + } + } + st.open.retain(|n| *n != name); + st.open.push(name.clone()); + st.disconnected.retain(|n| *n != name); + Some(name) +} + +fn to_value(v: &JsValue) -> Value { + js_sys::JSON::stringify(v).ok().and_then(|s| s.as_string()).and_then(|s| serde_json::from_str(&s).ok()).unwrap_or(Value::Null) +} + +/// Run a promise; on success, `then` gets its value (as JSON) on the UI thread. +fn spawn(p: js_sys::Promise, what: &'static str, then: impl FnOnce(&mut effectcraft_ui_egui::EffectcraftApp, Value) + 'static) { + wasm_bindgen_futures::spawn_local(async move { + match wasm_bindgen_futures::JsFuture::from(p).await { + Ok(v) => { + let v = to_value(&v); + crate::post(move |app| then(app, v)); + } + Err(e) => { + let msg = e.as_string().or_else(|| js_sys::Reflect::get(&e, &"message".into()).ok().and_then(|m| m.as_string())).unwrap_or_default(); + // Cancelling a picker is not an error. + if !msg.contains("abort") && !msg.contains("AbortError") { + crate::post(move |app| app.ui.status = format!("{what}: {msg}")); + } + } + } + }); +} + +fn go(app: &mut effectcraft_ui_egui::EffectcraftApp, path: &str) { + if let Err(e) = app.session.execute("mediaBrowser.go", json!({"path": path})) { + app.ui.status = e.to_string(); + } +} + +/// Bring back the folders opened on earlier visits (those the browser still grants access to). +pub fn restore() { + spawn(restore_folders(), "Media Browser", |_, v| { + for f in v.as_array().into_iter().flatten() { + if f["granted"].as_bool() == Some(true) { + add_folder(f); + } else if let Some(n) = f["name"].as_str() { + let mut st = state(); + if !st.disconnected.iter().any(|d| d == n) { + st.disconnected.push(n.to_string()); + } + } + } + }); +} + +impl Browser for WebBrowser { + fn home(&self) -> String { + STORAGE.into() + } + + fn places(&self) -> Vec<(String, String)> { + let mut v = vec![("Browser Storage".to_string(), STORAGE.to_string())]; + v.extend(state().open.iter().map(|n| (n.clone(), format!("{FOLDERS}/{n}")))); + v + } + + fn list(&self, dir: &str, importable_only: bool) -> Result, String> { + let dir = if dir.len() > 1 { dir.trim_end_matches('/') } else { dir }; + if dir == STORAGE || dir.starts_with(&format!("{STORAGE}/")) { + return storage_tree().list(dir, importable_only); + } + if dir == "/" { + let mut t = VirtualTree::default(); + t.add_dir(STORAGE); + t.add_dir(FOLDERS); + return t.list("/", false); + } + let st = state(); + if dir == FOLDERS && st.open.is_empty() { + return Ok(vec![]); + } + st.folders.list(dir, importable_only).map_err(|e| { + let name = dir.trim_start_matches(FOLDERS).trim_start_matches('/').split('/').next().unwrap_or(""); + if st.disconnected.iter().any(|d| d == name) { format!("{name}: click Reconnect {name} to allow access again") } else { e } + }) + } + + fn parent(&self, dir: &str) -> Option { + let d = dir.trim_end_matches('/'); + match d.rfind('/') { + Some(0) if d.len() > 1 => Some("/".into()), + Some(i) if i > 0 => Some(d[..i].to_string()), + _ => None, + } + } + + fn fetch(&self, paths: &[String], params: &Value) -> bool { + let missing: Vec = paths.iter().filter(|p| p.starts_with(&format!("{FOLDERS}/")) && crate::files::get(p).is_none()).cloned().collect(); + if missing.is_empty() { + return true; + } + let params = params.clone(); + wasm_bindgen_futures::spawn_local(async move { + for p in &missing { + match wasm_bindgen_futures::JsFuture::from(read(p)).await { + Ok(b) => crate::files::put(p, js_sys::Uint8Array::new(&b).to_vec().into()), + Err(e) => { + let msg = format!("{p}: {}", e.as_string().unwrap_or_else(|| "cannot read the file (reconnect its folder)".into())); + crate::post(move |app| app.ui.status = msg); + return; + } + } + } + crate::post(move |app| match app.session.execute("mediaBrowser.import", params) { + Ok(r) => { + if let Some(errs) = r["errors"].as_array().filter(|e| !e.is_empty()) { + app.ui.status = errs.iter().filter_map(|e| e.as_str()).collect::>().join("; "); + } + } + Err(e) => app.ui.status = e.to_string(), + }); + }); + false + } + + fn actions(&self) -> Vec<(String, String)> { + let mut v = vec![]; + if can_pick_folder() { + v.push(("openFolder".to_string(), "Open Folder…".to_string())); + } + v.push(("addFiles".into(), "Add Files…".into())); + v.push(("uploadFolder".into(), "Upload Folder…".into())); + for n in &state().disconnected { + v.push((format!("reconnect:{n}"), format!("Reconnect {n}"))); + } + v + } + + fn action(&self, id: &str) -> Result { + match id { + "openFolder" => { + if !can_pick_folder() { + return Err("this browser cannot open folders (use Add Files… or Upload Folder…)".into()); + } + spawn(pick_folder(), "Open Folder", |app, v| { + if let Some(n) = add_folder(&v) { + go(app, &format!("{FOLDERS}/{n}")); + } + }); + Ok(json!({"pending": true})) + } + "addFiles" | "uploadFolder" => { + let p = pick_files(id == "uploadFolder"); + wasm_bindgen_futures::spawn_local(async move { + let Ok(list) = wasm_bindgen_futures::JsFuture::from(p).await else { return }; + let mut n = 0; + for f in js_sys::Array::from(&list).iter() { + let path = js_sys::Reflect::get(&f, &"path".into()).ok().and_then(|v| v.as_string()); + let bytes = js_sys::Reflect::get(&f, &"bytes".into()).ok().and_then(|b| b.dyn_into::().ok()); + if let (Some(path), Some(bytes)) = (path, bytes) { + crate::files::put(&format!("/{}", path.trim_start_matches('/')), bytes.to_vec().into()); + n += 1; + } + } + crate::post(move |app| { + app.ui.status = format!("{n} file(s) added to Browser Storage"); + go(app, STORAGE); + }); + }); + Ok(json!({"pending": true})) + } + _ => { + if let Some(name) = id.strip_prefix("reconnect:") { + spawn(reconnect(name), "Reconnect", |app, v| { + if let Some(n) = add_folder(&v) { + go(app, &format!("{FOLDERS}/{n}")); + } + }); + return Ok(json!({"pending": true})); + } + if let Some(name) = id.strip_prefix("forget:") { + let _ = forget(name); + let mut st = state(); + st.open.retain(|n| n != name); + st.disconnected.retain(|n| n != name); + st.folders.remove(&format!("{FOLDERS}/{name}")); + return Ok(json!({"forgotten": name})); + } + Err(format!("unknown Media Browser action `{id}`")) + } + } + } +} diff --git a/apps/effectcraft-web/src/frames.rs b/apps/effectcraft-web/src/frames.rs new file mode 100644 index 00000000..abad352f --- /dev/null +++ b/apps/effectcraft-web/src/frames.rs @@ -0,0 +1,261 @@ +//! Viewer frames rendered in Web Workers ([`effectcraft_engine::remote`]). +//! +//! **Page side** — [`WebFrames`], the frame cache's [`RemoteFrames`]: a few long-lived frame +//! workers (`js/host.js` `frameWorkerStart`), each holding a replica of the project. Before a +//! render, the worker gets the footage files it lacks and a sync (the whole project the first +//! time, then diffs: [`Mirror`]); the frame comes back as premultiplied RGBA8 in a transferred +//! buffer. One frame per worker at a time, so the page always decides what renders next (the +//! viewer's frame before prefetch). A worker that fails is dropped; with none left, frames render +//! on the page's thread again (`Frames::pump`). +//! +//! **Worker side** — [`worker_frame`]: the worker's [`FrameServer`] (footage from the worker's +//! media pool, expressions) handles each message and posts its replies. + +use std::cell::RefCell; +use std::collections::HashSet; + +use effectcraft_engine::offload::footage_files; +use effectcraft_engine::remote::{FrameMsg, FrameReply, FrameServer, Mirror}; +use effectcraft_ui_egui::frames::{RemoteDone, RemoteFrame, RemoteFrames, RemoteJob}; +use serde_json::{Value, json}; +use wasm_bindgen::JsCast; +use wasm_bindgen::prelude::*; + +#[wasm_bindgen(module = "/js/host.js")] +extern "C" { + #[wasm_bindgen(js_name = frameWorkerStart)] + fn frame_worker_start(base: &str, on_message: &js_sys::Function) -> u32; + #[wasm_bindgen(js_name = frameWorkerPost)] + fn frame_worker_post(index: u32, msg: &JsValue, transfer: &js_sys::Array); +} + +/// One frame worker as the page sees it. +struct Fw { + index: u32, + mirror: Mirror, + files: HashSet, + busy: Option<(u64, RemoteDone)>, + alive: bool, +} + +#[derive(Default)] +struct State { + workers: Vec, + next_id: u64, + rendered: u64, + failed: u64, + last_ms: f64, + /// Bytes of project sync messages sent (diffs keep this small). + sync_bytes: u64, + syncs: u64, +} + +type OnMessage = Closure; + +thread_local! { + static STATE: RefCell = RefCell::new(State::default()); + static HANDLER: RefCell> = const { RefCell::new(None) }; + /// The worker's frame server (worker side). + static SERVER: RefCell> = const { RefCell::new(None) }; +} + +fn obj(pairs: &[(&str, JsValue)]) -> JsValue { + let o = js_sys::Object::new(); + for (k, v) in pairs { + let _ = js_sys::Reflect::set(&o, &JsValue::from_str(k), v); + } + o.into() +} + +/// The page's frame workers (see the module docs). +pub struct WebFrames; + +impl WebFrames { + /// Start `n` frame workers. + pub fn start(n: usize) -> WebFrames { + let on: OnMessage = Closure::new(|index: u32, json: JsValue, bytes: JsValue, error: JsValue| on_message(index, json, bytes, error)); + STATE.with(|s| { + let mut s = s.borrow_mut(); + for _ in 0..n { + let index = frame_worker_start(&crate::page_url(""), on.as_ref().unchecked_ref()); + s.workers.push(Fw { index, mirror: Mirror::default(), files: HashSet::new(), busy: None, alive: true }); + } + }); + HANDLER.with(|h| *h.borrow_mut() = Some(on)); + WebFrames + } +} + +impl RemoteFrames for WebFrames { + fn slots(&self) -> usize { + STATE.with(|s| s.borrow().workers.iter().filter(|w| w.alive).count()) + } + + fn purge(&self) { + purge(); + } + + fn start(&self, job: RemoteJob, done: RemoteDone) { + let r = STATE.with(|s| { + let mut s = s.borrow_mut(); + s.next_id += 1; + let id = s.next_id; + let Some(k) = s.workers.iter().position(|w| w.alive && w.busy.is_none()) else { return Err((done, "no idle frame worker".to_string())) }; + // Footage the worker hasn't got yet. + let w = &mut s.workers[k]; + for p in footage_files(&job.project) { + if w.files.contains(&p) { + continue; + } + if let Some(d) = crate::files::get(&p) { + let bytes = js_sys::Uint8Array::from(&d[..]); + frame_worker_post(w.index, &obj(&[("type", "file".into()), ("path", p.as_str().into()), ("bytes", bytes.into())]), &js_sys::Array::new()); + w.files.insert(p); + } + } + let sync = w.mirror.sync(job.revision, &job.project); + let index = w.index; + let mut sent = 0; + if let Some(m) = sync { + let t = serde_json::to_string(&m).unwrap_or_default(); + sent = t.len() as u64; + frame_worker_post(index, &obj(&[("type", "frame".into()), ("json", t.into())]), &js_sys::Array::new()); + } + let req = FrameMsg::Render { id, revision: job.revision, comp: job.comp, time: job.t, opts: Box::new(job.opts) }; + frame_worker_post( + index, + &obj(&[("type", "frame".into()), ("json", serde_json::to_string(&req).unwrap_or_default().into())]), + &js_sys::Array::new(), + ); + s.workers[k].busy = Some((id, done)); + if sent > 0 { + s.syncs += 1; + s.sync_bytes += sent; + } + Ok(()) + }); + if let Err((done, e)) = r { + done(Err(e)); + } + } +} + +/// A message from frame worker `index`: a JSON [`FrameReply`] (with the pixels in `bytes`), or +/// `error` when the worker died. +fn on_message(index: u32, json: JsValue, bytes: JsValue, error: JsValue) { + let done = STATE.with(|s| { + let mut s = s.borrow_mut(); + let st = &mut *s; + let w = st.workers.iter_mut().find(|w| w.index == index)?; + if let Some(e) = error.as_string() { + log::warn!("frame worker {index}: {e}; frames render on the page's thread if none is left"); + w.alive = false; + return w.busy.take().map(|(_, d)| (d, Err(e))); + } + let reply: FrameReply = serde_json::from_str(&json.as_string()?).ok()?; + match reply { + FrameReply::Frame { id, width, height, ms } => { + let (_, d) = w.busy.take_if(|b| b.0 == id)?; + let rgba = bytes.dyn_into::().map(|b| b.to_vec()).unwrap_or_default(); + st.rendered += 1; + st.last_ms = ms; + Some((d, Ok(RemoteFrame { width, height, rgba, ms }))) + } + FrameReply::Failed { id, error, .. } => { + st.failed += 1; + let (_, d) = w.busy.take_if(|b| b.0 == id)?; + Some((d, Err(error))) + } + FrameReply::Resync { error } => { + log::info!("frame worker {index}: {error}: sending the whole project again"); + w.mirror.reset(); + None + } + } + }); + if let Some((d, r)) = done { + d(r); + } + crate::repaint(); +} + +/// Relay Roto Brush segmentations (from a propagation worker) to the frame workers, so their +/// renders use them instead of computing them again. +pub fn relay_segs(segs: &[effectcraft_engine::offload::SegData]) { + if segs.is_empty() { + return; + } + let t = serde_json::to_string(&FrameMsg::Segs { segs: segs.to_vec() }).unwrap_or_default(); + STATE.with(|s| { + for w in s.borrow().workers.iter().filter(|w| w.alive) { + frame_worker_post(w.index, &obj(&[("type", "frame".into()), ("json", t.as_str().into())]), &js_sys::Array::new()); + } + }); +} + +/// Ask every frame worker to drop its cached layer buffers (Edit ▸ Purge). +pub fn purge() { + let t = serde_json::to_string(&FrameMsg::Purge).unwrap_or_default(); + STATE.with(|s| { + for w in s.borrow().workers.iter().filter(|w| w.alive) { + frame_worker_post(w.index, &obj(&[("type", "frame".into()), ("json", t.as_str().into())]), &js_sys::Array::new()); + } + }); +} + +/// `effectcraft.info().frameWorkers`. +pub fn stats() -> Value { + STATE.with(|s| { + let s = s.borrow(); + json!({ + "workers": s.workers.len(), + "alive": s.workers.iter().filter(|w| w.alive).count(), + "busy": s.workers.iter().filter(|w| w.busy.is_some()).count(), + "rendered": s.rendered, + "failed": s.failed, + "lastMs": s.last_ms, + "syncs": s.syncs, + "syncBytes": s.sync_bytes, + }) + }) +} + +// ---------------------------------------------------------------- worker side + +/// Handle one frame message (a JSON [`FrameMsg`]) in a frame worker; replies are posted with +/// their pixels transferred. +#[wasm_bindgen(js_name = workerFrame)] +pub fn worker_frame(json: String) { + let msg: FrameMsg = match serde_json::from_str(&json) { + Ok(m) => m, + Err(e) => { + log::warn!("frame worker: bad message: {e}"); + return; + } + }; + let replies = SERVER.with(|c| { + let mut c = c.borrow_mut(); + let server = c.get_or_insert_with(|| { + let pool = crate::worker::pool(); + FrameServer::new(pool, Some(std::sync::Arc::new(effectcraft_expr::Expressions))) + }); + server.handle(msg) + }); + let scope: web_sys::DedicatedWorkerGlobalScope = js_sys::global().unchecked_into(); + for (r, px) in replies { + let t = serde_json::to_string(&r).unwrap_or_default(); + let res = match px { + Some(px) => { + let buf = js_sys::Uint8Array::from(&px[..]).buffer(); + scope.post_message_with_transfer( + &obj(&[("type", "frameReply".into()), ("json", t.into()), ("bytes", buf.clone().into())]), + &js_sys::Array::of1(&buf), + ) + } + None => scope.post_message(&obj(&[("type", "frameReply".into()), ("json", t.into())])), + }; + if let Err(e) = res { + log::warn!("frame worker: postMessage: {e:?}"); + } + } +} diff --git a/apps/effectcraft-web/src/lib.rs b/apps/effectcraft-web/src/lib.rs index 59de4433..96b17db2 100644 --- a/apps/effectcraft-web/src/lib.rs +++ b/apps/effectcraft-web/src/lib.rs @@ -9,8 +9,9 @@ //! | file system (`FsServices`, media reads, export writes) | a virtual file table ([`files`]) persisted to the Origin Private File System / IndexedDB ([`persist`], [`store`]) | //! | config directory (settings, shortcuts, recent projects, auto-saves) | the same store ([`store::WebConfig`]) | //! | rfd file dialogs | `` pickers; drop files on the page | +//! | Media Browser on the file system | browser storage and folders opened with the File System Access API ([`browse`]) | //! | written files (Save, Render Queue) | browser downloads (several render files as one `.zip`) | -//! | frame render threads | `Frames::pump` on the UI thread between egui frames | +//! | frame render threads | frame workers, each with a project replica fed by diffs ([`frames`]); `Frames::pump` on the UI thread without workers | //! | Render Queue / analysis threads | Web Workers running their own engine instance ([`worker`]) | //! | cpal audio output | Web Audio: an AudioWorklet fed from the preview mixdown ([`audio`]) | //! | TCP control channel / MCP | `window.effectcraft` JavaScript API ([`api`]) | @@ -26,8 +27,12 @@ mod app; #[cfg(target_arch = "wasm32")] pub mod audio; #[cfg(target_arch = "wasm32")] +pub mod browse; +#[cfg(target_arch = "wasm32")] pub mod files; #[cfg(target_arch = "wasm32")] +pub mod frames; +#[cfg(target_arch = "wasm32")] pub mod persist; #[cfg(target_arch = "wasm32")] pub mod worker; diff --git a/apps/effectcraft-web/src/worker.rs b/apps/effectcraft-web/src/worker.rs index 2d96f641..07c9aadc 100644 --- a/apps/effectcraft-web/src/worker.rs +++ b/apps/effectcraft-web/src/worker.rs @@ -5,6 +5,9 @@ //! `workerRun`), and turns the worker's messages back into [`WorkerReply`]s in the job's //! [`Inbox`]. Rendered files come back as their bytes and download like any render. //! +//! Viewer frames use separate, long-lived workers ([`crate::frames`]); Roto Brush +//! segmentations a propagation job sends back are relayed to them. +//! //! **Worker side** — `web/worker.js` instantiates the same wasm module (the page's compiled //! `WebAssembly.Module`, so nothing is compiled twice) and calls [`worker_init`], then //! [`worker_file`] per footage file and [`worker_job`] per job: a plain engine session (footage @@ -60,6 +63,9 @@ impl Offload for WorkerOffload { let Some(t) = json.as_string() else { return }; match serde_json::from_str::(&t) { Ok(r) => { + if let WorkerReply::Segs { segs } = &r { + crate::frames::relay_segs(segs); + } let done = matches!(r, WorkerReply::Done); inbox.push(r); if done { @@ -146,6 +152,11 @@ pub fn worker_init() { SESSION.with(|c| *c.borrow_mut() = Some(s)); } +/// The worker's media pool (footage files sent by the page). +pub fn pool() -> Arc { + POOL.with(|p| p.borrow().clone()).unwrap_or_else(|| Arc::new(effectcraft_media::MediaPool::new())) +} + /// A footage file for the jobs that follow. #[wasm_bindgen(js_name = workerFile)] pub fn worker_file(path: String, bytes: js_sys::Uint8Array) { diff --git a/apps/effectcraft-web/tests/smoke.mjs b/apps/effectcraft-web/tests/smoke.mjs index 3ba19343..81bc81cc 100644 --- a/apps/effectcraft-web/tests/smoke.mjs +++ b/apps/effectcraft-web/tests/smoke.mjs @@ -167,14 +167,48 @@ try { const f = await js("effectcraft.renderFrame({max_side: 480}).then(r => ({w: r.width, h: r.height, pngBytes: atob(r.png).length}))"); report.steps.renderFrame = f; - // Render Queue: a short, small GIF → download. + // Viewer frames render in frame workers (project replicas fed by diffs): with the CPU + // renderer, scrubbing (and an edit, which travels as a diff) keeps the page's event loop free. + await js(`effectcraft.execute("render.backend", {backend: "cpu"})`); + report.steps.frameWorkers = await js(`(async () => { + const before = effectcraft.info().frameWorkers; + let maxGapMs = 0, last = performance.now(); + const timer = setInterval(() => { const now = performance.now(); maxGapMs = Math.max(maxGapMs, now - last); last = now; }, 10); + await effectcraft.execute("layer.newSolid", {name: "Frame Worker Solid", color: "#3366cc", width: 120, height: 80}); + for (let k = 0; k < 12; k++) { + await effectcraft.execute("time.set", {time: k / 4}); + await new Promise((r) => setTimeout(r, 150)); + } + const t0 = performance.now(); + while (effectcraft.info().frameWorkers.rendered < before.rendered + 3 && performance.now() - t0 < 120000) await new Promise((r) => setTimeout(r, 100)); + clearInterval(timer); + await effectcraft.execute("edit.undo", {}); + return {before, after: effectcraft.info().frameWorkers, maxGapMs}; + })()`); + const fwk = report.steps.frameWorkers; + await js(`effectcraft.execute("render.backend", {backend: "gpu"})`); + check(fwk.after.alive > 0, `frame workers alive: ${JSON.stringify(fwk.after)}`); + check(fwk.after.rendered >= fwk.before.rendered + 3, `frames rendered in workers: ${JSON.stringify(fwk)}`); + check(fwk.after.syncs >= 2, `project synced as diffs: ${JSON.stringify(fwk.after)}`); + check(fwk.maxGapMs < 400, `event loop blocked ${fwk.maxGapMs} ms while scrubbing`); + + // Render Queue: a short, small GIF → download. `renderQueue.render` waits by default; the + // render still runs in a worker and the reply comes when it ends (the page keeps running). const rqT = Date.now(); // (`ui.menu.invoke`: `engine.execute`'s strict parameter check does not know the spread // Render Settings / Output Module keys) await js(`effectcraft.request("ui.menu.invoke", {id: "renderQueue.add", params: {format: "gif", output: "smoke_[compName].gif", resolution: 0.25, timeSpan: "custom", start: 0, end: 1}})`); - await js(`effectcraft.execute("renderQueue.render", {})`); + const waited = await js(`(async () => { + let maxGapMs = 0, last = performance.now(); + const timer = setInterval(() => { const now = performance.now(); maxGapMs = Math.max(maxGapMs, now - last); last = now; }, 10); + const r = await effectcraft.execute("renderQueue.render", {}); + clearInterval(timer); + return {rendering: r.rendering, status: r.items.map((i) => i.status), maxGapMs}; + })()`); const gif = await waitDownload((n) => n.endsWith(".gif")); - report.steps.gifExport = { ...gif, ms: Date.now() - rqT }; + report.steps.gifExport = { ...gif, ms: Date.now() - rqT, waited }; + check(!waited.rendering && waited.status.every((s) => s === "Done"), `wait:true replies when done: ${JSON.stringify(waited)}`); + check(waited.maxGapMs < 400, `event loop blocked ${waited.maxGapMs} ms during a waiting render`); await idle(); // An image sequence arrives as one .zip. @@ -231,6 +265,13 @@ try { return effectcraft.addFile(new File([b], "tone.wav")); })()`); await until(`effectcraft.execute("layer.addItem", {item: "tone.wav", time: 0}).then(() => true, () => false)`, 20000, 300); + // Media Browser: browser storage (where tone.wav now is) and its actions. + const mb = await js(`effectcraft.execute("mediaBrowser.go", {})`); + report.steps.mediaBrowser = { path: mb.path, entries: mb.entries.map((e) => e.name), places: mb.places, actions: mb.actions.map((a) => a.id) }; + check(mb.path === "/Browser Storage" && mb.entries.some((e) => e.name === "tone.wav" && e.kind === "audio"), `Media Browser: ${JSON.stringify(report.steps.mediaBrowser)}`); + check(mb.actions.some((a) => a.id === "addFiles"), "Media Browser: Add Files…"); + const mbImport = await js(`effectcraft.execute("mediaBrowser.import", {paths: [${JSON.stringify(mb.entries.find((e) => e.name === "tone.wav")?.path ?? "/tone.wav")}]})`); + check(!mbImport.pending && mbImport.items && mbImport.items.length === 1, `Media Browser import: ${JSON.stringify(mbImport)}`); await js(`effectcraft.request("ui.playback", {action: "stop"})`); await js(`effectcraft.execute("time.set", {time: 0})`); await js(`effectcraft.request("ui.playback", {action: "play"})`); diff --git a/apps/effectcraft-web/web/worker.js b/apps/effectcraft-web/web/worker.js index c29663d7..6c6853c5 100644 --- a/apps/effectcraft-web/web/worker.js +++ b/apps/effectcraft-web/web/worker.js @@ -1,7 +1,9 @@ -// An EffectCraft engine instance in a Web Worker: renders the Render Queue and runs analyses -// off the page's thread (src/worker.rs, crates/engine/src/offload.rs). The page sends the -// compiled module, then footage files and jobs; the worker replies with JSON messages and -// rendered files. +// An EffectCraft engine instance in a Web Worker (src/worker.rs, src/frames.rs, +// crates/engine/src/offload.rs and remote.rs). The page sends the compiled module, then: +// - job workers: footage files and jobs (Render Queue, analyses, Roto Brush); the worker replies +// with JSON messages and rendered files; +// - frame workers: footage files and frame messages (project syncs as diffs, render requests, +// Roto Brush segmentations); the worker replies with frames (pixels transferred). import init, * as ec from "./effectcraft_web.js"; let ready = null; @@ -16,4 +18,5 @@ self.onmessage = async (e) => { await ready; if (m.type === "file") ec.workerFile(m.path, m.bytes); else if (m.type === "job") ec.workerJob(m.json); + else if (m.type === "frame") ec.workerFrame(m.json); }; 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/color3.rs b/crates/effects/src/color3.rs index 7d626d19..c85ab8d0 100644 --- a/crates/effects/src/color3.rs +++ b/crates/effects/src/color3.rs @@ -269,7 +269,7 @@ fn parse_points(s: &str) -> Vec<(f32, f32)> { /// around (x = 0 and x = 1 are the same hue); the others are flat beyond their end points. /// Tabulated as offsets `y − 0.5`. #[derive(Clone, Debug)] -pub(crate) struct OffsetCurve { +pub struct OffsetCurve { lut: Vec, } @@ -277,7 +277,7 @@ impl OffsetCurve { const N: usize = 512; /// `None` for an empty or flat-neutral curve. - pub(crate) fn parse(s: &str, periodic: bool) -> Option { + pub fn parse(s: &str, periodic: bool) -> Option { let pts = parse_points(s); if pts.is_empty() || pts.iter().all(|p| (p.1 - 0.5).abs() < 1e-6) { return None; @@ -327,7 +327,7 @@ impl OffsetCurve { } /// Offset from neutral at `x` (clamped to 0..1). - pub(crate) fn at(&self, x: f32) -> f32 { + pub fn at(&self, x: f32) -> f32 { let f = x.clamp(0.0, 1.0) * Self::N as f32; let i = (f as usize).min(Self::N - 1); let t = f - i as f32; diff --git a/crates/effects/src/lib.rs b/crates/effects/src/lib.rs index 3486454d..feed53bf 100644 --- a/crates/effects/src/lib.rs +++ b/crates/effects/src/lib.rs @@ -66,11 +66,13 @@ use std::collections::HashMap; use std::sync::OnceLock; pub use card3d::{CompLight, CompScene}; +pub use color_fx::{COLORAMA_PRESETS, ColoramaPalette}; pub use color_fx::{ HUESAT_CHANNELS, HUESAT_RANGES, LEVELS_CHANNELS, PHOTO_FILTER_CUSTOM, PHOTO_FILTERS, exposure_settings, fill_uses_masks, huesat_ranges_identity, levels_channel_ids, levels_channel_settings, levels_channels_identity, levels_clip, }; pub use color2::{Curve, auto_correct_settings, equalize_tables, luma_clip_points, shadow_highlight_amounts}; +pub use color3::OffsetCurve; pub use distort::transform_shutter; pub use distort2::{MAGNIFY_MODES, bezier_grid, parse_mesh}; pub use distort3::{RESHAPE_POINTS, ReshapeSetup, SmearSetup, reshape_setup, smear_setup}; @@ -79,7 +81,7 @@ use effectcraft_keyframe::Value; use effectcraft_project::build::Ids; use effectcraft_project::{GroupKind, ParamUi, PropGroup, Property}; pub use effectcraft_raster::{AuxChannels, Image}; -pub use misc::{INVERT_ALPHA, INVERT_CHANNELS, glow_operation}; +pub use misc::{INVERT_ALPHA, INVERT_CHANNELS, glow_ab_t, glow_operation}; pub use sim::particle_state; pub use sim3::playground_state; pub use stylize2::strobe_on; diff --git a/crates/effects/src/misc.rs b/crates/effects/src/misc.rs index 89b496bb..c119118d 100644 --- a/crates/effects/src/misc.rs +++ b/crates/effects/src/misc.rs @@ -103,7 +103,7 @@ fn unsharp(ctx: &EffectCtx, mut b: Buf) -> Buf { /// Position (0 = Color A, 1 = Color B) in Glow's A & B gradient for brightness `l`: `loops` /// cycles starting at `phase` (revolutions), shaped by Color Looping (0 Sawtooth A>B, /// 1 Sawtooth B>A, 2 Triangle A>B>A, 3 Triangle B>A>B) and biased so `mid` maps to the middle. -pub(crate) fn glow_ab_t(l: f32, looping: u32, loops: f32, phase: f32, mid: f32) -> f32 { +pub fn glow_ab_t(l: f32, looping: u32, loops: f32, phase: f32, mid: f32) -> f32 { let u = l.clamp(0.0, 1.0) * loops.max(0.0) + phase; let mut t = u - u.floor(); if u > 0.0 && t <= 0.0 { 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/panels_cmds.rs b/crates/engine/src/commands/panels_cmds.rs index eb056878..325e0da0 100644 --- a/crates/engine/src/commands/panels_cmds.rs +++ b/crates/engine/src/commands/panels_cmds.rs @@ -29,38 +29,61 @@ fn jobs_wait(s: &mut Session, _: &Value) -> Result { // ---------------------------------------------------------------- Media Browser -fn browser_dir(s: &Session, p: &Value) -> String { - str_p(p, "path").map(str::to_string).or_else(|| s.state.media_browser.folder.clone()).unwrap_or_else(mb::home) +fn browser(s: &Session) -> Result> { + s.media_browser().ok_or_else(|| EngineError::Other("the Media Browser is not available in this build".into())) +} + +fn browser_dir(s: &Session, b: &dyn mb::Browser, p: &Value) -> String { + str_p(p, "path").map(str::to_string).or_else(|| s.state.media_browser.folder.clone()).unwrap_or_else(|| b.home()) } fn browser_list(s: &mut Session, p: &Value) -> Result { - let dir = browser_dir(s, p); + let b = browser(s)?; + let dir = browser_dir(s, b.as_ref(), p); let only = b_p(p, "importableOnly").unwrap_or(s.state.media_browser.importable_only); - let entries = mb::list(&dir, only).map_err(EngineError::Other)?; - Ok(json!({"path": dir, "parent": mb::parent(&dir), "entries": entries, "favorites": s.state.media_browser.favorites})) + let entries = b.list(&dir, only).map_err(EngineError::Other)?; + let places: Vec = b.places().into_iter().map(|(name, path)| json!({"name": name, "path": path})).collect(); + let actions: Vec = b.actions().into_iter().map(|(id, label)| json!({"id": id, "label": label})).collect(); + Ok(json!({ + "path": dir, + "parent": b.parent(&dir), + "entries": entries, + "favorites": s.state.media_browser.favorites, + "places": places, + "actions": actions, + })) } -fn browser_enabled(_: &Session) -> std::result::Result<(), String> { - if mb::available() { Ok(()) } else { Err("the Media Browser needs the desktop app".into()) } +fn browser_enabled(s: &Session) -> std::result::Result<(), String> { + if s.media_browser().is_some() { Ok(()) } else { Err("the Media Browser is not available in this build".into()) } } fn browser_go(s: &mut Session, p: &Value) -> Result { + let b = browser(s)?; let dir = match str_p(p, "path") { - Some("..") => s.state.media_browser.folder.clone().and_then(|d| mb::parent(&d)).unwrap_or_else(mb::home), + Some("..") => s.state.media_browser.folder.clone().and_then(|d| b.parent(&d)).unwrap_or_else(|| b.home()), Some(d) => d.to_string(), - None => mb::home(), + None => b.home(), }; - if let Some(b) = b_p(p, "importableOnly") { - s.state.media_browser.importable_only = b; + if let Some(v) = b_p(p, "importableOnly") { + s.state.media_browser.importable_only = v; } // Validate before switching. - mb::list(&dir, false).map_err(EngineError::Other)?; + b.list(&dir, false).map_err(EngineError::Other)?; s.state.media_browser.folder = Some(dir.clone()); browser_list(s, &json!({"path": dir})) } +/// A browser-specific toolbar action (web: Open Folder…, Add Files…). +fn browser_action(s: &mut Session, p: &Value) -> Result { + let b = browser(s)?; + let id = str_p(p, "action").ok_or_else(|| bad("mediaBrowser.action", "missing `action`"))?; + b.action(id).map_err(|e| bad("mediaBrowser.action", e)) +} + fn browser_favorite(s: &mut Session, p: &Value, add: bool) -> Result { - let dir = browser_dir(s, p); + let b = browser(s)?; + let dir = browser_dir(s, b.as_ref(), p); let favs = &mut s.state.media_browser.favorites; favs.retain(|f| f != &dir); if add { @@ -71,6 +94,16 @@ fn browser_favorite(s: &mut Session, p: &Value, add: bool) -> Result { fn browser_import(s: &mut Session, p: &Value) -> Result { let paths = p.get("paths").or(p.get("path")).cloned().ok_or_else(|| bad("mediaBrowser.import", "missing `paths`"))?; + // Files the browser still has to read (the web app's opened folders): imported when they + // arrive. + let list: Vec = match &paths { + Value::String(one) => vec![one.clone()], + Value::Array(a) => a.iter().filter_map(|v| v.as_str().map(str::to_string)).collect(), + _ => vec![], + }; + if !browser(s)?.fetch(&list, p) { + return Ok(json!({"pending": true, "paths": list})); + } let r = s.execute("file.import", json!({"paths": paths}))?; // Add to the active comp (drag into the timeline). if b_p(p, "addToComp").unwrap_or(false) @@ -201,6 +234,15 @@ pub fn specs() -> Vec { cmd!("mediaBrowser.addFavorite", "Add to Favorites", [], None, "{path?}", browser_enabled, |s, p| browser_favorite(s, p, true)), cmd!("mediaBrowser.removeFavorite", "Remove from Favorites", [], None, "{path?}", browser_enabled, |s, p| browser_favorite(s, p, false)), cmd!("mediaBrowser.import", "Import", [], None, "{paths, addToComp?}", browser_enabled, browser_import), + cmd!( + "mediaBrowser.action", + "Media Browser Action", + [], + None, + "{action}: an action from mediaBrowser.list's `actions` (web: openFolder, addFiles, uploadFolder)", + browser_enabled, + browser_action + ), query!("mediaBrowser.fileInfo", "File Info", "{path}", file_info), query!("item.metadata", "Metadata", "{item?: id|name}", metadata), cmd!("project.setProjectComment", "Project Comment", [], None, "{comment}", always, project_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 b798d5dc..5190ba82 100644 --- a/crates/engine/src/lib.rs +++ b/crates/engine/src/lib.rs @@ -26,6 +26,7 @@ pub mod offload; pub mod prefs; pub mod preview; pub mod psd_import; +pub mod remote; pub mod render_queue; pub mod roto; pub mod scriptui; @@ -353,6 +354,11 @@ pub struct Session { pub offload: Option>, /// Analyses running in the [`Session::offload`] worker. pub offloaded: Vec, + /// What the Media Browser browses (`None`: the local file system where there is one). + pub browser: Option>, + /// Offloaded jobs that ended, newest last (kind, error): how a caller waiting for one + /// (`wait: true` over the control channel in the browser) learns its outcome. + pub offload_log: Vec<(offload::JobKind, Option)>, /// Generic background tasks (Content-Aware Fill, Scene Edit Detection), see [`jobs`]. pub tasks: Vec, /// Finished tasks, newest last (Progress panel, `jobs.list`). @@ -425,6 +431,8 @@ impl Default for Session { applied_nested_switches: None, offload: None, offloaded: vec![], + offload_log: vec![], + browser: None, tasks: vec![], job_log: vec![], next_task_id: 1, @@ -782,6 +790,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/media_browser.rs b/crates/engine/src/media_browser.rs index afc7dc34..9b0a01da 100644 --- a/crates/engine/src/media_browser.rs +++ b/crates/engine/src/media_browser.rs @@ -1,10 +1,16 @@ -//! Media Browser (Window ▸ Media Browser): browse the local file system for footage, keep -//! favourite folders, read file metadata, and import (`file.import`). Desktop only: the web build -//! has no file system to browse ([`available`] is false there). +//! Media Browser (Window ▸ Media Browser): browse for footage, keep favourite folders, read file +//! metadata, and import (`file.import`). +//! +//! What it browses is a [`Browser`]: the local file system on the desktop ([`FsBrowser`]); in the +//! web app, a virtual tree ([`VirtualTree`]) of the browser's storage and of folders the user +//! opened through the File System Access API, whose files are read only when imported +//! ([`Browser::fetch`]). //! //! Also the metadata the Metadata panel shows for project items and files (codec, size, rate, //! duration, colour profile, dates). +use std::collections::BTreeMap; + use serde::{Deserialize, Serialize}; use serde_json::{Value, json}; @@ -25,11 +31,153 @@ pub struct BrowserState { pub importable_only: bool, } -/// Whether this build can browse files. +/// Whether this build can browse the local file system ([`FsBrowser`]). pub fn available() -> bool { !cfg!(target_arch = "wasm32") } +/// Where the Media Browser browses. +pub trait Browser: Send + Sync { + /// The folder shown first. + fn home(&self) -> String; + /// Places listed above the favourites: (name, folder). The default is Home. + fn places(&self) -> Vec<(String, String)> { + vec![("Home".into(), self.home())] + } + /// A folder's entries: folders first, then files, by name. + fn list(&self, dir: &str, importable_only: bool) -> Result, String>; + /// The folder above `dir`. + fn parent(&self, dir: &str) -> Option { + parent(dir) + } + /// Make `paths` readable by the importer. `false`: they are being fetched, and the host runs + /// `mediaBrowser.import` with `params` again once they are (the web app reads opened folders' + /// files on demand). + fn fetch(&self, paths: &[String], params: &Value) -> bool { + let _ = (paths, params); + true + } + /// Extra toolbar actions: (id, label), run by `mediaBrowser.action`. + fn actions(&self) -> Vec<(String, String)> { + vec![] + } + fn action(&self, id: &str) -> Result { + Err(format!("unknown Media Browser action `{id}`")) + } +} + +/// The local file system (desktop). +pub struct FsBrowser; + +impl Browser for FsBrowser { + fn home(&self) -> String { + home() + } + fn list(&self, dir: &str, importable_only: bool) -> Result, String> { + list(dir, importable_only) + } +} + +impl crate::Session { + /// What the Media Browser browses: [`crate::Session::browser`], else the local file system + /// where this build has one. + pub fn media_browser(&self) -> Option> { + self.browser.clone().or_else(|| available().then(|| std::sync::Arc::new(FsBrowser) as std::sync::Arc)) + } +} + +/// One entry of a [`VirtualTree`]. +#[derive(Clone, Debug, PartialEq)] +struct VNode { + /// What importing it reads (`None`: a folder). + import: Option, + size: u64, + modified: Option, +} + +/// A browsable tree of virtual paths (`/Folder/sub/file.mov`), each file standing for an +/// importable path (the web app's file table, or a file of an opened folder). +#[derive(Clone, Debug, Default)] +pub struct VirtualTree { + nodes: BTreeMap, +} + +fn vparent(path: &str) -> Option<&str> { + if path == "/" || path.is_empty() { + return None; + } + let p = path.trim_end_matches('/'); + match p.rfind('/') { + Some(0) => Some("/"), + Some(i) => Some(&p[..i]), + None => None, + } +} + +impl VirtualTree { + /// Add a folder (and the folders above it). + pub fn add_dir(&mut self, path: &str) { + let mut cur = Some(path.trim_end_matches('/')); + while let Some(p) = cur.filter(|p| !p.is_empty() && *p != "/") { + self.nodes.entry(p.to_string()).or_insert(VNode { import: None, size: 0, modified: None }); + cur = vparent(p); + } + } + + /// Add a file shown at `path` that imports `import`. + pub fn add_file(&mut self, path: &str, import: &str, size: u64, modified: Option) { + if let Some(p) = vparent(path) { + self.add_dir(p); + } + self.nodes.insert(path.to_string(), VNode { import: Some(import.to_string()), size, modified }); + } + + /// Remove `path` and everything under it. + pub fn remove(&mut self, path: &str) { + let pre = format!("{}/", path.trim_end_matches('/')); + self.nodes.retain(|k, _| k != path && !k.starts_with(&pre)); + } + + pub fn is_dir(&self, path: &str) -> bool { + path == "/" || self.nodes.get(path.trim_end_matches('/')).is_some_and(|n| n.import.is_none()) + } + + /// What a file shown at `path` imports. + pub fn import_path(&self, path: &str) -> Option<&str> { + self.nodes.get(path).and_then(|n| n.import.as_deref()) + } + + pub fn list(&self, dir: &str, importable_only: bool) -> Result, String> { + let dir = if dir.len() > 1 { dir.trim_end_matches('/') } else { dir }; + if !self.is_dir(dir) { + return Err(format!("{dir}: no such folder")); + } + let mut out: Vec = self + .nodes + .iter() + .filter(|(k, _)| vparent(k) == Some(dir)) + .filter_map(|(k, n)| { + let name = k.rsplit('/').next().unwrap_or(k).to_string(); + let kind = n.import.as_deref().and_then(kind_of); + if importable_only && n.import.is_some() && kind.is_none() { + return None; + } + Some(Entry { + name, + // Folders browse by their virtual path; files import their own path. + path: n.import.clone().unwrap_or_else(|| k.clone()), + is_dir: n.import.is_none(), + size: n.size, + modified: n.modified, + kind, + }) + }) + .collect(); + out.sort_by(|a, b| b.is_dir.cmp(&a.is_dir).then_with(|| a.name.to_lowercase().cmp(&b.name.to_lowercase()))); + Ok(out) + } +} + /// One directory entry. #[derive(Clone, Debug, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] @@ -261,6 +409,32 @@ mod tests { std::fs::remove_dir_all(&dir).unwrap(); } + #[test] + fn virtual_tree_lists_like_a_folder() { + let mut t = VirtualTree::default(); + t.add_file("/Browser Storage/clip.mov", "/clip.mov", 10, Some(5)); + t.add_file("/Folders/Shoot/B/b.png", "/Folders/Shoot/B/b.png", 3, None); + t.add_file("/Folders/Shoot/a.wav", "/Folders/Shoot/a.wav", 4, None); + t.add_file("/Folders/Shoot/notes.txt", "/Folders/Shoot/notes.txt", 1, None); + t.add_dir("/Folders/Empty"); + let names = |t: &VirtualTree, d: &str, only: bool| t.list(d, only).unwrap().into_iter().map(|e| e.name).collect::>(); + assert_eq!(names(&t, "/", false), ["Browser Storage", "Folders"]); + assert_eq!(names(&t, "/Folders", false), ["Empty", "Shoot"]); + assert_eq!(names(&t, "/Folders/Shoot", false), ["B", "a.wav", "notes.txt"]); + assert_eq!(names(&t, "/Folders/Shoot/", true), ["B", "a.wav"]); + let e = &t.list("/Browser Storage", false).unwrap()[0]; + assert_eq!((e.path.as_str(), e.size, e.modified, e.kind, e.is_dir), ("/clip.mov", 10, Some(5), Some("video"), false)); + let d = &t.list("/Folders", false).unwrap()[1]; + assert_eq!((d.path.as_str(), d.is_dir), ("/Folders/Shoot", true)); + assert_eq!(t.import_path("/Browser Storage/clip.mov"), Some("/clip.mov")); + assert!(t.list("/Folders/Shoot/a.wav", false).is_err(), "a file is not a folder"); + assert!(t.list("/Nope", false).is_err()); + t.remove("/Folders/Shoot"); + assert_eq!(names(&t, "/Folders", false), ["Empty"]); + assert_eq!(vparent("/a"), Some("/")); + assert_eq!(vparent("/"), None); + } + #[test] fn iso_dates() { assert_eq!(iso_date(0), "1970-01-01T00:00:00Z"); diff --git a/crates/engine/src/offload.rs b/crates/engine/src/offload.rs index 3e92fbd4..712549bd 100644 --- a/crates/engine/src/offload.rs +++ b/crates/engine/src/offload.rs @@ -16,10 +16,11 @@ //! | Track Motion / Stabilize Motion | the tracker analysis | the tracker group | //! | Mask tracking | the mask track | the mask group | //! | Roto Brush Freeze | the freeze | the effect group | +//! | Roto Brush propagation | the propagation | segmentations ([`WorkerReply::Segs`], streamed while it runs) go into the page's segmentation cache | //! | 3D Camera Tracker analysis | [`Session::start_camera`] | the effect's property group | //! -//! Roto Brush propagation fills the session's segmentation cache rather than the project, so it -//! keeps running on the UI thread. +//! Viewer frames go to a separate, long-lived frame worker fed with project diffs +//! ([`crate::remote`]). //! //! Cancelling terminates the worker: what an analysis tracked so far is dropped and the Render //! Queue item being rendered becomes "User Stopped". @@ -76,6 +77,13 @@ pub enum WorkerJob { layer: LayerId, effect: Uid, }, + /// Roto Brush propagation: the segmentations come back as [`WorkerReply::Segs`]. + RotoPropagate { + comp: ItemId, + layer: LayerId, + effect: Uid, + direction: crate::roto::Direction, + }, Track { comp: ItemId, layer: LayerId, @@ -103,6 +111,7 @@ impl WorkerJob { WorkerJob::Warp { .. } => JobKind::Warp, WorkerJob::Camera { .. } => JobKind::Camera, WorkerJob::RotoFreeze { .. } => JobKind::RotoFreeze, + WorkerJob::RotoPropagate { .. } => JobKind::RotoPropagate, WorkerJob::Track { .. } => JobKind::Track, WorkerJob::MaskTrack { .. } => JobKind::MaskTrack, } @@ -112,9 +121,10 @@ impl WorkerJob { pub fn target(&self) -> Option<(ItemId, LayerId, Uid)> { match *self { WorkerJob::Render { .. } => None, - WorkerJob::Warp { comp, layer, effect } | WorkerJob::Camera { comp, layer, effect } | WorkerJob::RotoFreeze { comp, layer, effect } => { - Some((comp, layer, effect)) - } + WorkerJob::Warp { comp, layer, effect } + | WorkerJob::Camera { comp, layer, effect } + | WorkerJob::RotoFreeze { comp, layer, effect } + | WorkerJob::RotoPropagate { comp, layer, effect, .. } => Some((comp, layer, effect)), WorkerJob::Track { comp, layer, tracker, .. } => Some((comp, layer, tracker)), WorkerJob::MaskTrack { comp, layer, mask, .. } => Some((comp, layer, mask)), } @@ -128,6 +138,7 @@ pub enum JobKind { Warp, Camera, RotoFreeze, + RotoPropagate, Track, MaskTrack, } @@ -140,6 +151,7 @@ impl JobKind { JobKind::Warp => "Warp Stabilizer Analysis", JobKind::Camera => "3D Camera Tracker Analysis", JobKind::RotoFreeze => "Freeze", + JobKind::RotoPropagate => "Roto Brush Propagation", JobKind::Track => "Analyze Track", JobKind::MaskTrack => "Track Mask", } @@ -170,6 +182,10 @@ pub enum WorkerReply { group: Box, message: String, }, + /// Roto Brush segmentations computed so far (propagation), not sent before. + Segs { + segs: Vec, + }, Failed { error: String, }, @@ -177,6 +193,41 @@ pub enum WorkerReply { Done, } +/// One full-resolution Roto Brush segmentation: its chain key and the base64 of +/// [`effectcraft_track::roto::FrameSeg::to_bytes`]. +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct SegData { + pub key: u64, + pub data: String, +} + +impl SegData { + pub fn new(key: u64, seg: &effectcraft_track::roto::FrameSeg) -> SegData { + SegData { key, data: effectcraft_track::roto::rle::base64_encode(&seg.to_bytes()) } + } + + /// Put the segmentation into this instance's cache (what the Roto Brush effect renders from). + pub fn store(&self) -> bool { + let Some(seg) = effectcraft_track::roto::rle::base64_decode(&self.data).and_then(|b| effectcraft_track::roto::FrameSeg::from_bytes(&b)) else { + return false; + }; + effectcraft_effects::roto::store(self.key, 1.0, Arc::new(seg)); + true + } +} + +/// The full-resolution segmentations of `keys` that are cached here and not in `sent` yet. +pub fn segs_to_send(keys: &[u64], sent: &mut std::collections::HashSet) -> Vec { + let new: Vec<(u64, Arc)> = + keys.iter().filter(|k| !sent.contains(k)).filter_map(|k| effectcraft_effects::roto::cached(*k, 1.0).map(|s| (*k, s))).collect(); + new.into_iter() + .map(|(k, s)| { + sent.insert(k); + SegData::new(k, &s) + }) + .collect() +} + /// Replies waiting for the UI thread. #[derive(Default)] pub struct Inbox(Mutex>); @@ -292,6 +343,7 @@ fn run_job(s: &mut Session, job: WorkerJob, post: Post) -> Result<(), String> { WorkerJob::RotoFreeze { comp, layer, effect } => analysis(s, kind, target, post, |s| { s.start_roto(comp, layer, effect, crate::roto::RotoTask::Freeze, crate::roto::Direction::Both, true).map(|_| ()) }), + WorkerJob::RotoPropagate { comp, layer, effect, direction } => propagate(s, comp, layer, effect, direction, post), WorkerJob::Track { comp, layer, tracker, direction, settings, points, times } => analysis(s, kind, target, post, |s| { let work = crate::tracking::Work { project: s.project.clone(), @@ -323,6 +375,38 @@ fn run_job(s: &mut Session, job: WorkerJob, post: Post) -> Result<(), String> { } } +/// Roto Brush propagation in a worker: the segmentations stream back (at most every 250 ms) +/// as they are computed, so stopping the job keeps what was propagated. +fn propagate(s: &mut Session, comp: ItemId, layer: LayerId, effect: Uid, dir: crate::roto::Direction, post: Post) -> Result<(), String> { + let keys: Vec = s.roto_chain(comp, layer, effect).map(|c| c.keys.values().copied().collect()).ok_or("no Roto Brush strokes")?; + let sent = Rc::new(RefCell::new(std::collections::HashSet::new())); + let (p, k2, s2) = (post.clone(), keys.clone(), sent.clone()); + let mut last = None::; + let hook = Box::new(move |done: u64, total: u64| { + if last.is_none_or(|t: web_time::Instant| t.elapsed().as_millis() >= 250) { + last = Some(web_time::Instant::now()); + let segs = segs_to_send(&k2, &mut s2.borrow_mut()); + if !segs.is_empty() { + p(WorkerReply::Segs { segs }); + } + p(WorkerReply::Progress { done, total }); + } + }); + s.events.clear(); + with_hook(hook, || s.start_roto(comp, layer, effect, crate::roto::RotoTask::Propagate, dir, true))?; + let segs = segs_to_send(&keys, &mut sent.borrow_mut()); + if !segs.is_empty() { + post(WorkerReply::Segs { segs }); + } + if let Some(e) = s.events.iter().find_map(|e| match e { + Event::Toast { message, error: true } => Some(message.clone()), + _ => None, + }) { + return Err(e); + } + Ok(()) +} + fn analysis( s: &mut Session, kind: JobKind, @@ -398,6 +482,8 @@ pub struct OffloadedJob { pub uid: Uid, pub inbox: Arc, pub progress: OffloadProgress, + /// Segmentations received (Roto Brush propagation). + pub segs: u64, started: web_time::Instant, } @@ -432,7 +518,17 @@ impl Session { let inbox = Arc::new(Inbox::default()); let req = WorkerRequest { id, project: self.project.to_json(), files: footage_files(&self.project), job }; off.start(req, inbox.clone())?; - self.offloaded.push(OffloadedJob { id, kind, comp, layer, uid, inbox, progress: OffloadProgress::default(), started: web_time::Instant::now() }); + self.offloaded.push(OffloadedJob { + id, + kind, + comp, + layer, + uid, + inbox, + progress: OffloadProgress::default(), + segs: 0, + started: web_time::Instant::now(), + }); self.events.push(Event::ProjectChanged { revision: self.revision }); Ok(()) } @@ -469,6 +565,12 @@ impl Session { j.progress.total = total; } WorkerReply::Group { comp, layer, group, message } => results.push((j.kind, comp, layer, group, message)), + WorkerReply::Segs { segs } => { + for sd in &segs { + sd.store(); + } + j.segs += segs.len() as u64; + } WorkerReply::Failed { error } => j.progress.error = Some(error), WorkerReply::Done => j.progress.finished = true, WorkerReply::Render { .. } | WorkerReply::Item { .. } => {} @@ -495,17 +597,25 @@ impl Session { let mut done = vec![]; self.offloaded.retain(|j| { if j.progress.finished { - done.push((j.kind, j.progress.clone())); + done.push((j.kind, j.progress.clone(), j.segs)); } !j.progress.finished }); - for (kind, p) in done { + for (kind, p, segs) in done { changed = true; + self.offload_log.push((kind, p.error.clone().or_else(|| p.cancelled.then(|| "stopped".to_string())))); + if self.offload_log.len() > 32 { + self.offload_log.remove(0); + } if let Some(e) = p.error { self.events.push(Event::Toast { message: format!("{}: {e}", kind.label()), error: true }); } else if p.cancelled { - self.events.push(Event::Toast { message: format!("{} stopped", kind.label()), error: false }); + let kept = if segs > 0 { format!(" ({segs} frame(s) kept)") } else { String::new() }; + self.events.push(Event::Toast { message: format!("{} stopped{kept}", kind.label()), error: false }); + } else if kind == JobKind::RotoPropagate { + self.events.push(Event::Toast { message: format!("Roto Brush: propagated {} frame(s) in {:.1} s", segs, p.elapsed), error: false }); } + // Rendered frames depend on the result (an analysis' group, the segmentation cache). self.bump(); } changed @@ -567,6 +677,7 @@ mod tests { WorkerJob::Warp { comp, layer: LayerId(3), effect: 9 }, WorkerJob::RotoFreeze { comp, layer: LayerId(3), effect: 9 }, WorkerJob::Camera { comp, layer: LayerId(3), effect: 9 }, + WorkerJob::RotoPropagate { comp, layer: LayerId(3), effect: 9, direction: crate::roto::Direction::Forward }, WorkerJob::Track { comp, layer: LayerId(1), @@ -606,6 +717,7 @@ mod tests { WorkerReply::Item { update: ItemUpdate::Finished { id: 3, status: RenderStatus::Failed("x".into()), seconds: 1.5, output: Some("/a.gif".into()) } }, WorkerReply::Progress { done: 1, total: 2 }, WorkerReply::Group { comp, layer: LayerId(1), group: Box::new(g), message: "ok".into() }, + WorkerReply::Segs { segs: vec![SegData::new(5, &effectcraft_track::roto::FrameSeg::empty(4, 3))] }, WorkerReply::Failed { error: "boom".into() }, WorkerReply::Done, ] { diff --git a/crates/engine/src/remote.rs b/crates/engine/src/remote.rs new file mode 100644 index 00000000..ec0640ff --- /dev/null +++ b/crates/engine/src/remote.rs @@ -0,0 +1,384 @@ +//! Viewer frames rendered by another engine instance (the browser's frame worker). +//! +//! The page keeps no frame threads (a wasm build without shared memory has one thread per +//! instance), so the web app renders viewer frames in a long-lived Web Worker that holds a +//! replica of the project. The page sends the replica **project diffs** rather than the whole +//! project per edit, then render requests; the worker answers each with the frame's pixels. +//! +//! - [`diff`] / [`patch`]: a structural JSON diff of the serialized project (object members +//! added, changed or removed; arrays element-wise when their length is unchanged, else +//! replaced). Lossless (unlike JSON Merge Patch, `null` values survive). +//! - [`Mirror`] (page side): what the worker holds; turns the next project revision into a +//! [`FrameMsg::Sync`] (a full project the first time, a diff afterwards). +//! - [`FrameServer`] (worker side): applies syncs, relayed Roto Brush segmentations and render +//! requests, and renders frames as premultiplied RGBA8 ([`rgba8_premultiplied`]). +//! +//! The transport (structured-clone messages, transferable pixel buffers) is the web crate's; +//! everything here is portable and tested natively. + +use std::sync::Arc; + +use effectcraft_project::{ItemId, Project}; +use effectcraft_render::{ExprHost, FootageSource, Image, LayerCache, RenderOpts, Renderer}; +use effectcraft_time::Tick; +use serde::{Deserialize, Serialize}; +use serde_json::Value; + +use crate::offload::SegData; + +/// One step of a path into a JSON document. +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(untagged)] +pub enum Step { + Key(String), + Index(usize), +} + +/// One change: set the value at `path` (creating or replacing the member/element), or remove +/// the object member at `path` (`value` absent). +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct Op { + pub path: Vec, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub value: Option, +} + +/// The changes that turn `old` into `new` (empty when equal). +pub fn diff(old: &Value, new: &Value) -> Vec { + let mut ops = vec![]; + diff_into(old, new, &mut vec![], &mut ops); + ops +} + +fn diff_into(old: &Value, new: &Value, path: &mut Vec, ops: &mut Vec) { + if old == new { + return; + } + match (old, new) { + (Value::Object(a), Value::Object(b)) => { + for (k, va) in a { + path.push(Step::Key(k.clone())); + match b.get(k) { + Some(vb) => diff_into(va, vb, path, ops), + None => ops.push(Op { path: path.clone(), value: None }), + } + path.pop(); + } + for (k, vb) in b { + if !a.contains_key(k) { + path.push(Step::Key(k.clone())); + ops.push(Op { path: path.clone(), value: Some(vb.clone()) }); + path.pop(); + } + } + } + (Value::Array(a), Value::Array(b)) if a.len() == b.len() => { + for (i, (va, vb)) in a.iter().zip(b).enumerate() { + path.push(Step::Index(i)); + diff_into(va, vb, path, ops); + path.pop(); + } + } + _ => ops.push(Op { path: path.clone(), value: Some(new.clone()) }), + } +} + +/// Apply [`diff`]'s changes to `doc`. +pub fn patch(doc: &mut Value, ops: &[Op]) -> Result<(), String> { + for op in ops { + let Some((last, parents)) = op.path.split_last() else { + *doc = op.value.clone().ok_or("cannot remove the document")?; + continue; + }; + let mut cur = &mut *doc; + for st in parents { + cur = match (st, cur) { + (Step::Key(k), Value::Object(m)) => m.get_mut(k).ok_or_else(|| format!("no member `{k}`"))?, + (Step::Index(i), Value::Array(a)) => a.get_mut(*i).ok_or_else(|| format!("no element {i}"))?, + _ => return Err("path does not match the document".into()), + }; + } + match (last, cur, &op.value) { + (Step::Key(k), Value::Object(m), Some(v)) => { + m.insert(k.clone(), v.clone()); + } + (Step::Key(k), Value::Object(m), None) => { + m.remove(k); + } + (Step::Index(i), Value::Array(a), Some(v)) => *a.get_mut(*i).ok_or_else(|| format!("no element {i}"))? = v.clone(), + _ => return Err("path does not match the document".into()), + } + } + Ok(()) +} + +/// Page → frame worker. +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(tag = "type", rename_all = "camelCase")] +pub enum FrameMsg { + /// The whole project (JSON) at `revision`. + Project { revision: u64, project: String }, + /// Changes from the worker's revision `from` to `revision`. + Patch { from: u64, revision: u64, ops: Vec }, + /// Render a frame of the current project. + Render { id: u64, revision: u64, comp: ItemId, time: Tick, opts: Box }, + /// Roto Brush segmentations computed elsewhere (a propagation worker). + Segs { segs: Vec }, + /// Drop cached layer buffers (Edit ▸ Purge). + Purge, +} + +/// Frame worker → page. A [`FrameReply::Frame`]'s pixels travel next to the message (a +/// transferable buffer in the browser). +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(tag = "type", rename_all = "camelCase")] +pub enum FrameReply { + /// Premultiplied RGBA8 pixels of request `id`, rendered in `ms` milliseconds. + Frame { id: u64, width: u32, height: u32, ms: f64 }, + /// Request `id` was not rendered (`stale`: the worker holds another revision). + Failed { id: u64, error: String, stale: bool }, + /// A sync could not be applied: the page must send the whole project again. + Resync { error: String }, +} + +/// The page's record of a worker's replica. +#[derive(Default)] +pub struct Mirror { + sent: Option<(u64, Value)>, +} + +impl Mirror { + /// The revision the worker holds. + pub fn revision(&self) -> Option { + self.sent.as_ref().map(|s| s.0) + } + + /// Forget the replica (the next sync sends the whole project). + pub fn reset(&mut self) { + self.sent = None; + } + + /// The message that brings the worker to `revision` of `project` (`None` when it has it). + pub fn sync(&mut self, revision: u64, project: &Project) -> Option { + if self.revision() == Some(revision) { + return None; + } + let v = serde_json::to_value(project).unwrap_or(Value::Null); + let msg = match self.sent.take() { + Some((from, old)) => FrameMsg::Patch { from, revision, ops: diff(&old, &v) }, + None => FrameMsg::Project { revision, project: v.to_string() }, + }; + self.sent = Some((revision, v)); + Some(msg) + } +} + +/// Premultiplied f32 → premultiplied RGBA8 (colour clamped to alpha), as the viewer shows it. +pub fn rgba8_premultiplied(img: &Image) -> Vec { + let mut out = Vec::with_capacity(img.data.len() * 4); + for p in &img.data { + let a = p[3].clamp(0.0, 1.0); + let c = |v: f32| (v.clamp(0.0, a) * 255.0 + 0.5) as u8; + out.extend_from_slice(&[c(p[0]), c(p[1]), c(p[2]), (a * 255.0 + 0.5) as u8]); + } + out +} + +/// The worker's side: a project replica and what rendering needs. +pub struct FrameServer { + pub footage: Arc, + pub expr: Option>, + pub cache: Arc, + doc: Value, + project: Option>, + revision: Option, +} + +impl FrameServer { + pub fn new(footage: Arc, expr: Option>) -> FrameServer { + FrameServer { footage, expr, cache: Arc::new(LayerCache::default()), doc: Value::Null, project: None, revision: None } + } + + pub fn revision(&self) -> Option { + self.revision + } + + pub fn project(&self) -> Option<&Arc> { + self.project.as_ref() + } + + fn load(&mut self, revision: u64) -> Result<(), String> { + let p = Project::from_json(&self.doc.to_string()).map_err(|e| e.to_string())?; + self.project = Some(Arc::new(p)); + self.revision = Some(revision); + Ok(()) + } + + /// Handle one message; returns the replies with their pixels. + pub fn handle(&mut self, msg: FrameMsg) -> Vec<(FrameReply, Option>)> { + match msg { + FrameMsg::Project { revision, project } => { + let r = serde_json::from_str(&project).map_err(|e| e.to_string()).and_then(|v| { + self.doc = v; + self.load(revision) + }); + match r { + Ok(()) => vec![], + Err(e) => self.lost(e), + } + } + FrameMsg::Patch { from, revision, ops } => { + if self.revision != Some(from) { + return self.lost(format!("the replica is at {:?}, the patch starts at {from}", self.revision)); + } + match patch(&mut self.doc, &ops).and_then(|_| self.load(revision)) { + Ok(()) => vec![], + Err(e) => self.lost(e), + } + } + FrameMsg::Segs { segs } => { + for s in &segs { + s.store(); + } + vec![] + } + FrameMsg::Purge => { + self.cache.clear(); + vec![] + } + FrameMsg::Render { id, revision, comp, time, opts } => { + let fail = |error: String, stale: bool| vec![(FrameReply::Failed { id, error, stale }, None)]; + if self.revision != Some(revision) { + return fail(format!("the replica is at {:?}, not {revision}", self.revision), true); + } + let Some(p) = self.project.clone() else { return fail("no project".into(), true) }; + if p.comp(comp).is_none() { + return fail(format!("no composition {}", comp.0), false); + } + let t0 = web_time::Instant::now(); + let mut r = Renderer::new(&p, self.footage.as_ref(), *opts); + r.expr = self.expr.as_deref(); + r.cache = Some(&self.cache); + let img = r.comp_frame(comp, time); + let ms = t0.elapsed().as_secs_f64() * 1000.0; + vec![(FrameReply::Frame { id, width: img.width, height: img.height, ms }, Some(rgba8_premultiplied(&img)))] + } + } + } + + fn lost(&mut self, error: String) -> Vec<(FrameReply, Option>)> { + self.doc = Value::Null; + self.project = None; + self.revision = None; + vec![(FrameReply::Resync { error }, None)] + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::Session; + use serde_json::json; + + fn round Deserialize<'de>>(v: &T) -> T { + serde_json::from_str(&serde_json::to_string(v).unwrap()).unwrap() + } + + #[test] + fn diff_and_patch_round_trip() { + let a = json!({"a": 1, "b": [1, 2, {"c": null}], "d": {"e": "x", "f": [1]}, "g": null}); + let b = json!({"a": 2, "b": [1, 3, {"c": 4}], "d": {"f": [1, 2]}, "g": null, "h": {"i": null}}); + let ops = diff(&a, &b); + assert!(!ops.is_empty()); + let mut x = a.clone(); + patch(&mut x, &round(&ops)).unwrap(); + assert_eq!(x, b); + // Removal and the reverse direction. + let mut y = b.clone(); + patch(&mut y, &diff(&b, &a)).unwrap(); + assert_eq!(y, a); + assert!(diff(&a, &a).is_empty()); + // A whole-document change. + let mut z = json!(1); + patch(&mut z, &diff(&json!(1), &json!([1]))).unwrap(); + assert_eq!(z, json!([1])); + // A patch for another document fails cleanly. + assert!(patch(&mut json!(3), &diff(&a, &b)).is_err()); + } + + fn server(s: &Session) -> FrameServer { + FrameServer::new(s.footage.clone(), s.expr.clone()) + } + + /// Pixels the page itself would render. + fn local(s: &Session, comp: ItemId, t: Tick, opts: RenderOpts) -> Vec { + let mut r = Renderer::new(&s.project, s.footage.as_ref(), opts); + r.expr = s.expr.as_deref(); + rgba8_premultiplied(&r.comp_frame(comp, t)) + } + + #[test] + fn replica_follows_edits_through_diffs_and_renders_the_same_frames() { + let mut s = Session::default(); + s.execute("file.openDemoProject", json!({})).unwrap(); + let comp = s.active_comp_id().unwrap(); + let opts = RenderOpts { scale: 0.25, guides: true, ..Default::default() }; + let mut m = Mirror::default(); + let mut w = server(&s); + let send = |m: FrameMsg, w: &mut FrameServer| w.handle(round(&m)); + // First sync: the whole project. + let first = m.sync(s.revision, &s.project).unwrap(); + assert!(matches!(first, FrameMsg::Project { .. })); + assert!(send(first, &mut w).is_empty()); + assert!(m.sync(s.revision, &s.project).is_none(), "nothing new"); + let t = Tick::from_seconds_f64(1.0); + let render = |w: &mut FrameServer, rev: u64, id: u64| w.handle(round(&FrameMsg::Render { id, revision: rev, comp, time: t, opts: Box::new(opts) })); + let out = render(&mut w, s.revision, 1); + let (FrameReply::Frame { id, width, height, .. }, Some(px)) = &out[0] else { panic!("{:?}", out[0].0) }; + assert_eq!((*id, *width as usize * *height as usize * 4), (1, px.len())); + assert_eq!(*px, local(&s, comp, t, opts)); + // An edit travels as a small diff; the replica renders the edited frame. + s.execute("layer.newSolid", json!({"color": "#ff8800", "name": "Orange"})).unwrap(); + let msg = m.sync(s.revision, &s.project).unwrap(); + let FrameMsg::Patch { ops, .. } = &msg else { panic!("expected a patch") }; + assert!(!ops.is_empty() && serde_json::to_string(ops).unwrap().len() < s.project.to_json().len() / 2); + assert!(send(msg, &mut w).is_empty()); + assert_eq!(**w.project().unwrap(), *s.project); + let out = render(&mut w, s.revision, 2); + assert_eq!(out[0].1.as_deref(), Some(&local(&s, comp, t, opts)[..])); + // A request for another revision is refused as stale. + let out = render(&mut w, s.revision + 7, 3); + assert!(matches!(out[0].0, FrameReply::Failed { id: 3, stale: true, .. })); + // A patch from the wrong base asks for a resync; the mirror then sends the project. + let bad = FrameMsg::Patch { from: 999, revision: 1000, ops: vec![] }; + assert!(matches!(send(bad, &mut w)[0].0, FrameReply::Resync { .. })); + m.reset(); + assert!(matches!(m.sync(s.revision, &s.project), Some(FrameMsg::Project { .. }))); + } + + #[test] + fn frame_messages_round_trip() { + for m in [ + FrameMsg::Purge, + FrameMsg::Segs { segs: vec![SegData { key: 7, data: "AAAA".into() }] }, + FrameMsg::Render { + id: 3, + revision: 4, + comp: ItemId(2), + time: Tick::from_seconds_f64(0.5), + opts: Box::new(RenderOpts { scale: 0.5, draft: true, roi: Some([1.0, 2.0, 3.0, 4.0]), ..Default::default() }), + }, + ] { + let a = serde_json::to_string(&m).unwrap(); + assert_eq!(serde_json::to_string(&round(&m)).unwrap(), a); + assert!(a.starts_with("{\"type\":"), "{a}"); + } + for r in [ + FrameReply::Frame { id: 1, width: 2, height: 3, ms: 4.5 }, + FrameReply::Failed { id: 1, error: "x".into(), stale: true }, + FrameReply::Resync { error: "y".into() }, + ] { + assert_eq!(round(&r), r); + } + } +} 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/roto.rs b/crates/engine/src/roto.rs index b6086d4e..2c1828f8 100644 --- a/crates/engine/src/roto.rs +++ b/crates/engine/src/roto.rs @@ -3,7 +3,8 @@ //! //! Segmentations are derived data in `effectcraft_effects::roto`'s content-keyed cache (see that //! module). `roto.propagate` fills it at full resolution in the background (or blocking with -//! `wait`), frame by frame outward from the base frame, rendering the effect's input +//! `wait`; in the browser the background is a Web Worker whose segmentations stream back, +//! [`crate::offload`]), frame by frame outward from the base frame, rendering the effect's input //! (`Renderer::layer_input`, source → masks → the effects above it). `roto.freeze` runs the same //! pass and stores the final mattes in the effect (one undo step). [`sync`], run after every //! edit, keeps each instance's hidden Input Key equal to a signature of what its frames are made @@ -403,16 +404,21 @@ pub(crate) fn job_frames(d: &effectcraft_track::roto::RotoData, dir: Direction) } impl Session { + /// The Roto Brush job running in a worker (Freeze or propagation), with its task. + fn offloaded_roto(&self) -> Option<(&crate::offload::OffloadedJob, RotoTask)> { + self.offloaded(JobKind::RotoFreeze).map(|j| (j, RotoTask::Freeze)).or_else(|| self.offloaded(JobKind::RotoPropagate).map(|j| (j, RotoTask::Propagate))) + } + pub fn is_roto_running(&self) -> bool { - self.roto_job.as_ref().is_some_and(|j| !j.is_finished()) || self.offloaded(JobKind::RotoFreeze).is_some() + self.roto_job.as_ref().is_some_and(|j| !j.is_finished()) || self.offloaded_roto().is_some() } pub fn roto_progress(&self) -> Option { self.roto_job.as_ref().map(|j| j.progress()).or_else(|| { - self.offloaded(JobKind::RotoFreeze).map(|j| { + self.offloaded_roto().map(|(j, task)| { let p = &j.progress; RotoProgress { - task: Some(RotoTask::Freeze), + task: Some(task), done: p.done, total: p.total, elapsed: p.elapsed, @@ -425,14 +431,22 @@ impl Session { /// (comp, layer, effect uid, task) of the running job. pub fn roto_target(&self) -> Option<(ItemId, LayerId, Uid, RotoTask)> { - self.roto_job - .as_ref() - .map(|j| (j.comp, j.layer, j.effect, j.task)) - .or_else(|| self.offloaded(JobKind::RotoFreeze).map(|j| (j.comp, j.layer, j.uid, RotoTask::Freeze))) + self.roto_job.as_ref().map(|j| (j.comp, j.layer, j.effect, j.task)).or_else(|| self.offloaded_roto().map(|(j, task)| (j.comp, j.layer, j.uid, task))) + } + + /// The segmentation chain a propagation of `effect` computes (its frames' cache keys). + pub fn roto_chain(&self, comp: ItemId, layer: LayerId, effect: Uid) -> Option { + let c = self.project.comp(comp)?; + let l = c.layer(layer)?; + let g = l.effects()?.groups().find(|g| g.uid == effect && is_roto(g))?; + let d = fx::data(¶ms_static(g)); + let t = comp_time_of(c, l, d.base?); + let params = params_at(&self.project, comp, l, g, t, self.expr.as_deref()); + Some(fx::Chain::new(¶ms, layer_size(&self.project, c, l), c.frame_rate.as_f64(), 1.0)) } pub fn stop_roto(&mut self) -> bool { - if self.cancel_offloaded(JobKind::RotoFreeze) { + if self.cancel_offloaded(JobKind::RotoFreeze) || self.cancel_offloaded(JobKind::RotoPropagate) { return true; } match &self.roto_job { @@ -491,9 +505,12 @@ impl Session { task, }; let strokes = str_of(g, fx::STROKES); - if !wait && task == RotoTask::Freeze && self.offloads() { + if !wait && self.offloads() { drop(work); - self.offload_analysis(crate::offload::WorkerJob::RotoFreeze { comp, layer, effect })?; + self.offload_analysis(match task { + RotoTask::Freeze => crate::offload::WorkerJob::RotoFreeze { comp, layer, effect }, + RotoTask::Propagate => crate::offload::WorkerJob::RotoPropagate { comp, layer, effect, direction: dir }, + })?; return Ok(n); } let wait = wait || cfg!(target_arch = "wasm32"); @@ -567,7 +584,7 @@ impl Session { /// instances in the background. pub fn poll_roto(&mut self, auto: bool) -> bool { let changed = self.poll_roto_job(); - if auto && self.roto_job.is_none() && self.state.roto.auto_propagate { + if auto && self.roto_job.is_none() && self.offloaded_roto().is_none() && self.state.roto.auto_propagate { while let Some((c, l, e)) = self.roto_pending.first().copied() { self.roto_pending.remove(0); if self.start_roto(c, l, e, RotoTask::Propagate, Direction::Both, false).is_ok() { 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/session_settings.rs b/crates/engine/src/session_settings.rs index 96cd5a29..ecb7f44a 100644 --- a/crates/engine/src/session_settings.rs +++ b/crates/engine/src/session_settings.rs @@ -48,8 +48,12 @@ impl Session { if !s.is_empty() { return Some(std::path::PathBuf::from(s)); } + // (the browser has no file system: no default folder there) + if self.config.is_none() || cfg!(target_arch = "wasm32") { + return None; + } let base = effectcraft_render::disk_cache::default_folder(); - self.config.is_some().then(|| base.parent().map(|p| p.join(name)).unwrap_or_else(|| base.join(name))) + Some(base.parent().map(|p| p.join(name)).unwrap_or_else(|| base.join(name))) } /// Settings ▸ Disk ▸ Database and Cache Folder (media cache: audio waveform summaries). 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_panels.rs b/crates/engine/src/tests_panels.rs index d989b28e..8f5cb7f1 100644 --- a/crates/engine/src/tests_panels.rs +++ b/crates/engine/src/tests_panels.rs @@ -516,3 +516,64 @@ fn panels_commands_are_wired() { assert!(s.is_enabled(id), "{id}"); } } + +/// A browser like the web app's: a virtual tree whose files are read on demand. +#[derive(Default)] +struct VirtualBrowser { + tree: Mutex, + fetched: Mutex>, + actions: Mutex>, +} + +impl crate::media_browser::Browser for VirtualBrowser { + fn home(&self) -> String { + "/Browser Storage".into() + } + fn places(&self) -> Vec<(String, String)> { + vec![("Browser Storage".into(), self.home()), ("Shoot".into(), "/Folders/Shoot".into())] + } + fn list(&self, dir: &str, only: bool) -> Result, String> { + self.tree.lock().unwrap().list(dir, only) + } + fn fetch(&self, paths: &[String], _: &Value) -> bool { + // Opened folders' files arrive later. + let pending: Vec = paths.iter().filter(|p| p.starts_with("/Folders/")).cloned().collect(); + self.fetched.lock().unwrap().extend(pending.iter().cloned()); + pending.is_empty() + } + fn actions(&self) -> Vec<(String, String)> { + vec![("openFolder".into(), "Open Folder…".into())] + } + fn action(&self, id: &str) -> Result { + self.actions.lock().unwrap().push(id.into()); + Ok(json!({"pending": true})) + } +} + +#[test] +fn media_browser_over_a_virtual_tree() { + let b = Arc::new(VirtualBrowser::default()); + { + let mut t = b.tree.lock().unwrap(); + t.add_dir("/Browser Storage"); + t.add_file("/Folders/Shoot/a.csv", "/Folders/Shoot/a.csv", 12, Some(1)); + } + let mut s = Session { browser: Some(b.clone()), ..Default::default() }; + let r = s.execute("mediaBrowser.list", json!({})).unwrap(); + assert_eq!(r["path"], "/Browser Storage"); + assert_eq!(r["places"][1]["path"], "/Folders/Shoot"); + assert_eq!(r["actions"][0]["id"], "openFolder"); + let r = s.execute("mediaBrowser.go", json!({"path": "/Folders/Shoot"})).unwrap(); + assert_eq!(r["entries"][0]["name"], "a.csv"); + assert_eq!(r["parent"], "/Folders"); + assert!(s.execute("mediaBrowser.go", json!({"path": "/Nope"})).is_err()); + // Importing a file that is not read yet: pending (the host imports it once it arrives). + let r = s.execute("mediaBrowser.import", json!({"paths": ["/Folders/Shoot/a.csv"]})).unwrap(); + assert_eq!(r["pending"], true); + assert_eq!(*b.fetched.lock().unwrap(), ["/Folders/Shoot/a.csv"]); + assert!(s.project.items.values().all(|i| !matches!(i.kind, ItemKind::Footage(_)))); + let r = s.execute("mediaBrowser.action", json!({"action": "openFolder"})).unwrap(); + assert_eq!(r["pending"], true); + assert_eq!(*b.actions.lock().unwrap(), ["openFolder"]); + assert!(s.execute("mediaBrowser.action", json!({})).is_err()); +} diff --git a/crates/engine/src/tests_roto.rs b/crates/engine/src/tests_roto.rs index 79403626..41d8e86e 100644 --- a/crates/engine/src/tests_roto.rs +++ b/crates/engine/src/tests_roto.rs @@ -286,3 +286,52 @@ fn propagation_is_deterministic_and_runs_in_background() { let r = s.execute("roto.stroke", json!({"layer": clip.0, "points": [[90, 110]]})).unwrap(); assert_eq!(r["radius"], 15.0); } + +/// Runs offloaded jobs at once in a second session sharing the footage, through JSON (what the +/// browser's Web Worker does, minus the thread). +struct InlineWorker(std::sync::Arc); + +impl crate::offload::Offload for InlineWorker { + fn start(&self, req: crate::offload::WorkerRequest, inbox: std::sync::Arc) -> Result<(), String> { + let req: crate::offload::WorkerRequest = serde_json::from_str(&serde_json::to_string(&req).unwrap()).unwrap(); + let mut w = Session { footage: self.0.clone(), ..Default::default() }; + let post: crate::offload::Post = std::rc::Rc::new(move |r| inbox.push(serde_json::from_str(&serde_json::to_string(&r).unwrap()).unwrap())); + crate::offload::run_request(&mut w, req, &post); + Ok(()) + } + fn cancel(&self, _: u64) {} +} + +#[test] +fn offloaded_propagation_streams_segmentations_back() { + let (mut s, clip, uid) = painted(center, "disk-offload.mov"); + s.execute("roto.span", json!({"layer": clip.0, "end": 5})).unwrap(); + s.offload = Some(std::sync::Arc::new(InlineWorker(s.footage.clone()))); + let r = s.execute("roto.propagate", json!({"layer": clip.0})).unwrap(); + assert_eq!(r["frames"], 6, "{r}"); + assert!(s.roto_job.is_none(), "offloaded, not on a thread"); + assert!(s.is_roto_running()); + assert_eq!(s.roto_progress().unwrap().task, Some(crate::roto::RotoTask::Propagate)); + assert_eq!(s.roto_target().map(|t| (t.1, t.2)), Some((clip, uid))); + assert!(s.jobs().iter().any(|j| j.id == "roto")); + // The worker has its own segmentation cache: here nothing is computed until its replies + // are applied. + forget(&s, clip, uid); + assert_eq!(s.execute("roto.status", json!({"layer": clip.0})).unwrap()["computed"], json!([])); + let rev = s.revision; + assert!(s.poll_offload()); + assert!(!s.is_roto_running()); + assert!(s.revision > rev, "frames are redrawn"); + let st = s.execute("roto.status", json!({"layer": clip.0})).unwrap(); + assert_eq!(st["computed"], json!([0, 1, 2, 3, 4, 5]), "{st}"); + assert!(s.events.iter().any(|e| matches!(e, crate::Event::Toast { message, error: false } if message.contains("propagated 6 frame")))); + // The matte matches one computed here. + let a = s.execute("roto.status", json!({"layer": clip.0, "frame": 5, "matte": true})).unwrap()["matte"].clone(); + forget(&s, clip, uid); + let b = s.execute("roto.status", json!({"layer": clip.0, "frame": 5, "matte": true})).unwrap()["matte"].clone(); + assert!(a.is_string()); + assert_eq!(a, b); + // `wait` still runs here. + s.execute("roto.propagate", json!({"layer": clip.0, "wait": true})).unwrap(); + assert!(s.offloaded.is_empty()); +} 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 1de302ce..3d8d2916 100644 --- a/crates/gpu/src/context.rs +++ b/crates/gpu/src/context.rs @@ -624,7 +624,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; &mut self .enc diff --git a/crates/gpu/src/lib.rs b/crates/gpu/src/lib.rs index 9750cf79..ca9bdbfb 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 @@ -168,6 +171,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 dcca81a7..a8075e10 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 ca708696..390c68c6 100644 --- a/crates/render/src/lib.rs +++ b/crates/render/src/lib.rs @@ -280,6 +280,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> { @@ -327,7 +333,8 @@ impl FxTarget for CpuFx<'_> { } } -#[derive(Clone, Copy, Debug)] +#[derive(Clone, Copy, Debug, serde::Serialize, serde::Deserialize)] +#[serde(default, rename_all = "camelCase")] pub struct RenderOpts { /// Output scale relative to comp pixels (1 = Full, 0.5 = Half…). pub scale: f64, @@ -598,22 +605,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]); } } @@ -1604,6 +1597,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/track/src/roto/mod.rs b/crates/track/src/roto/mod.rs index 393f743e..7103624b 100644 --- a/crates/track/src/roto/mod.rs +++ b/crates/track/src/roto/mod.rs @@ -230,6 +230,61 @@ impl FrameSeg { } (n > 0.0).then(|| [sx / n, sy / n]) } + + /// A compact binary form (to ship a segmentation between engine instances, e.g. from a Web + /// Worker to the page): `ECSG`, width, height (u32 LE), the colour models as JSON (length + /// prefixed), then the matte and the Refine Edge band run-length encoded (u16 LE run, byte). + pub fn to_bytes(&self) -> Vec { + let models = serde_json::to_vec(&(&self.fg, &self.bg)).unwrap_or_default(); + let mut out = Vec::with_capacity(16 + models.len() + 64); + out.extend_from_slice(b"ECSG"); + out.extend_from_slice(&(self.w as u32).to_le_bytes()); + out.extend_from_slice(&(self.h as u32).to_le_bytes()); + out.extend_from_slice(&(models.len() as u32).to_le_bytes()); + out.extend_from_slice(&models); + for plane in [&self.matte, &self.refine] { + let mut i = 0; + while i < plane.len() { + let v = plane[i]; + let mut n = 1; + while i + n < plane.len() && plane[i + n] == v && n < u16::MAX as usize { + n += 1; + } + out.extend_from_slice(&(n as u16).to_le_bytes()); + out.push(v); + i += n; + } + } + out + } + + /// Inverse of [`FrameSeg::to_bytes`] (`None` for malformed input). + pub fn from_bytes(b: &[u8]) -> Option { + if b.len() < 16 || &b[..4] != b"ECSG" { + return None; + } + let u = |i: usize| u32::from_le_bytes([b[i], b[i + 1], b[i + 2], b[i + 3]]) as usize; + let (w, h, ml) = (u(4), u(8), u(12)); + let n = w.checked_mul(h)?; + let models = b.get(16..16 + ml)?; + let (fg, bg): (Gmm, Gmm) = serde_json::from_slice(models).ok()?; + let mut rest = &b[16 + ml..]; + let mut plane = || -> Option> { + let mut v = Vec::with_capacity(n); + while v.len() < n { + if rest.len() < 3 { + return None; + } + let run = u16::from_le_bytes([rest[0], rest[1]]) as usize; + v.resize(v.len() + run, rest[2]); + rest = &rest[3..]; + } + (v.len() == n).then_some(v) + }; + let matte = plane()?; + let refine = plane()?; + Some(FrameSeg { w, h, matte, refine, fg, bg }) + } } /// Straight RGB of an image (transparent pixels are black). diff --git a/crates/track/src/roto/tests.rs b/crates/track/src/roto/tests.rs index bec3f378..15bd1f68 100644 --- a/crates/track/src/roto/tests.rs +++ b/crates/track/src/roto/tests.rs @@ -89,6 +89,12 @@ fn strokes_segment_the_base_frame() { // Deterministic. let again = segment(&img, &refs, None, &SegOpts::default(), 0.0); assert_eq!(seg, again); + // The binary form round-trips exactly and is compact (run-length encoded planes). + let bytes = seg.to_bytes(); + assert_eq!(FrameSeg::from_bytes(&bytes).as_ref(), Some(&seg)); + assert!(bytes.len() < seg.w * seg.h / 4, "{} bytes", bytes.len()); + assert!(FrameSeg::from_bytes(&bytes[..bytes.len() - 1]).is_none()); + assert!(FrameSeg::from_bytes(b"nope").is_none()); } #[test] 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/control.rs b/crates/ui-egui/src/control.rs index ff3db561..3e6bdd10 100644 --- a/crates/ui-egui/src/control.rs +++ b/crates/ui-egui/src/control.rs @@ -55,6 +55,87 @@ pub enum Outcome { path: Option, crop: Option<[f32; 4]>, }, + /// Reply once background jobs end (a `wait: true` command whose job runs in a worker). + AwaitJobs(JobWaiter), +} + +/// A `wait: true` engine command whose job was sent to the session's offload (the browser's +/// Web Workers) instead of blocking the UI thread: the reply goes out when the job ends. +pub struct JobWaiter { + pub command: String, + pub params: Value, + /// The command's own result (with `wait: false`). + pub result: Value, + /// Ids of the jobs it started (`jobs.list` ids: `render`, `track`, `roto`…). + pub jobs: Vec, + /// Length of [`effectcraft_engine::Session::offload_log`] when it started. + pub log_len: usize, +} + +/// Commands that block with `wait` (and its default). +fn waits(command: &str, params: &Value) -> bool { + let default = command == "renderQueue.render"; + params.get("wait").and_then(Value::as_bool).unwrap_or(default) +} + +/// Run an engine command. A waiting command (`wait: true`) whose job the session offloads +/// runs with `wait: false` instead, and the reply waits for the job ([`Outcome::AwaitJobs`]), +/// so the browser's UI keeps drawing while it runs. +fn execute_engine(app: &mut EffectcraftApp, id: &str, params: Value) -> Result { + if !app.session.offloads() || !waits(id, ¶ms) { + return app.session.execute_checked(id, params).map(ok).map_err(|e| e.to_string()); + } + let before: Vec = app.session.jobs().into_iter().map(|j| j.id).collect(); + let log_len = app.session.offload_log.len(); + let mut p = params.clone(); + p["wait"] = json!(false); + let result = app.session.execute_checked(id, p).map_err(|e| e.to_string())?; + let jobs: Vec = app.session.jobs().into_iter().map(|j| j.id).filter(|j| !before.contains(j)).collect(); + if jobs.is_empty() { + return Ok(ok(result)); + } + Ok(Outcome::AwaitJobs(JobWaiter { command: id.to_string(), params, result, jobs, log_len })) +} + +impl JobWaiter { + /// Whether its jobs are still running. + pub fn pending(&self, app: &EffectcraftApp) -> bool { + app.session.jobs().iter().any(|j| self.jobs.contains(&j.id)) + } + + /// The reply: what the blocking command would have returned (`running: false`, the final + /// progress, render items and analysis status), or the job's error. + pub fn finish(self, app: &mut EffectcraftApp) -> Value { + let s = &mut app.session; + s.poll_render(); + let errors: Vec = s.offload_log.iter().skip(self.log_len).filter_map(|(_, e)| e.clone()).collect(); + if let Some(e) = errors.first() { + return json!({"ok": false, "error": e}); + } + let mut r = self.result; + if r.get("running").is_some() { + r["running"] = json!(false); + } + if self.command == "renderQueue.render" { + let ids: Vec = r["items"].as_array().map(|a| a.iter().map(|i| i["id"].clone()).collect()).unwrap_or_default(); + if let Ok(list) = s.execute("renderQueue.list", json!({})) { + r["items"] = + json!(list["items"].as_array().map(|a| a.iter().filter(|i| ids.contains(&i["id"])).cloned().collect::>()).unwrap_or_default()); + } + r["rendering"] = json!(s.is_rendering()); + } + let prefix = self.command.split('.').next().unwrap_or(""); + if matches!(prefix, "warp" | "camera" | "roto") + && r.get("layer").is_some() + && let Ok(st) = s.execute(&format!("{prefix}.status"), json!({"layer": r["layer"], "effect": r["effect"]})) + { + r["status"] = st; + } + if r.get("progress").is_some() { + r["progress"] = Value::Null; + } + json!({"ok": true, "result": r}) + } } fn ok(v: Value) -> Outcome { @@ -94,12 +175,12 @@ pub fn handle(app: &mut EffectcraftApp, ctx: &egui::Context, req: &ControlReques // `comp.new {}` creates a default comp); UI-only ids (`tool.*`, `view.*`, `window.*`, // `playback.*` …) and `ui.menu.invoke` go through the menu dispatcher like a click. let r = if req.method == "engine.execute" && effectcraft_engine::find_command(id).is_some() { - app.session.execute_checked(id, params).map_err(|e| e.to_string()) + execute_engine(app, id, params) } else { - crate::menus::invoke(app, ctx, id, params) + crate::menus::invoke(app, ctx, id, params).map(ok) }; match r { - Ok(v) => ok(v), + Ok(o) => o, Err(e) => { app.ui.status = e.clone(); err(e) diff --git a/crates/ui-egui/src/dock.rs b/crates/ui-egui/src/dock.rs index 1fb2ef79..590843d7 100644 --- a/crates/ui-egui/src/dock.rs +++ b/crates/ui-egui/src/dock.rs @@ -722,6 +722,18 @@ pub enum DockAction { PanelMenu(PanelKind, egui::Pos2), /// A tab started being dragged (re-dock or undock). BeginDrag(PanelKind), + /// The tab's lock was toggled (Composition / Timeline viewer lock). + ToggleLock(PanelKind), +} + +/// After Effects-style extras on a tab that shows a composition (Composition, Timeline): a close +/// button (×), the item's label colour as a small square and the viewer lock. +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct TabDeco { + pub swatch: egui::Color32, + pub locked: bool, + /// Close button and lock (Composition, Timeline); Effect Controls shows just the swatch. + pub viewer: bool, } /// Lay out the tree into group rects (with `gap` gutters) and handle gutter dragging. @@ -807,6 +819,7 @@ pub fn draw_group_chrome( t: &Tokens, reg: &mut crate::automation::Registry, title: &dyn Fn(PanelKind) -> String, + decos: &[(PanelKind, TabDeco)], ) -> Vec { let mut actions = Vec::new(); let painter = ui.painter().clone(); @@ -833,7 +846,14 @@ pub fn draw_group_chrome( let label = title(*p); let galley = painter.layout_no_wrap(label.clone(), Tokens::ui(12.0), if is_active { t.tab_text_active } else { t.tab_text }); let menu_w = if is_active { 20.0 } else { 0.0 }; - let w = galley.size().x + 16.0 + menu_w; + let deco = decos.iter().find(|(k, _)| k == p).map(|(_, d)| *d); + // × (active tab only), swatch and lock before the label. + let deco_w = match deco { + Some(d) if d.viewer => (if is_active { 16.0 } else { 0.0 }) + 14.0 + 16.0, + Some(_) => 14.0, + None => 0.0, + }; + let w = galley.size().x + 16.0 + menu_w + deco_w; if x + w > strip.max.x - 20.0 && i > g.active { let r = Rect::from_min_size(pos2(strip.max.x - 22.0, strip.min.y + 6.0), vec2(18.0, 20.0)); let resp = ui.interact(r, egui::Id::new(("tab-overflow", g.path.clone())), Sense::click()); @@ -851,7 +871,44 @@ pub fn draw_group_chrome( actions.push(DockAction::BeginDrag(*p)); } reg.add(&format!("panel.tab.{}", p.id()), tab, &label); - let label_x = tab.min.x + 8.0; + let mut label_x = tab.min.x + 8.0; + if let Some(d) = deco { + let mut dx = tab.min.x + 6.0; + if is_active && d.viewer { + let cr = Rect::from_center_size(pos2(dx + 5.0, text_y), vec2(10.0, 10.0)); + let cresp = ui.interact(cr.expand(2.0), egui::Id::new(("tab-close", g.path.clone(), i)), Sense::click()); + let cc = if cresp.hovered() { t.tab_text_active } else { t.tab_text }; + let k = 3.5; + painter.line_segment([cr.center() + vec2(-k, -k), cr.center() + vec2(k, k)], Stroke::new(1.2, cc)); + painter.line_segment([cr.center() + vec2(-k, k), cr.center() + vec2(k, -k)], Stroke::new(1.2, cc)); + reg.add(&format!("panel.tab.{}.close", p.id()), cr, "Close"); + if cresp.clicked() { + actions.push(DockAction::Close(*p)); + } + dx += 16.0; + } + let sw = Rect::from_center_size(pos2(dx + 5.0, text_y), vec2(9.0, 9.0)); + painter.rect_filled(sw, 1.0, d.swatch); + dx += 14.0; + label_x = dx; + if d.viewer { + let lr = Rect::from_center_size(pos2(dx + 6.0, text_y), vec2(12.0, 12.0)); + let lresp = ui.interact(lr.expand(2.0), egui::Id::new(("tab-lock", g.path.clone(), i)), Sense::click()); + let lc = if d.locked { + t.tab_text_active + } else if lresp.hovered() { + t.tab_text + } else { + t.text_faint + }; + icons::paint(&painter, lr, Icon::Lock, lc); + reg.add(&format!("panel.tab.{}.lock", p.id()), lr, if d.locked { "Unlock" } else { "Lock" }); + if lresp.clicked() { + actions.push(DockAction::ToggleLock(*p)); + } + label_x = dx + 16.0; + } + } let label_w = galley.size().x; let col = if is_active || resp.hovered() { t.tab_text_active } else { t.tab_text }; painter.galley_with_override_text_color(pos2(label_x, text_y - galley.size().y / 2.0), galley, col); diff --git a/crates/ui-egui/src/dock_ui.rs b/crates/ui-egui/src/dock_ui.rs index 61fa5ad6..1f42df2e 100644 --- a/crates/ui-egui/src/dock_ui.rs +++ b/crates/ui-egui/src/dock_ui.rs @@ -35,6 +35,26 @@ impl EffectcraftApp { out } + /// The Composition and Timeline tabs' close button, label-colour swatch and viewer lock. + fn tab_decos(&self) -> Vec<(PanelKind, dock::TabDeco)> { + let Some(cid) = self.session.active_comp_id() else { return vec![] }; + let swatch = match self.session.project.item(cid).map(|i| i.label) { + Some(l) if l != effectcraft_engine::color::Label::None => self.tokens.label(l), + _ => self.tokens.text_faint, + }; + let mut v: Vec<(PanelKind, dock::TabDeco)> = [PanelKind::Composition, PanelKind::Timeline] + .into_iter() + .map(|p| (p, dock::TabDeco { swatch, locked: self.ui.locked_tabs.contains(&p.id()), viewer: true })) + .collect(); + // Effect Controls carries the selected layer's label colour. + if let Some(l) = self.session.active_comp().and_then(|c| self.session.state.selected_layers.first().and_then(|id| c.layer(*id))) + && l.label != effectcraft_engine::color::Label::None + { + v.push((PanelKind::EffectControls, dock::TabDeco { swatch: self.tokens.label(l.label), locked: false, viewer: false })); + } + v + } + /// Settings ▸ Appearance ▸ Use Label Color for Related Tabs: the Composition and Timeline /// tabs carry their comp's label colour, Effect Controls and Properties the layer's. fn label_tab_marks(&self, ui: &egui::Ui) { @@ -44,12 +64,9 @@ impl EffectcraftApp { let Some(cid) = self.session.active_comp_id() else { return }; let comp_label = self.session.project.item(cid).map(|i| i.label); let layer_label = self.session.active_comp().and_then(|c| self.session.state.selected_layers.first().and_then(|l| c.layer(*l))).map(|l| l.label); - for (p, label) in [ - (PanelKind::Composition, comp_label), - (PanelKind::Timeline, comp_label), - (PanelKind::EffectControls, layer_label), - (PanelKind::Properties, layer_label), - ] { + // (the Composition, Timeline and Effect Controls tabs carry their swatch already) + let _ = comp_label; + for (p, label) in [(PanelKind::Properties, layer_label)] { let (Some(label), Some(e)) = (label, self.auto.find(&format!("panel.tab.{}", p.id()))) else { continue }; if label == effectcraft_engine::color::Label::None { continue; @@ -131,9 +148,10 @@ impl EffectcraftApp { dock::layout(ui, &mut dock, body, &t, "", &mut groups, &mut self.auto); let mut actions = Vec::new(); let titles = self.tab_titles(); + let decos = self.tab_decos(); let title = |p: PanelKind| titles.iter().find(|(k, _)| *k == p).map(|(_, s)| s.clone()).unwrap_or_else(|| p.title().to_string()); for g in &groups { - actions.extend(dock::draw_group_chrome(ui, g, self.ui.focused, &t, &mut self.auto, &title)); + actions.extend(dock::draw_group_chrome(ui, g, self.ui.focused, &t, &mut self.auto, &title, &decos)); } self.label_tab_marks(ui); if maximized.is_none() { @@ -174,6 +192,11 @@ impl EffectcraftApp { DockAction::BeginDrag(p) => ctx.data_mut(|d| { d.insert_temp(drag_id(), p); }), + DockAction::ToggleLock(p) => { + if !self.ui.locked_tabs.remove(&p.id()) { + self.ui.locked_tabs.insert(p.id().to_string()); + } + } } } self.tab_drag(&ctx, &groups, &float_groups); @@ -213,7 +236,8 @@ impl EffectcraftApp { active: f.active.min(f.panels.len().saturating_sub(1)), stacked: None, }; - actions.extend(dock::draw_group_chrome(ui, &g, self.ui.focused, &t, &mut self.auto, title)); + let decos = self.tab_decos(); + actions.extend(dock::draw_group_chrome(ui, &g, self.ui.focused, &t, &mut self.auto, title, &decos)); ui.painter().rect_stroke(r, t.radius, Stroke::new(1.0, t.field_border), StrokeKind::Outside); if let Some(p) = g.panels.get(g.active).copied() { self.panel_body(ui, p, g.content); diff --git a/crates/ui-egui/src/frames.rs b/crates/ui-egui/src/frames.rs index b5a0c2f5..f2662763 100644 --- a/crates/ui-egui/src/frames.rs +++ b/crates/ui-egui/src/frames.rs @@ -4,8 +4,10 @@ //! never waits for the compositor. Results land in a memory-budgeted cache keyed by (project //! revision, comp, frame, scale); the timeline draws the cached range as the green cache bar. //! -//! wasm32 has no threads: jobs wait in the same priority queue and [`Frames::pump`] renders them -//! on the UI thread between egui frames, within a time budget. +//! wasm32 has no threads: jobs wait in the same priority queue and [`Frames::pump`] hands them +//! to a [`RemoteFrames`] (the browser's frame worker, a second engine instance fed with project +//! diffs, `effectcraft_engine::remote`) or, without one, renders them on the UI thread between +//! egui frames, within a time budget. //! //! With the disk cache on (Settings ▸ Media & Disk Cache), every CPU-rendered frame is also //! written to disk under a content key (the comp's content hash, frame, scale, view and render @@ -163,6 +165,38 @@ fn frame_disk_key(keys: &ContentKeys, project: &Project, key: &FrameKey, opts_ha disk_cache::frame_key(content, key.frame, key.scale, key.view ^ opts_hash) } +/// A frame for a [`RemoteFrames`] renderer. +pub struct RemoteJob { + pub project: Arc, + pub revision: u64, + pub comp: ItemId, + pub t: Tick, + pub opts: RenderOpts, +} + +/// Premultiplied RGBA8 pixels from a [`RemoteFrames`] renderer. +pub struct RemoteFrame { + pub width: u32, + pub height: u32, + pub rgba: Vec, + /// Render time (ms). + pub ms: f64, +} + +/// Called once with a remote frame (or why there is none). +pub type RemoteDone = Box) + Send>; + +/// Renders viewer frames elsewhere (the browser's frame worker), so the UI thread never does. +pub trait RemoteFrames: Send + Sync { + /// Frames it can render at the same time (0 when it is not usable: frames then render on + /// the UI thread again). + fn slots(&self) -> usize; + /// Start rendering `job`; `done` runs when the frame arrives. + fn start(&self, job: RemoteJob, done: RemoteDone); + /// Drop cached intermediate results (Edit ▸ Purge). + fn purge(&self) {} +} + pub struct Frames { cache: Arc>, /// Keys queued or rendering. @@ -175,6 +209,9 @@ pub struct Frames { /// Render time of the last viewer (urgent) frame in ms, for the Info panel / perf readout. pub last_ms: Arc>, content_keys: ContentKeys, + /// Where frames render instead of the UI thread (wasm32), and how many it is rendering. + remote: Option>, + remote_busy: Arc>, } impl Default for Frames { @@ -190,6 +227,8 @@ impl Default for Frames { ctx: None, last_ms: Arc::new(Mutex::new(0.0)), content_keys: Arc::default(), + remote: None, + remote_busy: Arc::default(), } } } @@ -270,6 +309,9 @@ impl Frames { } pub fn clear(&self) { + if let Some(r) = &self.remote { + r.purge(); + } if let Ok(mut c) = self.cache.lock() { c.map.clear(); c.order.clear(); @@ -324,7 +366,7 @@ impl Frames { q.jobs.push(Job { key, src: src.clone(), comp, t, opts, urgent, seq }); drop(q); #[cfg(not(target_arch = "wasm32"))] - { + if self.remote.is_none() { let w = self.worker(); // One spawn per job; each spawn renders whichever queued job matters most right now. self.pool.spawn(move || { @@ -349,13 +391,102 @@ impl Frames { } } - /// Render queued frames on this thread, most important first, until the queue is empty or - /// `budget` has passed (at least one frame). Returns whether frames are still queued. Only - /// wasm32 needs this (native frames render on the pool); elsewhere it does nothing. + /// Render frames with `remote` from now on (`None`: on this thread again). + pub fn set_remote(&mut self, remote: Option>) { + self.remote = remote; + } + + /// Whether frames go to a usable [`RemoteFrames`]. + pub fn remote_active(&self) -> bool { + self.remote.as_ref().is_some_and(|r| r.slots() > 0) + } + + /// Frames the remote renderer renders at once (0 without one). + pub fn remote_slots(&self) -> usize { + self.remote.as_ref().map(|r| r.slots()).unwrap_or(0) + } + + /// Frames being rendered remotely. + pub fn remote_busy(&self) -> usize { + self.remote_busy.lock().map(|b| *b).unwrap_or(0) + } + + /// Hand queued frames to the remote renderer while it has free slots, most important first + /// (the viewer's frame, then prefetch in request order; a viewer frame requested while every + /// slot is busy goes to the first one that frees up). Frames that the GPU compositor can keep + /// on the GPU render here instead (no readback). Returns how many were started. + pub fn dispatch_remote(&self) -> usize { + let Some(remote) = self.remote.clone() else { return 0 }; + let slots = remote.slots(); + let w = self.worker(); + let mut started = 0; + loop { + let busy = self.remote_busy(); + if busy >= slots { + break; + } + let job = { + let Ok(mut q) = self.queue.lock() else { break }; + let Some(i) = Queue::best(&q.jobs) else { break }; + let job = q.jobs.swap_remove(i); + q.running.insert(job.key); + if job.urgent { + q.urgent_running += 1; + } + job + }; + if job.src.gpu.is_some() && job.src.gpu_display { + let t0 = web_time::Instant::now(); + if let Some(img) = w.render_gpu(&job) { + w.finish(&job, img, t0); + continue; + } + } + if let Ok(mut b) = self.remote_busy.lock() { + *b += 1; + } + started += 1; + let (key, urgent) = (job.key, job.urgent); + let fw = self.worker(); + let busy = self.remote_busy.clone(); + let t0 = web_time::Instant::now(); + let rj = RemoteJob { project: job.src.project.clone(), revision: key.revision, comp: job.comp, t: job.t, opts: job.opts }; + remote.start( + rj, + Box::new(move |r: Result| { + if let Ok(mut b) = busy.lock() { + *b = b.saturating_sub(1); + } + match r { + Ok(f) => { + let img = egui::ColorImage::from_rgba_premultiplied([f.width as usize, f.height as usize], &f.rgba); + fw.finish_key(key, urgent, FrameImage::Cpu(Arc::new(img)), t0); + } + Err(e) => { + // Not rendered (a stale revision, a lost worker): the viewer asks again. + log::debug!("remote frame: {e}"); + fw.release(key, urgent); + } + } + }), + ); + } + started + } + + /// Render queued frames, most important first. With a usable [`RemoteFrames`] they are only + /// handed over ([`Frames::dispatch_remote`]); otherwise they render on this thread until the + /// queue is empty or `budget` has passed (at least one frame). Returns whether frames are + /// still queued or rendering remotely. Only wasm32 needs this (native frames render on the + /// pool); elsewhere it does nothing. pub fn pump(&self, budget: std::time::Duration) -> bool { if cfg!(not(target_arch = "wasm32")) { return false; } + if self.remote_active() { + self.dispatch_remote(); + return self.queue.lock().map(|q| !q.jobs.is_empty()).unwrap_or(false) || self.remote_busy() > 0; + } let t0 = web_time::Instant::now(); let w = self.worker(); while w.run_next() { @@ -380,12 +511,10 @@ struct Worker { impl Worker { /// Render the queued job that matters most right now. `false` when the queue was empty. fn run_next(&self) -> bool { - let Worker { queue, cache, inflight, ctx, last, content_keys } = self; + let Worker { queue, content_keys, .. } = self; let job = { let Ok(mut q) = queue.lock() else { return false }; - // Urgent first (newest), then prefetch in request order. - let best = q.jobs.iter().enumerate().max_by_key(|(_, j)| (j.urgent, if j.urgent { j.seq as i64 } else { -(j.seq as i64) })).map(|(i, _)| i); - let Some(i) = best else { return false }; + let Some(i) = Queue::best(&q.jobs) else { return false }; let job = q.jobs.swap_remove(i); q.running.insert(job.key); if job.urgent { @@ -407,27 +536,50 @@ impl Worker { ci } }; - if job.urgent - && let Ok(mut l) = last.lock() - { + self.finish(&job, ci, t0); + true + } + + /// Store a finished frame and release its job. + fn finish(&self, job: &Job, img: FrameImage, t0: web_time::Instant) { + self.finish_key(job.key, job.urgent, img, t0); + } + + fn finish_key(&self, key: FrameKey, urgent: bool, img: FrameImage, t0: web_time::Instant) { + if urgent && let Ok(mut l) = self.last.lock() { *l = t0.elapsed().as_secs_f64() * 1000.0; } - if let Ok(mut c) = cache.lock() { - c.insert(job.key, ci); + if let Ok(mut c) = self.cache.lock() { + c.insert(key, img); } - if let Ok(mut inf) = inflight.lock() { - inf.remove(&job.key); + self.release(key, urgent); + } + + /// The job of `key` is no longer queued or running (rendered or dropped). + fn release(&self, key: FrameKey, urgent: bool) { + if let Ok(mut inf) = self.inflight.lock() { + inf.remove(&key); } - if let Ok(mut q) = queue.lock() { - q.running.remove(&job.key); - if job.urgent { - q.urgent_running -= 1; + if let Ok(mut q) = self.queue.lock() { + q.running.remove(&key); + if urgent { + q.urgent_running = q.urgent_running.saturating_sub(1); } } - if let Some(ctx) = ctx { + if let Some(ctx) = &self.ctx { ctx.request_repaint(); } - true + } + + /// The frame on the GPU, kept there for the viewer, if the compositor can do it alone. + fn render_gpu(&self, job: &Job) -> Option { + let g = job.src.gpu.as_ref()?; + let mut r = Renderer::new(&job.src.project, job.src.footage.as_ref(), job.opts); + r.expr = job.src.expr.as_deref(); + r.cache = Some(&job.src.layer_cache); + r.accel = Some(g as &dyn effectcraft_engine::render::Accelerator); + r.active_accel()?; + g.render_display(&r, job.comp, job.t).map(|f| FrameImage::Gpu(Arc::new(f))) } fn render_job(&self, job: &Job) -> FrameImage { @@ -487,3 +639,10 @@ struct Queue { urgent_running: usize, seq: u64, } + +impl Queue { + /// The job that matters most: urgent first (newest), then prefetch in request order. + fn best(jobs: &[Job]) -> Option { + jobs.iter().enumerate().max_by_key(|(_, j)| (j.urgent, if j.urgent { j.seq as i64 } else { -(j.seq as i64) })).map(|(i, _)| i) + } +} diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index 5f1d5980..1380553a 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -143,6 +143,8 @@ pub struct EffectcraftApp { pub(crate) deferred: Vec<(ControlRequest, f64)>, pub(crate) synthetic: Vec, pub(crate) input_waiters: Vec>, + /// Control requests waiting for an offloaded job (`wait: true` in the browser). + pub(crate) job_waiters: Vec<(control::JobWaiter, Sender)>, queued_screenshots: Vec<(u64, f64, u32)>, pending_screenshots: Vec<(u64, Option, Option<[f32; 4]>, Sender, f64)>, next_token: u64, @@ -210,6 +212,7 @@ impl EffectcraftApp { deferred: vec![], synthetic: vec![], input_waiters: vec![], + job_waiters: vec![], queued_screenshots: vec![], pending_screenshots: vec![], next_token: 1, @@ -847,8 +850,9 @@ impl EffectcraftApp { fn frames_parallelism(&self) -> usize { if cfg!(target_arch = "wasm32") { - // frames render one at a time on the UI thread (`Frames::pump`) - return 1; + // frame workers render one frame each; without them frames render one at a time + // on the UI thread (`Frames::pump`) + return self.frames.remote_slots().max(1); } std::thread::available_parallelism().map(|n| n.get()).unwrap_or(4) } @@ -881,6 +885,10 @@ impl EffectcraftApp { } } control::Outcome::AfterInput => self.input_waiters.push(reply), + control::Outcome::AwaitJobs(w) => { + self.job_waiters.push((w, reply)); + ctx.request_repaint(); + } control::Outcome::Screenshot { path, crop } => { // A covered or minimized window presents no frames (macOS skips its redraws), so // a screenshot would never arrive and nothing would tick its timeout. @@ -897,6 +905,28 @@ impl EffectcraftApp { } } self.control_rx = Some(rx); + self.finish_job_waiters(ctx); + } + + /// Reply to the control requests whose offloaded jobs ended. + fn finish_job_waiters(&mut self, ctx: &egui::Context) { + if self.job_waiters.is_empty() { + return; + } + self.session.poll_render(); + self.session.poll_offload(); + let waiters = std::mem::take(&mut self.job_waiters); + for (w, reply) in waiters { + if w.pending(self) { + self.job_waiters.push((w, reply)); + } else { + let v = w.finish(self); + let _ = reply.send(v); + } + } + if !self.job_waiters.is_empty() { + ctx.request_repaint_after(std::time::Duration::from_millis(50)); + } } fn issue_screenshots(&mut self, ctx: &egui::Context) { @@ -961,7 +991,9 @@ impl EffectcraftApp { for ev in self.session.drain_events() { match ev { effectcraft_engine::Event::OpenComp(_) => { - self.ui.dock.activate(PanelKind::Composition); + if !self.ui.locked_tabs.contains(&PanelKind::Composition.id()) { + self.ui.dock.activate(PanelKind::Composition); + } self.ui.timeline.pps = None; } effectcraft_engine::Event::Toast { message, .. } => self.toast = Some((message, ctx.input(|i| i.time))), diff --git a/crates/ui-egui/src/panels/effect_controls.rs b/crates/ui-egui/src/panels/effect_controls.rs index 02dcb1a9..b5160410 100644 --- a/crates/ui-egui/src/panels/effect_controls.rs +++ b/crates/ui-egui/src/panels/effect_controls.rs @@ -12,7 +12,8 @@ //! - layer parameters have a layer popup and a Source / Masks / Effects & Masks popup; mask //! ("Path") parameters list the layer's masks; //! - Curves has its graph editor, Levels its histogram with input/output triangles, Auto -//! Levels a histogram. +//! Levels a histogram; Lumetri's curves, Colorama's output cycle, Glow's colour map and +//! Reshape's correspondence points have visual editors ([`super::fx_editors`]). use effectcraft_engine::geom::Mat3; use effectcraft_engine::keyframe::Value; @@ -40,7 +41,7 @@ fn selected_layer(app: &EffectcraftApp) -> Option { /// An undo-merge key for one gesture on `id`: a new key each time a drag starts, so separate /// drags are separate undo steps while one drag is one step. -fn gesture_key(ui: &egui::Ui, id: egui::Id, started: bool) -> String { +pub(super) fn gesture_key(ui: &egui::Ui, id: egui::Id, started: bool) -> String { let kid = id.with("gesture"); let n: u64 = if started { let n = ui.data(|d| d.get_temp::(kid)).unwrap_or(0) + 1; @@ -536,6 +537,15 @@ fn group_rows( } if open { let sub = fx.sub(&sg.match_id); + // A visual editor for the group's hidden parameters (Lumetri curves, …). + let eh = super::fx_editors::inline_height(fx.effect, &sub.path, rect.width()); + if eh > 0.0 { + let er = Rect::from_min_size(pos2(rect.min.x, *y), vec2(rect.width(), eh)); + *y += eh; + if er.max.y >= rect.min.y && er.min.y <= rect.max.y { + super::fx_editors::inline_editor(app, ui, p, layer, fx.root, sg, fx.effect, &sub.path, ectx, er, actions); + } + } group_rows(app, ui, p, layer, sg, ectx, rect, y, depth + 1, &sub, actions); } } @@ -603,8 +613,9 @@ fn roto_editor(app: &mut EffectcraftApp, ui: &mut egui::Ui, p: &egui::Painter, l // Curves / Levels editors /// Height of the custom editor shown under an effect's header (0 when none). -fn editor_height(effect: &str, width: f32) -> f32 { +fn editor_height(effect: &str, g: &PropGroup, width: f32) -> f32 { match effect { + super::fx_editors::GLOW | super::fx_editors::RESHAPE => super::fx_editors::header_height(effect, g, width), "ec.color.curves" => 34.0 + curves_size(width) + 26.0, effectcraft_engine::effects::warp_stab::ID => 50.0, effectcraft_engine::effects::camera_tracker::ID => super::camera_tracker_ui::EDITOR_HEIGHT, @@ -995,7 +1006,7 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) { item(ui, "Move Down", fi + 1 < fx.len(), "effect.reorder", json!({"layer": lid, "effect": g.uid, "index": fi + 2})); }); if open { - let eh = editor_height(effect, body.width()); + let eh = editor_height(effect, g, body.width()); if eh > 0.0 { let er = Rect::from_min_size(pos2(body.min.x, y), vec2(body.width(), eh)); y += eh; @@ -1006,6 +1017,9 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) { effectcraft_engine::effects::camera_tracker::ID => super::camera_tracker_ui::editor(app, ui, &bp, &layer, g, er, &mut actions), effectcraft_engine::effects::roto::ID => roto_editor(app, ui, &bp, &layer, g, er, &mut actions), "ec.color.levels" | "ec.color.levelsic" => levels_editor(app, ui, &bp, &layer, g, effect, &ectx, er, &mut actions), + super::fx_editors::GLOW | super::fx_editors::RESHAPE => { + super::fx_editors::header_editor(app, ui, &bp, &layer, g, effect, &ectx, er, &mut actions) + } _ => histogram_only(app, ui, &bp, g, er), } } @@ -1130,10 +1144,14 @@ pub fn viewer_hook( } else if app.ui.viewer.show_layer_controls { // ⊕ controls for the point parameters of the selected effects. let sel = app.session.state.selected_props.clone(); - for layer in ectx.comp.layers.iter().filter(|l| app.session.state.selected_layers.contains(&l.id) && l.is_active_at(ectx.time)) { + let selected = app.session.state.selected_layers.clone(); + for layer in ectx.comp.layers.iter().filter(|l| selected.contains(&l.id) && l.is_active_at(ectx.time)) { let Some(fx) = layer.effects() else { continue }; let m = l2c(ectx, layer); for g in fx.groups().filter(|g| g.enabled && sel.iter().any(|(l, u)| *l == layer.id && *u == g.uid)) { + if matches!(&g.kind, GroupKind::Effect { effect } if effect == super::fx_editors::RESHAPE) { + super::fx_editors::reshape_overlay(app, ui, painter, map, ectx, layer, g, &m, &mut actions); + } let pts: Vec<&Property> = g.props().filter(|p| matches!(p.ui, ParamUi::Point)).collect(); let screen: Vec = pts .iter() diff --git a/crates/ui-egui/src/panels/essential.rs b/crates/ui-egui/src/panels/essential.rs index b22263b9..0ccc46e6 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/fx_editors.rs b/crates/ui-egui/src/panels/fx_editors.rs new file mode 100644 index 00000000..e47d0a8d --- /dev/null +++ b/crates/ui-egui/src/panels/fx_editors.rs @@ -0,0 +1,932 @@ +//! Visual editors for effect parameters stored as hidden or text parameters (Effect Controls): +//! +//! - Lumetri Color ▸ Curves ▸ RGB Curves: master / red / green / blue curve graphs (channel +//! swatches like After Effects'), and ▸ Hue Saturation Curves: Hue vs Sat, Hue vs Hue, Hue vs +//! Luma, Luma vs Sat and Sat vs Sat graphs over their colour gradients (y = ½ is neutral); +//! - Colorama ▸ Output Cycle: the palette wheel, with draggable colour stops (click the ring to +//! add one, Alt-click or drag one off to remove it, select one to pick its colour); editing a +//! preset palette turns it into Custom; +//! - Glow: the colour map (A & B Colors: a preview strip of the gradient over glow brightness; +//! Arbitrary Map: red / green / blue curve graphs and the resulting strip); +//! - Reshape: correspondence point list buttons here, and on the Composition viewer the source +//! and destination mask outlines with draggable correspondence handles ([`reshape_overlay`]). +//! +//! Every edit is a `prop.set` engine command (undoable; one drag = one undo step) and every +//! handle registers an automation id. + +use effectcraft_engine::effects::{ColoramaPalette, OffsetCurve}; +use effectcraft_engine::geom::Mat3; +use effectcraft_engine::keyframe::Value; +use effectcraft_engine::project::{Layer, PropGroup}; +use effectcraft_engine::render::EvalCtx; +use egui::{Align2, Color32, Pos2, Rect, Sense, Stroke, StrokeKind, pos2, vec2}; +use serde_json::json; + +use super::effect_controls::gesture_key; +use super::fx_widgets as fw; +use crate::theme::Tokens; +use crate::{EffectcraftApp, widgets}; + +type Actions = Vec<(String, serde_json::Value)>; + +pub const LUMETRI: &str = "ec.color.lumetri"; +pub const COLORAMA: &str = "ec.color.colorama"; +pub const GLOW: &str = "ec.stylize.glow"; +pub const RESHAPE: &str = "ec.distort.reshape"; + +fn str_value(layer: &Layer, ectx: &EvalCtx, g: &PropGroup, id: &str) -> String { + match g.get(id).map(|p| ectx.value(layer, p)) { + Some(Value::Str(s)) => s, + _ => String::new(), + } +} + +fn set_str(actions: &mut Actions, layer: &Layer, g: &PropGroup, id: &str, v: String, merge: Option) { + let Some(p) = g.get(id) else { return }; + let mut a = json!({"layer": layer.id.0, "prop": p.uid, "value": v}); + if let Some(m) = merge { + a["merge"] = json!(m); + } + actions.push(("prop.set".into(), a)); +} + +/// Register automation ids for a curve's control points (`.point.`). +fn register_points(app: &mut EffectcraftApp, prefix: &str, gr: Rect, pts: &[[f32; 2]]) { + for (i, q) in pts.iter().enumerate() { + let s = pos2(gr.min.x + q[0] * gr.width(), gr.max.y - q[1] * gr.height()); + app.auto.add(&format!("{prefix}.point.{i}"), Rect::from_center_size(s, vec2(9.0, 9.0)), &format!("Point {}", i + 1)); + } +} + +/// Small coloured tabs (`names[i]`, swatch `cols[i]`); returns the clicked one. +fn tabs( + app: &mut EffectcraftApp, + ui: &mut egui::Ui, + p: &egui::Painter, + x0: f32, + cy: f32, + cur: usize, + names: &[&str], + cols: &[Color32], + id: &str, +) -> Option { + let t = app.tokens; + let mut x = x0; + let mut out = None; + for (i, name) in names.iter().enumerate() { + let w = if cols.is_empty() { 16.0 + name.chars().count() as f32 * 6.2 } else { 20.0 }; + let r = Rect::from_min_size(pos2(x, cy - 9.0), vec2(w, 18.0)); + let resp = ui.interact(r, egui::Id::new((id, i)), Sense::click()); + let sel = i == cur; + if sel { + p.rect_filled(r, 3.0, t.row_selected); + } else if resp.hovered() { + p.rect_filled(r, 3.0, t.hover); + } + match cols.get(i) { + Some(c) => { + p.circle_filled(r.center(), 5.5, *c); + if sel { + p.circle_stroke(r.center(), 7.0, Stroke::new(1.0, t.text)); + } + } + None => { + p.text(r.center(), Align2::CENTER_CENTER, *name, Tokens::ui(11.0), if sel { t.text } else { t.text_dim }); + } + } + app.auto.add(&format!("{id}.{i}"), r, name); + if resp.clicked() { + out = Some(i); + } + resp.on_hover_text(*name); + x += w + 4.0; + } + out +} + +fn reset_link(app: &mut EffectcraftApp, ui: &mut egui::Ui, p: &egui::Painter, r: Rect, id: &str) -> bool { + let t = app.tokens; + let resp = ui.interact(r, egui::Id::new(id), Sense::click()); + p.text(r.center(), Align2::CENTER_CENTER, "Reset", Tokens::ui(11.5), if resp.hovered() { t.hot_text } else { t.text_dim }); + app.auto.add(id, r, "Reset"); + resp.clicked() +} + +fn graph_size(width: f32) -> f32 { + (width - 72.0).clamp(120.0, 256.0) +} + +// --------------------------------------------------------------------------------------------- +// Inline editors (inside a parameter group) + +/// Height of the editor shown at the top of group `path` (spec-id path with a trailing `/`) +/// of `effect` (0 when none). +pub fn inline_height(effect: &str, path: &str, width: f32) -> f32 { + match (effect, path) { + (LUMETRI, "curves/rgbCurves/") => 30.0 + graph_size(width) + 22.0, + (LUMETRI, "curves/hueSaturationCurves/") => 30.0 + graph_size(width) * 0.6 + 34.0, + (COLORAMA, "outputCycle/") => 30.0 + wheel_size(width) + 34.0, + _ => 0.0, + } +} + +/// Draw the editor of group `g` (at `path`) in `r`. `root`: the effect's group. +#[allow(clippy::too_many_arguments)] +pub fn inline_editor( + app: &mut EffectcraftApp, + ui: &mut egui::Ui, + p: &egui::Painter, + layer: &Layer, + root: &PropGroup, + g: &PropGroup, + effect: &str, + path: &str, + ectx: &EvalCtx, + r: Rect, + actions: &mut Actions, +) { + match (effect, path) { + (LUMETRI, "curves/rgbCurves/") => rgb_curves(app, ui, p, layer, root, g, ectx, r, actions), + (LUMETRI, "curves/hueSaturationCurves/") => huesat_curves(app, ui, p, layer, root, g, ectx, r, actions), + (COLORAMA, "outputCycle/") => colorama_wheel(app, ui, p, layer, root, g, ectx, r, actions), + _ => {} + } +} + +const RGB_CURVES: [(&str, &str); 4] = [("master", "Master"), ("red", "Red"), ("green", "Green"), ("blue", "Blue")]; + +#[allow(clippy::too_many_arguments)] +fn rgb_curves( + app: &mut EffectcraftApp, + ui: &mut egui::Ui, + p: &egui::Painter, + layer: &Layer, + root: &PropGroup, + g: &PropGroup, + ectx: &EvalCtx, + r: Rect, + actions: &mut Actions, +) { + let t = app.tokens; + let euid = root.uid; + let prefix = format!("effectControls.effect.{euid}.lumetri.rgbCurves"); + let ch = app.ui.fx_curve_channel.get(&g.uid).copied().unwrap_or(0).min(3); + let x0 = r.min.x + 38.0; + let cy = r.min.y + 15.0; + let cols = [Color32::from_gray(0xe8), Color32::from_rgb(0xf0, 0x50, 0x50), Color32::from_rgb(0x50, 0xd0, 0x60), Color32::from_rgb(0x50, 0x80, 0xff)]; + let names: Vec<&str> = RGB_CURVES.iter().map(|c| c.1).collect(); + if let Some(n) = tabs(app, ui, p, x0, cy, ch, &names, &cols, &format!("{prefix}.channel")) { + app.ui.fx_curve_channel.insert(g.uid, n); + } + let id = RGB_CURVES[ch].0; + let cur = fw::CurvePoints::parse(&str_value(layer, ectx, g, id)); + let size = graph_size(r.width()); + if reset_link(app, ui, p, Rect::from_min_size(pos2(x0 + size - 44.0, cy - 9.0), vec2(44.0, 18.0)), &format!("{prefix}.reset")) { + set_str(actions, layer, g, id, fw::CurvePoints::identity().format(), None); + } + let gr = Rect::from_min_size(pos2(x0, r.min.y + 30.0), vec2(size, size)); + let gid = egui::Id::new(("lumetri-rgb", euid, ch)); + let (resp, edit) = fw::curves_graph(ui, gr, &cur, ch, false, gid, &t); + app.auto.add(&format!("{prefix}.graph"), gr, "RGB Curves"); + register_points(app, &prefix, gr, &cur.0); + let key = gesture_key(ui, gid, resp.drag_started() || resp.clicked()); + if let Some(np) = edit.changed { + set_str(actions, layer, g, id, np.format(), Some(key)); + } + p.text(pos2(gr.min.x, gr.max.y + 11.0), Align2::LEFT_CENTER, "Click adds a point; drag one off to remove", Tokens::ui(10.5), t.text_faint); +} + +/// A hue / saturation curve's points (`x,y` in 0..1, y = ½ neutral), any count (empty = flat). +#[derive(Clone, Debug, Default, PartialEq)] +pub struct OffsetPoints(pub Vec<[f32; 2]>); + +impl OffsetPoints { + pub fn parse(s: &str) -> OffsetPoints { + let mut v: Vec<[f32; 2]> = s + .split(|c: char| c.is_whitespace() || c == ';') + .filter_map(|tk| { + let (x, y) = tk.split_once(',')?; + let (x, y) = (x.trim().parse::().ok()?, y.trim().parse::().ok()?); + (x.is_finite() && y.is_finite()).then_some([x.clamp(0.0, 1.0), y.clamp(0.0, 1.0)]) + }) + .collect(); + v.sort_by(|a, b| a[0].total_cmp(&b[0])); + OffsetPoints(v) + } + pub fn format(&self) -> String { + let f = |v: f32| { + let s = format!("{v:.4}"); + let s = s.trim_end_matches('0').trim_end_matches('.').to_string(); + if s.is_empty() || s == "-0" { "0".into() } else { s } + }; + self.0.iter().map(|q| format!("{},{}", f(q[0]), f(q[1]))).collect::>().join(" ") + } + pub fn insert(&mut self, x: f32, y: f32) -> usize { + let i = self.0.partition_point(|q| q[0] < x); + self.0.insert(i, [x.clamp(0.0, 1.0), y.clamp(0.0, 1.0)]); + i + } + /// Move point `i`, keeping the order. + pub fn move_point(&mut self, i: usize, x: f32, y: f32) { + let n = self.0.len(); + if i >= n { + return; + } + let lo = if i == 0 { 0.0 } else { self.0[i - 1][0] + 0.002 }; + let hi = if i + 1 == n { 1.0 } else { self.0[i + 1][0] - 0.002 }; + self.0[i] = [x.clamp(lo, hi.max(lo)), y.clamp(0.0, 1.0)]; + } + pub fn hit(&self, x: f32, y: f32, r: [f32; 2]) -> Option { + self.0 + .iter() + .enumerate() + .map(|(i, q)| (i, ((q[0] - x) / r[0]).hypot((q[1] - y) / r[1]))) + .filter(|(_, d)| *d <= 1.0) + .min_by(|a, b| a.1.total_cmp(&b.1)) + .map(|(i, _)| i) + } +} + +/// Lumetri's hue / saturation curves: parameter id, tab name, x axis (0 hue, 1 luma, 2 sat), +/// whether x wraps around. +const HUESAT: [(&str, &str, u8, bool); 5] = [ + ("hueVsSat", "Hue vs Sat", 0, true), + ("hueVsHue", "Hue vs Hue", 0, true), + ("hueVsLuma", "Hue vs Luma", 0, true), + ("lumaVsSat", "Luma vs Sat", 1, false), + ("satVsSat", "Sat vs Sat", 2, false), +]; + +fn hue_color(h: f32) -> Color32 { + let c = egui::ecolor::Hsva::new(h.rem_euclid(1.0), 0.85, 0.9, 1.0); + Color32::from(c) +} + +#[allow(clippy::too_many_arguments)] +fn huesat_curves( + app: &mut EffectcraftApp, + ui: &mut egui::Ui, + p: &egui::Painter, + layer: &Layer, + root: &PropGroup, + g: &PropGroup, + ectx: &EvalCtx, + r: Rect, + actions: &mut Actions, +) { + let t = app.tokens; + let euid = root.uid; + let prefix = format!("effectControls.effect.{euid}.lumetri.hueSatCurves"); + let k = app.ui.fx_curve_channel.get(&g.uid).copied().unwrap_or(0).min(4); + let x0 = r.min.x + 38.0; + let cy = r.min.y + 15.0; + let names: Vec<&str> = HUESAT.iter().map(|c| c.1).collect(); + if let Some(n) = tabs(app, ui, p, x0, cy, k, &names, &[], &format!("{prefix}.curve")) { + app.ui.fx_curve_channel.insert(g.uid, n); + } + let (id, name, axis, periodic) = HUESAT[k]; + let cur = OffsetPoints::parse(&str_value(layer, ectx, g, id)); + let size = graph_size(r.width()); + let gr = Rect::from_min_size(pos2(x0, r.min.y + 30.0), vec2(size, size * 0.6)); + // Background: the x axis' colours (hue spectrum, grey ramp) as a band, the neutral line. + p.rect_filled(gr, 0.0, Color32::from_rgb(0x1c, 0x1c, 0x1c)); + let n = 48; + for i in 0..n { + let f = (i as f32 + 0.5) / n as f32; + let c = match axis { + 0 => hue_color(f), + 1 => Color32::from_gray((f * 255.0) as u8), + _ => Color32::from(egui::ecolor::Hsva::new(0.0, f, 0.85, 1.0)), + }; + let x = gr.min.x + gr.width() * i as f32 / n as f32; + let band = Rect::from_min_max(pos2(x, gr.max.y - 8.0), pos2(x + gr.width() / n as f32 + 0.5, gr.max.y)); + p.rect_filled(band, 0.0, c); + p.rect_filled(Rect::from_min_max(pos2(band.min.x, gr.min.y), pos2(band.max.x, gr.max.y - 8.0)), 0.0, c.gamma_multiply(0.12)); + } + let to_s = |x: f32, y: f32| pos2(gr.min.x + x * gr.width(), gr.max.y - y * gr.height()); + p.line_segment([to_s(0.0, 0.5), to_s(1.0, 0.5)], Stroke::new(1.0, Color32::from_white_alpha(60))); + p.rect_stroke(gr, 0.0, Stroke::new(1.0, t.field_border), StrokeKind::Outside); + let curve = OffsetCurve::parse(&cur.format(), periodic); + let line: Vec = (0..=96) + .map(|i| { + let x = i as f32 / 96.0; + to_s(x, 0.5 + curve.as_ref().map(|c| c.at(x)).unwrap_or(0.0)) + }) + .collect(); + p.add(egui::Shape::line(line, Stroke::new(1.5, Color32::from_gray(0xe8)))); + let gid = egui::Id::new(("lumetri-hs", euid, k)); + let resp = ui.interact(gr.expand(6.0), gid, Sense::click_and_drag()); + app.auto.add(&format!("{prefix}.graph"), gr, name); + let drag_id = gid.with("drag"); + let dragging: Option = ui.data(|d| d.get_temp(drag_id)); + for (i, q) in cur.0.iter().enumerate() { + let s = to_s(q[0], q[1]); + let hr = Rect::from_center_size(s, vec2(7.0, 7.0)); + p.circle_filled(s, 3.5, if dragging == Some(i) { Color32::WHITE } else { Color32::from_rgb(0x1c, 0x1c, 0x1c) }); + p.circle_stroke(s, 3.5, Stroke::new(1.2, Color32::WHITE)); + app.auto.add(&format!("{prefix}.point.{i}"), hr, &format!("Point {}", i + 1)); + } + let to_g = |s: Pos2| [((s.x - gr.min.x) / gr.width()).clamp(0.0, 1.0), ((gr.max.y - s.y) / gr.height()).clamp(0.0, 1.0)]; + let hit_r = [7.0 / gr.width(), 7.0 / gr.height()]; + let mut changed: Option = None; + let alt = ui.input(|i| i.modifiers.alt); + // One undo step per click or drag. + let key = gesture_key(ui, gid, resp.drag_started() || resp.clicked()); + if resp.drag_started() + && let Some(pt) = ui.input(|i| i.pointer.press_origin()) + { + let q = to_g(pt); + let i = match cur.hit(q[0], q[1], hit_r) { + Some(i) => i, + None => { + let mut np = cur.clone(); + let i = np.insert(q[0], q[1]); + changed = Some(np); + i + } + }; + ui.data_mut(|d| d.insert_temp(drag_id, i)); + } else if resp.dragged() + && let (Some(i), Some(pt)) = (dragging, resp.interact_pointer_pos()) + { + let q = to_g(pt); + let mut np = cur.clone(); + np.move_point(i, q[0], q[1]); + if np != cur { + changed = Some(np); + } + } + if resp.drag_stopped() { + if let (Some(i), Some(pt)) = (dragging, resp.interact_pointer_pos()) + && !gr.expand(14.0).contains(pt) + && i < cur.0.len() + { + let mut np = cur.clone(); + np.0.remove(i); + changed = Some(np); + } + ui.data_mut(|d| d.remove::(drag_id)); + } + if resp.clicked() + && let Some(pt) = resp.interact_pointer_pos() + { + let q = to_g(pt); + let mut np = cur.clone(); + match cur.hit(q[0], q[1], hit_r) { + Some(i) if alt => { + np.0.remove(i); + } + Some(_) => {} + None => { + np.insert(q[0], q[1]); + } + } + if np != cur { + changed = Some(np); + } + } + if let Some(np) = changed { + set_str(actions, layer, g, id, np.format(), Some(key)); + } + if reset_link(app, ui, p, Rect::from_min_size(pos2(gr.max.x - 44.0, gr.max.y + 4.0), vec2(44.0, 18.0)), &format!("{prefix}.reset")) { + set_str(actions, layer, g, id, String::new(), None); + } + p.text(pos2(gr.min.x, gr.max.y + 13.0), Align2::LEFT_CENTER, "Click adds a point (½ height = no change)", Tokens::ui(10.5), t.text_faint); +} + +// --------------------------------------------------------------------------------------------- +// Colorama output cycle + +fn wheel_size(width: f32) -> f32 { + (width - 90.0).clamp(110.0, 200.0) +} + +/// Colorama's palette as stops: (position, straight RGBA). +pub fn format_palette(stops: &[(f32, [f32; 4])]) -> String { + let f = |v: f32| { + let s = format!("{v:.4}"); + let s = s.trim_end_matches('0').trim_end_matches('.').to_string(); + if s.is_empty() || s == "-0" { "0".into() } else { s } + }; + stops + .iter() + .map(|(pos, c)| { + let rgb = format!("{},{},{}", f(c[0]), f(c[1]), f(c[2])); + if (c[3] - 1.0).abs() > 1e-4 { format!("{}:{rgb},{}", f(*pos), f(c[3])) } else { format!("{}:{rgb}", f(*pos)) } + }) + .collect::>() + .join(" ") +} + +fn rgba8(c: [f32; 4]) -> Color32 { + let b = |v: f32| (v.clamp(0.0, 1.0) * 255.0 + 0.5) as u8; + Color32::from_rgb(b(c[0]), b(c[1]), b(c[2])) +} + +/// Phase (0..1, clockwise from the top) of a point around `c`. +fn phase_at(c: Pos2, q: Pos2) -> f32 { + let a = (q.x - c.x).atan2(-(q.y - c.y)); + (a / std::f32::consts::TAU).rem_euclid(1.0) +} + +fn wheel_point(c: Pos2, radius: f32, phase: f32) -> Pos2 { + let a = phase * std::f32::consts::TAU; + pos2(c.x + radius * a.sin(), c.y - radius * a.cos()) +} + +#[allow(clippy::too_many_arguments)] +fn colorama_wheel( + app: &mut EffectcraftApp, + ui: &mut egui::Ui, + p: &egui::Painter, + layer: &Layer, + root: &PropGroup, + g: &PropGroup, + ectx: &EvalCtx, + r: Rect, + actions: &mut Actions, +) { + let t = app.tokens; + let euid = root.uid; + let prefix = format!("effectControls.effect.{euid}.colorama"); + let preset = g.get("usePresetPalette").map(|pr| ectx.value(layer, pr).as_enum()).unwrap_or(0); + let custom = preset as usize == effectcraft_engine::effects::COLORAMA_PRESETS.len() - 1; + let interp = g.get("interpolatePalette").is_none_or(|pr| ectx.value(layer, pr).as_bool()); + let pal = if custom { ColoramaPalette::parse(&str_value(layer, ectx, g, "palette")) } else { None }.unwrap_or_else(|| ColoramaPalette::preset(preset)); + let x0 = r.min.x + 38.0; + let label = if custom { "Custom palette" } else { effectcraft_engine::effects::COLORAMA_PRESETS.get(preset as usize).copied().unwrap_or("Preset") }; + p.text(pos2(x0, r.min.y + 15.0), Align2::LEFT_CENTER, format!("Output Cycle: {label}"), Tokens::ui(11.5), t.text_dim); + let size = wheel_size(r.width()); + let c = pos2(x0 + size / 2.0, r.min.y + 30.0 + size / 2.0); + let (outer, inner) = (size / 2.0 - 8.0, size / 2.0 - 26.0); + // The ring: palette colours around the wheel. + let n = 96; + for i in 0..n { + let (a0, a1) = (i as f32 / n as f32, (i + 1) as f32 / n as f32); + let col = rgba8(pal.eval((a0 + a1) * 0.5, interp)); + let quad = vec![wheel_point(c, inner, a0), wheel_point(c, outer, a0), wheel_point(c, outer, a1 + 0.002), wheel_point(c, inner, a1 + 0.002)]; + p.add(egui::Shape::convex_polygon(quad, col, Stroke::NONE)); + } + p.circle_stroke(c, outer, Stroke::new(1.0, Color32::from_black_alpha(160))); + p.circle_stroke(c, inner, Stroke::new(1.0, Color32::from_black_alpha(160))); + let wid = egui::Id::new(("colorama-wheel", euid)); + let ring = Rect::from_center_size(c, vec2(outer * 2.0 + 40.0, outer * 2.0 + 40.0)); + let resp = ui.interact(ring, wid, Sense::click_and_drag()); + app.auto.add(&format!("{prefix}.wheel"), ring, "Output Cycle"); + let sel_id = wid.with("sel"); + let drag_id = wid.with("drag"); + let mut sel: usize = ui.data(|d| d.get_temp(sel_id)).unwrap_or(0); + if sel >= pal.stops.len() { + sel = 0; + } + let dragging: Option = ui.data(|d| d.get_temp(drag_id)); + // Stops: triangles on the outside of the ring. + let mut stop_rects = vec![]; + for (i, (ph, col)) in pal.stops.iter().enumerate() { + let tip = wheel_point(c, outer + 2.0, *ph); + let a = wheel_point(c, outer + 12.0, *ph - 0.018); + let b = wheel_point(c, outer + 12.0, *ph + 0.018); + let fill = rgba8(*col); + p.add(egui::Shape::convex_polygon( + vec![tip, a, b], + fill, + Stroke::new(if i == sel || dragging == Some(i) { 1.6 } else { 1.0 }, if i == sel { Color32::WHITE } else { Color32::from_gray(0x80) }), + )); + let hr = Rect::from_center_size(wheel_point(c, outer + 8.0, *ph), vec2(14.0, 14.0)); + app.auto.add(&format!("{prefix}.stop.{i}"), hr, &format!("Stop {}", i + 1)); + stop_rects.push(hr); + } + let hit = |q: Pos2| -> Option { + stop_rects + .iter() + .enumerate() + .filter(|(_, hr)| hr.expand(3.0).contains(q)) + .min_by(|a, b| a.1.center().distance(q).total_cmp(&b.1.center().distance(q))) + .map(|(i, _)| i) + }; + let mut new_stops: Option> = None; + let alt = ui.input(|i| i.modifiers.alt); + // One undo step per click or drag (a colour pick is its own step). + let key = gesture_key(ui, wid, resp.drag_started() || resp.clicked() || !resp.dragged()); + if resp.drag_started() + && let Some(q) = ui.input(|i| i.pointer.press_origin()) + && let Some(i) = hit(q) + { + ui.data_mut(|d| { + d.insert_temp(drag_id, i); + d.insert_temp(sel_id, i); + }); + } else if resp.dragged() + && let (Some(i), Some(q)) = (dragging, resp.interact_pointer_pos()) + && i < pal.stops.len() + { + let mut st = pal.stops.clone(); + st[i].0 = phase_at(c, q); + new_stops = Some(st); + } + if resp.drag_stopped() { + if let (Some(i), Some(q)) = (dragging, resp.interact_pointer_pos()) + && q.distance(c) > outer + 40.0 + && pal.stops.len() > 1 + && i < pal.stops.len() + { + let mut st = pal.stops.clone(); + st.remove(i); + new_stops = Some(st); + } + ui.data_mut(|d| d.remove::(drag_id)); + } + if resp.clicked() + && let Some(q) = resp.interact_pointer_pos() + { + match hit(q) { + Some(i) if alt && pal.stops.len() > 1 => { + let mut st = pal.stops.clone(); + st.remove(i); + new_stops = Some(st); + } + Some(i) => { + ui.data_mut(|d| d.insert_temp(sel_id, i)); + } + // A click on the ring adds a stop with the colour there (and selects it). + None if (inner - 4.0..=outer + 4.0).contains(&q.distance(c)) => { + let ph = phase_at(c, q); + let mut st = pal.stops.clone(); + st.push((ph, pal.eval(ph, interp))); + st.sort_by(|a, b| a.0.total_cmp(&b.0)); + let k = st.iter().position(|s| s.0 == ph).unwrap_or(0); + ui.data_mut(|d| d.insert_temp(sel_id, k)); + new_stops = Some(st); + } + None => {} + } + } + // The selected stop's colour. + let sr = Rect::from_min_size(pos2(x0, c.y + size / 2.0 + 8.0), vec2(30.0, 16.0)); + if let Some((ph, col)) = pal.stops.get(sel).copied() { + let pop = wid.with("color"); + if widgets::swatch(ui, sr, col, wid.with("swatch"), &t).clicked() { + widgets::open_popup(ui, pop); + } + app.auto.add(&format!("{prefix}.color"), sr, "Stop color"); + p.text(pos2(sr.max.x + 8.0, sr.center().y), Align2::LEFT_CENTER, format!("Stop {} at {:.0}°", sel + 1, ph * 360.0), Tokens::ui(11.0), t.text_dim); + let mut rgb = [col[0], col[1], col[2]]; + if crate::header::color_popup(ui, pop, sr.left_bottom(), &mut rgb) { + let mut st = pal.stops.clone(); + st[sel].1 = [rgb[0], rgb[1], rgb[2], col[3]]; + new_stops = Some(st); + } + } + p.text( + pos2(sr.min.x, sr.max.y + 9.0), + Align2::LEFT_CENTER, + "Click the ring to add a colour; Alt-click a stop (or drag it off) to remove it", + Tokens::ui(10.5), + t.text_faint, + ); + if let Some(mut st) = new_stops { + st.sort_by(|a, b| a.0.total_cmp(&b.0)); + // Editing a preset makes it the custom palette (both in one undo step). + if !custom && let Some(pr) = g.get("usePresetPalette") { + actions.push(( + "prop.set".into(), + json!({"layer": layer.id.0, "prop": pr.uid, "value": effectcraft_engine::effects::COLORAMA_PRESETS.len() - 1, "merge": key}), + )); + } + set_str(actions, layer, g, "palette", format_palette(&st), Some(key)); + } +} + +// --------------------------------------------------------------------------------------------- +// Header editors (under the effect's title) + +/// Height of the editor under `effect`'s header (`g`: the instance), 0 when none. +pub fn header_height(effect: &str, g: &PropGroup, width: f32) -> f32 { + match effect { + GLOW => match g.get("colors").map(|p| p.value.as_enum()) { + Some(2) => 30.0 + graph_size(width) + 40.0, + Some(1) => 34.0, + _ => 0.0, + }, + RESHAPE => 50.0, + _ => 0.0, + } +} + +#[allow(clippy::too_many_arguments)] +pub fn header_editor( + app: &mut EffectcraftApp, + ui: &mut egui::Ui, + p: &egui::Painter, + layer: &Layer, + g: &PropGroup, + effect: &str, + ectx: &EvalCtx, + r: Rect, + actions: &mut Actions, +) { + match effect { + GLOW => glow_map(app, ui, p, layer, g, ectx, r, actions), + RESHAPE => reshape_buttons(app, ui, p, layer, g, ectx, r, actions), + _ => {} + } +} + +/// Glow's Arbitrary Map: "red | green | blue" Curves point lists. +pub fn split_map(s: &str) -> [fw::CurvePoints; 3] { + let mut parts = s.split('|'); + std::array::from_fn(|_| parts.next().map(fw::CurvePoints::parse).unwrap_or_else(fw::CurvePoints::identity)) +} + +pub fn join_map(m: &[fw::CurvePoints; 3]) -> String { + m.iter().map(|c| c.format()).collect::>().join(" | ") +} + +#[allow(clippy::too_many_arguments)] +fn glow_map(app: &mut EffectcraftApp, ui: &mut egui::Ui, p: &egui::Painter, layer: &Layer, g: &PropGroup, ectx: &EvalCtx, r: Rect, actions: &mut Actions) { + let t = app.tokens; + let euid = g.uid; + let prefix = format!("effectControls.effect.{euid}.glow"); + let mode = g.get("colors").map(|pr| ectx.value(layer, pr).as_enum()).unwrap_or(0); + let x0 = r.min.x + 38.0; + let num = |id: &str| g.get(id).map(|pr| ectx.value(layer, pr).as_f64()).unwrap_or(0.0); + let looping = g.get("colorLooping").map(|pr| ectx.value(layer, pr).as_enum()).unwrap_or(2); + let (loops, phase, mid) = (num("colorLoops") as f32, (num("colorPhase") / 360.0) as f32, (num("abMidpoint") / 100.0) as f32); + let map = split_map(&str_value(layer, ectx, g, "arbitraryMap")); + let color_at = |l: f32| -> Color32 { + let tt = effectcraft_engine::effects::glow_ab_t(l, looping, loops, phase, mid); + if mode == 2 { + Color32::from_rgb( + (map[0].eval(tt).clamp(0.0, 1.0) * 255.0) as u8, + (map[1].eval(tt).clamp(0.0, 1.0) * 255.0) as u8, + (map[2].eval(tt).clamp(0.0, 1.0) * 255.0) as u8, + ) + } else { + let col = |id: &str| match g.get(id).map(|pr| ectx.value(layer, pr)) { + Some(Value::Color(c)) => c, + _ => [0.0; 4], + }; + let (a, b) = (col("colorA"), col("colorB")); + let m = |i: usize| ((a[i] + (b[i] - a[i]) * tt as f64).clamp(0.0, 1.0) * 255.0) as u8; + Color32::from_rgb(m(0), m(1), m(2)) + } + }; + let size = graph_size(r.width()); + let strip_y = if mode == 2 { r.min.y + 30.0 + size + 12.0 } else { r.min.y + 8.0 }; + let strip = Rect::from_min_size(pos2(x0, strip_y), vec2(size, 12.0)); + let n = 64; + for i in 0..n { + let f = (i as f32 + 0.5) / n as f32; + let x = strip.min.x + strip.width() * i as f32 / n as f32; + p.rect_filled(Rect::from_min_max(pos2(x, strip.min.y), pos2(x + strip.width() / n as f32 + 0.5, strip.max.y)), 0.0, color_at(f)); + } + p.rect_stroke(strip, 0.0, Stroke::new(1.0, t.field_border), StrokeKind::Outside); + app.auto.add(&format!("{prefix}.colorMap"), strip, "Glow colour map"); + p.text(pos2(strip.max.x + 8.0, strip.center().y), Align2::LEFT_CENTER, "Colour map", Tokens::ui(10.5), t.text_faint); + if mode != 2 { + return; + } + let ch = app.ui.fx_curve_channel.get(&euid).copied().unwrap_or(0).min(2); + let cols = [Color32::from_rgb(0xf0, 0x50, 0x50), Color32::from_rgb(0x50, 0xd0, 0x60), Color32::from_rgb(0x50, 0x80, 0xff)]; + if let Some(n) = tabs(app, ui, p, x0, r.min.y + 15.0, ch, &["Red", "Green", "Blue"], &cols, &format!("{prefix}.channel")) { + app.ui.fx_curve_channel.insert(euid, n); + } + if reset_link(app, ui, p, Rect::from_min_size(pos2(x0 + size - 44.0, r.min.y + 6.0), vec2(44.0, 18.0)), &format!("{prefix}.reset")) { + let mut m = map.clone(); + m[ch] = fw::CurvePoints::identity(); + set_str(actions, layer, g, "arbitraryMap", join_map(&m), None); + } + let gr = Rect::from_min_size(pos2(x0, r.min.y + 30.0), vec2(size, size)); + let gid = egui::Id::new(("glow-map", euid, ch)); + let (resp, edit) = fw::curves_graph(ui, gr, &map[ch], ch + 1, false, gid, &t); + app.auto.add(&format!("{prefix}.graph"), gr, "Arbitrary Map"); + register_points(app, &prefix, gr, &map[ch].0); + let key = gesture_key(ui, gid, resp.drag_started() || resp.clicked()); + if let Some(np) = edit.changed { + let mut m = map.clone(); + m[ch] = np; + set_str(actions, layer, g, "arbitraryMap", join_map(&m), Some(key)); + } +} + +// --------------------------------------------------------------------------------------------- +// Reshape + +/// Reshape's correspondence pairs (source, destination outline fractions), in stored order. +pub fn parse_pairs(s: &str) -> Vec<(f64, f64)> { + s.split_whitespace() + .filter_map(|tk| { + let (a, b) = tk.split_once(',')?; + Some((a.trim().parse::().ok()?.rem_euclid(1.0), b.trim().parse::().ok()?.rem_euclid(1.0))) + }) + .collect() +} + +pub fn format_pairs(v: &[(f64, f64)]) -> String { + let f = |x: f64| { + let s = format!("{x:.4}"); + let s = s.trim_end_matches('0').trim_end_matches('.').to_string(); + if s.is_empty() { "0".into() } else { s } + }; + v.iter().map(|(a, b)| format!("{},{}", f(*a), f(*b))).collect::>().join(" ") +} + +/// A new pair in the widest gap between the destination fractions (the source fraction +/// interpolated between its neighbours'), or (0, 0) for the first. +pub fn add_pair(v: &[(f64, f64)]) -> (f64, f64) { + if v.is_empty() { + return (0.0, 0.0); + } + let mut s: Vec<(f64, f64)> = v.to_vec(); + s.sort_by(|a, b| a.1.total_cmp(&b.1)); + let n = s.len(); + let (mut best, mut gap) = (0, -1.0); + for i in 0..n { + let g = (s[(i + 1) % n].1 - s[i].1).rem_euclid(1.0); + let g = if n == 1 { 1.0 } else { g }; + if g > gap { + gap = g; + best = i; + } + } + let (a, b) = (s[best], s[(best + 1) % n]); + let ds = if n == 1 { 1.0 } else { (b.0 - a.0).rem_euclid(1.0) }; + ((a.0 + ds * 0.5).rem_euclid(1.0), (a.1 + gap * 0.5).rem_euclid(1.0)) +} + +#[allow(clippy::too_many_arguments)] +fn reshape_buttons( + app: &mut EffectcraftApp, + ui: &mut egui::Ui, + p: &egui::Painter, + layer: &Layer, + g: &PropGroup, + ectx: &EvalCtx, + r: Rect, + actions: &mut Actions, +) { + let t = app.tokens; + let euid = g.uid; + let pairs = parse_pairs(&str_value(layer, ectx, g, "correspondencePoints")); + let b1 = Rect::from_min_size(pos2(r.min.x + 24.0, r.min.y + 4.0), vec2(84.0, 22.0)); + let b2 = Rect::from_min_size(pos2(b1.max.x + 8.0, b1.min.y), vec2(96.0, 22.0)); + let b3 = Rect::from_min_size(pos2(b2.max.x + 8.0, b1.min.y), vec2(56.0, 22.0)); + if widgets::text_button(ui, b1, "Add Point", false, &t, egui::Id::new(("reshape-add", euid))).clicked() { + let mut v = pairs.clone(); + v.push(add_pair(&pairs)); + set_str(actions, layer, g, "correspondencePoints", format_pairs(&v), None); + } + app.auto.add(&format!("effectControls.effect.{euid}.reshape.add"), b1, "Add Point"); + if widgets::text_button(ui, b2, "Remove Point", false, &t, egui::Id::new(("reshape-remove", euid))).clicked() && !pairs.is_empty() { + let mut v = pairs.clone(); + v.pop(); + set_str(actions, layer, g, "correspondencePoints", format_pairs(&v), None); + } + app.auto.add(&format!("effectControls.effect.{euid}.reshape.remove"), b2, "Remove Point"); + if widgets::text_button(ui, b3, "Clear", false, &t, egui::Id::new(("reshape-clear", euid))).clicked() && !pairs.is_empty() { + set_str(actions, layer, g, "correspondencePoints", String::new(), None); + } + app.auto.add(&format!("effectControls.effect.{euid}.reshape.clear"), b3, "Clear"); + let status = if pairs.is_empty() { + "Correspondence: automatic (select the effect to edit points on the viewer)".to_string() + } else { + format!("{} correspondence point(s): drag them along the mask outlines in the viewer", pairs.len()) + }; + p.text(pos2(r.min.x + 24.0, r.min.y + 38.0), Align2::LEFT_CENTER, status, Tokens::ui(11.5), t.text_dim); +} + +/// Arc-length fraction (0..1) of the point of a polyline nearest to `q`. +pub fn nearest_fraction(pts: &[[f64; 2]], closed: bool, q: [f64; 2]) -> f64 { + let n = pts.len(); + if n < 2 { + return 0.0; + } + let total = effectcraft_engine::effects::util::poly_length(pts, closed).max(1e-9); + let segs = if closed { n } else { n - 1 }; + let (mut best, mut best_s, mut acc) = (f64::INFINITY, 0.0, 0.0); + for i in 0..segs { + let (a, b) = (pts[i], pts[(i + 1) % n]); + let (dx, dy) = (b[0] - a[0], b[1] - a[1]); + let l2 = dx * dx + dy * dy; + let l = l2.sqrt(); + let tt = if l2 > 0.0 { (((q[0] - a[0]) * dx + (q[1] - a[1]) * dy) / l2).clamp(0.0, 1.0) } else { 0.0 }; + let d = (a[0] + dx * tt - q[0]).hypot(a[1] + dy * tt - q[1]); + if d < best { + best = d; + best_s = acc + l * tt; + } + acc += l; + } + (best_s / total).rem_euclid(1.0) +} + +/// The Composition viewer's Reshape controls while the effect is selected: the source (yellow) +/// and destination (cyan) mask outlines and each correspondence pair as a line between its two +/// handles; dragging a handle slides it along its outline (one undo step per drag). +#[allow(clippy::too_many_arguments)] +pub fn reshape_overlay( + app: &mut EffectcraftApp, + ui: &mut egui::Ui, + painter: &egui::Painter, + map: &super::viewer::ViewerMap, + ectx: &EvalCtx, + layer: &Layer, + g: &PropGroup, + m: &Mat3, + actions: &mut Actions, +) { + let shapes = effectcraft_engine::render::masks::shapes(ectx, layer); + let mask = |id: &str| -> Option<(Vec<[f64; 2]>, bool)> { + let i = g.get(id).map(|pr| ectx.value(layer, pr).as_enum()).unwrap_or(0) as usize; + let s = shapes.get(i.checked_sub(1)?)?; + (s.points.len() >= 2).then(|| (s.points.clone(), s.closed)) + }; + let (Some(src), Some(dst)) = (mask("sourceMask"), mask("destinationMask")) else { return }; + let to_screen = |q: [f64; 2]| map.to_screen(fw::layer_to_comp(m, q)); + let outline = |pts: &[[f64; 2]], closed: bool, col: Color32| { + let mut v: Vec = pts.iter().map(|q| to_screen(*q)).collect(); + if closed && let Some(f) = v.first().copied() { + v.push(f); + } + painter.add(egui::Shape::dashed_line(&v, Stroke::new(1.0, col), 5.0, 3.0)); + }; + let (ys, cs) = (Color32::from_rgb(0xff, 0xd8, 0x40), Color32::from_rgb(0x40, 0xd8, 0xff)); + outline(&src.0, src.1, ys); + outline(&dst.0, dst.1, cs); + let at = |pts: &[[f64; 2]], closed: bool, f: f64| { + let len = effectcraft_engine::effects::util::poly_length(pts, closed); + effectcraft_engine::effects::util::poly_point_at(pts, closed, f * len).0 + }; + let raw = match g.get("correspondencePoints").map(|pr| ectx.value(layer, pr)) { + Some(Value::Str(s)) => s, + _ => String::new(), + }; + let pairs = parse_pairs(&raw); + let ctx = ui.ctx().clone(); + for (i, (fs, fd)) in pairs.iter().enumerate() { + let (ps, pd) = (to_screen(at(&src.0, src.1, *fs)), to_screen(at(&dst.0, dst.1, *fd))); + painter.line_segment([ps, pd], Stroke::new(1.0, Color32::from_white_alpha(150))); + for (end, s, col) in [(0usize, ps, ys), (1, pd, cs)] { + let hr = Rect::from_center_size(s, vec2(12.0, 12.0)); + let id = egui::Id::new(("reshape-pt", g.uid, i, end)); + let resp = ui.interact(hr, id, Sense::drag()); + let hot = resp.hovered() || resp.dragged(); + painter.rect_filled(Rect::from_center_size(s, vec2(7.0, 7.0)), 0.0, if hot { Color32::WHITE } else { col }); + painter.rect_stroke(Rect::from_center_size(s, vec2(7.0, 7.0)), 0.0, Stroke::new(1.0, Color32::BLACK), StrokeKind::Outside); + let which = if end == 0 { "source" } else { "dest" }; + app.auto.add(&format!("viewer.reshape.{}.{which}.{i}", g.uid), hr, &format!("Correspondence point {} ({which})", i + 1)); + if hot { + ctx.set_cursor_icon(egui::CursorIcon::Move); + } + if resp.dragged() + && let Some(pos) = resp.interact_pointer_pos() + && let Some(lp) = fw::comp_to_layer(m, map.to_comp(pos)) + { + let mut v = pairs.clone(); + if end == 0 { + v[i].0 = nearest_fraction(&src.0, src.1, lp); + } else { + v[i].1 = nearest_fraction(&dst.0, dst.1, lp); + } + let key = gesture_key(ui, id, resp.drag_started()); + if let Some(pr) = g.get("correspondencePoints") { + actions.push(("prop.set".into(), json!({"layer": layer.id.0, "prop": pr.uid, "value": format_pairs(&v), "merge": key}))); + } + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn offset_points_and_palettes_round_trip() { + let p = OffsetPoints::parse("0.5,0.75 0.1,0.5"); + assert_eq!(p.0, vec![[0.1, 0.5], [0.5, 0.75]]); + assert_eq!(OffsetPoints::parse(&p.format()), p); + assert!(OffsetPoints::parse("").0.is_empty()); + let stops = vec![(0.0, [1.0, 0.0, 0.0, 1.0]), (0.5, [0.0, 0.25, 1.0, 0.5])]; + let s = format_palette(&stops); + assert_eq!(s, "0:1,0,0 0.5:0,0.25,1,0.5"); + assert_eq!(ColoramaPalette::parse(&s).unwrap().stops, stops); + let m = split_map("0,0 1,0.5 | | 0,1 1,0"); + assert_eq!(m[1], fw::CurvePoints::identity()); + assert_eq!(split_map(&join_map(&m)), m); + } + + #[test] + fn reshape_pairs_and_outline_fractions() { + assert_eq!(parse_pairs("0.25,0 1.5,0.5"), vec![(0.25, 0.0), (0.5, 0.5)]); + assert_eq!(format_pairs(&[(0.25, 0.0), (0.5, 0.125)]), "0.25,0 0.5,0.125"); + assert_eq!(add_pair(&[]), (0.0, 0.0)); + assert_eq!(add_pair(&[(0.1, 0.0)]), (0.6, 0.5)); + let (s, d) = add_pair(&[(0.0, 0.0), (0.2, 0.25)]); + assert!((d - 0.625).abs() < 1e-9 && (s - 0.6).abs() < 1e-9, "{s} {d}"); + // A 10×10 square (closed): the point nearest (10, 5) is 3/8 of the way round. + let sq = [[0.0, 0.0], [10.0, 0.0], [10.0, 10.0], [0.0, 10.0]]; + assert!((nearest_fraction(&sq, true, [12.0, 5.0]) - 0.375).abs() < 1e-9); + assert!(nearest_fraction(&sq, true, [-1.0, -1.0]).abs() < 1e-9); + } +} diff --git a/crates/ui-egui/src/panels/media_panels.rs b/crates/ui-egui/src/panels/media_panels.rs index 4f6fd0ae..8cc14dcc 100644 --- a/crates/ui-egui/src/panels/media_panels.rs +++ b/crates/ui-egui/src/panels/media_panels.rs @@ -65,13 +65,13 @@ pub fn media_browser(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) { let ctx = ui.ctx().clone(); let p = ui.painter().with_clip_rect(rect); p.rect_filled(rect, 0.0, t.panel_bg); - if !mb::available() { - p.text(rect.center(), Align2::CENTER_CENTER, "The Media Browser needs the desktop app (use File ▸ Import here)", Tokens::ui(12.0), t.text_faint); + let Some(b) = app.session.media_browser() else { + p.text(rect.center(), Align2::CENTER_CENTER, "The Media Browser is not available here (use File ▸ Import)", Tokens::ui(12.0), t.text_faint); app.auto.add("mediaBrowser.unavailable", rect, "Media Browser unavailable"); return; - } + }; let st = app.session.state.media_browser.clone(); - let dir = st.folder.clone().unwrap_or_else(mb::home); + let dir = st.folder.clone().unwrap_or_else(|| b.home()); // Listing cached per folder for a second. let lid = egui::Id::new(("mb-list", dir.clone(), st.importable_only)); let now = ctx.input(|i| i.time); @@ -79,7 +79,7 @@ pub fn media_browser(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) { let entries = match listing { Some((at, l)) if now - at < 1.0 => l, _ => { - let l = mb::list(&dir, st.importable_only); + let l = b.list(&dir, st.importable_only); ctx.data_mut(|d| d.insert_temp(lid, (now, l.clone()))); l } @@ -104,6 +104,20 @@ pub fn media_browser(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) { if only != st.importable_only { kit::exec(app, "mediaBrowser.go", json!({"path": dir, "importableOnly": only})); } + // The browser's own actions (web: Open Folder…, Add Files…), right-aligned. + let mut ax = rect.max.x - 8.0; + for (id, label) in b.actions().iter().rev() { + let w = 14.0 + label.chars().count() as f32 * 6.4; + ax -= w; + if ax < x + 96.0 { + break; + } + if kit::button(app, ui, Rect::from_min_size(pos2(ax, y), vec2(w, 20.0)), &format!("mediaBrowser.action.{id}"), label, false) { + kit::exec(app, "mediaBrowser.action", json!({"action": id})); + ctx.data_mut(|d| d.remove::<(f64, Result, String>)>(lid)); + } + ax -= 6.0; + } // The path on its own row. let _ = x; let py = y + 24.0; @@ -114,7 +128,7 @@ pub fn media_browser(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) { p.line_segment([side.right_top(), side.right_bottom()], Stroke::new(1.0, t.separator)); p.text(pos2(side.min.x + 10.0, side.min.y + 10.0), Align2::LEFT_CENTER, "Favorites", Tokens::semibold(11.5), t.text_dim); let mut fy = side.min.y + 22.0; - let mut places: Vec<(String, String)> = vec![("Home".into(), mb::home())]; + let mut places: Vec<(String, String)> = b.places(); places.extend( st.favorites.iter().map(|f| (std::path::Path::new(f).file_name().map(|n| n.to_string_lossy().to_string()).unwrap_or_else(|| f.clone()), f.clone())), ); diff --git a/crates/ui-egui/src/panels/misc.rs b/crates/ui-egui/src/panels/misc.rs index 7b750be7..6c9bac94 100644 --- a/crates/ui-egui/src/panels/misc.rs +++ b/crates/ui-egui/src/panels/misc.rs @@ -21,7 +21,16 @@ pub fn preview(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) { let t = app.tokens; let ctx = ui.ctx().clone(); let p = ui.painter().with_clip_rect(rect); - let y = rect.min.y + 12.0; + // The panel scrolls when it is shorter than its controls (the default workspace shows just + // the transport row, as in After Effects). + let scroll_id = egui::Id::new("preview-scroll"); + let mut scroll: f32 = ctx.data(|d| d.get_temp(scroll_id).unwrap_or(0.0)); + if ui.rect_contains_pointer(rect) { + scroll = (scroll - ui.input(|i| i.smooth_scroll_delta.y)).max(0.0); + } + let panel_clip = ui.clip_rect(); + ui.set_clip_rect(rect.intersect(panel_clip)); + let y = rect.min.y + 12.0 - scroll; let bw = 30.0; let total = bw * 5.0 + 16.0; let mut x = rect.center().x - total / 2.0; @@ -223,6 +232,18 @@ pub fn preview(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) { let status = if app.playback.playing && app.playback.caching { " — caching before playback" } else { "" }; p.text(pos2(bar.min.x, bar.max.y + 12.0), Align2::LEFT_CENTER, format!("{cached} / {total} frames cached{status}"), Tokens::ui(11.0), t.text_faint); } + ui.set_clip_rect(panel_clip); + let content = yy + 30.0 - (rect.min.y - scroll); + let max = (content - rect.height()).max(0.0); + scroll = scroll.min(max); + ctx.data_mut(|d| d.insert_temp(scroll_id, scroll)); + if max > 0.0 { + // A thin scroll indicator on the right edge. + let h = (rect.height() * rect.height() / content).max(12.0); + let top = rect.min.y + (rect.height() - h) * (scroll / max); + p.rect_filled(Rect::from_min_size(pos2(rect.max.x - 5.0, top), vec2(3.0, h)), 1.5, t.text_faint.gamma_multiply(0.6)); + app.auto.add("preview.scroll", Rect::from_min_size(pos2(rect.max.x - 6.0, rect.min.y), vec2(6.0, rect.height())), "Preview scroll"); + } } fn fmt_rate(r: f64) -> String { diff --git a/crates/ui-egui/src/panels/mod.rs b/crates/ui-egui/src/panels/mod.rs index 8ebd6ad3..d43b4e79 100644 --- a/crates/ui-egui/src/panels/mod.rs +++ b/crates/ui-egui/src/panels/mod.rs @@ -14,6 +14,7 @@ pub mod expr_editor; pub mod flowchart; pub mod footage_panel; pub mod forms; +pub mod fx_editors; pub mod fx_widgets; pub mod graph; pub mod graph_tools; 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/src/state.rs b/crates/ui-egui/src/state.rs index 4b657f52..57434526 100644 --- a/crates/ui-egui/src/state.rs +++ b/crates/ui-egui/src/state.rs @@ -533,6 +533,10 @@ pub struct UiState { /// The panel maximized to fill the dock area (`~`), if any. #[serde(default)] pub maximized: Option, + /// Locked Composition / Timeline tabs (panel ids): opening another composition does not + /// bring a locked panel forward (the tab's lock icon, as After Effects' viewer lock). + #[serde(default)] + pub locked_tabs: BTreeSet, pub focused: PanelKind, pub viewer: ViewerState, pub timeline: TimelineState, @@ -634,6 +638,7 @@ impl Default for UiState { floating: vec![], saved_floating: Default::default(), maximized: None, + locked_tabs: BTreeSet::new(), focused: PanelKind::Composition, viewer: ViewerState::default(), timeline: TimelineState::default(), diff --git a/crates/ui-egui/src/widgets.rs b/crates/ui-egui/src/widgets.rs index 9703d6ba..184bf9ec 100644 --- a/crates/ui-egui/src/widgets.rs +++ b/crates/ui-egui/src/widgets.rs @@ -154,8 +154,27 @@ pub fn dropdown(ui: &mut Ui, rect: Rect, text: &str, t: &Tokens, id: egui::Id) - let resp = ui.interact(rect, id, Sense::click()); ui.painter().rect_filled(rect, 3.0, if resp.hovered() { t.hover } else { t.field_bg }); ui.painter().rect_stroke(rect, 3.0, Stroke::new(1.0, t.field_border), StrokeKind::Inside); - ui.painter().text(pos2(rect.min.x + 7.0, rect.center().y), Align2::LEFT_CENTER, text, Tokens::ui(11.5), t.text); + // Text that doesn't fit before the arrow is elided (narrow panels). + let room = rect.width() - 7.0 - 18.0; + let fits = |s: &str| ui.painter().layout_no_wrap(s.to_string(), Tokens::ui(11.5), t.text).size().x <= room; + let shown = if fits(text) { + text.to_string() + } else { + let chars: Vec = text.chars().collect(); + let mut n = chars.len(); + loop { + n = n.saturating_sub(1); + let s: String = chars[..n].iter().collect::().trim_end().to_string() + "…"; + if n == 0 || fits(&s) { + break s; + } + } + }; + ui.painter().text(pos2(rect.min.x + 7.0, rect.center().y), Align2::LEFT_CENTER, shown, Tokens::ui(11.5), t.text); icons::paint(ui.painter(), Rect::from_center_size(pos2(rect.max.x - 9.0, rect.center().y), vec2(8.0, 8.0)), Icon::ChevronDown, t.text_dim); + if text.chars().count() > 0 && !fits(text) { + return resp.on_hover_text(text.to_string()); + } resp } diff --git a/crates/ui-egui/tests/ui_fx_editors.rs b/crates/ui-egui/tests/ui_fx_editors.rs new file mode 100644 index 00000000..331df879 --- /dev/null +++ b/crates/ui-egui/tests/ui_fx_editors.rs @@ -0,0 +1,269 @@ +//! Headless checks (egui_kittest) of the visual editors for hidden / text effect parameters +//! (M13.10): Lumetri's RGB and hue/saturation curves, Colorama's output cycle wheel, Glow's +//! colour map and Reshape's correspondence points (Effect Controls buttons and viewer handles). +//! Each edit goes through `prop.set` and undoes in one step. + +use effectcraft_engine::Session; +use effectcraft_engine::keyframe::Value as KV; +use effectcraft_engine::project::{LayerId, Node, PropGroup}; +use effectcraft_ui_egui::EffectcraftApp; +use effectcraft_ui_egui::dock::PanelKind; +use egui::{Event, Pos2, pos2}; +use egui_kittest::Harness; +use serde_json::json; + +fn harness(s: Session) -> Harness<'static, EffectcraftApp> { + let mut app = EffectcraftApp::new(s); + app.show_panel(PanelKind::EffectControls); + app.toggle_maximize(PanelKind::EffectControls); + let mut h = Harness::builder().with_size(egui::vec2(1600.0, 1400.0)).build_eframe(|_| app); + h.run_steps(4); + h +} + +/// A comp with one 320×180 solid carrying `effect`; returns the session, layer and effect uid. +fn with_effect(effect: &str) -> (Session, u64, u64) { + let mut s = Session::default(); + s.execute("comp.new", json!({"name": "Fx", "width": 320, "height": 180, "frameRate": 24, "duration": 4})).unwrap(); + let l = s.execute("layer.newSolid", json!({"name": "S", "color": "#6080a0"})).unwrap()["layer"].as_u64().unwrap(); + let fx = s.execute("effect.apply", json!({"layers": [l], "effect": effect})).unwrap()["effects"][0].as_u64().unwrap(); + s.state.selected_layers = vec![LayerId(l)]; + (s, l, fx) +} + +fn group(h: &Harness<'_, EffectcraftApp>, layer: u64, fx: u64) -> PropGroup { + h.state().session.active_comp().unwrap().layer(LayerId(layer)).unwrap().props.find_group(fx).unwrap().clone() +} + +/// A parameter's value by its spec-id path inside the effect (`curves/rgbCurves/master`). +fn value(h: &Harness<'_, EffectcraftApp>, layer: u64, fx: u64, path: &str) -> KV { + let g = group(h, layer, fx); + let mut cur = &g; + let parts: Vec<&str> = path.split('/').collect(); + for p in &parts[..parts.len() - 1] { + cur = cur.sub(p).unwrap_or_else(|| panic!("no group {p}")); + } + cur.get(parts[parts.len() - 1]).unwrap().value.clone() +} + +fn s(v: KV) -> String { + match v { + KV::Str(s) => s, + other => panic!("{other:?}"), + } +} + +/// Close every parameter group of the effect except those named (and their parents). +fn only_groups(h: &mut Harness<'_, EffectcraftApp>, layer: u64, fx: u64, keep: &[&str]) { + fn walk(g: &PropGroup, keep: &[&str], out: &mut Vec) { + for c in &g.children { + if let Node::Group(sg) = c { + if !keep.contains(&sg.match_id.as_str()) { + out.push(sg.uid); + } + walk(sg, keep, out); + } + } + } + let g = group(h, layer, fx); + let mut closed = vec![]; + walk(&g, keep, &mut closed); + h.state_mut().ui.fx_closed.extend(closed); + h.run_steps(3); +} + +fn rect(h: &Harness<'_, EffectcraftApp>, id: &str) -> egui::Rect { + let e = h.state().auto.previous.iter().chain(h.state().auto.elements.iter()).find(|e| e.id == id).cloned().unwrap_or_else(|| { + let near: Vec = h + .state() + .auto + .elements + .iter() + .map(|e| e.id.clone()) + .filter(|i| i.starts_with("effectControls.effect") || i.starts_with("viewer.reshape")) + .collect(); + panic!("no element {id}; have {near:?}") + }); + egui::Rect::from_min_size(pos2(e.rect[0], e.rect[1]), egui::vec2(e.rect[2], e.rect[3])) +} + +fn at(r: egui::Rect, fx: f32, fy: f32) -> Pos2 { + pos2(r.min.x + r.width() * fx, r.min.y + r.height() * fy) +} + +fn click_n(h: &mut Harness<'_, EffectcraftApp>, p: Pos2, n: u32) { + h.event(Event::PointerMoved(p)); + h.step(); + // (all in one frame: the harness' frame time is longer than a double-click's) + for _ in 0..n { + h.event(Event::PointerButton { pos: p, button: egui::PointerButton::Primary, pressed: true, modifiers: Default::default() }); + h.event(Event::PointerButton { pos: p, button: egui::PointerButton::Primary, pressed: false, modifiers: Default::default() }); + } + h.step(); + h.run_steps(2); +} + +fn click_id(h: &mut Harness<'_, EffectcraftApp>, id: &str) { + let p = rect(h, id).center(); + click_n(h, p, 1); +} + +fn drag(h: &mut Harness<'_, EffectcraftApp>, from: Pos2, to: Pos2) { + h.event(Event::PointerMoved(from)); + h.step(); + h.event(Event::PointerButton { pos: from, button: egui::PointerButton::Primary, pressed: true, modifiers: Default::default() }); + h.step(); + for k in 1..=6 { + let t = k as f32 / 6.0; + h.event(Event::PointerMoved(from + (to - from) * t)); + h.step(); + } + h.event(Event::PointerButton { pos: to, button: egui::PointerButton::Primary, pressed: false, modifiers: Default::default() }); + h.run_steps(3); +} + +fn undo(h: &mut Harness<'_, EffectcraftApp>) { + h.state_mut().session.execute("edit.undo", json!({})).unwrap(); + h.run_steps(2); +} + +#[test] +fn lumetri_rgb_and_hue_saturation_curve_editors() { + let (s0, l, fx) = with_effect("Lumetri Color"); + let mut h = harness(s0); + only_groups(&mut h, l, fx, &["curves", "rgbCurves", "hueSaturationCurves"]); + let pre = format!("effectControls.effect.{fx}.lumetri.rgbCurves"); + for k in 0..4 { + rect(&h, &format!("{pre}.channel.{k}")); + } + rect(&h, &format!("{pre}.point.0")); + // Red channel: a click in the graph adds a point to the red curve only. + click_id(&mut h, &format!("{pre}.channel.1")); + let gr = rect(&h, &format!("{pre}.graph")); + click_n(&mut h, at(gr, 0.5, 0.25), 1); + let red = s(value(&h, l, fx, "curves/rgbCurves/red")); + assert_eq!(red.split_whitespace().count(), 3, "{red}"); + assert_eq!(s(value(&h, l, fx, "curves/rgbCurves/master")), "0,0 1,1"); + rect(&h, &format!("{pre}.point.2")); + undo(&mut h); + assert_eq!(s(value(&h, l, fx, "curves/rgbCurves/red")), "0,0 1,1"); + // Hue vs Hue: pick the tab, add a point, drag it: one undo step for the drag. + let hs = format!("effectControls.effect.{fx}.lumetri.hueSatCurves"); + click_id(&mut h, &format!("{hs}.curve.1")); + let gr = rect(&h, &format!("{hs}.graph")); + click_n(&mut h, at(gr, 0.3, 0.5), 1); + let v = s(value(&h, l, fx, "curves/hueSaturationCurves/hueVsHue")); + assert_eq!(v.split_whitespace().count(), 1, "{v}"); + assert_eq!(s(value(&h, l, fx, "curves/hueSaturationCurves/hueVsSat")), ""); + let p0 = rect(&h, &format!("{hs}.point.0")).center(); + let undo_len = h.state().session.history.undo.len(); + drag(&mut h, p0, p0 + egui::vec2(0.0, -gr.height() * 0.25)); + let v = s(value(&h, l, fx, "curves/hueSaturationCurves/hueVsHue")); + let y: f32 = v.split(',').nth(1).unwrap().parse().unwrap(); + assert!(y > 0.7, "{v}"); + assert_eq!(h.state().session.history.undo.len(), undo_len + 1, "one undo step per drag"); + // Reset empties the curve. + click_id(&mut h, &format!("{hs}.reset")); + assert_eq!(s(value(&h, l, fx, "curves/hueSaturationCurves/hueVsHue")), ""); +} + +#[test] +fn colorama_output_cycle_wheel() { + let (s0, l, fx) = with_effect("Colorama"); + let mut h = harness(s0); + only_groups(&mut h, l, fx, &["outputCycle"]); + let pre = format!("effectControls.effect.{fx}.colorama"); + // The Hue Cycle preset has six stops. + for i in 0..6 { + rect(&h, &format!("{pre}.stop.{i}")); + } + // Clicking the ring adds a stop: the palette becomes Custom with seven stops. + let w = rect(&h, &format!("{pre}.wheel")); + let mid = w.width() / 2.0 - 20.0 - 9.0; + let ring = pos2(w.center().x + mid * std::f32::consts::FRAC_1_SQRT_2, w.center().y + mid * std::f32::consts::FRAC_1_SQRT_2); + click_n(&mut h, ring, 1); + assert_eq!(value(&h, l, fx, "outputCycle/usePresetPalette"), KV::Enum(8)); + let pal = s(value(&h, l, fx, "outputCycle/palette")); + assert_eq!(pal.split_whitespace().count(), 7, "{pal}"); + rect(&h, &format!("{pre}.stop.6")); + // Dragging a stop moves its position. + let st = rect(&h, &format!("{pre}.stop.0")).center(); + drag(&mut h, st, pos2(w.max.x - 6.0, w.center().y)); + let pal2 = s(value(&h, l, fx, "outputCycle/palette")); + assert_ne!(pal, pal2); + assert!(pal2.split_whitespace().any(|t| t.starts_with("0.25") || t.starts_with("0.24") || t.starts_with("0.26")), "{pal2}"); + rect(&h, &format!("{pre}.color")); +} + +#[test] +fn glow_arbitrary_map_editor() { + let (mut s0, l, fx) = with_effect("Glow"); + let colors = s0.active_comp().unwrap().layer(LayerId(l)).unwrap().props.find_group(fx).unwrap().get("colors").unwrap().uid; + s0.execute("prop.set", json!({"layer": l, "prop": colors, "value": 1})).unwrap(); + let mut h = harness(s0); + let pre = format!("effectControls.effect.{fx}.glow"); + rect(&h, &format!("{pre}.colorMap")); + assert!(!h.state().auto.elements.iter().any(|e| e.id == format!("{pre}.graph")), "A & B: just the strip"); + h.state_mut().session.execute("prop.set", json!({"layer": l, "prop": colors, "value": 2})).unwrap(); + h.run_steps(3); + click_id(&mut h, &format!("{pre}.channel.2")); + let gr = rect(&h, &format!("{pre}.graph")); + click_n(&mut h, at(gr, 0.5, 0.9), 1); + let m = s(value(&h, l, fx, "arbitraryMap")); + let parts: Vec<&str> = m.split('|').map(str::trim).collect(); + assert_eq!(parts.len(), 3, "{m}"); + assert_eq!(parts[0], "0,0 1,1"); + assert_eq!(parts[2].split_whitespace().count(), 3, "{m}"); +} + +#[test] +fn reshape_correspondence_points_on_the_viewer() { + let (mut s0, l, fx) = with_effect("Reshape"); + let sq = |x0: f64, y0: f64, d: f64| json!([[x0, y0], [x0 + d, y0], [x0 + d, y0 + d], [x0, y0 + d]]); + s0.execute("mask.new", json!({"layer": l, "vertices": sq(60.0, 40.0, 60.0), "closed": true})).unwrap(); + s0.execute("mask.new", json!({"layer": l, "vertices": sq(180.0, 60.0, 80.0), "closed": true})).unwrap(); + let g = s0.active_comp().unwrap().layer(LayerId(l)).unwrap().props.find_group(fx).unwrap().clone(); + s0.execute("prop.set", json!({"layer": l, "prop": g.get("sourceMask").unwrap().uid, "value": 1})).unwrap(); + s0.execute("prop.set", json!({"layer": l, "prop": g.get("destinationMask").unwrap().uid, "value": 2})).unwrap(); + let mut h = harness(s0); + let pre = format!("effectControls.effect.{fx}.reshape"); + click_id(&mut h, &format!("{pre}.add")); + assert_eq!(s(value(&h, l, fx, "correspondencePoints")), "0,0"); + click_id(&mut h, &format!("{pre}.add")); + assert_eq!(s(value(&h, l, fx, "correspondencePoints")).split_whitespace().count(), 2); + click_id(&mut h, &format!("{pre}.remove")); + assert_eq!(s(value(&h, l, fx, "correspondencePoints")), "0,0"); + // With the effect selected, the viewer shows the handles: drag the destination one along + // its outline. + h.state_mut().toggle_maximize(PanelKind::EffectControls); + h.state_mut().session.state.selected_props = vec![(LayerId(l), fx)]; + for _ in 0..6 { + h.step(); + } + let d0 = rect(&h, &format!("viewer.reshape.{fx}.dest.0")); + rect(&h, &format!("viewer.reshape.{fx}.source.0")); + let undo_len = h.state().session.history.undo.len(); + drag(&mut h, d0.center(), d0.center() + egui::vec2(60.0, 0.0)); + let v = s(value(&h, l, fx, "correspondencePoints")); + let d: f64 = v.split(',').nth(1).unwrap().parse().unwrap(); + assert!(d > 0.02 && d < 0.3, "moved along the top edge: {v}"); + assert_eq!(h.state().session.history.undo.len(), undo_len + 1); +} + +/// UI fixes of M13.10: the Composition / Timeline tabs' close button, swatch and lock; the +/// Preview panel scrolls in the default workspace (where it shows just the transport). +#[test] +fn composition_tabs_and_scrolling_preview() { + let mut s = Session::default(); + s.execute("file.openDemoProject", json!({})).unwrap(); + let mut h = Harness::builder().with_size(egui::vec2(1600.0, 1000.0)).build_eframe(|_| EffectcraftApp::new(s)); + h.run_steps(4); + rect(&h, "panel.tab.Timeline.close"); + rect(&h, "preview.scroll"); + click_id(&mut h, "panel.tab.Composition.lock"); + assert!(h.state().ui.locked_tabs.contains("Composition")); + click_id(&mut h, "panel.tab.Composition.lock"); + assert!(h.state().ui.locked_tabs.is_empty()); + click_id(&mut h, "panel.tab.Timeline.close"); + assert!(!h.state().ui.dock.contains(PanelKind::Timeline)); +} 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/crates/ui-egui/tests/ui_web_offload.rs b/crates/ui-egui/tests/ui_web_offload.rs new file mode 100644 index 00000000..54ae1a37 --- /dev/null +++ b/crates/ui-egui/tests/ui_web_offload.rs @@ -0,0 +1,204 @@ +//! Headless checks for M13.10 (web app depth), run natively with stand-ins for the browser's +//! workers: +//! +//! - viewer frames go to a [`RemoteFrames`] renderer (the browser's frame worker, here an +//! in-process [`FrameServer`] fed through JSON with project diffs) instead of rendering on the +//! UI thread, the viewer's frame first, and come back identical; +//! - a control-channel `wait: true` command whose job is offloaded replies only when the job +//! ends, while the UI keeps running. + +use std::sync::mpsc::TryRecvError; +use std::sync::{Arc, Mutex}; + +use effectcraft_engine::Session; +use effectcraft_engine::offload::{Inbox, Offload, Post, WorkerRequest}; +use effectcraft_engine::remote::{FrameMsg, FrameReply, FrameServer, Mirror}; +use effectcraft_engine::render::RenderOpts; +use effectcraft_ui_egui::frames::{FrameImage, FrameKey, Frames, RemoteDone, RemoteFrame, RemoteFrames, RemoteJob, RenderSource}; +use effectcraft_ui_egui::{ControlRequest, EffectcraftApp}; +use egui_kittest::Harness; +use serde_json::json; + +/// A frame worker in the same process: messages go through JSON like `postMessage`; rendered +/// frames are held until [`FakeWorker::deliver`] (a busy worker) or dropped by +/// [`FakeWorker::lose`]. +struct FakeWorker { + slots: usize, + state: Mutex<(Mirror, FrameServer, Vec<(RemoteDone, RemoteFrame)>, u64)>, + /// Patch / full-project syncs sent. + syncs: Mutex>, +} + +impl FakeWorker { + fn new(s: &Session, slots: usize) -> FakeWorker { + FakeWorker { slots, state: Mutex::new((Mirror::default(), FrameServer::new(s.footage.clone(), s.expr.clone()), vec![], 0)), syncs: Mutex::default() } + } + fn pending(&self) -> usize { + self.state.lock().unwrap().2.len() + } + fn take(&self) -> Vec<(RemoteDone, RemoteFrame)> { + std::mem::take(&mut self.state.lock().unwrap().2) + } + /// Post every rendered frame back. + fn deliver(&self) { + for (done, f) in self.take() { + done(Ok(f)); + } + } + /// The worker died: its frames never arrive. + fn lose(&self) { + for (done, _) in self.take() { + done(Err("worker lost".into())); + } + } +} + +fn rt serde::Deserialize<'de>>(v: &T) -> T { + serde_json::from_str(&serde_json::to_string(v).unwrap()).unwrap() +} + +impl RemoteFrames for FakeWorker { + fn slots(&self) -> usize { + self.slots + } + fn start(&self, job: RemoteJob, done: RemoteDone) { + let mut st = self.state.lock().unwrap(); + let (mirror, server, pending, next) = &mut *st; + if let Some(m) = mirror.sync(job.revision, &job.project) { + self.syncs.lock().unwrap().push(if matches!(m, FrameMsg::Patch { .. }) { "patch" } else { "project" }); + assert!(server.handle(rt(&m)).is_empty()); + } + *next += 1; + let out = server.handle(rt(&FrameMsg::Render { id: *next, revision: job.revision, comp: job.comp, time: job.t, opts: Box::new(job.opts) })); + match out.into_iter().next() { + Some((FrameReply::Frame { width, height, ms, .. }, Some(rgba))) => pending.push((done, RemoteFrame { width, height, rgba, ms })), + other => done(Err(format!("{:?}", other.map(|o| o.0)))), + } + } +} + +fn source(s: &Session) -> RenderSource { + RenderSource { + project: s.project.clone(), + footage: s.footage.clone(), + expr: s.expr.clone(), + layer_cache: s.layer_cache.clone(), + gpu: None, + gpu_display: false, + disk: None, + } +} + +#[test] +fn viewer_frames_render_remotely_from_project_diffs() { + let mut s = Session::default(); + s.execute("file.openDemoProject", json!({})).unwrap(); + let comp = s.active_comp_id().unwrap(); + let c = s.project.comp(comp).unwrap().clone(); + let opts = RenderOpts { scale: 0.25, guides: true, ..Default::default() }; + let key = |rev: u64, f: i64| FrameKey { revision: rev, comp: comp.0, frame: f, scale: 250, view: 0 }; + let worker = Arc::new(FakeWorker::new(&s, 2)); + let mut frames = Frames::default(); + frames.set_remote(Some(worker.clone())); + assert!(frames.remote_active()); + // Three prefetch frames, then the viewer's frame. + for f in 1..=3 { + frames.request(&source(&s), key(s.revision, f), comp, c.frame_rate.tick_of(f), opts); + } + frames.request_urgent(&source(&s), key(s.revision, 10), comp, c.frame_rate.tick_of(10), opts); + assert_eq!(frames.inflight(), 4); + // Nothing renders until dispatched (no frame threads with a remote renderer); the viewer's + // frame goes first, then prefetch in request order. + assert_eq!(frames.dispatch_remote(), 2); + assert_eq!(frames.remote_busy(), 2); + assert_eq!(worker.pending(), 2); + assert_eq!(frames.dispatch_remote(), 0, "both slots are busy"); + assert_eq!(*worker.syncs.lock().unwrap(), vec!["project"], "the first sync is the whole project"); + // Frames arrive: the cache holds exactly what the page would have rendered. + worker.deliver(); + assert_eq!(frames.remote_busy(), 0); + let img = match frames.get(&key(s.revision, 10)).expect("viewer frame cached") { + FrameImage::Cpu(c) => c, + FrameImage::Gpu(_) => unreachable!(), + }; + let local = effectcraft_ui_egui::frames::to_color_image(&s.render(comp, c.frame_rate.tick_of(10), opts)); + assert_eq!(img.size, local.size); + assert_eq!(img.pixels, local.pixels); + assert!(frames.is_cached(&key(s.revision, 1))); + assert!(!frames.is_cached(&key(s.revision, 2))); + // The rest; an edit later travels as a patch. + frames.dispatch_remote(); + worker.deliver(); + frames.dispatch_remote(); + worker.deliver(); + assert_eq!(frames.inflight(), 0); + s.execute("layer.newSolid", json!({"color": "#ff8800"})).unwrap(); + frames.request_urgent(&source(&s), key(s.revision, 10), comp, c.frame_rate.tick_of(10), opts); + frames.dispatch_remote(); + worker.deliver(); + assert_eq!(worker.syncs.lock().unwrap().last(), Some(&"patch")); + let FrameImage::Cpu(img) = frames.get(&key(s.revision, 10)).unwrap() else { unreachable!() }; + let local = effectcraft_ui_egui::frames::to_color_image(&s.render(comp, c.frame_rate.tick_of(10), opts)); + assert_eq!(img.pixels, local.pixels); + // A lost frame is released (requested again later), not cached. + frames.request(&source(&s), key(s.revision, 20), comp, c.frame_rate.tick_of(20), opts); + frames.dispatch_remote(); + worker.lose(); + assert_eq!(frames.inflight(), 0); + assert!(!frames.is_cached(&key(s.revision, 20))); + // Without slots the remote renderer is unusable (frames render here again). + frames.set_remote(Some(Arc::new(FakeWorker::new(&s, 0)))); + assert!(!frames.remote_active()); +} + +/// Holds offloaded jobs until the test runs them. +#[derive(Default)] +struct Manual(Mutex)>>); + +impl Offload for Manual { + fn start(&self, req: WorkerRequest, inbox: Arc) -> Result<(), String> { + self.0.lock().unwrap().push((req, inbox)); + Ok(()) + } + fn cancel(&self, _: u64) {} +} + +fn exporter() -> Arc { + let sink: Arc) + Send + Sync> = Arc::new(|_: &str, _: Vec| {}); + Arc::new(effectcraft_host::FileExporter { sink: Some(sink) }) +} + +#[test] +fn waiting_commands_reply_when_their_offloaded_job_ends() { + let off = Arc::new(Manual::default()); + let mut s = Session { offload: Some(off.clone()), exporter: Some(exporter()), ..Default::default() }; + s.execute("comp.new", json!({"name": "W", "width": 64, "height": 48, "frameRate": 10, "duration": 1})).unwrap(); + s.execute("layer.newSolid", json!({"color": "#406080"})).unwrap(); + s.execute("renderQueue.add", json!({"format": "gif", "output": "/w.gif"})).unwrap(); + let (tx, rx) = std::sync::mpsc::channel(); + let mut h = Harness::builder().with_size(egui::vec2(1280.0, 800.0)).build_eframe(|_| EffectcraftApp::new(s).with_control(rx)); + h.run_steps(2); + // `renderQueue.render` waits by default: the job goes to the worker, the reply waits. + let (req, reply) = ControlRequest::new("engine.execute", json!({"command": "renderQueue.render", "params": {}})); + tx.send(req).unwrap(); + h.run_steps(4); + assert_eq!(reply.try_recv().unwrap_err(), TryRecvError::Empty, "no reply while the job runs"); + assert!(h.state().session.is_rendering()); + let (job, inbox) = off.0.lock().unwrap().pop().expect("the render went to the worker"); + // The worker runs it. + let mut w = Session { exporter: Some(exporter()), ..Default::default() }; + let post: Post = std::rc::Rc::new(move |r| inbox.push(r)); + effectcraft_engine::offload::run_request(&mut w, job, &post); + h.run_steps(4); + let v = reply.try_recv().expect("replied once the job ended"); + assert_eq!(v["ok"], true, "{v}"); + assert_eq!(v["result"]["rendering"], false, "{v}"); + assert_eq!(v["result"]["items"][0]["status"], "Done", "{v}"); + // `wait: false` replies at once. + h.state_mut().session.execute("renderQueue.add", json!({"format": "gif", "output": "/w2.gif"})).unwrap(); + let (req, reply) = ControlRequest::new("engine.execute", json!({"command": "renderQueue.render", "params": {"wait": false}})); + tx.send(req).unwrap(); + h.run_steps(2); + let v = reply.try_recv().expect("immediate reply"); + assert_eq!(v["result"]["rendering"], true, "{v}"); +} 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 fee439c4..4eac5a3c 100644 --- a/docs/parity.md +++ b/docs/parity.md @@ -15,8 +15,8 @@ How close EffectCraft is to After Effects 2026, feature by feature, and how much | Remaining work | ≈ 19–22 agent-hours at the pace measured so far (≈ 58 on the conservative audit scale) | | **Wall-clock estimate** | **≈ 4–5 hours** with five agents in parallel; ≈ 3 hours for 100% of P0 + P1 | -Partial features: GPU coverage (Advanced 3D, more GPU effects), the web app (no threads for -viewer frames and Roto Brush), Essential Graphics mirrored properties, and the learned-model quality +Partial features: GPU coverage (Advanced 3D, more GPU effects), the web app (GPU effects and +the disk cache in the browser; M13.10 moved viewer frames and Roto Brush off the page's thread), Essential Graphics mirrored properties, and the learned-model quality of Roto Brush and face tracking (both are classical). Since this audit, M9.13 completed all 42 simplified effects, M13.5 added the Learn panel, and M13.8 the Preview panel settings, Align to Selection / Distribute and Advanced 3D iris DOF, collapsed precomps and extruded strokes. M13.7 added ScriptUI resource strings, @@ -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) | -| Web | 85% | 1.0 | viewer frames and Roto Brush propagation still on the page's thread; GPU effects in the browser (browser storage, Web Audio, Web Worker renders/analyses, WebGPU viewer and offline install landed in M15.2) | -| 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 | +| 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; 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; 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) | +| 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`): @@ -239,7 +274,33 @@ precompose), **Auto-trace** (alpha / RGB / luminance, current frame or work area or a new layer) and **Align Video to Data** (JSON / CSV / TSV time keys) replace their disabled menu entries. Not yet: creating a reference frame by editing a still (After Effects hands it to Photoshop), Content-Aware Fill's learned model (ours is PatchMatch + flow propagation), and Media -Browser in the web app. +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 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/docs/web.md b/docs/web.md index 72eb75ff..52d3419c 100644 --- a/docs/web.md +++ b/docs/web.md @@ -41,14 +41,15 @@ with every L0–L4 crate and `effectcraft-ui-egui`, for `wasm32-unknown-unknown` URL flags: `?empty` (a blank project; the session is not restored or recorded), `?demo` (the demo project instead of the last session), `?home` (show the start screen), `?storage=indexeddb` / -`?storage=memory` (force a storage backend), `?noworkers` (render and analyse on the page's -thread), `?nosw` (don't register the service worker). +`?storage=memory` (force a storage backend), `?noworkers` (render, analyse and draw viewer +frames on the page's thread), `?frameworkers=N` (frame workers, default 2; 0 = viewer frames on +the page's thread), `?nosw` (don't register the service worker). ## How the desktop pieces map to the browser | Desktop | Web | |---|---| -| frame render thread pool (`ui-egui/src/frames.rs`) | `Frames::pump`: queued frames render on the UI thread after each egui frame, most important first (the viewer's frame, then prefetch), within 40 ms (24 ms while playing) | +| frame render thread pool (`ui-egui/src/frames.rs`) | frame workers (below): `Frames::pump` hands queued frames to them, most important first (the viewer's frame, then prefetch); frames the GPU compositor can keep on the GPU render on the page. Without workers, queued frames render on the UI thread after each egui frame within 40 ms (24 ms while playing) | | Render Queue on a background thread | a Web Worker running a second engine instance (below); progress streams back, the page never waits | | tracker / mask tracker / Warp Stabilizer / 3D Camera Tracker / Roto Brush Freeze threads | the same workers; the analysed property group comes back as one undo step | | `rayon` parallel loops (raster, effects, export batches) | rayon's global pool falls back to the calling thread when threads are unavailable: same code, serial | @@ -59,6 +60,7 @@ thread), `?nosw` (don't register the service worker). | media reads (`MediaPool`, `probe`) | `files::WebImporter`: bytes from the file table, handed to `MediaPool::add_bytes` / `probe_bytes` | | export writes (`effectcraft-export`) | the job's `sink` (`effectcraft_host::FileExporter { sink }`): files land in the table and download after the render; several files (an image sequence) download as one stored `.zip` | | rfd file dialogs | `` for File ▸ Open / Import; drop files anywhere on the page (`.ecproj` opens, everything else imports); "Save As" / "Output To" pick a download name | +| Media Browser on the file system | browser storage and folders opened with the File System Access API (below) | | system fonts | not scanned; the bundled fonts (Inter, Noto Serif, JetBrains Mono) are always there | | TCP control channel / MCP | `window.effectcraft` (below) | | cpal audio output | Web Audio (below) | @@ -109,10 +111,65 @@ compiled twice) and its own memory (`crates/engine/src/offload.rs`, `src/worker. tracker / mask group is replaced; other edits made meanwhile are kept) and the downloads start. Stopping a job terminates its worker (a worker can't be interrupted mid-frame): the item being -rendered becomes "User Stopped", an analysis writes nothing. `renderQueue.render {wait: true}` (the -default for agents) and analysis commands with `wait: true` still run on the page's thread and -return when done. Roto Brush propagation fills the session's segmentation cache rather than the -project, so it runs on the page's thread too, as do viewer frames (`Frames::pump`). +rendered becomes "User Stopped", an analysis writes nothing. Every job shows in Window ▸ Progress +(`jobs.list`) with its progress and a cancel button (`jobs.cancel`). + +**Roto Brush propagation** runs in a job worker too (`WorkerJob::RotoPropagate`): the worker +streams the segmentations it computes back at most every 250 ms (`WorkerReply::Segs`: each +`FrameSeg` in a compact binary form, run-length coded planes, base64), the page puts them into its +segmentation cache and relays them to the frame workers. Stopping it keeps the frames propagated +so far. + +**`wait: true`** (the default of `renderQueue.render` for agents, and an option of the analysis +and Roto Brush commands) no longer blocks the page: over `window.effectcraft` (the control +channel) such a command runs with `wait: false`, and the promise resolves when the job it started +ends, with what the blocking command returns (final render item statuses, the analysis status) +or rejects with the job's error (`ui-egui/src/control.rs`, `JobWaiter`). Engine commands executed +directly on the session (scripts) still block. + +### Viewer frames: frame workers + +Viewer frames (the frame on screen, RAM preview and prefetch) render in long-lived **frame +workers** (`src/frames.rs`, `crates/engine/src/remote.rs`), two by default. Each holds a replica +of the project: + +1. Before a render, the page sends a worker the footage files it lacks, then a sync: the whole + project the first time, afterwards a **diff** of the serialized project from the revision the + worker holds (`remote::diff`: object members added / changed / removed, arrays element-wise + when their length is unchanged; a slider drag sends a few hundred bytes, not the project). +2. Then the render request (`FrameMsg::Render`: revision, comp, time, render options). The worker + renders on its CPU renderer and posts the frame back as premultiplied RGBA8 in a transferred + buffer (`FrameReply::Frame`); the page puts it in the frame cache. +3. One frame per worker at a time, so the page always picks what renders next (the viewer's frame + before prefetch); a request for a revision the worker does not hold is refused, a worker that + lost its replica asks for the whole project again (`FrameReply::Resync`). + +Frames the WebGPU compositor can finish alone still render on the page (they stay on the GPU, no +readback); everything else (CPU-only effects, 3D runs, the Software Only renderer, WebGL2) goes to +the workers. If no worker is usable, frames render on the page's thread as before. +`effectcraft.info().frameWorkers` reports `{workers, alive, busy, rendered, failed, lastMs, +syncs, syncBytes}`. + +**Why workers and not wasm threads:** shared-memory threads (SharedArrayBuffer + atomics, a +`wasm-bindgen-rayon`-style pool) need the standard library rebuilt with atomics (`build-std`, +nightly Rust); the repository builds on stable, so each worker runs its own engine instance with +its own memory. The page is still cross-origin isolated (`--serve` and the service worker send +COOP / COEP), so a threaded build can be added later without changing the deployment. + +### Media Browser + +Window ▸ Media Browser browses a virtual tree (`src/browse.rs`): + +- **Browser Storage**: every file in browser storage (imported media, saved projects, files added + with **Add Files…** or **Upload Folder…**, plain file inputs that work in every browser). This is + the persistent recent list where folders can't be opened. +- **Folders**: **Open Folder…** (File System Access API, `showDirectoryPicker`, Chromium) lists a + folder from the user's disk; only the listing is read, and a file's bytes are read when it is + imported (then kept in browser storage). The folder handles are remembered in IndexedDB; after + a reload the folders come back if the browser still grants access, else **Reconnect ** + asks again. + +`mediaBrowser.list` reports the places and these actions; `mediaBrowser.action {action}` runs one. ### Audio @@ -192,15 +249,24 @@ cargo xtask web --serve 8765 & node apps/effectcraft-web/tests/smoke.mjs --url http://127.0.0.1:8765/ --out target/web/smoke ``` +It also checks the M13.10 paths: viewer frames rendered in frame workers while scrubbing with the +CPU renderer (project synced as diffs, event-loop gaps under 400 ms), a `wait: true` render that +replies when done while the page keeps running, and the Media Browser over browser storage. + Native unit tests cover the storage model (`apps/effectcraft-web/src/store.rs`: write coalescing, file table, `ConfigStore` / `FileOps`, auto-save and crash recovery through the browser store) and the worker protocol (`crates/engine/src/offload.rs`: serde round trips of -every request and reply, a render through an in-process offload). +every request and reply, a render through an in-process offload; `tests_roto.rs`: Roto Brush +propagation through an offload streaming segmentations), the frame-worker protocol +(`crates/engine/src/remote.rs`: diff / patch, a replica following edits renders the same pixels; +`crates/ui-egui/tests/ui_web_offload.rs`: the frame cache through a remote renderer, `wait: true` +deferred until the offloaded job ends) and the Media Browser's virtual tree (`media_browser.rs`, +`tests_panels.rs`). ## Gaps -- Viewer frames, RAM preview and Roto Brush propagation still render on the page's thread (one - frame at a time between UI frames). +- No shared-memory threads: each worker is a separate engine instance (memory per worker: the + project replica and the footage it reads). - GPU effects run only in the desktop GPU path: in the browser, effects run on the CPU (inside workers for renders) because the page's thread can't wait for mid-render readbacks. - No disk cache (Settings ▸ Media & Disk Cache) in the browser: the layer cache stays in memory. 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),