diff --git a/Cargo.lock b/Cargo.lock index 073487278..d0b5c8dc0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1164,6 +1164,13 @@ dependencies = [ "thiserror 2.0.21", ] +[[package]] +name = "effectcraft-av1enc" +version = "0.1.0" +dependencies = [ + "filmcraft-av1", +] + [[package]] name = "effectcraft-cli" version = "0.1.0" @@ -1238,7 +1245,9 @@ dependencies = [ name = "effectcraft-export" version = "0.1.0" dependencies = [ + "effectcraft-av1enc", "effectcraft-color", + "effectcraft-hevcenc", "effectcraft-keyframe", "effectcraft-media", "effectcraft-opusenc", @@ -1305,6 +1314,13 @@ dependencies = [ "wgpu", ] +[[package]] +name = "effectcraft-hevcenc" +version = "0.1.0" +dependencies = [ + "filmcraft-hevc", +] + [[package]] name = "effectcraft-host" version = "0.1.0" @@ -1393,6 +1409,9 @@ dependencies = [ [[package]] name = "effectcraft-opusenc" version = "0.1.0" +dependencies = [ + "filmcraft-opus", +] [[package]] name = "effectcraft-path" diff --git a/Cargo.toml b/Cargo.toml index 6641f2c4d..d5f6101a1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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 8aa1381b2..09a9f233d 100644 --- a/apps/effectcraft-cli/src/main.rs +++ b/apps/effectcraft-cli/src/main.rs @@ -9,9 +9,11 @@ //! 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 //! (--gpu: CPU vs GPU ms/frame for every comp at Full and Half) @@ -44,7 +46,11 @@ 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})); diff --git a/apps/effectcraft-web/js/host.js b/apps/effectcraft-web/js/host.js index 7b6200eb8..1fcdc40bf 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 0076cd4f3..e6712e3d7 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 000000000..aba2eb22d --- /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 000000000..abad352f9 --- /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 59de4433f..96b17db24 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 2d96f641c..07c9aadc7 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 3ba193433..81bc81ccb 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 c29663d79..6c6853c5c 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 000000000..2a4c7ddfe --- /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 000000000..6ced20ba1 --- /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 000000000..e4c8a1e69 --- /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 000000000..0466a1592 --- /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 000000000..e71ad6e9d --- /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 000000000..0eeded9c5 --- /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 000000000..2add41edc --- /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 000000000..d61a6fa94 --- /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 000000000..579a51e20 --- /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 000000000..3e333a519 --- /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 000000000..386133b56 --- /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 000000000..903492b13 --- /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 000000000..6577f8f9f --- /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 000000000..cf5dba73d --- /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 000000000..c1a087d28 --- /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 000000000..1e777e509 --- /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/catalog.rs b/crates/effects/src/catalog.rs index f41b4b2e4..a711ce43c 100644 --- a/crates/effects/src/catalog.rs +++ b/crates/effects/src/catalog.rs @@ -87,6 +87,10 @@ const CPU_ONLY_CONTROLS: &[(&str, &[&str])] = &[ ), ("ec.noise.addgrain", &["viewingMode", "preset", "color/redBalance", "color/greenBalance", "color/blueBalance", "previewRegion/"]), ("ec.blur.cameralens", &["irisProperties/diffractionFringe", "blurMap/blurMapLayer"]), + ("ec.noise.removegrain", &["temporalFiltering/enabled"]), + ("ec.distort.liquify", &["viewFreezeAreaMask", "viewMesh"]), + ("ec.matte.refinesoft", &["reduceChatter", "useMotionBlur", "invert"]), + ("ec.matte.refinehard", &["reduceChatter", "useMotionBlur", "invert"]), ]; /// Whether the GPU kernel of effect `id` covers the instance's current settings (see diff --git a/crates/effects/src/color2.rs b/crates/effects/src/color2.rs index 67063d319..8f929874a 100644 --- a/crates/effects/src/color2.rs +++ b/crates/effects/src/color2.rs @@ -330,10 +330,21 @@ fn stretch(v: f32, lo: f32, hi: f32) -> f32 { /// 0 = Auto Levels (per channel), 1 = Auto Contrast (shared), 2 = Auto Color (per channel + neutral mids). fn auto_correct(ctx: &EffectCtx, mut b: Buf, kind: u32) -> Buf { + let blend = (ctx.params.f("blend") / 100.0).clamp(0.0, 1.0) as f32; + let (ranges, gam) = auto_correct_settings(ctx, &b.img, kind); + map_ca(&mut b.img, |c, a| { + let o = [0, 1, 2].map(|k| stretch(c[k], ranges[k].0, ranges[k].1).powf(gam[k])); + (lerp3(o, c, blend), a) + }); + b +} + +/// What Auto Levels (`kind` 0), Auto Contrast (1) and Auto Color (2) measure on `img`: each +/// channel's input range (low, high) and gamma. (The GPU effects apply them.) +pub fn auto_correct_settings(ctx: &EffectCtx, img: &Image, kind: u32) -> ([(f32, f32); 3], [f32; 3]) { let black = ctx.params.f("blackClip").clamp(0.0, 49.0) / 100.0; let white = ctx.params.f("whiteClip").clamp(0.0, 49.0) / 100.0; - let blend = (ctx.params.f("blend") / 100.0).clamp(0.0, 1.0) as f32; - let h = smoothed_histograms(ctx, &b.img); + let h = smoothed_histograms(ctx, img); let ranges: [(f32, f32); 3] = if kind == 1 { let mut combined = vec![0.0; BINS]; for k in 0..3 { @@ -350,7 +361,7 @@ fn auto_correct(ctx: &EffectCtx, mut b: Buf, kind: u32) -> Buf { let mut gam = [1.0f32; 3]; if kind == 2 && ctx.params.b("snapNeutralMidtones") { let mut sums = [0.0f64; 4]; - for px in &b.img.data { + for px in &img.data { let (c, a) = unpremul(*px); if a > 1e-4 { for k in 0..3 { @@ -369,11 +380,7 @@ fn auto_correct(ctx: &EffectCtx, mut b: Buf, kind: u32) -> Buf { } } } - map_ca(&mut b.img, |c, a| { - let o = [0, 1, 2].map(|k| stretch(c[k], ranges[k].0, ranges[k].1).powf(gam[k])); - (lerp3(o, c, blend), a) - }); - b + (ranges, gam) } fn auto_levels(ctx: &EffectCtx, b: Buf) -> Buf { @@ -397,24 +404,12 @@ fn cdf(h: &[f64]) -> Vec { .collect() } -fn equalize(ctx: &EffectCtx, mut b: Buf) -> Buf { - let amt = (ctx.params.f("amount") / 100.0).clamp(0.0, 1.0) as f32; - if amt <= 0.0 { - return b; - } - let style = ctx.params.e("style"); - let h = histograms(&b.img); - let look = |c: &Vec, v: f32| c[((v.clamp(0.0, 1.0) * (BINS - 1) as f32).round() as usize).min(BINS - 1)]; +/// Equalize's lookup tables (`BINS` entries each) for `img`: the R, G and B cumulative +/// histograms (Style RGB), the luminance one (Brightness) or the combined one (Photoshop Style). +pub fn equalize_tables(img: &Image, style: u32) -> Vec> { + let h = histograms(img); match style { - 1 => { - let c = cdf(&h[3]); - map_ca(&mut b.img, |col, a| { - let l = luminance(col[0], col[1], col[2]); - let nl = look(&c, l); - let o = if l > 1e-5 { col.map(|v| v * nl / l) } else { [nl; 3] }; - (lerp3(col, o, amt), a) - }); - } + 1 => vec![cdf(&h[3])], 2 => { let mut combined = vec![0.0; BINS]; for k in 0..3 { @@ -422,12 +417,34 @@ fn equalize(ctx: &EffectCtx, mut b: Buf) -> Buf { combined[i] += h[k][i]; } } - let c = cdf(&combined); - map_ca(&mut b.img, |col, a| (lerp3(col, col.map(|v| look(&c, v)), amt), a)); + vec![cdf(&combined)] + } + _ => vec![cdf(&h[0]), cdf(&h[1]), cdf(&h[2])], + } +} + +fn equalize(ctx: &EffectCtx, mut b: Buf) -> Buf { + let amt = (ctx.params.f("amount") / 100.0).clamp(0.0, 1.0) as f32; + if amt <= 0.0 { + return b; + } + let style = ctx.params.e("style"); + let t = equalize_tables(&b.img, style); + let look = |c: &Vec, v: f32| c[((v.clamp(0.0, 1.0) * (BINS - 1) as f32).round() as usize).min(BINS - 1)]; + match style { + 1 => { + map_ca(&mut b.img, |col, a| { + let l = luminance(col[0], col[1], col[2]); + let nl = look(&t[0], l); + let o = if l > 1e-5 { col.map(|v| v * nl / l) } else { [nl; 3] }; + (lerp3(col, o, amt), a) + }); + } + 2 => { + map_ca(&mut b.img, |col, a| (lerp3(col, col.map(|v| look(&t[0], v)), amt), a)); } _ => { - let cs = [cdf(&h[0]), cdf(&h[1]), cdf(&h[2])]; - map_ca(&mut b.img, |col, a| (lerp3(col, [0, 1, 2].map(|k| look(&cs[k], col[k])), amt), a)); + map_ca(&mut b.img, |col, a| (lerp3(col, [0, 1, 2].map(|k| look(&t[k], col[k])), amt), a)); } } b @@ -436,17 +453,29 @@ fn equalize(ctx: &EffectCtx, mut b: Buf) -> Buf { // --------------------------------------------------------------------------------------------- // Shadow/Highlight -fn shadow_highlight(ctx: &EffectCtx, mut b: Buf) -> Buf { +/// Shadow/Highlight's shadow and highlight amounts for `img` (Auto Amounts measures them). +pub fn shadow_highlight_amounts(ctx: &EffectCtx, img: &Image) -> (f32, f32) { let (mut s_amt, mut h_amt) = (ctx.params.f("shadowAmount") as f32 / 100.0, ctx.params.f("highlightAmount") as f32 / 100.0); - let luma = Plane::luma(&b.img); - let src_hist = histograms(&b.img); if ctx.params.b("autoAmounts") { // Temporal Smoothing / Scene Detect average the analysis over neighbouring frames. - let mean = - if ctx.params.f("temporalSmoothing") > 0.0 { hist_mean(&smoothed_histograms(ctx, &b.img)[3]) as f32 } else { hist_mean(&src_hist[3]) as f32 }; + let mean = if ctx.params.f("temporalSmoothing") > 0.0 { hist_mean(&smoothed_histograms(ctx, img)[3]) } else { hist_mean(&histograms(img)[3]) } as f32; s_amt = ((0.6 - mean) * 1.5).clamp(0.0, 1.0) * 0.8 + 0.1; h_amt = ((mean - 0.6) * 1.5).clamp(0.0, 1.0) * 0.8; } + (s_amt, h_amt) +} + +/// The luminance values clipping `black` / `white` fractions of `img`'s alpha-weighted +/// luminance histogram (Shadow/Highlight's Black / White Clip). +pub fn luma_clip_points(img: &Image, black: f64, white: f64) -> (f32, f32) { + clip_points(&histograms(img)[3], black, white) +} + +fn shadow_highlight(ctx: &EffectCtx, mut b: Buf) -> Buf { + let (s_amt, h_amt) = shadow_highlight_amounts(ctx, &b.img); + let luma = Plane::luma(&b.img); + let (bc, wc) = (ctx.params.f("moreOptions/blackClip").clamp(0.0, 49.0) / 100.0, ctx.params.f("moreOptions/whiteClip").clamp(0.0, 49.0) / 100.0); + let clip0 = (bc > 0.0 || wc > 0.0).then(|| luma_clip_points(&b.img, bc, wc)); let s_tw = (ctx.params.f("moreOptions/shadowTonalWidth") as f32 / 100.0).max(0.01); let h_tw = (ctx.params.f("moreOptions/highlightTonalWidth") as f32 / 100.0).max(0.01); let s_r = ctx.params.f("moreOptions/shadowRadius").max(0.0) * b.scale / 2.0; @@ -478,10 +507,8 @@ fn shadow_highlight(ctx: &EffectCtx, mut b: Buf) -> Buf { // Black / White Clip: the adjusted image's extremes (ignoring those fractions of pixels) // are mapped back onto the original's, so lifting shadows or recovering highlights doesn't // flatten the ends of the tonal range. - let (bc, wc) = (ctx.params.f("moreOptions/blackClip").clamp(0.0, 49.0) / 100.0, ctx.params.f("moreOptions/whiteClip").clamp(0.0, 49.0) / 100.0); - if bc > 0.0 || wc > 0.0 { - let (lo0, hi0) = clip_points(&src_hist[3], bc, wc); - let (lo1, hi1) = clip_points(&histograms(&b.img)[3], bc, wc); + if let Some((lo0, hi0)) = clip0 { + let (lo1, hi1) = luma_clip_points(&b.img, bc, wc); if (lo0, hi0) != (lo1, hi1) && hi1 - lo1 > 1e-3 { let k = (hi0 - lo0) / (hi1 - lo1); map_ca(&mut b.img, |c, a| (c.map(|v| (lo0 + (v - lo1) * k).max(0.0)), a)); diff --git a/crates/effects/src/color3.rs b/crates/effects/src/color3.rs index 7d626d190..c85ab8d09 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/distort2.rs b/crates/effects/src/distort2.rs index 43378528c..8f430a969 100644 --- a/crates/effects/src/distort2.rs +++ b/crates/effects/src/distort2.rs @@ -354,7 +354,7 @@ fn magnify(ctx: &EffectCtx, mut b: Buf) -> Buf { } /// Magnify's Blending Mode choices after "None". -const MAGNIFY_MODES: [BlendMode; 17] = [ +pub const MAGNIFY_MODES: [BlendMode; 17] = [ BlendMode::Normal, BlendMode::Add, BlendMode::Multiply, diff --git a/crates/effects/src/distort4.rs b/crates/effects/src/distort4.rs index c576feadb..d1d1b0c2c 100644 --- a/crates/effects/src/distort4.rs +++ b/crates/effects/src/distort4.rs @@ -325,7 +325,9 @@ pub fn build_mesh(strokes: &[LiquifyStroke], w: f64, h: f64, frozen: &(dyn Fn(f6 type MeshCache = Mutex>>; -fn cached_mesh(ctx: &EffectCtx) -> Arc { +/// Liquify's displacement mesh for the instance (replayed strokes, cached; also used by the +/// GPU kernels). +pub fn liquify_mesh(ctx: &EffectCtx) -> Arc { use std::hash::{Hash, Hasher}; let text = ctx.params.s("distortionMesh"); let mask = ctx.params.f("freezeAreaMask").round() as usize; @@ -369,7 +371,7 @@ fn liquify(ctx: &EffectCtx, mut b: Buf) -> Buf { if ctx.params.s("distortionMesh").trim().is_empty() && !show_freeze && !show_mesh { return b; } - let mesh = cached_mesh(ctx); + let mesh = liquify_mesh(ctx); let off = ctx.params.v2("distortionMeshOffset"); let mesh_color = ctx.params.color("meshColor"); let src = b.img.clone(); diff --git a/crates/effects/src/lib.rs b/crates/effects/src/lib.rs index 6f6716306..8432a88d4 100644 --- a/crates/effects/src/lib.rs +++ b/crates/effects/src/lib.rs @@ -66,25 +66,36 @@ 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, LEVELS_CHANNELS, exposure_settings, fill_uses_masks, huesat_ranges_identity, levels_channel_ids, levels_channels_identity, levels_clip, + 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; +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::parse_mesh; +pub use distort2::{MAGNIFY_MODES, parse_mesh}; +pub use distort4::liquify_mesh; 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; // CPU helpers the GPU kernels share (effectcraft-gpu). pub use blur2::{camera_lens_plain, camera_lens_spans}; pub use generate::gen_mode; pub use generate2::pattern_kind as cell_pattern_kind; pub use noise::GrainLook; pub use transition::place_layer; +// effectcraft-gpu fx_noise. +pub use noise::remove_grain_level; +pub use noise3::{FractalGpu, fractal_gpu}; +pub use time_fx::{posterized_time, time_frames}; +// effectcraft-gpu fx_tone. +pub use ocio::color_stabilizer_maps; /// Effect categories in Effects & Presets order. pub const CATEGORIES: &[&str] = &[ @@ -806,6 +817,103 @@ pub const GPU_EFFECTS: &[&str] = &[ "ec.color.selectivecolor", "ec.key.linearcolor", "ec.keying.keylight", + // effectcraft-gpu fx_key (keying, matte, channel) + "ec.key.colorkey", + "ec.key.luma", + "ec.key.colorrange", + "ec.key.extract", + "ec.key.differencematte", + "ec.key.colordifference", + "ec.key.screen", + "ec.key.spill", + "ec.key.advancedspill", + "ec.key.keycleaner", + "ec.key.unmult", + "ec.matte.simplechoker", + "ec.matte.mattechoker", + "ec.matte.refinesoft", + "ec.matte.refinehard", + "ec.channel.setmatte", + "ec.channel.setchannels", + "ec.channel.shiftchannels", + "ec.channel.removecolormatting", + "ec.channel.arithmetic", + "ec.channel.solidcomposite", + "ec.channel.combiner", + "ec.channel.blend", + "ec.channel.calculations", + "ec.channel.compoundarithmetic", + // effectcraft-gpu fx_tone (colour correction) + "ec.color.levelsic", + "ec.color.gammapedestalgain", + "ec.color.photofilter", + "ec.color.changecolor", + "ec.color.changetocolor", + "ec.color.leavecolor", + "ec.color.broadcast", + "ec.color.colorbalancehls", + "ec.color.videolimiter", + "ec.color.psarbitrarymap", + "ec.color.cctoner", + "ec.color.cccoloroffset", + "ec.color.cckernel", + "ec.color.autolevels", + "ec.color.autocontrast", + "ec.color.autocolor", + "ec.color.equalize", + "ec.color.shadowhighlight", + "ec.color.cccolorneutralizer", + "ec.color.colorstabilizer", + // effectcraft-gpu fx_stylize (stylize, distort) + "ec.stylize.posterize", + "ec.stylize.threshold", + "ec.stylize.ccthreshold", + "ec.stylize.ccthresholdrgb", + "ec.stylize.strobe", + "ec.stylize.ccvignette", + "ec.stylize.scatter", + "ec.stylize.brushstrokes", + "ec.stylize.roughenedges", + "ec.stylize.texturize", + "ec.stylize.motiontile", + "ec.stylize.cckaleida", + "ec.stylize.ccrepetile", + "ec.distort.mirror", + "ec.distort.offset", + "ec.distort.polar", + "ec.distort.spherize", + "ec.distort.cornerpin", + "ec.distort.opticscompensation", + "ec.distort.magnify", + "ec.distort.ccslant", + "ec.distort.ccsmear", + "ec.distort.ccsplit", + "ec.distort.ccsplit2", + "ec.distort.cctiler", + "ec.distort.ccgriddler", + "ec.distort.liquify", + "ec.distort.twirllegacy", + "ec.distort.ccripplepulse", + "ec.distort.ccpowerpin", + "ec.distort.ccflomotion", + // effectcraft-gpu fx_noise (noise, blur, time) + "ec.noise.turbulent", + "ec.noise.median", + "ec.noise.medianlegacy", + "ec.noise.dustscratches", + "ec.noise.removegrain", + "ec.noise.noisealpha", + "ec.noise.noisehls", + "ec.noise.noisehlsauto", + "ec.blur.smart", + "ec.blur.bilateral", + "ec.blur.sharpen", + "ec.blur.unsharp", + "ec.blur.compound", + "ec.blur.channel", + "ec.channel.minimax", + "ec.time.echo", + "ec.time.posterizetime", ]; /// Effects whose output depends on [`EffectCtx::time`] directly (not only through animated diff --git a/crates/effects/src/misc.rs b/crates/effects/src/misc.rs index 89b496bb5..c119118db 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/noise.rs b/crates/effects/src/noise.rs index 4681fd991..d39fd6528 100644 --- a/crates/effects/src/noise.rs +++ b/crates/effects/src/noise.rs @@ -477,6 +477,28 @@ pub(crate) fn sampling_boxes(ctx: &EffectCtx, img: &Image, scale: f64) -> Vec<(u sample_boxes(img, n, s) } +/// The grain level (0.5..2) measured in the sample boxes of `img`. +fn grain_level(img: &Image, boxes: &[(usize, usize, usize)]) -> f32 { + let mut m = vec![false; img.data.len()]; + let w = img.width as usize; + for &(x, y, s) in boxes { + for yy in y..y + s { + for xx in x..x + s { + m[yy * w + xx] = true; + } + } + } + let st = crate::noise2::grain_stats_masked(img, Some(&m)); + let s = (st.std[0] + st.std[1] + st.std[2]) / 3.0; + if boxes.is_empty() { 1.0 } else { (s / 0.02).clamp(0.5, 2.0) } +} + +/// Remove Grain's measured grain level for buffer `b` without Temporal Filtering (the GPU +/// kernels' spatial pass, effectcraft-gpu `fx_noise`). +pub fn remove_grain_level(ctx: &EffectCtx, b: &Buf) -> f32 { + grain_level(&b.img, &sampling_boxes(ctx, &b.img, b.scale)) +} + fn remove_grain(ctx: &EffectCtx, mut b: Buf) -> Buf { let pr = ctx.params; let amt = pr.f("noiseReductionSettings/noiseReduction") as f32; @@ -535,20 +557,7 @@ fn remove_grain(ctx: &EffectCtx, mut b: Buf) -> Buf { } } // The grain level measured in the sample boxes scales the smoothing. - let level = { - let mut m = vec![false; b.img.data.len()]; - let w = b.img.width as usize; - for &(x, y, s) in &boxes { - for yy in y..y + s { - for xx in x..x + s { - m[yy * w + xx] = true; - } - } - } - let st = crate::noise2::grain_stats_masked(&b.img, Some(&m)); - let s = (st.std[0] + st.std[1] + st.std[2]) / 3.0; - if boxes.is_empty() { 1.0 } else { (s / 0.02).clamp(0.5, 2.0) } - }; + let level = grain_level(&b.img, &boxes); let passes = pr.f("noiseReductionSettings/passes").round().clamp(1.0, 8.0) as usize; let multichannel = pr.e("noiseReductionSettings/mode") == 0; let texture = (pr.f("fineTuning/texture") as f32).clamp(0.0, 1.0); diff --git a/crates/effects/src/noise3.rs b/crates/effects/src/noise3.rs index 6a952be1a..37bf853aa 100644 --- a/crates/effects/src/noise3.rs +++ b/crates/effects/src/noise3.rs @@ -326,6 +326,89 @@ impl Fractal { } } +/// Fractal / Turbulent Noise resolved for the GPU kernel (effectcraft-gpu `fx_noise`): the +/// per-pixel constants and, per octave, the terms [`Fractal::raw`] computes before touching the +/// pixel, in the same f32 order. +pub struct FractalGpu { + pub kind: u32, + pub noise_type: u32, + pub seed: u32, + pub invert: bool, + pub overflow: u32, + pub contrast: f32, + pub brightness: f32, + /// Rotation (sin, cos). + pub rot: (f32, f32), + pub size: [f32; 2], + /// Cycle Evolution: (t, cycle, t / cycle). + pub cycle: Option<[f32; 3]>, + pub sub_rotation: bool, + /// Σ amplitude × weight (the normaliser). + pub norm: f32, + /// Per octave: origin x, y, sub-rotation sin, cos, frequency, amplitude, weight, evolution + /// (turbulence applied), z offset, Dynamic Progressive warp factor. + pub octaves: Vec<[f32; 10]>, + /// Blending Mode (`None` = the noise alone), Opacity and Blend With Original. + pub mode: Option, + pub opacity: f32, + pub blend: f32, +} + +/// [`FractalGpu`] for a buffer with `offset` / `scale`. +pub fn fractal_gpu(ctx: &EffectCtx, offset: [f64; 2], scale: f64, turbulent: bool) -> FractalGpu { + let b = Buf { img: effectcraft_raster::Image::new(0, 0), offset, scale }; + let fr = Fractal::from_params(ctx, &b, turbulent); + let n_oct = fr.octaves.ceil() as usize; + let frac = fr.octaves - fr.octaves.floor(); + let (mut norm, mut amp, mut f, mut depth) = (0.0f32, 1.0f32, 1.0f32, 1.0f32); + let mut octaves = Vec::with_capacity(n_oct); + for o in 0..n_oct { + let w = if o + 1 == n_oct && frac > 0.0 { frac } else { 1.0 }; + let of = o as f32; + let k = if fr.perspective { depth } else { 1.0 } * (1.0 + fr.turbulence * of); + let mut org = fr.origin; + if k != 1.0 { + org = [fr.center[0] + (fr.origin[0] - fr.center[0]) * k, fr.center[1] + (fr.origin[1] - fr.center[1]) * k]; + } + if !fr.center_subscale { + org = [org[0] + fr.sub_offset[0] * of, org[1] + fr.sub_offset[1] * of]; + } + let (s, c) = (fr.sub_rotation * of).sin_cos(); + let evo = fr.evolution * (1.0 + fr.turbulence * of); + let dk = if fr.kind == 6 { 0.5 + of * 0.35 } else { 1.0 }; + octaves.push([org[0], org[1], s, c, f, amp, w, evo, of * 7.31, dk]); + norm += amp * w; + amp *= fr.influence; + f /= fr.sub_scaling; + depth *= fr.sub_scaling; + } + let cycle = fr.cycle.map(|c| { + let t = fr.evolution.rem_euclid(c); + [t, c, t / c] + }); + FractalGpu { + kind: fr.kind, + noise_type: fr.noise_type, + seed: fr.seed, + invert: fr.invert, + overflow: fr.overflow, + contrast: fr.contrast, + brightness: fr.brightness, + rot: fr.rot, + size: fr.size, + cycle, + sub_rotation: fr.sub_rotation != 0.0, + norm, + octaves, + mode: match ctx.params.get("blendingMode").map(Value::as_enum).unwrap_or(BLEND_NONE) { + BLEND_NONE => None, + m => Some(BLEND_MODES.get(m as usize).and_then(|l| BlendMode::from_name(l)).unwrap_or(BlendMode::Normal)), + }, + opacity: (ctx.params.f("opacity") as f32 / 100.0).clamp(0.0, 1.0), + blend: ctx.params.f("blend") as f32 / 100.0, + } +} + /// Map a value by an [`OVERFLOWS`] option. pub fn overflow(v: f32, mode: u32) -> f32 { match mode { diff --git a/crates/effects/src/ocio.rs b/crates/effects/src/ocio.rs index 6dd17c4fe..1969f50ad 100644 --- a/crates/effects/src/ocio.rs +++ b/crates/effects/src/ocio.rs @@ -905,6 +905,46 @@ fn curve3(xs: [f64; 3], ys: [f64; 3], v: f64) -> f64 { a.1 + (c.1 - a.1) * (v - a.0) / (c.0 - a.0).max(1e-9) } +/// Color Stabilizer's correction of buffer `b` as one piecewise-linear map per channel (points +/// with increasing x; the end segments extend), the same maps [`color_stabilizer`] applies. +/// `None` without a host or reference frame (the layer passes through). (The GPU effect.) +pub fn color_stabilizer_maps(ctx: &EffectCtx, b: &Buf) -> Option<[Vec<(f64, f64)>; 3]> { + let reference = ctx.env.host?.self_at(ctx.params.f("referenceFrame"), ctx.env.effect_index)?; + let r = ctx.params.f("sampleSize").max(0.5); + let pts = [ctx.params.v2("blackPoint"), ctx.params.v2("midPoint"), ctx.params.v2("whitePoint")]; + let cur = pts.map(|pt| sample_mean(b, pt, r)); + let refv = pts.map(|pt| sample_mean(&reference, pt, r)); + let mode = ctx.params.e("stabilize"); + Some([0, 1, 2].map(|k| { + let offset = |c0: f64, r0: f64| vec![(0.0, r0 - c0), (1.0, 1.0 + r0 - c0)]; + match mode { + 0 => offset(cur[0][k], refv[0][k]), + 1 => { + let (c0, c1, r0, r1) = (cur[0][k], cur[2][k], refv[0][k], refv[2][k]); + if (c1 - c0).abs() < 1e-6 { + offset(c0, r0) + } else if c0 < c1 { + vec![(c0, r0), (c1, r1)] + } else { + vec![(c1, r1), (c0, r0)] + } + } + _ => { + let mut v = vec![(0.0, 0.0)]; + let mut pairs: Vec<(f64, f64)> = (0..3).map(|i| (cur[i][k], refv[i][k])).collect(); + pairs.sort_by(|a, b| a.0.total_cmp(&b.0)); + for (x, y) in pairs { + if x > v.last().expect("non-empty").0 + 1e-4 && x < 1.0 - 1e-4 { + v.push((x, y)); + } + } + v.push((1.0, 1.0)); + v + } + } + })) +} + fn color_stabilizer(ctx: &EffectCtx, mut b: Buf) -> Buf { let Some(host) = ctx.env.host else { return b }; let Some(reference) = host.self_at(ctx.params.f("referenceFrame"), ctx.env.effect_index) else { return b }; diff --git a/crates/effects/src/stylize2.rs b/crates/effects/src/stylize2.rs index 06c09d2ef..b957b4e41 100644 --- a/crates/effects/src/stylize2.rs +++ b/crates/effects/src/stylize2.rs @@ -223,7 +223,8 @@ fn scatter(ctx: &EffectCtx, mut b: Buf) -> Buf { // ---- Strobe Light ---- -pub(crate) fn strobe_on(time: f64, duration: f64, period: f64, prob: f64, seed: u32) -> bool { +/// Whether Strobe Light flashes at `time` (also used by the GPU kernels). +pub fn strobe_on(time: f64, duration: f64, period: f64, prob: f64, seed: u32) -> bool { if period <= 0.0 { return false; } diff --git a/crates/effects/src/time_fx.rs b/crates/effects/src/time_fx.rs index e0c1aad79..e01283b9b 100644 --- a/crates/effects/src/time_fx.rs +++ b/crates/effects/src/time_fx.rs @@ -219,6 +219,12 @@ fn fetch(ctx: &EffectCtx, times: &[f64]) -> Option { Some(Frames { grid, imgs }) } +/// The layer's frames at layer times `times` placed on one grid (the GPU Echo / Posterize Time, +/// effectcraft-gpu `fx_noise`): (grid buffer, one image per time). `None` without a host. +pub fn time_frames(ctx: &EffectCtx, times: &[f64]) -> Option<(Buf, Vec)> { + fetch(ctx, times).map(|f| (f.grid, f.imgs)) +} + fn with_img(grid: Buf, img: Image) -> Buf { Buf { img, ..grid } } diff --git a/crates/engine/src/commands/panels_cmds.rs b/crates/engine/src/commands/panels_cmds.rs index eb0568781..325e0da02 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 365d7e555..c0eedaec4 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 b798d5dcb..5190ba823 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 afc7dc342..9b0a01da7 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 3e92fbd45..712549bde 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 000000000..ec0640ffa --- /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 5e77131c4..9756b183e 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 b6086d4ed..2c1828f85 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 d4c127d05..8ffa8dcb8 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/session_settings.rs b/crates/engine/src/session_settings.rs index 96cd5a298..ecb7f44ae 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 000000000..21dc7e454 --- /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_panels.rs b/crates/engine/src/tests_panels.rs index d989b28e2..8f5cb7f1b 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 794036269..41d8e86ea 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/export/Cargo.toml b/crates/export/Cargo.toml index 6900c2047..11f398cf2 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 b0566d9bf..167b5f332 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 000000000..f42ccda24 --- /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 f1e3269fd..ae889742a 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 af38eaeb3..de7c86841 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 000000000..69378e4ff --- /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 000000000..93a82719a --- /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/context.rs b/crates/gpu/src/context.rs index cb90fd9be..eac24208c 100644 --- a/crates/gpu/src/context.rs +++ b/crates/gpu/src/context.rs @@ -154,6 +154,10 @@ impl GpuContext { include_str!("shaders/fx_color.wgsl"), include_str!("shaders/fx_distort.wgsl"), include_str!("shaders/fx_generate.wgsl"), + include_str!("shaders/fx_key.wgsl"), + include_str!("shaders/fx_stylize.wgsl"), + include_str!("shaders/fx_noise.wgsl"), + include_str!("shaders/fx_tone.wgsl"), ] .concat(); let module = @@ -207,6 +211,10 @@ impl GpuContext { .chain(crate::fx_color::KERNELS) .chain(crate::fx_distort::KERNELS) .chain(crate::fx_generate::KERNELS) + .chain(crate::fx_key::KERNELS) + .chain(crate::fx_stylize::KERNELS) + .chain(crate::fx_noise::KERNELS) + .chain(crate::fx_tone::KERNELS) .map(|e| (e, &layout)) .chain(EXT_ENTRIES.iter().map(|e| (e, &layout_ext))) .map(|(e, layout)| { diff --git a/crates/gpu/src/effects.rs b/crates/gpu/src/effects.rs index 3691ae17e..0b97bc7a6 100644 --- a/crates/gpu/src/effects.rs +++ b/crates/gpu/src/effects.rs @@ -113,14 +113,24 @@ fn apply(e: &mut Enc, id: &str, ctx: &EffectCtx, b: GBuf) -> Option { "ec.blur.gaussian" => gaussian(e, ctx, b), "ec.blur.fastbox" => box_blur(e, ctx, b), "ec.blur.directional" => directional(e, ctx, b), + "ec.stylize.glow" if crate::fx_stylize::glow_extra(ctx) => crate::fx_stylize::glow(e, ctx, b), + "ec.distort.transform" if crate::fx_stylize::transform_blur(ctx) => crate::fx_stylize::transform(e, ctx, b), "ec.stylize.glow" => glow(e, ctx, b), "ec.perspective.dropshadow" => drop_shadow(e, ctx, b), "ec.distort.transform" => transform(e, ctx, b), "ec.color.curves" => curves(e, ctx, b), + "ec.color.huesaturation" if !effectcraft_effects::huesat_ranges_identity(ctx) => crate::fx_tone::hue_saturation(e, ctx, b), + "ec.color.levels" if !effectcraft_effects::levels_channels_identity(ctx) => crate::fx_tone::levels(e, ctx, b), + "ec.channel.invert" if (4..=11).contains(&ctx.params.e("channel")) => crate::fx_tone::invert(e, ctx, b), + "ec.noise.fractal" if crate::fx_noise::fractal_extra(ctx) => crate::fx_noise::fractal(e, ctx, b, false), _ if id.starts_with("ec.control.") => Some(b), _ if crate::fx_color::IDS.contains(&id) => crate::fx_color::apply(e, id, ctx, b), _ if crate::fx_distort::IDS.contains(&id) => crate::fx_distort::apply(e, id, ctx, b), _ if crate::fx_generate::IDS.contains(&id) => crate::fx_generate::apply(e, id, ctx, b), + _ if crate::fx_key::IDS.contains(&id) => crate::fx_key::apply(e, id, ctx, b), + _ if crate::fx_stylize::IDS.contains(&id) => crate::fx_stylize::apply(e, id, ctx, b), + _ if crate::fx_noise::IDS.contains(&id) => crate::fx_noise::apply(e, id, ctx, b), + _ if crate::fx_tone::IDS.contains(&id) => crate::fx_tone::apply(e, id, ctx, b), _ => pointwise(e, id, ctx, b), } } diff --git a/crates/gpu/src/fx_color.rs b/crates/gpu/src/fx_color.rs index ee6ab7e8f..f411e97e2 100644 --- a/crates/gpu/src/fx_color.rs +++ b/crates/gpu/src/fx_color.rs @@ -235,7 +235,7 @@ fn morph(e: &mut Enc, img: &GpuImage, r: u32, modes: [u32; 4]) -> GpuImage { } /// util::morph_frac (blend between the two nearest integer radii). -fn morph_frac(e: &mut Enc, img: &GpuImage, radius: f64, modes: [u32; 4]) -> GpuImage { +pub(crate) fn morph_frac(e: &mut Enc, img: &GpuImage, radius: f64, modes: [u32; 4]) -> GpuImage { if radius <= 0.0 { return img.clone(); } @@ -253,8 +253,8 @@ fn morph_frac(e: &mut Enc, img: &GpuImage, radius: f64, modes: [u32; 4]) -> GpuI out } -const ERODE_X: [u32; 4] = [0, 2, 2, 2]; -const DILATE_X: [u32; 4] = [1, 2, 2, 2]; +pub(crate) const ERODE_X: [u32; 4] = [0, 2, 2, 2]; +pub(crate) const DILATE_X: [u32; 4] = [1, 2, 2, 2]; // ---------------------------------------------------------------- Key Light diff --git a/crates/gpu/src/fx_key.rs b/crates/gpu/src/fx_key.rs new file mode 100644 index 000000000..8aedbd3d2 --- /dev/null +++ b/crates/gpu/src/fx_key.rs @@ -0,0 +1,630 @@ +//! GPU effects, keying, matte and channel family (kernels in `shaders/fx_key.wgsl`, every entry +//! point prefixed `fxk_`): the CPU effects' exact steps with the pixel loops as compute kernels. +//! +//! The keyers (Color Key, Luma Key, Color Range, Extract, Difference Matte, Color Difference +//! Key, Screen Key) build their matte in a plane pass, run the CPU's edge thin (morphology) and +//! feather (Gaussian) on it, and apply it in a second pass; spill suppression, Unmult and the +//! channel effects are one per-pixel pass (`fxk_point`). Simple / Matte Choker and Key Cleaner +//! work on the alpha plane and put it back with `util::set_alpha` (straight colour kept, the +//! transparent pixels' colour from a small blur). Advanced Spill Suppressor's Standard method +//! finds the dominant key primary with a GPU sum reduction (`fxk_reduce`). Other layers (layer +//! parameters) are resampled into the buffer like `util::fit_layer` (`fxk_fit`). + +use effectcraft_color::rgb_to_hsl; +use effectcraft_effects::EffectCtx; +use effectcraft_effects::util::{SRC_ORDER, Src}; + +use crate::context::{Enc, GpuImage, Params}; +use crate::effects::{GBuf, box_passes, gaussian_blur}; +use crate::fx_color::{DILATE_X, ERODE_X, morph_frac}; + +/// Compute entry points in `fx_key.wgsl`. +pub(crate) const KERNELS: &[&str] = &["fxk_point", "fxk_fit", "fxk_reduce"]; + +/// Effect ids implemented here. +pub(crate) const IDS: &[&str] = &[ + "ec.key.colorkey", + "ec.key.luma", + "ec.key.colorrange", + "ec.key.extract", + "ec.key.differencematte", + "ec.key.colordifference", + "ec.key.screen", + "ec.key.spill", + "ec.key.advancedspill", + "ec.key.keycleaner", + "ec.key.unmult", + "ec.matte.simplechoker", + "ec.matte.mattechoker", + "ec.matte.refinesoft", + "ec.matte.refinehard", + "ec.channel.setmatte", + "ec.channel.setchannels", + "ec.channel.shiftchannels", + "ec.channel.removecolormatting", + "ec.channel.arithmetic", + "ec.channel.solidcomposite", + "ec.channel.combiner", + "ec.channel.blend", + "ec.channel.calculations", + "ec.channel.compoundarithmetic", +]; + +/// Run effect `id` (one of [`IDS`]); `None` = this parameter combination runs on the CPU. +pub(crate) fn apply(e: &mut Enc, id: &str, ctx: &EffectCtx, b: GBuf) -> Option { + match id { + "ec.key.colorkey" | "ec.key.luma" | "ec.key.colorrange" | "ec.key.extract" => matte_key(e, id, ctx, b), + "ec.key.differencematte" => difference_matte(e, ctx, b), + "ec.key.screen" => screen_key(e, ctx, b), + "ec.key.advancedspill" => advanced_spill(e, ctx, b), + "ec.key.keycleaner" => key_cleaner(e, ctx, b), + "ec.matte.simplechoker" => simple_choker(e, ctx, b), + "ec.matte.mattechoker" => matte_choker(e, ctx, b), + "ec.matte.refinesoft" => refine(e, ctx, b, false), + "ec.matte.refinehard" => refine(e, ctx, b, true), + "ec.channel.setchannels" => set_channels(e, ctx, b), + _ => point(e, id, ctx, b), + } +} + +// ---------------------------------------------------------------- helpers + +/// One `fxk_point` pass over `src`'s size. +fn run(e: &mut Enc, p: &Params, src: &GpuImage, aux: Option<&GpuImage>, data: Option<&wgpu::Buffer>) -> GpuImage { + let out = e.image(src.width, src.height); + e.pixels("fxk_point", p, src, aux, &out, data); + out +} + +fn op(code: u32) -> Params { + let mut p = Params::default(); + p.u[0][0] = code; + p +} + +fn src_index(s: Src) -> u32 { + SRC_ORDER.iter().position(|x| *x == s).unwrap_or(10) as u32 +} + +/// Layer parameter `id` resampled into the buffer's pixel grid (`util::fit_layer`): +/// `Some(None)` when no layer is chosen (the effect then uses its own layer). +fn fitted(e: &mut Enc, ctx: &EffectCtx, b: &GBuf, id: &str, masks_and_effects: bool, stretch: bool) -> Option> { + let Some(o) = ctx.layer_param(id, masks_and_effects) else { return Some(None) }; + let (w, h) = (b.img.width, b.img.height); + if o.buf.img.is_empty() { + return Some(Some(e.image(w, h))); + } + let ls = ctx.layer_size; + let os = o.size; + let (sx, sy) = if stretch && ls[0] > 0.0 && ls[1] > 0.0 { (os[0] / ls[0], os[1] / ls[1]) } else { (1.0, 1.0) }; + let (dx, dy) = if stretch { (0.0, 0.0) } else { ((os[0] - ls[0]) * 0.5, (os[1] - ls[1]) * 0.5) }; + let inv = 1.0 / b.scale.max(1e-9); + // Per column / row: the other layer's buffer coordinate. + let mut data = Vec::with_capacity((w + h) as usize); + data.extend((0..w).map(|x| (((x as f64 + 0.5 - b.offset[0]) * inv * sx + dx) * o.buf.scale + o.buf.offset[0]) as f32)); + data.extend((0..h).map(|y| (((y as f64 + 0.5 - b.offset[1]) * inv * sy + dy) * o.buf.scale + o.buf.offset[1]) as f32)); + let src = e.g.upload_image(&o.buf.img)?; + let buf = e.data(&data); + let out = e.image(w, h); + e.pixels("fxk_fit", &Params::default(), &src, None, &out, Some(&buf)); + Some(Some(out)) +} + +/// `util::layer_or_self`. +fn layer_or_self(e: &mut Enc, ctx: &EffectCtx, b: &GBuf, id: &str, stretch: bool) -> Option { + Some(fitted(e, ctx, b, id, true, stretch)?.unwrap_or_else(|| b.img.clone())) +} + +/// keying::thin_feather on a matte plane (x channel). +fn thin_feather(e: &mut Enc, m: GpuImage, thin: f64, feather: f64) -> GpuImage { + let m = if thin > 0.0 { + morph_frac(e, &m, thin, ERODE_X) + } else if thin < 0.0 { + morph_frac(e, &m, -thin, DILATE_X) + } else { + m + }; + if feather > 0.0 { gaussian_blur(e, &m, feather * 0.5, feather * 0.5, true) } else { m } +} + +/// keying::apply_matte (view 0 final, 1 source, 2 matte). +fn apply_matte(e: &mut Enc, b: GBuf, m: &GpuImage, view: u32) -> GBuf { + if view == 1 { + return b; + } + let mut p = op(5); + p.u[0][1] = view; + let img = run(e, &p, &b.img, Some(m), None); + GBuf { img, ..b } +} + +/// keying::primary. +fn primary(c: [f32; 4]) -> [u32; 3] { + if c[1] >= c[0] && c[1] >= c[2] { + [1, 0, 2] + } else if c[2] >= c[0] && c[2] >= c[1] { + [2, 0, 1] + } else { + [0, 1, 2] + } +} + +/// util::set_alpha with the new alpha in `na`'s x channel. +fn set_alpha(e: &mut Enc, img: &GpuImage, na: &GpuImage) -> GpuImage { + let fill = gaussian_blur(e, img, 3.0, 3.0, true); + let (rows, row) = e.image_rows(&fill); + let mut p = op(27); + p.u[3][0] = row; + run(e, &p, img, Some(na), Some(&rows)) +} + +// ---------------------------------------------------------------- keyers + +fn matte_key(e: &mut Enc, id: &str, ctx: &EffectCtx, b: GBuf) -> Option { + let pr = ctx.params; + let f = |k: &str| pr.f(k) as f32; + let mut p = Params::default(); + let mut thin = 0.0; + let mut feather = 0.0; + match id { + "ec.key.colorkey" => { + let k = pr.color("keyColor"); + p.u[0][0] = 1; + p.f[0] = [k[0], k[1], k[2], f("colorTolerance") / 255.0]; + thin = pr.f("edgeThin") * b.scale; + feather = pr.f("edgeFeather") * b.scale; + } + "ec.key.luma" => { + p.u[0] = [2, pr.e("keyType"), 0, 0]; + p.f[0] = [f("threshold") / 255.0, f("tolerance") / 255.0, 0.0, 0.0]; + thin = pr.f("edgeThin") * b.scale; + feather = pr.f("edgeFeather") * b.scale; + } + "ec.key.colorrange" => { + p.u[0] = [3, pr.e("colorSpace"), 0, 0]; + p.f[0] = [f("minL"), f("minA"), f("minB"), 0.0]; + p.f[1] = [f("maxL"), f("maxA"), f("maxB"), 0.0]; + p.f[2][0] = f("fuzziness"); + } + _ => { + p.u[0] = [4, pr.e("channel"), pr.b("invert") as u32, 0]; + p.f[0] = [f("blackPoint"), f("whitePoint"), f("blackSoftness"), f("whiteSoftness")]; + } + } + let m = run(e, &p, &b.img, None, None); + let m = thin_feather(e, m, thin, feather); + Some(apply_matte(e, b, &m, 0)) +} + +fn difference_matte(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option { + let pr = ctx.params; + let view = pr.e("view"); + if view == 1 { + return Some(b); + } + let blur = pr.f("blurBeforeDifference") * b.scale; + let other = layer_or_self(e, ctx, &b, "differenceLayer", pr.e("ifLayerSizesDiffer") == 1)?; + let (src, other) = if blur > 0.05 { + (gaussian_blur(e, &b.img, blur * 0.5, blur * 0.5, true), gaussian_blur(e, &other, blur * 0.5, blur * 0.5, true)) + } else { + (b.img.clone(), other) + }; + let mut p = op(6); + p.f[0] = [(pr.f("matchingTolerance") / 100.0) as f32, (pr.f("matchingSoftness") / 100.0) as f32, 0.0, 0.0]; + let m = run(e, &p, &src, Some(&other), None); + Some(apply_matte(e, b, &m, if view == 2 { 3 } else { 4 })) +} + +/// keying::secondary's screen-key matte and composite. +fn screen_key(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option { + let pr = ctx.params; + let view = pr.e("view"); + if view == 3 { + return Some(b); + } + let sc = pr.color("screenColor"); + let ix = primary(sc); + let (pi, o1, o2) = (ix[0] as usize, ix[1] as usize, ix[2] as usize); + let gain = pr.f("screenGain") as f32 / 100.0; + let bal = pr.f("screenBalance") as f32 / 100.0; + let cb = pr.f("clipBlack") as f32 / 100.0; + let cw = pr.f("clipWhite") as f32 / 100.0; + let preblur = pr.f("screenPreblur") * b.scale; + let sg = pr.f("screenShrinkGrow") * b.scale; + let soft = pr.f("screenSoftness") * b.scale; + let (hi, lo) = if sc[o1] > sc[o2] { (sc[o1], sc[o2]) } else { (sc[o2], sc[o1]) }; + let sd = (sc[pi] - (bal * hi + (1.0 - bal) * lo)).max(1e-3); + let key_src = if preblur > 0.0 { gaussian_blur(e, &b.img, preblur * 0.5, preblur * 0.5, true) } else { b.img.clone() }; + let mut p = op(22); + p.u[1] = [ix[0], ix[1], ix[2], 0]; + p.f[0] = [sd, gain, bal, cb]; + p.f[1][0] = cw; + let mut m = run(e, &p, &key_src, None, None); + if sg < 0.0 { + m = morph_frac(e, &m, -sg, ERODE_X); + } else if sg > 0.0 { + m = morph_frac(e, &m, sg, DILATE_X); + } + if soft > 0.0 { + m = gaussian_blur(e, &m, soft * 0.5, soft * 0.5, true); + } + let mut p = op(23); + p.u[0] = [23, view, (pr.e("edgeColor") == 1) as u32, 0]; + p.u[1] = [ix[0], ix[1], ix[2], 0]; + p.f[0] = [sc[0], sc[1], sc[2], bal]; + p.f[1][0] = pr.f("despill") as f32 / 100.0; + let img = run(e, &p, &b.img, Some(&m), None); + Some(GBuf { img, ..b }) +} + +fn advanced_spill(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option { + let pr = ctx.params; + let ultra = pr.e("method") == 1; + let key = pr.color("ultraSettings/keyColor"); + let mut p = op(8); + let sums = if ultra { + let ix = primary(key); + p.u[1] = [ix[0], ix[1], ix[2], 0]; + None + } else { + // Standard: the dominant key primary over the whole buffer (sum reduction). + p.u[1][3] = 1; + let mut cur = b.img.clone(); + let mut first = true; + while first || cur.width > 1 || cur.height > 1 { + let out = e.image(cur.width.div_ceil(16), cur.height.div_ceil(16)); + let mut rp = Params::default(); + rp.u[0][0] = first as u32; + e.pixels("fxk_reduce", &rp, &cur, None, &out, None); + cur = out; + first = false; + } + Some(cur) + }; + let tol = if ultra { pr.get("ultraSettings/tolerance").map(|v| v.as_f64()).unwrap_or(100.0) as f32 / 100.0 } else { 1.0 }; + let f = |k: &str| pr.f(k) as f32 / 100.0; + let (desat, neutral) = if ultra { (f("ultraSettings/desaturate"), f("ultraSettings/spillColorCorrection")) } else { (0.0, 0.0) }; + p.f[0] = [f("suppression"), f("ultraSettings/spillRange"), f("ultraSettings/lumaCorrection"), tol]; + p.f[1] = [desat, neutral, rgb_to_hsl(key[0], key[1], key[2]).0, 0.0]; + let img = run(e, &p, &b.img, sums.as_ref(), None); + Some(GBuf { img, ..b }) +} + +fn key_cleaner(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option { + let pr = ctx.params; + let r = (pr.f("additionalEdgeRadius") * b.scale).max(0.0); + let strength = pr.f("strength") as f32 / 100.0; + let chatter = pr.b("reduceChatter"); + let mut a = run(e, &op(24), &b.img, None, None); + if chatter { + a = gaussian_blur(e, &a, 1.0, 1.0, true); + } + let est = (r > 0.0 && strength > 0.0).then(|| gaussian_blur(e, &b.img, r * 0.5, r * 0.5, true)); + let rows = est.as_ref().map(|img| e.image_rows(img)); + let mut p = op(25); + p.u[0][1] = chatter as u32; + p.u[3] = [rows.as_ref().map_or(0, |r| r.1), rows.is_some() as u32, 0, 0]; + p.f[0] = [pr.f("alphaContrast") as f32 / 100.0, strength, 0.0, 0.0]; + let img = run(e, &p, &b.img, Some(&a), rows.as_ref().map(|r| &r.0)); + Some(GBuf { img, ..b }) +} + +// ---------------------------------------------------------------- mattes + +fn simple_choker(e: &mut Enc, ctx: &EffectCtx, mut b: GBuf) -> Option { + let c = ctx.params.f("chokeMatte") * b.scale; + if c.abs() > 1e-6 { + if c < 0.0 && !ctx.adjustment { + b.pad(e, (-c).ceil() as u32 + 1)?; + } + let a = run(e, &op(24), &b.img, None, None); + let na = if c > 0.0 { morph_frac(e, &a, c, ERODE_X) } else { morph_frac(e, &a, -c, DILATE_X) }; + b.img = set_alpha(e, &b.img, &na); + } + if ctx.params.e("view") == 1 { + b.img = run(e, &op(28), &b.img, None, None); + } + Some(b) +} + +fn matte_choker(e: &mut Enc, ctx: &EffectCtx, mut b: GBuf) -> Option { + let pr = ctx.params; + let g1 = pr.f("geometricSoftness1") * b.scale; + let c1 = pr.f("choke1") as f32; + let s1 = pr.f("grayLevelSoftness1") as f32 / 100.0; + let g2 = pr.f("geometricSoftness2") * b.scale; + let c2 = pr.f("choke2") as f32; + let s2 = pr.f("grayLevelSoftness2") as f32 / 100.0; + let it = pr.f("iterations").round().clamp(1.0, 100.0) as usize; + if (c1 < 0.0 || c2 < 0.0 || g1 > 0.0 || g2 > 0.0) && !ctx.adjustment { + b.pad(e, ((g1 + g2) * 1.5 * it as f64).ceil() as u32 + 1)?; + } + let mut a = run(e, &op(24), &b.img, None, None); + let stage = |e: &mut Enc, a: GpuImage, geo: f64, choke: f32, gray: f32| -> GpuImage { + let a = if geo > 0.0 { gaussian_blur(e, &a, geo * 0.5, geo * 0.5, true) } else { a }; + let t = 0.5 + choke / 255.0 * 0.5; + let w = gray.max(1e-3); + if (t - 0.5).abs() < 1e-6 && (w - 1.0).abs() < 1e-6 { + return a; + } + let mut p = op(26); + p.f[0] = [t, w, 0.0, 0.0]; + run(e, &p, &a, None, None) + }; + for _ in 0..it { + a = stage(e, a, g1, c1, s1); + a = stage(e, a, g2, c2, s2); + } + b.img = set_alpha(e, &b.img, &a); + Some(b) +} + +/// matte::refine (Refine Soft / Hard Matte) for one frame: Reduce Chatter, the matte motion +/// blur (they read neighbouring frames) and Invert run on the CPU. +fn refine(e: &mut Enc, ctx: &EffectCtx, b: GBuf, hard: bool) -> Option { + let pr = ctx.params; + // Invert gives alpha to the empty areas, whose colour then comes from a blur of nearly + // transparent pixels (util::set_alpha): ill-conditioned, so it renders on the CPU. + if pr.f("reduceChatter") > 0.0 || pr.b("useMotionBlur") || pr.b("invert") { + return None; + } + let dk = |id: &str| if hard { id.to_string() } else { format!("decontamination/{id}") }; + let r = (pr.f("radius") * b.scale).round().max(1.0) as usize; + let smooth = pr.f("smooth") as f32 / 100.0; + let feather = pr.f("feather") / 100.0 * r as f64 * 0.5; + let choke = (pr.f("choke") - if hard { 0.0 } else { pr.f("shiftEdge") }).clamp(-100.0, 100.0) as f32 / 100.0 * 0.5; + let decon = pr.b("decontaminate"); + let decon_amt = pr.f(&dk("decontaminationAmount")) as f32 / 100.0; + let extend = pr.b(&dk("extendWhereSmoothed")); + let decon_r = r as f64 + pr.f(&dk("increaseDecontaminationRadius")).max(0.0) * b.scale; + let view_map = pr.b(&dk("viewDecontaminationMap")); + let contrast = if hard { pr.f("alphaContrast") as f32 / 100.0 } else { 1.0 + pr.f("contrast").max(0.0) as f32 / 100.0 * 3.0 }; + let edge_details = hard || pr.b("calculateEdgeDetails"); + let view_edges = !hard && pr.b("viewEdgeRegion"); + let (w, h) = (b.img.width, b.img.height); + + // (luminance guide, alpha, guide × alpha, guide²); the edge band from the alpha's range. + let pack = run(e, &op(29), &b.img, None, None); + let hi = morph_frac(e, &pack, r as f64, [2, 1, 2, 2]); + let lo = morph_frac(e, &pack, r as f64, [2, 0, 2, 2]); + let band = run(e, &op(32), &hi, Some(&lo), None); + let mut na = if edge_details { + let eps = 1e-4 + smooth * smooth * 0.05; + let means = box_passes(e, &pack, &[r], &[r], true); + let mut p = op(30); + p.f[0][0] = eps; + let coef = run(e, &p, &means, None, None); + let coef = box_passes(e, &coef, &[r], &[r], true); + let (rows, row) = e.image_rows(&band); + let mut p = op(31); + p.u[3][0] = row; + run(e, &p, &coef, Some(&pack), Some(&rows)) + } else { + let a = run(e, &op(24), &b.img, None, None); + if smooth > 0.0 { + let s = smooth as f64 * r as f64 * 0.5; + gaussian_blur(e, &a, s, s, true) + } else { + a + } + }; + if feather > 0.0 { + na = gaussian_blur(e, &na, feather, feather, true); + } + let mut p = op(33); + p.u[0][1] = pr.b("invert") as u32; + p.f[0] = [choke, contrast, 0.0, 0.0]; + let na = run(e, &p, &na, None, None); + if view_edges { + let img = run(e, &op(34), &na, Some(&band), None); + return Some(GBuf { img, ..b }); + } + let est = (decon && decon_amt > 0.0 && !view_map).then(|| gaussian_blur(e, &b.img, decon_r * 0.5, decon_r * 0.5, true)); + let rows = est.as_ref().map(|img| e.image_rows(img)); + let mut p = op(35); + p.u[0] = [35, view_map as u32, (decon && (view_map || decon_amt > 0.0)) as u32, extend as u32]; + p.u[3] = [rows.as_ref().map_or(0, |r| r.1), rows.is_some() as u32, 0, 0]; + p.f[0][0] = decon_amt; + let img = run(e, &p, &b.img, Some(&na), rows.as_ref().map(|r| &r.0)); + if view_map { + return Some(GBuf { img, ..b }); + } + debug_assert_eq!((img.width, img.height), (w, h)); + let img = set_alpha(e, &img, &na); + Some(GBuf { img, ..b }) +} + +// ---------------------------------------------------------------- channel + +const SET_CHANNEL_LAYERS: [&str; 4] = ["sourceLayer1", "sourceLayer2", "sourceLayer3", "sourceLayer4"]; + +const SHIFT_ORDER: [Src; 11] = + [Src::Alpha, Src::Red, Src::Green, Src::Blue, Src::Luminance, Src::Hue, Src::Lightness, Src::Saturation, Src::Full, Src::Half, Src::Off]; + +const MATTE_ORDER: [Src; 10] = [Src::Red, Src::Green, Src::Blue, Src::Alpha, Src::Luminance, Src::Hue, Src::Lightness, Src::Saturation, Src::Full, Src::Off]; + +fn set_channels(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option { + let pr = ctx.params; + let s = ["setRedTo", "setGreenTo", "setBlueTo", "setAlphaTo"].map(|id| src_index(effectcraft_effects::util::src_at(pr.e(id)))); + let stretch = pr.b("stretchLayersToFit"); + let mut layers = vec![]; + for id in SET_CHANNEL_LAYERS { + layers.push(fitted(e, ctx, &b, id, true, stretch)?); + } + if layers.iter().all(Option::is_none) { + return route(e, b, s); + } + let mut vals = e.image(b.img.width, b.img.height); + for (k, layer) in layers.iter().enumerate() { + let mut p = op(20); + p.u[0] = [20, k as u32, s[k], 0]; + vals = run(e, &p, &vals, Some(layer.as_ref().unwrap_or(&b.img)), None); + } + let img = run(e, &op(21), &vals, None, None); + Some(GBuf { img, ..b }) +} + +/// channel::route. +fn route(e: &mut Enc, b: GBuf, s: [u32; 4]) -> Option { + if s == [0, 1, 2, 3] { + return Some(b); + } + let mut p = op(11); + p.u[1] = s; + let img = run(e, &p, &b.img, None, None); + Some(GBuf { img, ..b }) +} + +fn point(e: &mut Enc, id: &str, ctx: &EffectCtx, b: GBuf) -> Option { + let pr = ctx.params; + let f = |k: &str| pr.f(k) as f32; + let mut p = Params::default(); + let mut aux = None; + match id { + "ec.key.spill" => { + let k = pr.color("colorToSuppress"); + let amt = f("suppression") / 100.0; + if pr.e("colorAccuracy") == 1 { + let kg = (k[0] + k[1] + k[2]) / 3.0; + let kc = [k[0] - kg, k[1] - kg, k[2] - kg]; + let kn = (kc[0] * kc[0] + kc[1] * kc[1] + kc[2] * kc[2]).sqrt(); + if kn < 1e-5 { + return Some(b); + } + p.u[0] = [7, 1, 0, 0]; + p.f[0] = [kc[0] / kn, kc[1] / kn, kc[2] / kn, amt]; + } else { + let ix = primary(k); + p.u[0] = [7, 0, 0, 0]; + p.u[1] = [ix[0], ix[1], ix[2], 0]; + p.f[0][3] = amt; + } + } + "ec.key.colordifference" => { + let view = pr.e("view"); + if view == 0 { + return Some(b); + } + let accurate = pr.e("colorMatchingAccuracy") == 1; + let lin = |c: [f32; 3]| if accurate { c.map(|v| effectcraft_color::srgb_to_linear(v.clamp(0.0, 1.0))) } else { c }; + let k = pr.color("keyColor"); + let key = lin([k[0], k[1], k[2]]); + let ix = primary([key[0], key[1], key[2], 1.0]); + let (pi, o1, o2) = (ix[0] as usize, ix[1] as usize, ix[2] as usize); + let lv = |pre: &str| ["InBlack", "InWhite", "Gamma", "OutBlack", "OutWhite"].map(|s| f(&format!("{pre}{s}"))); + let (pa, pb) = (lv("partialA"), lv("partialB")); + p.u[0] = [9, view, accurate as u32, 0]; + p.u[1] = [ix[0], ix[1], ix[2], 0]; + p.f[0] = [(key[pi] - key[o1]).max(1e-3), (key[pi] - key[o2]).max(1e-3), f("blackLevel"), f("whiteLevel")]; + p.f[1] = [pa[0], pa[1], pa[2], pa[3]]; + p.f[2] = [pa[4], pb[4], f("gamma"), 0.0]; + p.f[3] = [pb[0], pb[1], pb[2], pb[3]]; + } + "ec.key.unmult" => { + let white = pr.e("backgroundColor") == 1; + let level = if white { pr.f("whiteLevel") } else { pr.f("blackLevel") }.clamp(0.0, 100.0) as f32 / 100.0; + p.u[0] = [10, white as u32, pr.b("removeColorMatting") as u32, pr.b("clipHdrResults") as u32]; + p.f[0] = [level, pr.f("softness").clamp(0.0, 100.0) as f32 / 100.0, 0.0, 0.0]; + } + "ec.channel.shiftchannels" => { + let s = ["takeRedFrom", "takeGreenFrom", "takeBlueFrom", "takeAlphaFrom"].map(|id| src_index(SHIFT_ORDER[(pr.e(id) as usize).min(10)])); + return route(e, b, s); + } + "ec.channel.removecolormatting" => { + p.u[0] = [12, pr.b("clipHdr") as u32, 0, 0]; + p.f[0] = pr.color("backgroundColor"); + } + "ec.channel.arithmetic" => { + p.u[0] = [13, pr.e("operator"), pr.b("clip") as u32, 0]; + p.f[0] = [f("redValue") / 255.0, f("greenValue") / 255.0, f("blueValue") / 255.0, 0.0]; + } + "ec.channel.solidcomposite" => { + const MODES: [effectcraft_color::BlendMode; 12] = { + use effectcraft_color::BlendMode::*; + [Normal, Add, Multiply, Screen, Overlay, SoftLight, HardLight, Darken, Lighten, Difference, Color, Luminosity] + }; + let c = pr.color("color"); + let opa = (f("opacity") / 100.0 * c[3]).clamp(0.0, 1.0); + p.u[0] = [14, crate::ops::mode_id(MODES[(pr.e("blendingMode") as usize).min(11)]), 0, 0]; + p.f[0] = [c[0] * opa, c[1] * opa, c[2] * opa, opa]; + p.f[1][0] = f("sourceOpacity") / 100.0; + } + "ec.channel.setmatte" => { + let src = MATTE_ORDER[(pr.e("takeMatteFrom") as usize).min(9)]; + aux = fitted(e, ctx, &b, "takeMatteFromLayer", true, pr.b("stretchMatteToFit"))?; + let pre_ok = !matches!(src, Src::Alpha | Src::Full | Src::Off); + let flags = pr.b("invertMatte") as u32 + | (pr.b("compositeMatteWithOriginal") as u32) << 1 + | ((pr.b("premultiplyMatteLayer") && pre_ok) as u32) << 2 + | (aux.is_none() as u32) << 3 + | ((src == Src::Alpha) as u32) << 4; + p.u[0] = [15, src_index(src), flags, 0]; + } + "ec.channel.combiner" => { + if pr.b("useSecondLayer") { + aux = Some(layer_or_self(e, ctx, &b, "sourceLayer", true)?); + } + let flags = pr.b("invert") as u32 | (pr.b("solidAlpha") as u32) << 1 | (aux.is_some() as u32) << 2; + p.u[0] = [16, pr.e("from"), pr.e("to"), flags]; + } + "ec.channel.blend" => { + let orig = (f("blendWithOriginal") / 100.0).clamp(0.0, 1.0); + if orig >= 1.0 { + return Some(b); + } + aux = Some(layer_or_self(e, ctx, &b, "blendWithLayer", pr.e("ifLayerSizesDiffer") == 1)?); + p.u[0] = [17, pr.e("mode"), 0, 0]; + p.f[0][0] = orig; + } + "ec.channel.calculations" => { + const MODES: [effectcraft_color::BlendMode; 22] = { + use effectcraft_color::BlendMode::*; + [ + Normal, + Darken, + Multiply, + ColorBurn, + LinearBurn, + Add, + Lighten, + Screen, + ColorDodge, + LinearDodge, + Overlay, + SoftLight, + HardLight, + LinearLight, + VividLight, + PinLight, + HardMix, + Difference, + Exclusion, + Hue, + Saturation, + Luminosity, + ] + }; + if pr.get("secondLayer").and_then(|v| v.as_layer()).is_some() { + aux = Some(layer_or_self(e, ctx, &b, "secondLayer", pr.b("stretchSecondLayerToFit"))?); + } + let flags = + pr.b("invertInput") as u32 | (pr.b("invertSecondLayer") as u32) << 1 | (pr.b("preserveTransparency") as u32) << 2 | (aux.is_some() as u32) << 3; + let mode = MODES[(pr.e("blendingMode") as usize).min(MODES.len() - 1)]; + p.u[0] = [18, pr.e("inputChannel"), pr.e("secondLayerChannel"), crate::ops::mode_id(mode)]; + p.u[1][0] = flags; + p.f[0][0] = (f("secondLayerOpacity") / 100.0).clamp(0.0, 1.0); + } + "ec.channel.compoundarithmetic" => { + let orig = (f("blendWithOriginal") / 100.0).clamp(0.0, 1.0); + if orig >= 1.0 { + return Some(b); + } + aux = Some(layer_or_self(e, ctx, &b, "secondSource", pr.b("stretchSecondSourceToFit"))?); + p.u[0] = [19, pr.e("operator"), pr.e("operateOnChannels"), pr.e("overflowBehavior")]; + p.f[0][0] = orig; + } + _ => return None, + } + let img = run(e, &p, &b.img, aux.as_ref(), None); + Some(GBuf { img, ..b }) +} diff --git a/crates/gpu/src/fx_noise.rs b/crates/gpu/src/fx_noise.rs new file mode 100644 index 000000000..eaa7324a6 --- /dev/null +++ b/crates/gpu/src/fx_noise.rs @@ -0,0 +1,581 @@ +//! GPU effects, noise, blur and time family (kernels in `shaders/fx_noise.wgsl`, every entry point prefixed +//! `fxn_`): the CPU effects' exact steps with the pixel loops as compute kernels. +//! +//! Median, Median (Legacy) and Dust & Scratches take the median of the 512-level quantised +//! channels by bisection over the window (radius ≤ [`MEDIAN_MAX_R`] on the GPU). Fractal and +//! Turbulent Noise resolve every per-octave term on the CPU in the CPU's f32 order +//! (`effectcraft_effects::fractal_gpu`) and evaluate the pixels on the GPU. Echo and Posterize +//! Time fetch the layer's other frames through the effect host like the CPU does, then +//! accumulate on the GPU. Remove Grain measures the grain level in its sample boxes on the CPU +//! (one readback) and runs the guided filters, Texture, Unsharp Mask and Preview on the GPU; +//! Temporal Filtering and the Noise Samples / Blending Matte views render on the CPU. + +use effectcraft_color::BlendMode; +use effectcraft_effects::{Buf, EffectCtx}; +use effectcraft_keyframe::Value; + +use crate::context::{Enc, GpuImage, Params}; +use crate::effects::{GBuf, box_passes, gaussian_blur}; + +/// Compute entry points in `fx_noise.wgsl`. +pub(crate) const KERNELS: &[&str] = &[ + "fxn_quant", + "fxn_median", + "fxn_median_out", + "fxn_edge", + "fxn_smart_edges", + "fxn_unsharp", + "fxn_pick", + "fxn_cblur_out", + "fxn_straight", + "fxn_premul", + "fxn_morph", + "fxn_crop", + "fxn_cb_map", + "fxn_cb_level", + "fxn_echo", + "fxn_echo_fin", + "fxn_fractal", + "fxn_noise_alpha", + "fxn_noise_hls", + "fxn_gf_guide", + "fxn_gf", + "fxn_rg_texture", + "fxn_rg_unsharp", + "fxn_rg_out", +]; + +/// Effect ids implemented here (Fractal Noise's other modes come through [`fractal_extra`]). +pub(crate) const IDS: &[&str] = &[ + "ec.noise.turbulent", + "ec.noise.median", + "ec.noise.medianlegacy", + "ec.noise.dustscratches", + "ec.noise.removegrain", + "ec.noise.noisealpha", + "ec.noise.noisehls", + "ec.noise.noisehlsauto", + "ec.blur.smart", + "ec.blur.bilateral", + "ec.blur.sharpen", + "ec.blur.unsharp", + "ec.blur.compound", + "ec.blur.channel", + "ec.channel.minimax", + "ec.time.echo", + "ec.time.posterizetime", +]; + +/// Largest median radius (buffer pixels) the GPU takes; larger ones render on the CPU. +pub(crate) const MEDIAN_MAX_R: usize = 16; + +/// Run effect `id` (one of [`IDS`]); `None` = this parameter combination runs on the CPU. +pub(crate) fn apply(e: &mut Enc, id: &str, ctx: &EffectCtx, b: GBuf) -> Option { + match id { + "ec.noise.turbulent" => fractal(e, ctx, b, true), + "ec.noise.median" | "ec.noise.medianlegacy" | "ec.noise.dustscratches" => median(e, id, ctx, b), + "ec.noise.removegrain" => remove_grain(e, ctx, b), + "ec.noise.noisealpha" => noise_alpha(e, ctx, b), + "ec.noise.noisehls" => noise_hls(e, ctx, b, ctx.params.f("noisePhase") as f32 / 360.0), + "ec.noise.noisehlsauto" => noise_hls(e, ctx, b, ctx.time as f32 * ctx.params.f("noiseAnimationSpeed") as f32), + "ec.blur.smart" => smart_blur(e, ctx, b), + "ec.blur.bilateral" => bilateral(e, ctx, b), + "ec.blur.sharpen" => { + let s = b.scale; + unsharp(e, b, ctx.params.f("amount") as f32 / 50.0, s, 0.0) + } + "ec.blur.unsharp" => { + let s = (ctx.params.f("radius") * b.scale).max(0.1); + unsharp(e, b, ctx.params.f("amount") as f32 / 100.0, s, ctx.params.f("threshold") as f32) + } + "ec.blur.compound" => compound_blur(e, ctx, b), + "ec.blur.channel" => channel_blur(e, ctx, b), + "ec.channel.minimax" => minimax(e, ctx, b), + "ec.time.echo" => echo(e, ctx, b), + "ec.time.posterizetime" => posterize_time(e, ctx, b), + _ => None, + } +} + +/// A same-size per-pixel kernel. +fn run(e: &mut Enc, entry: &str, p: &Params, src: &GpuImage, aux: Option<&GpuImage>, data: Option<&wgpu::Buffer>) -> GpuImage { + let out = e.image(src.width, src.height); + e.pixels(entry, p, src, aux, &out, data); + out +} + +fn with_u0(u: [u32; 4]) -> Params { + let mut p = Params::default(); + p.u[0] = u; + p +} + +// ---------------------------------------------------------------- Fractal / Turbulent Noise (noise3.rs) + +/// Fractal Noise settings the `pointwise` kernel (effects.rs) leaves to the CPU, which this +/// family's kernel covers: every Noise and Fractal Type, Cycle Evolution, Perspective Offset, +/// Sub Rotation / Offset and all Blending Modes. +pub(crate) fn fractal_extra(ctx: &EffectCtx) -> bool { + let pr = ctx.params; + pr.e("noiseType") != 2 + || pr.e("fractalType") > 2 + || pr.e("blendingMode") > 1 + || pr.b("evolutionOptions/cycleEvolution") + || pr.b("transform/perspectiveOffset") + || pr.f("subSettings/subRotation") != 0.0 + || (!pr.b("subSettings/centerSubscale") && pr.v2("subSettings/subOffset") != [0.0, 0.0]) +} + +/// noise3::render. +pub(crate) fn fractal(e: &mut Enc, ctx: &EffectCtx, b: GBuf, turbulent: bool) -> Option { + let fr = effectcraft_effects::fractal_gpu(ctx, b.offset, b.scale, turbulent); + let mode = match fr.mode { + None => 255, + Some(m) => BlendMode::ALL.iter().position(|&x| x == m).unwrap_or(0) as u32, + }; + let data: Vec = fr.octaves.iter().flatten().copied().collect(); + let cycle = fr.cycle.unwrap_or([0.0; 3]); + let mut p = Params::default(); + p.u[0] = [fr.kind, fr.noise_type, fr.octaves.len() as u32, fr.seed]; + p.u[1] = [fr.sub_rotation as u32, fr.cycle.is_some() as u32, fr.invert as u32, fr.overflow]; + p.u[2][0] = mode; + p.f[0] = [fr.rot.0, fr.rot.1, fr.size[0], fr.size[1]]; + p.f[1] = [fr.contrast, fr.brightness, fr.opacity, fr.blend]; + p.f[2] = [cycle[0], cycle[1], cycle[2], fr.norm]; + let buf = e.data(&data); + let img = run(e, "fxn_fractal", &p, &b.img, None, Some(&buf)); + Some(GBuf { img, ..b }) +} + +// ---------------------------------------------------------------- Median family (noise.rs, noise2.rs) + +/// noise::median_image (of the straight colour with alpha 1 when `straight`). +fn median_image(e: &mut Enc, img: &GpuImage, r: usize, straight: bool) -> GpuImage { + let q = run(e, "fxn_quant", &with_u0([straight as u32, 0, 0, 0]), img, None, None); + run(e, "fxn_median", &with_u0([r as u32, 0, 0, 0]), &q, None, None) +} + +fn median(e: &mut Enc, id: &str, ctx: &EffectCtx, b: GBuf) -> Option { + let r = (ctx.params.f("radius") * b.scale).round().max(0.0) as usize; + if r == 0 { + return Some(b); + } + if r > MEDIAN_MAX_R { + return None; + } + let (mode, on_alpha, straight) = match id { + "ec.noise.medianlegacy" => { + let on = ctx.params.b("operateOnAlphaChannel"); + (if on { 2 } else { 3 }, on, !on) + } + "ec.noise.dustscratches" => (1, ctx.params.b("operateOnAlpha"), false), + _ => (0, ctx.params.b("operateOnAlpha"), false), + }; + let m = median_image(e, &b.img, r, straight); + let mut p = with_u0([mode, on_alpha as u32, 0, 0]); + if mode == 1 { + p.f[0][0] = ctx.params.f("threshold") as f32 / 255.0; + } + let img = run(e, "fxn_median_out", &p, &b.img, Some(&m), None); + Some(GBuf { img, ..b }) +} + +// ---------------------------------------------------------------- Remove Grain (noise.rs) + +fn remove_grain(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option { + let pr = ctx.params; + let amt = pr.f("noiseReductionSettings/noiseReduction") as f32; + let um_amount = pr.f("unsharpMask/amount") as f32 / 100.0; + let view = pr.get("viewingMode").map(Value::as_enum).unwrap_or(3); + // Noise Samples / Blending Matte views and Temporal Filtering render on the CPU. + if view == 1 || view == 2 || pr.b("temporalFiltering/enabled") { + return None; + } + if amt <= 0.0 && um_amount <= 0.0 { + return Some(b); + } + // The grain level measured in the sample boxes (CPU; only the smoothing uses it). + let level = if amt > 0.0 { + let img = e.download(&b.img)?; + effectcraft_effects::remove_grain_level(ctx, &Buf { img, offset: b.offset, scale: b.scale }) + } else { + 1.0 + }; + let passes = pr.f("noiseReductionSettings/passes").round().clamp(1.0, 8.0) as usize; + let multichannel = pr.e("noiseReductionSettings/mode") == 0; + let texture = (pr.f("fineTuning/texture") as f32).clamp(0.0, 1.0); + let r = (2.0 * b.scale).round().max(1.0) as usize; + let eps = (0.02 * amt * level).powi(2); + let orig = b.img.clone(); + let mut ch = orig.clone(); + if amt > 0.0 { + for _ in 0..passes { + let g = run(e, "fxn_gf_guide", &with_u0([!multichannel as u32, 0, 0, 0]), &ch, None, None); + ch = guided_filter(e, &g, &ch, r, eps); + } + if texture > 0.0 { + let mut p = Params::default(); + p.f[0][0] = texture; + ch = run(e, "fxn_rg_texture", &p, &ch, Some(&orig), None); + } + } + if um_amount > 0.0 { + let radius = (pr.f("unsharpMask/radius") * b.scale).max(0.1); + let low = gaussian_blur(e, &ch, radius, radius, true); + let mut p = Params::default(); + p.f[0] = [um_amount, pr.f("unsharpMask/threshold") as f32 / 255.0, 0.0, 0.0]; + ch = run(e, "fxn_rg_unsharp", &p, &ch, Some(&low), None); + } + let mut p = Params::default(); + if view == 0 { + let c = b.to_px(pr.v2("previewRegion/center")); + let (hw, hh) = (pr.f("previewRegion/width") * b.scale * 0.5, pr.f("previewRegion/height") * b.scale * 0.5); + let col = pr.color("previewRegion/boxColor"); + p.u[0] = [1, pr.b("previewRegion/showBox") as u32, 0, 0]; + p.f[0] = [(c.0 - hw) as f32, (c.0 + hw) as f32, (c.1 - hh) as f32, (c.1 + hh) as f32]; + p.f[1] = [col[0], col[1], col[2], 1.0]; + } + let img = run(e, "fxn_rg_out", &p, &ch, Some(&orig), None); + Some(GBuf { img, ..b }) +} + +/// One guided-filter arithmetic pass (`fxn_gf`), `d` = an image read through `data`. +fn gf(e: &mut Enc, mode: u32, s: &GpuImage, a: &GpuImage, d: Option<&GpuImage>, eps: f32) -> GpuImage { + let mut p = with_u0([mode, 0, 0, 0]); + p.f[0][0] = eps; + let rows = d.map(|d| e.image_rows(d)); + if let Some((_, row)) = &rows { + p.u[0][1] = *row; + } + run(e, "fxn_gf", &p, s, Some(a), rows.as_ref().map(|r| &r.0)) +} + +/// util::guided_filter on every channel at once (guide `g`, input `p`). +fn guided_filter(e: &mut Enc, g: &GpuImage, p: &GpuImage, r: usize, eps: f32) -> GpuImage { + let bx = |e: &mut Enc, img: &GpuImage| box_passes(e, img, &[r], &[r], true); + let mean_g = bx(e, g); + let mean_p = bx(e, p); + let gp = gf(e, 5, g, p, None, 0.0); + let gg = gf(e, 6, g, g, None, 0.0); + let corr_gp = bx(e, &gp); + let corr_gg = bx(e, &gg); + let cov = gf(e, 0, &corr_gp, &mean_p, Some(&mean_g), 0.0); + let var = gf(e, 1, &corr_gg, &mean_g, None, 0.0); + let a = gf(e, 2, &cov, &var, None, eps); + let bb = gf(e, 3, &a, &mean_p, Some(&mean_g), 0.0); + let ma = bx(e, &a); + let mb = bx(e, &bb); + gf(e, 4, &ma, &mb, Some(g), 0.0) +} + +// ---------------------------------------------------------------- Noise Alpha / Noise HLS (noise.rs) + +/// noise::phased_cycle's two seeds and smoothed fraction for `phase`. +fn phase_terms(seed: u32, phase: f32, cycle: i64) -> (u32, u32, f32) { + let k = phase.floor(); + let t = phase - k; + let k = k as i64; + let wrap = |k: i64| if cycle > 0 { k.rem_euclid(cycle) } else { k } as u32; + (seed.wrapping_add(wrap(k).wrapping_mul(0x632b)), seed.wrapping_add(wrap(k + 1).wrapping_mul(0x632b)), t * t * (3.0 - 2.0 * t)) +} + +fn noise_alpha(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option { + let k = ctx.params.f("amount") as f32 / 100.0; + if k <= 0.0 { + return Some(b); + } + let kind = ctx.params.e("noise"); + let seed = (ctx.params.f("randomSeed") as i64 as u32) ^ ctx.seed.wrapping_mul(0x2f1); + let phase = ctx.params.f("noisePhase") as f32 / 360.0 + if kind >= 2 { ctx.time as f32 } else { 0.0 }; + let cycle = if ctx.params.b("noiseOptions/cycleNoise") { ctx.params.f("noiseOptions/cycle").round().max(1.0) as i64 } else { 0 }; + let ph = if kind >= 2 { phase } else { phase.floor() }; + let (sa, sb, t) = phase_terms(seed, ph, cycle); + let mut p = with_u0([kind % 2, ctx.params.e("originalAlpha"), ctx.params.e("overflow"), sa]); + p.u[1][0] = sb; + p.f[0] = [k, t, 0.0, 0.0]; + let img = run(e, "fxn_noise_alpha", &p, &b.img, None, None); + Some(GBuf { img, ..b }) +} + +fn noise_hls(e: &mut Enc, ctx: &EffectCtx, b: GBuf, phase: f32) -> Option { + let hue = ctx.params.f("hue") as f32 / 100.0; + let light = ctx.params.f("lightness") as f32 / 100.0; + let satv = ctx.params.f("saturation") as f32 / 100.0; + if hue == 0.0 && light == 0.0 && satv == 0.0 { + return Some(b); + } + let gs = (ctx.params.f("grainSize") * b.scale).max(0.1) as f32; + let seed = ctx.seed.wrapping_mul(0x51f3); + let mut seeds = [0u32; 6]; + let mut t = 0.0; + for k in 0..3u32 { + let (sa, sb, tt) = phase_terms(seed.wrapping_add(k * 7919), phase, 0); + seeds[2 * k as usize] = sa; + seeds[2 * k as usize + 1] = sb; + t = tt; + } + let mut p = with_u0([ctx.params.e("noise"), seed, 0, 0]); + p.u[1] = [seeds[0], seeds[1], seeds[2], seeds[3]]; + p.u[2] = [seeds[4], seeds[5], 0, 0]; + p.f[0] = [hue, light, satv, gs]; + p.f[1] = [phase, t, 0.0, 0.0]; + let img = run(e, "fxn_noise_hls", &p, &b.img, None, None); + Some(GBuf { img, ..b }) +} + +// ---------------------------------------------------------------- blurs (blur2.rs, misc.rs) + +/// blur2::edge_preserving (+ Bilateral's grey output with `grey`). +fn edge_preserving(e: &mut Enc, img: &GpuImage, r: f64, range: f32, hard: bool, grey: bool) -> GpuImage { + let ri = r.ceil() as u32; + let stride = ((r / 6.0).ceil() as u32).max(1); + let sig_s2 = 2.0 * (r / 2.0).max(0.5).powi(2); + let sig_r2 = 2.0 * range.max(1e-3).powi(2); + let d2max = (r * r + 0.5).floor() as u32; + let mut p = with_u0([ri, stride, hard as u32, d2max]); + p.u[1][0] = grey as u32; + p.f[0] = [sig_s2 as f32, sig_r2, range, 0.0]; + run(e, "fxn_edge", &p, img, None, None) +} + +fn bilateral(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option { + let r = ctx.params.f("radius") * b.scale; + if r < 0.5 { + return Some(b); + } + let range = ctx.params.f("threshold") as f32 / 100.0; + let img = edge_preserving(e, &b.img, r, range, false, !ctx.params.b("colorize")); + Some(GBuf { img, ..b }) +} + +fn smart_blur(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option { + let r = ctx.params.f("radius") * b.scale; + let thr = ctx.params.f("threshold") as f32 / 100.0; + let mode = ctx.params.e("mode"); + let blurred = if r >= 0.5 { edge_preserving(e, &b.img, r, thr, true, false) } else { b.img.clone() }; + if mode == 0 { + return Some(GBuf { img: blurred, ..b }); + } + let mut p = with_u0([mode, 0, 0, 0]); + p.f[0][0] = thr.max(0.01) * 0.5; + let img = run(e, "fxn_smart_edges", &p, &blurred, None, None); + Some(GBuf { img, ..b }) +} + +/// misc::unsharp_core (Sharpen, Unsharp Mask). +fn unsharp(e: &mut Enc, b: GBuf, amount: f32, sigma: f64, threshold: f32) -> Option { + let blurred = gaussian_blur(e, &b.img, sigma, sigma, true); + let mut p = Params::default(); + p.f[0] = [amount, threshold, 0.0, 0.0]; + let img = run(e, "fxn_unsharp", &p, &b.img, Some(&blurred), None); + Some(GBuf { img, ..b }) +} + +fn dims_xy(dim: u32) -> (f64, f64) { + match dim { + 1 => (1.0, 0.0), + 2 => (0.0, 1.0), + _ => (1.0, 1.0), + } +} + +fn pick(e: &mut Enc, acc: &GpuImage, from: &GpuImage, mask: [bool; 4], alpha: bool) -> GpuImage { + let mut p = with_u0([alpha as u32, 0, 0, 0]); + p.u[1] = mask.map(|m| m as u32); + run(e, "fxn_pick", &p, acc, Some(from), None) +} + +fn channel_blur(e: &mut Enc, ctx: &EffectCtx, mut b: GBuf) -> Option { + let sig = ["redBlurriness", "greenBlurriness", "blueBlurriness", "alphaBlurriness"].map(|id| ctx.params.f(id).max(0.0) * 0.5 * b.scale); + if sig.iter().all(|&s| s < 0.05) { + return Some(b); + } + let (kx, ky) = dims_xy(ctx.params.e("dimensions")); + let repeat = ctx.params.b("repeatEdge") || ctx.adjustment; + let maxs = sig.iter().cloned().fold(0.0, f64::max); + if !repeat { + b.pad(e, (maxs * 3.0).ceil() as u32 + 1)?; + } + // n: each colour channel blurred by its own amount; a: the alpha blurred by the same amount + // (its divisor), alpha = the Alpha Blurriness result. + let mut n = b.img.clone(); + let mut a = pick(e, &b.img, &b.img, [true; 4], true); + let mut done: Vec = Vec::new(); + for &s in &sig { + if s < 0.05 || done.contains(&s.to_bits()) { + continue; + } + done.push(s.to_bits()); + let bl = gaussian_blur(e, &b.img, s * kx, s * ky, true); + let on = |c: usize| sig[c] >= 0.05 && sig[c].to_bits() == s.to_bits(); + n = pick(e, &n, &bl, [on(0), on(1), on(2), false], false); + a = pick(e, &a, &bl, [on(0), on(1), on(2), on(3)], true); + } + b.img = run(e, "fxn_cblur_out", &Params::default(), &n, Some(&a), None); + Some(b) +} + +fn compound_blur(e: &mut Enc, ctx: &EffectCtx, mut b: GBuf) -> Option { + let max_sigma = ctx.params.f("maximumBlur").max(0.0) * 0.5 * b.scale; + if max_sigma < 0.05 { + return Some(b); + } + let invert = ctx.params.b("invertBlur"); + if !ctx.adjustment { + b.pad(e, (max_sigma * 3.0).ceil() as u32 + 1)?; + } + let (w, h) = (b.img.width as usize, b.img.height as usize); + // The blur map: the Blur Layer fitted to the buffer (util::fit_layer), else the layer itself. + let map = match ctx.layer_param("blurLayer", true) { + Some(o) => { + if o.buf.img.is_empty() { + return None; + } + let stretch = ctx.params.b("stretchMapToFit"); + let (ls, os) = (ctx.layer_size, o.size); + let (sx, sy) = if stretch && ls[0] > 0.0 && ls[1] > 0.0 { (os[0] / ls[0], os[1] / ls[1]) } else { (1.0, 1.0) }; + let (dx, dy) = if stretch { (0.0, 0.0) } else { ((os[0] - ls[0]) * 0.5, (os[1] - ls[1]) * 0.5) }; + let inv = 1.0 / b.scale.max(1e-9); + let col = |x: usize| (((x as f64 + 0.5 - b.offset[0]) * inv) * sx + dx) * o.buf.scale + o.buf.offset[0]; + let row = |y: usize| (((y as f64 + 0.5 - b.offset[1]) * inv) * sy + dy) * o.buf.scale + o.buf.offset[1]; + let data: Vec = (0..w).map(|x| col(x) as f32).chain((0..h).map(|y| row(y) as f32)).collect(); + let layer = e.g.upload_image(&o.buf.img)?; + let buf = e.data(&data); + let mut p = with_u0([1, invert as u32, 0, 0]); + p.u[1][0] = w as u32; + let out = e.image(b.img.width, b.img.height); + e.pixels("fxn_cb_map", &p, &layer, None, &out, Some(&buf)); + out + } + None => run(e, "fxn_cb_map", &with_u0([0, invert as u32, 0, 0]), &b.img, None, None), + }; + const N: usize = 5; + let levels: Vec = (0..N) + .map(|k| { + if k == 0 { + b.img.clone() + } else { + let s = max_sigma * k as f64 / (N - 1) as f64; + gaussian_blur(e, &b.img, s, s, ctx.adjustment) + } + }) + .collect(); + let (rows, row) = e.image_rows(&map); + let out = e.image(b.img.width, b.img.height); + for k in 0..N - 1 { + e.pixels("fxn_cb_level", &with_u0([k as u32, row, 0, 0]), &levels[k], Some(&levels[k + 1]), &out, Some(&rows)); + } + b.img = out; + Some(b) +} + +// ---------------------------------------------------------------- Minimax (channel.rs) + +/// util::morph_plane per channel (`modes`: 0 min, 1 max, 2 keep) with radii (`rx`, `ry`). +fn morph(e: &mut Enc, img: &GpuImage, rx: usize, ry: usize, modes: [u32; 4]) -> GpuImage { + let mut cur = img.clone(); + for (vertical, r) in [(0, rx), (1, ry)] { + if r == 0 { + continue; + } + let mut p = with_u0([r as u32, vertical, 0, 0]); + p.u[1] = modes; + cur = run(e, "fxn_morph", &p, &cur, None, None); + } + cur +} + +fn minimax(e: &mut Enc, ctx: &EffectCtx, mut b: GBuf) -> Option { + let r = (ctx.params.f("radius") * b.scale).round().max(0.0) as usize; + if r == 0 { + return Some(b); + } + let op = ctx.params.e("operation"); + let dont_shrink = ctx.params.b("dontShrinkEdges"); + let (rx, ry) = match ctx.params.e("direction") { + 1 => (r, 0), + 2 => (0, r), + _ => (r, r), + }; + let sel: [bool; 4] = match ctx.params.e("channel") { + 1 => [false, false, false, true], + 2 => [true, true, true, true], + 3 => [true, false, false, false], + 4 => [false, true, false, false], + 5 => [false, false, true, false], + _ => [true, true, true, false], + }; + if sel[3] && op != 0 && !ctx.adjustment { + b.pad(e, rx.max(ry) as u32)?; + } + let (w, h) = (b.img.width, b.img.height); + let mut planes = run(e, "fxn_straight", &Params::default(), &b.img, None, None); + // Beyond the layer edge is empty (0) unless Don't Shrink Edges repeats the edge pixels. + if !dont_shrink { + let (pw, ph) = (w + 2 * rx as u32, h + 2 * ry as u32); + if !e.g.fits(pw, ph) { + return None; + } + let padded = e.image(pw, ph); + e.copy_into(&planes, &padded, rx as u32, ry as u32); + planes = padded; + } + let modes = |max: bool| sel.map(|s| if s { max as u32 } else { 2 }); + let stages: &[bool] = match op { + 0 => &[false], + 1 => &[true], + 2 => &[false, true], + _ => &[true, false], + }; + for &max in stages { + planes = morph(e, &planes, rx, ry, modes(max)); + } + if !dont_shrink { + let out = e.image(w, h); + e.pixels("fxn_crop", &with_u0([rx as u32, ry as u32, 0, 0]), &planes, None, &out, None); + planes = out; + } + b.img = run(e, "fxn_premul", &Params::default(), &planes, None, None); + Some(b) +} + +// ---------------------------------------------------------------- Echo / Posterize Time (time_fx.rs) + +fn echo(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option { + let n = ctx.params.f("numberOfEchoes").round().clamp(0.0, 255.0) as usize; + let dt = ctx.params.f("echoTime"); + let start = ctx.params.f("startingIntensity").clamp(0.0, 1.0) as f32; + let decay = ctx.params.f("decay").clamp(0.0, 1.0) as f32; + let op = ctx.params.e("echoOperator"); + let times: Vec = (0..=n).map(|i| ctx.time + i as f64 * dt).collect(); + // Without the effect host (or the frames) the layer passes through, as on the CPU. + let Some((grid, imgs)) = effectcraft_effects::time_frames(ctx, ×) else { return Some(b) }; + let (w, h) = (grid.img.width, grid.img.height); + let mut acc = [e.image(w, h), e.image(w, h)]; + for (i, img) in imgs.iter().enumerate() { + let frame = e.g.upload_image(img)?; + let mut p = with_u0([op, (i == 0) as u32, 0, 0]); + p.f[0][0] = start * decay.powi(i as i32); + let [a, o] = &acc; + e.pixels("fxn_echo", &p, a, Some(&frame), o, None); + acc.swap(0, 1); + } + let mut p = with_u0([op, 0, 0, 0]); + p.f[0][0] = (n + 1) as f32; + let img = run(e, "fxn_echo_fin", &p, &acc[0], None, None); + Some(GBuf { img, offset: grid.offset, scale: grid.scale }) +} + +fn posterize_time(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option { + if ctx.env.host.is_none() { + return Some(b); + } + let tt = effectcraft_effects::posterized_time(ctx.time, ctx.params.f("frameRate")); + match effectcraft_effects::time_frames(ctx, &[tt]) { + Some((grid, mut imgs)) => { + let img = e.g.upload_image(&imgs.pop().unwrap_or_default())?; + Some(GBuf { img, offset: grid.offset, scale: grid.scale }) + } + None => Some(b), + } +} diff --git a/crates/gpu/src/fx_stylize.rs b/crates/gpu/src/fx_stylize.rs new file mode 100644 index 000000000..6e83c7c8e --- /dev/null +++ b/crates/gpu/src/fx_stylize.rs @@ -0,0 +1,1002 @@ +//! GPU effects, stylize and distort family (kernels in `shaders/fx_stylize.wgsl`, every entry point prefixed +//! `fxs_`): the CPU effects' exact steps with the pixel loops as compute kernels. +//! +//! Parameter conversions, padding, output geometry and anything the CPU computes in f64 with a +//! floor or a wrap (tile indices, wrapped coordinates, brush cells) are computed here exactly as +//! the CPU does and uploaded as per-column / per-row / per-cell tables; the kernels do the +//! per-pixel work. Glow's other Glow Operations / Arbitrary Map and Transform's shutter-angle +//! motion blur extend the base kernels in `effects.rs` (see [`glow_extra`], [`transform_blur`]). + +use std::f64::consts::{FRAC_PI_2, TAU}; + +use effectcraft_color::BlendMode; +use effectcraft_effects::EffectCtx; +use effectcraft_geom::{Mat3, vec2}; +use effectcraft_raster::Sampling; + +use crate::context::{Enc, Params}; +use crate::effects::{GBuf, gaussian_blur}; +use crate::ops; + +/// Compute entry points in `fx_stylize.wgsl`. +pub(crate) const KERNELS: &[&str] = &[ + "fxs_point", + "fxs_warp", + "fxs_table", + "fxs_griddler", + "fxs_motiontile", + "fxs_repetile", + "fxs_magnify", + "fxs_scatter", + "fxs_brush", + "fxs_roughen", + "fxs_tex_luma", + "fxs_texturize", + "fxs_glow_map", + "fxs_glow_op", + "fxs_add", + "fxs_div", + "fxs_flomotion", +]; + +/// Effect ids implemented here. +pub(crate) const IDS: &[&str] = &[ + "ec.stylize.posterize", + "ec.stylize.threshold", + "ec.stylize.ccthreshold", + "ec.stylize.ccthresholdrgb", + "ec.stylize.strobe", + "ec.stylize.ccvignette", + "ec.stylize.scatter", + "ec.stylize.brushstrokes", + "ec.stylize.roughenedges", + "ec.stylize.texturize", + "ec.stylize.motiontile", + "ec.stylize.cckaleida", + "ec.stylize.ccrepetile", + "ec.distort.mirror", + "ec.distort.offset", + "ec.distort.polar", + "ec.distort.spherize", + "ec.distort.cornerpin", + "ec.distort.opticscompensation", + "ec.distort.magnify", + "ec.distort.ccslant", + "ec.distort.ccsmear", + "ec.distort.ccsplit", + "ec.distort.ccsplit2", + "ec.distort.cctiler", + "ec.distort.ccgriddler", + "ec.distort.liquify", + "ec.distort.twirllegacy", + "ec.distort.ccripplepulse", + "ec.distort.ccpowerpin", + "ec.distort.ccflomotion", +]; + +/// Run effect `id` (one of [`IDS`]); `None` = this parameter combination runs on the CPU. +pub(crate) fn apply(e: &mut Enc, id: &str, ctx: &EffectCtx, b: GBuf) -> Option { + match id { + "ec.stylize.posterize" => posterize(e, ctx, b), + "ec.stylize.threshold" => threshold(e, ctx, b), + "ec.stylize.ccthreshold" => cc_threshold(e, ctx, b), + "ec.stylize.ccthresholdrgb" => cc_threshold_rgb(e, ctx, b), + "ec.stylize.strobe" => strobe(e, ctx, b), + "ec.stylize.ccvignette" => vignette(e, ctx, b), + "ec.stylize.scatter" => scatter(e, ctx, b), + "ec.stylize.brushstrokes" => brush_strokes(e, ctx, b), + "ec.stylize.roughenedges" => roughen_edges(e, ctx, b), + "ec.stylize.texturize" => texturize(e, ctx, b), + "ec.stylize.motiontile" => motion_tile(e, ctx, b), + "ec.stylize.cckaleida" => kaleida(e, ctx, b), + "ec.stylize.ccrepetile" => repetile(e, ctx, b), + "ec.distort.mirror" => mirror(e, ctx, b), + "ec.distort.offset" => offset(e, ctx, b), + "ec.distort.polar" => polar(e, ctx, b), + "ec.distort.spherize" => spherize(e, ctx, b), + "ec.distort.cornerpin" => corner_pin(e, ctx, b), + "ec.distort.opticscompensation" => optics_compensation(e, ctx, b), + "ec.distort.magnify" => magnify(e, ctx, b), + "ec.distort.ccslant" => cc_slant(e, ctx, b), + "ec.distort.ccsmear" => cc_smear(e, ctx, b), + "ec.distort.ccsplit" => { + let s = ctx.params.f("split") / 100.0; + cc_split(e, ctx, b, s, s) + } + "ec.distort.ccsplit2" => cc_split(e, ctx, b, ctx.params.f("split1") / 100.0, ctx.params.f("split2") / 100.0), + "ec.distort.cctiler" => cc_tiler(e, ctx, b), + "ec.distort.ccgriddler" => cc_griddler(e, ctx, b), + "ec.distort.liquify" => liquify(e, ctx, b), + "ec.distort.twirllegacy" => twirl_legacy(e, ctx, b), + "ec.distort.ccripplepulse" => ripple_pulse(e, ctx, b), + "ec.distort.ccpowerpin" => power_pin(e, ctx, b), + "ec.distort.ccflomotion" => flo_motion(e, ctx, b), + _ => None, + } +} + +/// Run a same-size per-pixel kernel over the buffer. +fn run(e: &mut Enc, entry: &str, p: &Params, mut b: GBuf, data: Option<&wgpu::Buffer>) -> Option { + let out = e.image(b.img.width, b.img.height); + e.pixels(entry, p, &b.img, None, &out, data); + b.img = out; + Some(b) +} + +/// util::layer_rect. +fn layer_rect(ctx: &EffectCtx, b: &GBuf) -> (f64, f64, f64, f64) { + (b.offset[0], b.offset[1], ctx.layer_size[0] * b.scale, ctx.layer_size[1] * b.scale) +} + +// ---------------------------------------------------------------- per-pixel (fxs_point) + +fn point(e: &mut Enc, mode: u32, f: impl FnOnce(&mut Params), b: GBuf) -> Option { + let mut p = Params::default(); + p.u[0][0] = mode; + f(&mut p); + run(e, "fxs_point", &p, b, None) +} + +fn posterize(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option { + let lv = ctx.params.f("level").round().clamp(2.0, 255.0) as f32 - 1.0; + point(e, 1, |p| p.f[0][0] = lv, b) +} + +fn threshold(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option { + let t = ctx.params.f("level") as f32 / 255.0; + point(e, 2, |p| p.f[0][0] = t, b) +} + +fn cc_threshold(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option { + let t = ctx.params.f("threshold") as f32 / 255.0; + let blend = ctx.params.f("blend") as f32 / 100.0; + let (ch, inv) = (ctx.params.e("channel"), ctx.params.b("invert") as u32); + point( + e, + 3, + |p| { + p.u[0][1] = ch; + p.u[0][2] = inv; + p.f[0] = [t, blend, 0.0, 0.0]; + }, + b, + ) +} + +fn cc_threshold_rgb(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option { + let t = [ctx.params.f("redThreshold"), ctx.params.f("greenThreshold"), ctx.params.f("blueThreshold")].map(|v| v as f32 / 255.0); + let inv = [ctx.params.b("invertRed"), ctx.params.b("invertGreen"), ctx.params.b("invertBlue")].map(u32::from); + let blend = ctx.params.f("blend") as f32 / 100.0; + point( + e, + 4, + |p| { + p.u[0] = [4, inv[0], inv[1], inv[2]]; + p.f[0] = [t[0], t[1], t[2], blend]; + }, + b, + ) +} + +fn strobe(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option { + let on = effectcraft_effects::strobe_on( + ctx.time, + ctx.params.f("strobeDuration"), + ctx.params.f("strobePeriod"), + ctx.params.f("randomStrobeProbability") / 100.0, + ctx.seed, + ); + if !on { + return Some(b); + } + let blend = (ctx.params.f("blendWithOriginal") as f32 / 100.0).clamp(0.0, 1.0); + if ctx.params.e("strobe") == 1 { + // Image::scale_alpha. + if (blend - 1.0).abs() < 1e-7 { + return Some(b); + } + return point(e, 6, |p| p.f[0][0] = blend, b); + } + let s = ctx.params.color("strobeColor"); + let op = ctx.params.e("strobeOperator"); + point( + e, + 5, + |p| { + p.u[0][1] = op; + p.f[0] = [s[0], s[1], s[2], blend]; + }, + b, + ) +} + +fn vignette(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option { + let amt = ctx.params.f("amount") as f32 / 100.0; + let fov = ctx.params.f("angleOfView").clamp(0.0, 179.0).to_radians(); + if amt == 0.0 || fov <= 0.0 { + return Some(b); + } + let c = b.to_px(ctx.params.v2("center")); + let pin = ctx.params.f("pinHighlights") as f32 / 100.0; + let (lw, lh) = (ctx.layer_size[0] * b.scale, ctx.layer_size[1] * b.scale); + let half_diag = (lw * lw + lh * lh).sqrt() * 0.5; + let dist = half_diag.max(1.0) / (fov * 0.5).tan(); + point( + e, + 7, + |p| { + p.f[0] = [c.0 as f32, c.1 as f32, dist as f32, amt]; + p.f[1][0] = pin; + }, + b, + ) +} + +// ---------------------------------------------------------------- warps (fxs_warp) + +fn warp(e: &mut Enc, mode: u32, f: impl FnOnce(&mut Params), b: GBuf, data: Option<&wgpu::Buffer>) -> Option { + let mut p = Params::default(); + p.u[0][0] = mode; + f(&mut p); + run(e, "fxs_warp", &p, b, data) +} + +fn mirror(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option { + let c = b.to_px(ctx.params.v2("center")); + let a = ctx.params.f("angle").to_radians(); + warp(e, 1, |p| p.f[0] = [c.0 as f32, c.1 as f32, a.cos() as f32, a.sin() as f32], b, None) +} + +fn spherize(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option { + let r = (ctx.params.f("radius") * b.scale).max(1.0); + let c = b.to_px(ctx.params.v2("center")); + warp(e, 2, |p| p.f[0] = [c.0 as f32, c.1 as f32, r as f32, 0.0], b, None) +} + +fn polar(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option { + let amt = ctx.params.f("interpolation") / 100.0; + let to_polar = ctx.params.e("conversion") == 1; + let (w, h) = (b.img.width as f64, b.img.height as f64); + let (cx, cy) = (w / 2.0, h / 2.0); + let rmax = cx.min(cy); + warp( + e, + 3, + |p| { + p.u[0][1] = to_polar as u32; + p.f[0] = [w as f32, h as f32, cx as f32, cy as f32]; + p.f[1] = [rmax as f32, amt as f32, 0.0, 0.0]; + }, + b, + None, + ) +} + +fn cc_slant(e: &mut Enc, ctx: &EffectCtx, mut b: GBuf) -> Option { + let k = ctx.params.f("slant") / 100.0; + let height = ctx.params.f("height") / 100.0; + if k == 0.0 && (height - 1.0).abs() < 1e-12 { + return Some(b); + } + let stretch = ctx.params.b("stretching"); + let xs = if stretch { 1.0 / (1.0 + k * k).sqrt() } else { 1.0 }; + let hinv = if height.abs() < 1e-6 { 1e6 } else { 1.0 / height }; + if !ctx.adjustment { + let lean = (k * ctx.layer_size[1] * b.scale * height.max(1.0)).abs(); + let grow = ((height - 1.0).max(0.0) * ctx.layer_size[1] * b.scale).max(lean); + b.pad(e, grow.ceil().min(4096.0) as u32)?; + } + let fl = b.to_px(ctx.params.v2("floor")); + warp( + e, + 4, + |p| { + p.f[0] = [fl.0 as f32, fl.1 as f32, hinv as f32, k as f32]; + p.f[1][0] = xs as f32; + }, + b, + None, + ) +} + +fn cc_smear(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option { + let from = b.to_px(ctx.params.v2("from")); + let to = b.to_px(ctx.params.v2("to")); + let reach = ctx.params.f("reach") / 100.0; + let radius = (ctx.params.f("radius") * b.scale).max(0.5); + let (dx, dy) = (to.0 - from.0, to.1 - from.1); + let l2 = dx * dx + dy * dy; + if l2 < 1e-12 || reach == 0.0 { + return Some(b); + } + warp( + e, + 5, + |p| { + p.f[0] = [from.0 as f32, from.1 as f32, dx as f32, dy as f32]; + p.f[1] = [l2 as f32, reach as f32, radius as f32, 0.0]; + }, + b, + None, + ) +} + +/// distort3::split_impl (CC Split / CC Split 2). +fn cc_split(e: &mut Enc, ctx: &EffectCtx, b: GBuf, s1: f64, s2: f64) -> Option { + if s1 == 0.0 && s2 == 0.0 { + return Some(b); + } + let a = b.to_px(ctx.params.v2("pointA")); + let c = b.to_px(ctx.params.v2("pointB")); + let (dx, dy) = (c.0 - a.0, c.1 - a.1); + let len = (dx * dx + dy * dy).sqrt(); + if len < 1e-6 { + return Some(b); + } + let (ux, uy) = (dx / len, dy / len); + warp( + e, + 6, + |p| { + p.f[0] = [a.0 as f32, a.1 as f32, ux as f32, uy as f32]; + p.f[1] = [len as f32, s1 as f32, s2 as f32, (len * 0.5) as f32]; + }, + b, + None, + ) +} + +fn optics_compensation(e: &mut Enc, ctx: &EffectCtx, mut b: GBuf) -> Option { + let fov = ctx.params.f("fieldOfView").clamp(0.0, 179.0).to_radians(); + if fov < 1e-6 { + return Some(b); + } + let reverse = ctx.params.b("reverseLensDistortion"); + let (lx, ly, lw, lh) = layer_rect(ctx, &b); + let r_ref = match ctx.params.e("fovOrientation") { + 1 => lh * 0.5, + 2 => (lw * lw + lh * lh).sqrt() * 0.5, + _ => lw * 0.5, + } + .max(1.0); + let half = fov * 0.5; + let f = r_ref / half.tan(); + let optimal = ctx.params.b("optimalPixels"); + let resize = if optimal { 0 } else { ctx.params.e("resize") }; + let corners = [(lx, ly), (lx + lw, ly), (lx, ly + lh), (lx + lw, ly + lh)]; + if reverse && resize > 0 && !ctx.adjustment { + let c = b.to_px(ctx.params.v2("viewCenter")); + let rc = corners.iter().map(|p| ((p.0 - c.0).powi(2) + (p.1 - c.1).powi(2)).sqrt()).fold(0.0, f64::max); + let th = rc / r_ref * half; + let cap = lw.max(lh) * [0.0, 0.5, 1.5, 3.5][resize.min(3) as usize]; + let grow = if th < FRAC_PI_2 - 1e-3 { (f * th.tan() - rc).max(0.0) } else { cap }; + b.pad(e, grow.min(cap).min(4096.0).ceil() as u32)?; + } + let c = b.to_px(ctx.params.v2("viewCenter")); + let fit = if optimal { + let rc = corners.iter().map(|p| ((p.0 - c.0).powi(2) + (p.1 - c.1).powi(2)).sqrt()).fold(1.0, f64::max); + if reverse { + let th = (rc / r_ref * half).min(FRAC_PI_2 - 1e-3); + (f * th.tan() / rc).max(1.0) + } else { + ((rc / f).atan() / half * r_ref / rc).min(1.0) + } + } else { + 1.0 + }; + warp( + e, + 7, + |p| { + p.u[0][1] = reverse as u32; + p.f[0] = [c.0 as f32, c.1 as f32, fit as f32, f as f32]; + p.f[1] = [r_ref as f32, half as f32, 0.0, 0.0]; + }, + b, + None, + ) +} + +fn kaleida(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option { + let c = b.to_px(ctx.params.v2("center")); + let size = ctx.params.f("size").max(1.0) / 100.0; + let mode = ctx.params.e("mirroring"); + let rot = ctx.params.f("rotation").to_radians(); + let n = [6.0, 8.0, 4.0, 12.0, 3.0][mode.min(4) as usize]; + let seg = TAU / n; + warp( + e, + 8, + |p| { + p.u[0][1] = (mode != 3) as u32; + p.f[0] = [c.0 as f32, c.1 as f32, size as f32, rot as f32]; + p.f[1][0] = seg as f32; + }, + b, + None, + ) +} + +fn liquify(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option { + // View Freeze Area Mask / View Mesh overlays render on the CPU (catalog CPU_ONLY_CONTROLS). + if ctx.params.b("viewFreezeAreaMask") || ctx.params.b("viewMesh") { + return None; + } + if ctx.params.s("distortionMesh").trim().is_empty() { + return Some(b); + } + let pct = ctx.params.f("distortionPercentage") / 100.0; + let mesh = effectcraft_effects::liquify_mesh(ctx); + if mesh.nx < 2 || mesh.ny < 2 { + return None; + } + let off = ctx.params.v2("distortionMeshOffset"); + let data: Vec = mesh.d.iter().flat_map(|d| [d[0] as f32, d[1] as f32]).collect(); + let buf = e.data(&data); + let (scale, o) = (b.scale, b.offset); + warp( + e, + 9, + |p| { + p.u[0] = [9, mesh.nx as u32, mesh.ny as u32, 0]; + p.f[0] = [mesh.cell as f32, off[0] as f32, off[1] as f32, pct as f32]; + p.f[1] = [scale as f32, o[0] as f32, o[1] as f32, 0.0]; + }, + b, + Some(&buf), + ) +} + +fn twirl_legacy(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option { + let ang = ctx.params.f("angle").to_radians(); + if ang == 0.0 { + return Some(b); + } + let (lw, lh) = (ctx.layer_size[0] * b.scale, ctx.layer_size[1] * b.scale); + let r = (ctx.params.f("radius") / 100.0 * lw.min(lh) * 0.5).max(1.0); + let c = b.to_px(ctx.params.v2("center")); + warp(e, 10, |p| p.f[0] = [c.0 as f32, c.1 as f32, r as f32, ang as f32], b, None) +} + +fn ripple_pulse(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option { + let level = ctx.params.f("pulseLevel") / 100.0; + let amp = ctx.params.f("amplitude") / 100.0; + if level == 0.0 || amp == 0.0 { + return Some(b); + } + let c = b.to_px(ctx.params.v2("center")); + let span = ctx.params.f("timeSpan").max(0.01); + let size = ctx.layer_size[0].max(ctx.layer_size[1]) * b.scale; + let speed = size * 0.5 / span; + let lambda = (size * 0.06).max(2.0); + let height = level * amp * lambda * 0.5; + let front = speed * ctx.time.max(0.0); + let bump = ctx.params.b("renderBump"); + warp( + e, + 11, + |p| { + p.u[0][1] = bump as u32; + p.f[0] = [c.0 as f32, c.1 as f32, front as f32, speed as f32]; + p.f[1] = [span as f32, lambda as f32, height as f32, 0.0]; + }, + b, + None, + ) +} + +fn power_pin(e: &mut Enc, ctx: &EffectCtx, mut b: GBuf) -> Option { + // Perspective below 100 % (a bilinear inverse by Newton iterations with finite + // differences) renders on the CPU. + if ctx.params.f("perspective") / 100.0 < 1.0 { + return None; + } + let (lw, lh) = (ctx.layer_size[0].max(1.0), ctx.layer_size[1].max(1.0)); + let ex = [ctx.params.f("expandTop"), ctx.params.f("expandLeft"), ctx.params.f("expandRight"), ctx.params.f("expandBottom")]; + let (x0, y0) = (-ex[1] / 100.0 * lw, -ex[0] / 100.0 * lh); + let (x1, y1) = (lw + ex[2] / 100.0 * lw, lh + ex[3] / 100.0 * lh); + let corners = [ctx.params.v2("topLeft"), ctx.params.v2("topRight"), ctx.params.v2("bottomRight"), ctx.params.v2("bottomLeft")]; + if !ctx.adjustment { + let mut need = 0.0f64; + for c in &corners { + let (px, py) = b.to_px(*c); + need = need.max(-px).max(-py).max(px - b.img.width as f64).max(py - b.img.height as f64); + } + if need > 0.0 { + b.pad(e, need.ceil().min(4096.0) as u32 + 1)?; + } + } + let q = corners.map(|c| { + let p = b.to_px(c); + vec2(p.0, p.1) + }); + let Some(inv) = Mat3::square_to_quad(q).inverse() else { return Some(b) }; + let r = inv.0.map(|r| [r[0] as f32, r[1] as f32, r[2] as f32, 0.0]); + let (sc, off) = (b.scale, b.offset); + warp( + e, + 12, + |p| { + p.f[2] = r[0]; + p.f[3] = r[1]; + p.f[4] = r[2]; + p.f[5] = [x0 as f32, y0 as f32, (x1 - x0) as f32, (y1 - y0) as f32]; + p.f[6] = [sc as f32, off[0] as f32, off[1] as f32, 0.0]; + }, + b, + None, + ) +} + +fn flo_motion(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option { + let knots = [(ctx.params.v2("knot1"), ctx.params.f("amount1") / 100.0), (ctx.params.v2("knot2"), ctx.params.f("amount2") / 100.0)]; + if knots.iter().all(|k| k.1 == 0.0) { + return Some(b); + } + let ss = match ctx.params.e("antialiasing") { + 0 => 1, + 1 => 2, + _ => 3, + }; + let falloff = ctx.params.f("falloff").max(0.01); + let (lw, lh) = (ctx.layer_size[0].max(1.0), ctx.layer_size[1].max(1.0)); + let radius = (lw * lw + lh * lh).sqrt() * 0.5; + let mut p = Params::default(); + p.u[0] = [ss, ctx.params.b("tileEdges") as u32, 0, 0]; + for (i, (k, a)) in knots.iter().enumerate() { + p.f[i] = [k[0] as f32, k[1] as f32, *a as f32, 0.0]; + } + p.f[2] = [falloff as f32, radius as f32, lw as f32, lh as f32]; + p.f[3] = [b.scale as f32, b.offset[0] as f32, b.offset[1] as f32, 0.0]; + run(e, "fxs_flomotion", &p, b, None) +} + +// ---------------------------------------------------------------- separable tables + +/// Per-column `fx(x + 0.5)` then per-row `fy(y + 0.5)`, `k` values each. +fn axis_table(w: u32, h: u32, fx: impl Fn(f64) -> [f64; K], fy: impl Fn(f64) -> [f64; K]) -> Vec { + let mut v = Vec::with_capacity((w + h) as usize * K); + v.extend((0..w).flat_map(|x| fx(x as f64 + 0.5).map(|t| t as f32))); + v.extend((0..h).flat_map(|y| fy(y as f64 + 0.5).map(|t| t as f32))); + v +} + +fn table(e: &mut Enc, b: GBuf, data: &[f32], repeat: bool, blend: f32) -> Option { + let mut p = Params::default(); + p.u[0] = [b.img.width, repeat as u32, 0, 0]; + p.f[0][0] = blend; + let buf = e.data(data); + run(e, "fxs_table", &p, b, Some(&buf)) +} + +fn offset(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option { + let shift = ctx.params.v2("shift"); + let (w, h) = (b.img.width as f64, b.img.height as f64); + let blend = ctx.params.f("blend") as f32 / 100.0; + let (cx, cy) = (w / 2.0, h / 2.0); + let (sx, sy) = b.to_px(shift); + let data = axis_table(b.img.width, b.img.height, |x| [(x - (sx - cx)).rem_euclid(w)], |y| [(y - (sy - cy)).rem_euclid(h)]); + table(e, b, &data, false, blend.max(0.0)) +} + +fn cc_tiler(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option { + let s = (ctx.params.f("scale") / 100.0).max(0.001); + let c = b.to_px(ctx.params.v2("center")); + let blend = (ctx.params.f("blendWithOriginal") / 100.0) as f32; + let (x0, y0, w, h) = layer_rect(ctx, &b); + let (w, h) = (w.max(1.0), h.max(1.0)); + let data = axis_table(b.img.width, b.img.height, |x| [x0 + (c.0 + (x - c.0) / s - x0).rem_euclid(w)], |y| [y0 + (c.1 + (y - c.1) / s - y0).rem_euclid(h)]); + table(e, b, &data, true, blend) +} + +fn cc_griddler(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option { + let hs = (ctx.params.f("horizontalScale") / 100.0).max(0.001); + let vs = (ctx.params.f("verticalScale") / 100.0).max(0.001); + let rot = ctx.params.f("rotation").to_radians(); + let cut = ctx.params.b("cutTiles"); + let (x0, y0, lw, _) = layer_rect(ctx, &b); + let t = (ctx.params.f("tileSize") / 100.0 * lw).max(1.0); + if (hs - 1.0).abs() < 1e-9 && (vs - 1.0).abs() < 1e-9 && rot.abs() < 1e-9 && !cut { + return Some(b); + } + let (sr, cr) = rot.sin_cos(); + let centre = |p: f64, o: f64| { + let tc = o + (((p - o) / t).floor() + 0.5) * t; + [tc, p - tc] + }; + let data = axis_table(b.img.width, b.img.height, |x| centre(x, x0), |y| centre(y, y0)); + let mut p = Params::default(); + p.u[0] = [b.img.width, cut as u32, 0, 0]; + p.f[0] = [cr as f32, sr as f32, hs as f32, vs as f32]; + p.f[1][0] = (0.45 * t) as f32; + let buf = e.data(&data); + run(e, "fxs_griddler", &p, b, Some(&buf)) +} + +fn motion_tile(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option { + let s = b.scale; + let (lw, lh) = (ctx.layer_size[0], ctx.layer_size[1]); + if lw <= 0.0 || lh <= 0.0 { + return Some(b); + } + let tc = ctx.params.v2("tileCenter"); + let tw = (lw * ctx.params.f("tileWidth") / 100.0).max(0.01); + let th = (lh * ctx.params.f("tileHeight") / 100.0).max(0.01); + let mirror = ctx.params.b("mirrorEdges"); + let phase = ctx.params.f("phase") / 360.0; + let hshift = ctx.params.b("horizontalPhaseShift"); + let (nw, nh, noff) = if ctx.adjustment { + (b.img.width, b.img.height, b.offset) + } else { + let ow = (lw * ctx.params.f("outputWidth") / 100.0).max(1.0 / s); + let oh = (lh * ctx.params.f("outputHeight") / 100.0).max(1.0 / s); + ((ow * s).round().max(1.0) as u32, (oh * s).round().max(1.0) as u32, [(ow - lw) * 0.5 * s, (oh - lh) * 0.5 * s]) + }; + if !e.g.fits(nw, nh) { + return None; + } + let split = |t: f64| [t.floor(), t - t.floor()]; + let data = axis_table(nw, nh, |x| split(((x - noff[0]) / s - tc[0]) / tw + 0.5), |y| split(((y - noff[1]) / s - tc[1]) / th + 0.5)); + let mut p = Params::default(); + p.u[0] = [nw, hshift as u32, mirror as u32, 0]; + p.f[0] = [phase as f32, (lw * s) as f32, (lh * s) as f32, 0.0]; + p.f[1] = [b.offset[0] as f32, b.offset[1] as f32, 0.0, 0.0]; + let buf = e.data(&data); + let out = e.image(nw, nh); + e.pixels("fxs_motiontile", &p, &b.img, None, &out, Some(&buf)); + Some(GBuf { img: out, offset: noff, scale: s }) +} + +fn repetile(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option { + let s = b.scale; + let (r, l, d, u) = ( + ctx.params.f("expandRight").max(0.0) * s, + ctx.params.f("expandLeft").max(0.0) * s, + ctx.params.f("expandDown").max(0.0) * s, + ctx.params.f("expandUp").max(0.0) * s, + ); + if ctx.adjustment || r + l + d + u < 0.5 { + return Some(b); + } + let (x0, y0, w, h) = layer_rect(ctx, &b); + if w < 1.0 || h < 1.0 { + return Some(b); + } + let (nw, nh) = ((w + l + r).round().max(1.0) as u32, (h + u + d).round().max(1.0) as u32); + if !e.g.fits(nw, nh) { + return None; + } + // stylize2::tile_coord. + let tile = |v: f64, size: f64| { + let i = (v / size).floor(); + [i, v - i * size] + }; + let data = axis_table(nw, nh, |x| tile(x - l, w), |y| tile(y - u, h)); + let mut p = Params::default(); + p.u[0] = [nw, ctx.params.e("tiling"), 0, 0]; + p.f[0] = [x0 as f32, y0 as f32, w as f32, h as f32]; + let buf = e.data(&data); + let out = e.image(nw, nh); + e.pixels("fxs_repetile", &p, &b.img, None, &out, Some(&buf)); + Some(GBuf { img: out, offset: [l, u], scale: s }) +} + +fn magnify(e: &mut Enc, ctx: &EffectCtx, mut b: GBuf) -> Option { + let square = ctx.params.e("shape") == 1; + let c = b.to_px(ctx.params.v2("center")); + let mag = (ctx.params.f("magnification") / 100.0).max(0.01); + let link = ctx.params.e("link"); + let mut size = ctx.params.f("size") * b.scale; + let mut feather = ctx.params.f("feather") * b.scale; + if link >= 1 { + size *= mag; + } + if link == 2 { + feather *= mag; + } + let op = (ctx.params.f("opacity") / 100.0) as f32; + let mode = ctx.params.e("blendingMode") as usize; + let blend = effectcraft_effects::MAGNIFY_MODES.get(mode.wrapping_sub(1)).copied(); + if ctx.params.b("resizeLayer") && link == 0 && !ctx.adjustment { + let (lx, ly, lw, lh) = layer_rect(ctx, &b); + let reach = size.max(0.0); + let over = [lx - (c.0 - reach), (c.0 + reach) - (lx + lw), ly - (c.1 - reach), (c.1 + reach) - (ly + lh)].into_iter().fold(0.0, f64::max); + b.pad(e, over.min(4096.0).ceil() as u32)?; + } + let c = b.to_px(ctx.params.v2("center")); + let axis = |p: f64, c: f64| { + let d = p - c; + let sp = c + d / mag; + [d, sp, sp.floor() + 0.5] + }; + let data = axis_table(b.img.width, b.img.height, |x| axis(x, c.0), |y| axis(y, c.1)); + let (kind, mode_id) = match blend { + None => (0, 0), + Some(BlendMode::Normal) => (1, 0), + Some(m) => (2, ops::mode_id(m)), + }; + let mut p = Params::default(); + p.u[0] = [b.img.width, square as u32, ctx.params.e("scaling"), kind]; + p.u[1] = [mode_id, ctx.seed ^ 0x3a61, 0, 0]; + p.f[0] = [size as f32, feather as f32, op, mag as f32]; + p.f[1][0] = (size - feather) as f32; + let buf = e.data(&data); + run(e, "fxs_magnify", &p, b, Some(&buf)) +} + +// ---------------------------------------------------------------- stylize2.rs + +fn scatter(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option { + let amt = ctx.params.f("amount") * b.scale; + if amt <= 0.0 { + return Some(b); + } + let seed = if ctx.params.b("randomizeEveryFrame") { ctx.seed ^ ((ctx.time * 1000.0) as u32).wrapping_mul(0x9e37_79b9) } else { ctx.seed }; + let mut p = Params::default(); + p.u[0] = [ctx.params.e("grain"), seed, 0, 0]; + p.f[0][0] = amt as f32; + run(e, "fxs_scatter", &p, b, None) +} + +fn brush_strokes(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option { + let ang = ctx.params.f("strokeAngle"); + let size = (ctx.params.f("brushSize") * b.scale).max(0.5); + let len = ctx.params.f("strokeLength") * b.scale; + let density = ctx.params.f("strokeDensity") as f32; + let rnd = ctx.params.f("strokeRandomness"); + let blend = ctx.params.f("blendWithOriginal") as f32 / 100.0; + let seed = ctx.seed ^ 0xb705; + let cell = (size * 2.0).max(1.0); + let (w, h) = (b.img.width, b.img.height); + let cell_of = |i: u32| (i as f64 / cell).floor() as i64 as u32; + let (ncx, ncy) = (cell_of(w.saturating_sub(1)) + 1, cell_of(h.saturating_sub(1)) + 1); + if (ncx as u64 * ncy as u64 * 6 + (w + h) as u64) >= 1 << 24 { + return None; + } + let mut data: Vec = (0..w).map(|x| cell_of(x) as f32).chain((0..h).map(|y| cell_of(y) as f32)).collect(); + data.reserve((ncx * ncy * 6) as usize); + for cy in 0..ncy { + for cx in 0..ncx { + let hh = |k: u32| effectcraft_raster::hash_noise(cx.wrapping_add(k.wrapping_mul(7919)), cy, seed) as f64; + let a = (ang + (hh(0) - 0.5) * rnd * 60.0).to_radians(); + let l = len * (0.5 + hh(1)); + let (dx, dy) = (a.sin(), -a.cos()); + let jit = (hh(2) - 0.5) * size; + let n = l.ceil().clamp(1.0, 32.0); + let m = if (hh(3) as f32) < density.min(1.0) { 1.0 } else { 0.0 }; + data.extend([dx as f32, dy as f32, l as f32, jit as f32, n as f32, m]); + } + } + let mut p = Params::default(); + p.u[0] = [w, ncx, ctx.params.e("paintSurface"), h]; + p.f[0][0] = blend; + let buf = e.data(&data); + run(e, "fxs_brush", &p, b, Some(&buf)) +} + +fn roughen_edges(e: &mut Enc, ctx: &EffectCtx, mut b: GBuf) -> Option { + let kind = ctx.params.e("edgeType"); + let ec = ctx.params.color("edgeColor"); + let border = ctx.params.f("border") * b.scale; + let sharp = ctx.params.f("edgeSharpness").max(0.01) as f32; + let infl = ctx.params.f("fractalInfluence") as f32; + let scale = (ctx.params.f("scale") * b.scale * 0.1).max(0.5) as f32; + let (ox, oy) = b.to_px(ctx.params.v2("offset")); + let oct = ctx.params.f("complexity") as f32; + let evo = ctx.params.f("evolution") as f32 / 360.0; + let st = ctx.params.f("stretch") as f32; + let (sx, sy) = (1.0 + st.max(0.0), 1.0 + (-st).max(0.0)); + let cycle = ctx.params.b("evolutionOptions/cycleEvolution").then(|| ctx.params.f("evolutionOptions/cycle").round().max(1.0) as f32); + let (evo, evo_w, c) = match cycle { + Some(c) => { + let ev = evo.rem_euclid(c); + (ev, ev / c, c) + } + None => (evo, 0.0, 0.0), + }; + let (fm, im, sm, colored) = match kind { + 1 => (1.0, 1.0, 1.0, true), + 2 => (1.0, 1.0, 3.0, false), + 3 => (3.0, 1.0, 1.5, false), + 4 => (1.5, 1.5, 1.0, false), + 5 => (1.5, 1.5, 1.0, true), + 6 => (0.6, 1.2, 2.5, false), + 7 => (0.6, 1.2, 2.5, true), + _ => (1.0, 1.0, 1.0, false), + }; + let extra = if kind == 4 || kind == 5 { 1.0 } else { 0.0 }; + let seed = ctx.seed ^ 0x40f1 ^ (ctx.params.f("evolutionOptions/randomSeed").round() as i64 as u32).wrapping_mul(0x9e37_79b9); + // util::gauss_plane of the alpha (edges repeated); the kernel reads .w. + let d = if border > 0.05 { gaussian_blur(e, &b.img, border * 0.5, border * 0.5, true) } else { b.img.clone() }; + let mut p = Params::default(); + p.u[0] = [seed, cycle.is_some() as u32, colored as u32, 0]; + p.f[0] = [ox as f32, oy as f32, scale * sx, scale * sy]; + p.f[1] = [fm, evo, c, evo_w]; + p.f[2] = [oct + extra, infl, im, sharp]; + p.f[3] = [ec[0], ec[1], ec[2], sm]; + let out = e.image(b.img.width, b.img.height); + e.pixels("fxs_roughen", &p, &b.img, Some(&d), &out, None); + b.img = out; + Some(b) +} + +fn texturize(e: &mut Enc, ctx: &EffectCtx, mut b: GBuf) -> Option { + let ang = ctx.params.f("lightDirection").to_radians(); + let k = ctx.params.f("textureContrast") as f32; + if k == 0.0 { + return Some(b); + } + let Some(tex) = ctx.layer_param("textureLayer", true) else { return Some(b) }; + if tex.buf.img.is_empty() { + return None; + } + // transition::place_layer, per column / row in the texture buffer's pixels. + let (ls, os) = (ctx.layer_size, tex.size); + let inv = 1.0 / b.scale.max(1e-9); + let mode = ctx.params.e("texturePlacement"); + let place = |v: f64, k: usize| { + let p = (v - b.offset[k]) * inv; + let q = match mode { + 0 if os[0] >= 1.0 && os[1] >= 1.0 => p.rem_euclid(os[k]), + 1 => p + (os[k] - ls[k]) * 0.5, + _ if ls[0] > 0.0 && ls[1] > 0.0 => p * os[k] / ls[k], + _ => p, + }; + [q * tex.buf.scale + tex.buf.offset[k]] + }; + let data = axis_table(b.img.width, b.img.height, |x| place(x, 0), |y| place(y, 1)); + let timg = e.g.upload_image(&tex.buf.img)?; + let mut p = Params::default(); + p.u[0][0] = b.img.width; + let buf = e.data(&data); + let luma = e.image(b.img.width, b.img.height); + e.pixels("fxs_tex_luma", &p, &b.img, Some(&timg), &luma, Some(&buf)); + let s = 0.7 * b.scale; + let hgt = gaussian_blur(e, &luma, s, s, true); + let mut p = Params::default(); + p.f[0] = [ang.cos() as f32, -ang.sin() as f32, k, 0.0]; + let out = e.image(b.img.width, b.img.height); + e.pixels("fxs_texturize", &p, &b.img, Some(&hgt), &out, None); + b.img = out; + Some(b) +} + +// ---------------------------------------------------------------- distort.rs + +fn corner_pin(e: &mut Enc, ctx: &EffectCtx, mut b: GBuf) -> Option { + let (w, h) = (ctx.layer_size[0].max(1.0), ctx.layer_size[1].max(1.0)); + let q = ["ul", "ur", "lr", "ll"].map(|k| { + let p = b.to_px(ctx.params.v2(k)); + vec2(p.0, p.1) + }); + let quad = Mat3::square_to_quad(q); + let o = b.to_px([0.0, 0.0]); + let m = quad * Mat3::scale(vec2(1.0 / (w * b.scale), 1.0 / (h * b.scale))) * Mat3::translate(vec2(-o.0, -o.1)); + let empty = e.image(b.img.width, b.img.height); + b.img = ops::warp(e, &empty, &b.img, &m, Sampling::Bilinear, BlendMode::Normal, 1.0, 0, None); + Some(b) +} + +// ---------------------------------------------------------------- Glow / Transform extras + +/// Glow settings the base kernel in `effects.rs` leaves to the CPU (another Glow Operation or +/// the Arbitrary Map) and [`glow`] implements. +pub(crate) fn glow_extra(ctx: &EffectCtx) -> bool { + ctx.params.e("colors") == 2 || effectcraft_effects::glow_operation(ctx) != BlendMode::Add +} + +fn dims_xy(dim: u32) -> (f64, f64) { + match dim { + 1 => (1.0, 0.0), + 2 => (0.0, 1.0), + _ => (1.0, 1.0), + } +} + +/// misc::glow with any Glow Operation and the Arbitrary Map. +pub(crate) fn glow(e: &mut Enc, ctx: &EffectCtx, mut b: GBuf) -> Option { + let thr = ctx.params.f("threshold") as f32 / 100.0; + let radius = ctx.params.f("radius") * b.scale; + let intensity = ctx.params.f("intensity") as f32; + let colors = ctx.params.e("colors"); + let (kx, ky) = dims_xy(ctx.params.e("glowDimensions")); + if !ctx.adjustment { + b.pad(e, (radius * 1.5).ceil() as u32 + 2)?; + } + let mut p = Params::default(); + p.u[0] = [(colors == 1) as u32, (ctx.params.e("based") == 0) as u32, ctx.params.e("colorLooping"), 0]; + p.f[0] = [thr, ctx.params.f("colorLoops") as f32, (ctx.params.f("colorPhase") / 360.0) as f32, ctx.params.f("abMidpoint") as f32 / 100.0]; + let bright = e.image(b.img.width, b.img.height); + if colors == 2 { + // Arbitrary Map: Curves tables per channel (see effects::curves). + let mut parts = ctx.params.s("arbitraryMap").split('|'); + let curves: Vec> = (0..3).map(|_| parts.next().and_then(effectcraft_effects::Curve::parse)).collect(); + let mut data = vec![0.0f32]; + let mut offs = [-1.0f32; 3]; + for (i, c) in curves.iter().enumerate() { + let Some(c) = c else { continue }; + let (xs, ys, ms, lut) = c.tables(); + offs[i] = data.len() as f32; + data.push(xs.len() as f32); + data.extend_from_slice(xs); + data.extend_from_slice(ys); + data.extend_from_slice(ms); + data.extend_from_slice(lut); + } + p.f[1] = [offs[0], offs[1], offs[2], 0.0]; + let buf = e.data(&data); + e.pixels("fxs_glow_map", &p, &b.img, None, &bright, Some(&buf)); + } else { + p.f[1] = ctx.params.color("colorA"); + p.f[2] = ctx.params.color("colorB"); + e.pixels("glow_bright", &p, &b.img, None, &bright, None); + } + let s = (radius / 2.0).max(0.5); + let blurred = gaussian_blur(e, &bright, s * kx, s * ky, false); + let operation = ctx.params.e("operation"); + let glow_op = effectcraft_effects::glow_operation(ctx); + let mut p = Params::default(); + p.f[0][0] = intensity; + let out = e.image(b.img.width, b.img.height); + if operation == 0 && glow_op != BlendMode::Add { + p.u[0][0] = ops::mode_id(glow_op); + e.pixels("fxs_glow_op", &p, &b.img, Some(&blurred), &out, None); + } else { + p.u[0][0] = operation.min(2); + e.pixels("glow_combine", &p, &b.img, Some(&blurred), &out, None); + } + b.img = out; + Some(b) +} + +/// Transform renders with motion blur (a shutter and a host to read the parameters at other +/// times): [`transform`] implements it, the base kernel in `effects.rs` the sharp case. +pub(crate) fn transform_blur(ctx: &EffectCtx) -> bool { + ctx.env.host.is_some() && effectcraft_effects::transform_shutter(ctx).is_some() +} + +/// distort::transform_matrix. +fn transform_matrix(pr: &effectcraft_effects::Params, b: &GBuf) -> Mat3 { + let anchor = b.to_px(pr.v2("anchor")); + let pos = b.to_px(pr.v2("position")); + let sh = pr.f("scaleHeight"); + let sw = if pr.b("uniform") { sh } else { pr.f("scaleWidth") }; + Mat3::translate(vec2(pos.0, pos.1)) + * Mat3::rotate_deg(pr.f("rotation")) + * Mat3::skew_deg(pr.f("skew"), pr.f("skewAxis")) + * Mat3::scale(vec2(sw / 100.0, sh / 100.0)) + * Mat3::translate(vec2(-anchor.0, -anchor.1)) +} + +/// distort::transform's motion blur: the transform at Samples instants across the shutter, +/// averaged. +pub(crate) fn transform(e: &mut Enc, ctx: &EffectCtx, mut b: GBuf) -> Option { + let (angle, phase, n) = effectcraft_effects::transform_shutter(ctx)?; + let host = ctx.env.host?; + let sampling = if ctx.params.e("sampling") == 1 { Sampling::Bicubic } else { Sampling::Bilinear }; + let fd = 1.0 / ctx.fps(); + let (w, h) = (b.img.width, b.img.height); + let mut acc = e.image(w, h); + let mut count = 0u32; + for k in 0..n { + let t = ctx.time + (phase + angle * k as f64 / (n - 1) as f64) / 360.0 * fd; + let Some(pr) = host.params_at(t) else { continue }; + let op = pr.f("opacity") as f32 / 100.0; + let empty = e.image(w, h); + let one = ops::warp(e, &empty, &b.img, &transform_matrix(&pr, &b), sampling, BlendMode::Normal, op, 0, None); + let sum = e.image(w, h); + e.pixels("fxs_add", &Params::default(), &acc, Some(&one), &sum, None); + acc = sum; + count += 1; + } + if count == 0 { + // The CPU renders the frame sharp. + return None; + } + let mut p = Params::default(); + p.f[0][0] = count as f32; + let out = e.image(w, h); + e.pixels("fxs_div", &p, &acc, None, &out, None); + b.img = out; + Some(b) +} diff --git a/crates/gpu/src/fx_tone.rs b/crates/gpu/src/fx_tone.rs new file mode 100644 index 000000000..8025ffdd8 --- /dev/null +++ b/crates/gpu/src/fx_tone.rs @@ -0,0 +1,381 @@ +//! GPU effects, tonal and colour correction family (kernels in `shaders/fx_tone.wgsl`, every +//! entry point prefixed `fxt_`): one per-pixel pass each (`fxt_point`) with the CPU effect's +//! exact maths. +//! +//! Levels (Individual Controls), Gamma/Pedestal/Gain, Photo Filter, Change Color, Change to +//! Color, Leave Color, Broadcast Colors, Color Balance (HLS), Video Limiter, PS Arbitrary Map, +//! CC Toner, CC Color Offset and CC Kernel, plus Hue/Saturation's colour ranges and Levels' +//! per-channel controls (the parts the base kernels in `kernels.wgsl` leave to the CPU). Auto +//! Levels / Contrast / Color and Equalize measure the frame's histograms on the CPU (the image +//! is read back for that) and apply the correction on the GPU. + +use effectcraft_color::rgb_to_hsl; +use effectcraft_effects::EffectCtx; + +use crate::context::{Enc, Params}; +use crate::effects::{GBuf, gaussian_blur}; + +/// Compute entry points in `fx_tone.wgsl`. +pub(crate) const KERNELS: &[&str] = &["fxt_point"]; + +/// Effect ids implemented here. +pub(crate) const IDS: &[&str] = &[ + "ec.color.levelsic", + "ec.color.gammapedestalgain", + "ec.color.photofilter", + "ec.color.changecolor", + "ec.color.changetocolor", + "ec.color.leavecolor", + "ec.color.broadcast", + "ec.color.colorbalancehls", + "ec.color.videolimiter", + "ec.color.psarbitrarymap", + "ec.color.cctoner", + "ec.color.cccoloroffset", + "ec.color.cckernel", + "ec.color.autolevels", + "ec.color.autocontrast", + "ec.color.autocolor", + "ec.color.equalize", + "ec.color.shadowhighlight", + "ec.color.cccolorneutralizer", + "ec.color.colorstabilizer", +]; + +/// Run effect `id` (one of [`IDS`]); `None` = this parameter combination runs on the CPU. +pub(crate) fn apply(e: &mut Enc, id: &str, ctx: &EffectCtx, b: GBuf) -> Option { + match id { + "ec.color.shadowhighlight" => shadow_highlight(e, ctx, b), + _ => point(e, id, ctx, b), + } +} + +/// Invert in the HLS and YIQ channels (the base `pointwise` kernel does RGB and Alpha). +pub(crate) fn invert(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option { + let mut p = op(18); + p.u[0][1] = ctx.params.e("channel"); + p.f[0][0] = 1.0 - ctx.params.f("blend") as f32 / 100.0; + run(e, &p, b, None) +} + +/// Shadow/Highlight: the amounts (Auto Amounts) and the Black / White Clip ranges are measured +/// on the CPU; the luminance bases are blurred and the image adjusted on the GPU. +fn shadow_highlight(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option { + let pr = ctx.params; + let src = e.download(&b.img)?; + let (s_amt, h_amt) = effectcraft_effects::shadow_highlight_amounts(ctx, &src); + let (bc, wc) = (pr.f("moreOptions/blackClip").clamp(0.0, 49.0) / 100.0, pr.f("moreOptions/whiteClip").clamp(0.0, 49.0) / 100.0); + let clip0 = (bc > 0.0 || wc > 0.0).then(|| effectcraft_effects::luma_clip_points(&src, bc, wc)); + let s_r = pr.f("moreOptions/shadowRadius").max(0.0) * b.scale / 2.0; + let h_r = pr.f("moreOptions/highlightRadius").max(0.0) * b.scale / 2.0; + let (w, h) = (b.img.width, b.img.height); + let luma = e.image(w, h); + e.pixels("fxt_point", &op(19), &b.img, None, &luma, None); + let base_s = gaussian_blur(e, &luma, s_r, s_r, true); + let base_h = if (h_r - s_r).abs() < 1e-9 { base_s.clone() } else { gaussian_blur(e, &luma, h_r, h_r, true) }; + let bases = e.image(w, h); + e.pixels("fxt_point", &op(21), &base_s, Some(&base_h), &bases, None); + let mut p = op(20); + p.f[0] = + [s_amt, h_amt, (pr.f("moreOptions/shadowTonalWidth") as f32 / 100.0).max(0.01), (pr.f("moreOptions/highlightTonalWidth") as f32 / 100.0).max(0.01)]; + p.f[1] = [ + pr.f("moreOptions/colorCorrection") as f32 / 100.0, + pr.f("moreOptions/midtoneContrast") as f32 / 100.0, + (pr.f("blend") / 100.0).clamp(0.0, 1.0) as f32, + 0.0, + ]; + let out = e.image(w, h); + e.pixels("fxt_point", &p, &b.img, Some(&bases), &out, None); + let Some((lo0, hi0)) = clip0 else { return Some(GBuf { img: out, ..b }) }; + let adjusted = e.download(&out)?; + let (lo1, hi1) = effectcraft_effects::luma_clip_points(&adjusted, bc, wc); + if (lo0, hi0) == (lo1, hi1) || hi1 - lo1 <= 1e-3 { + return Some(GBuf { img: out, ..b }); + } + let mut p = op(22); + p.f[0] = [lo0, lo1, (hi0 - lo0) / (hi1 - lo1), 0.0]; + run(e, &p, GBuf { img: out, ..b }, None) +} + +fn rgb(c: [f32; 4]) -> [f32; 4] { + [c[0], c[1], c[2], 0.0] +} + +/// Hue/Saturation with colour ranges (Channel Control); Master and Colorize alone run in the +/// base `pointwise` kernel. +pub(crate) fn hue_saturation(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option { + let pr = ctx.params; + let f = |id: String, d: f64| pr.get(&id).map(|v| v.as_f64()).unwrap_or(d) as f32; + let mut data = vec![]; + for (r, _, c) in effectcraft_effects::HUESAT_RANGES { + let range = [ + f(format!("{r}RangeStart"), c - 15.0), + f(format!("{r}RangeEnd"), c + 15.0), + f(format!("{r}StartFalloff"), 30.0).max(0.0), + f(format!("{r}EndFalloff"), 30.0).max(0.0), + f(format!("{r}Hue"), 0.0) / 360.0, + f(format!("{r}Saturation"), 0.0) / 100.0, + f(format!("{r}Lightness"), 0.0) / 100.0, + ]; + if range[4] != 0.0 || range[5] != 0.0 || range[6] != 0.0 { + data.extend(range); + } + } + let g = |k: &str| pr.f(k) as f32; + let mut p = Params::default(); + p.u[0] = [14, pr.b("colorize") as u32, (data.len() / 7) as u32, 0]; + p.f[0] = [g("hue") / 360.0, g("saturation") / 100.0, g("lightness") / 100.0, 0.0]; + p.f[1] = [g("colorizeHue") / 360.0, g("colorizeSaturation") / 100.0, g("colorizeLightness") / 100.0, 0.0]; + run(e, &p, b, Some(data)) +} + +/// Levels with Red / Green / Blue / Alpha controls (the RGB controls alone run in the base +/// `pointwise` kernel). +pub(crate) fn levels(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option { + let pr = ctx.params; + let (cb, cw) = effectcraft_effects::levels_clip(ctx); + let mut data = vec![ + pr.f("inBlack") as f32, + pr.f("inWhite") as f32, + pr.f("gamma").max(0.01) as f32, + pr.f("outBlack") as f32, + pr.f("outWhite") as f32, + cb as u32 as f32, + cw as u32 as f32, + ]; + for (v, (cb, cw)) in effectcraft_effects::levels_channel_settings(ctx) { + data.extend(v); + data.extend([cb as u32 as f32, cw as u32 as f32]); + } + run(e, &op(15), b, Some(data)) +} + +fn op(code: u32) -> Params { + let mut p = Params::default(); + p.u[0][0] = code; + p +} + +fn run(e: &mut Enc, p: &Params, b: GBuf, data: Option>) -> Option { + let buf = data.map(|d| e.data(&d)); + let out = e.image(b.img.width, b.img.height); + e.pixels("fxt_point", p, &b.img, None, &out, buf.as_ref()); + Some(GBuf { img: out, ..b }) +} + +/// color3::parse_map. +fn parse_map(s: &str) -> Vec { + let vals: Vec = s.split(|ch: char| ch == ',' || ch.is_whitespace()).filter(|t| !t.is_empty()).filter_map(|t| t.parse::().ok()).collect(); + if vals.len() < 2 { + return (0..256).map(|i| i as f32 / 255.0).collect(); + } + let max = vals.iter().cloned().fold(0.0f32, f32::max); + let k = if max > 1.0 { 1.0 / 255.0 } else { 1.0 }; + vals.iter().map(|v| (v * k).clamp(0.0, 1.0)).collect() +} + +fn point(e: &mut Enc, id: &str, ctx: &EffectCtx, b: GBuf) -> Option { + let pr = ctx.params; + let f = |k: &str| pr.f(k) as f32; + let mut p = Params::default(); + let mut data = None; + match id { + "ec.color.levelsic" => { + const IDS: [[&str; 5]; 5] = [ + ["rgb/rgbInBlack", "rgb/rgbInWhite", "rgb/rgbGamma", "rgb/rgbOutBlack", "rgb/rgbOutWhite"], + ["red/redInBlack", "red/redInWhite", "red/redGamma", "red/redOutBlack", "red/redOutWhite"], + ["green/greenInBlack", "green/greenInWhite", "green/greenGamma", "green/greenOutBlack", "green/greenOutWhite"], + ["blue/blueInBlack", "blue/blueInWhite", "blue/blueGamma", "blue/blueOutBlack", "blue/blueOutWhite"], + ["alpha/alphaInBlack", "alpha/alphaInWhite", "alpha/alphaGamma", "alpha/alphaOutBlack", "alpha/alphaOutWhite"], + ]; + let lv: Vec<[f32; 5]> = IDS.iter().map(|ids| ids.map(f)).collect(); + let ident = |l: &[f32; 5]| *l == [0.0, 1.0, 1.0, 0.0, 1.0]; + if lv.iter().all(ident) { + return Some(b); + } + let (cb, cw) = effectcraft_effects::levels_clip(ctx); + let mut d = vec![]; + for l in &lv { + d.extend(l); + d.push(ident(l) as u32 as f32); + } + p.u[0] = [1, cb as u32, cw as u32, 0]; + data = Some(d); + } + "ec.color.gammapedestalgain" => { + let ch = |n: &str| [f(&format!("{n}Gamma")).max(0.01), f(&format!("{n}Pedestal")), f(&format!("{n}Gain")), 0.0]; + p.u[0][0] = 2; + p.f[0] = ch("red"); + p.f[1] = ch("green"); + p.f[2] = ch("blue"); + p.f[3][0] = f("blackStretch").max(0.0); + } + "ec.color.photofilter" => { + let fi = pr.e("filter"); + let c = match effectcraft_effects::PHOTO_FILTERS.get(fi as usize) { + Some((_, rgb)) if fi != effectcraft_effects::PHOTO_FILTER_CUSTOM => rgb.map(|v| v as f32 / 255.0), + _ => { + let c = pr.color("color"); + [c[0], c[1], c[2]] + } + }; + p.u[0] = [3, pr.b("preserveLuminosity") as u32, 0, 0]; + p.f[0] = [c[0], c[1], c[2], f("density") / 100.0]; + } + "ec.color.changetocolor" => { + let from = pr.color("from"); + let to = pr.color("to"); + let (fh, fs, fl) = rgb_to_hsl(from[0], from[1], from[2]); + let (th, ts, tl) = rgb_to_hsl(to[0], to[1], to[2]); + let change = pr.e("change"); + let flags = matches!(change, 1 | 3) as u32 + | (matches!(change, 2 | 3) as u32) << 1 + | ((pr.e("changeBy") == 1) as u32) << 2 + | (pr.b("viewCorrectionMatte") as u32) << 3; + p.u[0] = [4, flags, 0, 0]; + p.f[0] = [fh, fs, fl, f("softness") / 100.0]; + p.f[1] = [th, ts, tl, 0.0]; + p.f[2] = [f("toleranceGroup/hue") / 100.0, f("toleranceGroup/lightness") / 100.0, f("toleranceGroup/saturation") / 100.0, 0.0]; + } + "ec.color.leavecolor" => { + let amt = f("amount") / 100.0; + if amt <= 0.0 { + return Some(b); + } + let k = pr.color("color"); + p.u[0] = [5, (pr.e("matchColors") == 1) as u32, 0, 0]; + p.f[0] = [k[0], k[1], k[2], rgb_to_hsl(k[0], k[1], k[2]).0]; + p.f[1] = [amt, f("tolerance") / 100.0, f("edgeSoftness") / 100.0, 0.0]; + } + "ec.color.changecolor" => { + let view_mask = pr.e("view") == 1; + let (dh, dl, ds) = (f("hueTransform") / 360.0, f("lightnessTransform") / 100.0, f("saturationTransform") / 100.0); + if !view_mask && dh == 0.0 && dl == 0.0 && ds == 0.0 { + return Some(b); + } + let k = pr.color("colorToChange"); + p.u[0] = [6, pr.e("matchColors"), view_mask as u32 | (pr.b("invertColorCorrectionMask") as u32) << 1, 0]; + p.f[0] = [k[0], k[1], k[2], rgb_to_hsl(k[0], k[1], k[2]).0]; + p.f[1] = [dh, dl, ds, 0.0]; + p.f[2] = [f("matchingTolerance") / 100.0, f("matchingSoftness") / 100.0, 0.0, 0.0]; + } + "ec.color.broadcast" => { + let pal = pr.e("locale") == 1; + let (setup, span) = if pal { (0.0, 100.0) } else { (7.5, 92.5) }; + p.u[0] = [7, pal as u32, pr.e("howToMakeSafe"), 0]; + p.f[0][0] = (f("maxSignal") - setup) / span; + } + "ec.color.colorbalancehls" => { + let (dh, dl, ds) = (f("hue") / 360.0, f("lightness") / 100.0, f("saturation") / 100.0); + if dh == 0.0 && dl == 0.0 && ds == 0.0 { + return Some(b); + } + p.u[0][0] = 8; + p.f[0] = [dh, dl, ds, 0.0]; + } + "ec.color.cctoner" => { + let g = |id: &str| rgb(pr.color(id)); + let stops = match pr.e("tones") { + 0 => vec![g("shadows"), g("highlights")], + 2 => vec![g("shadows"), g("darktones"), g("midtones"), g("brights"), g("highlights")], + _ => vec![g("shadows"), g("midtones"), g("highlights")], + }; + p.u[0] = [9, stops.len() as u32 - 1, 0, 0]; + p.f[0][0] = (f("blend") / 100.0).clamp(0.0, 1.0); + for (i, s) in stops.iter().enumerate() { + p.f[1 + i] = *s; + } + } + "ec.color.cccoloroffset" => { + let offs = ["redPhase", "greenPhase", "bluePhase"].map(|id| (pr.f(id) / 360.0).rem_euclid(1.0) as f32); + if offs.iter().all(|o| *o == 0.0) { + return Some(b); + } + p.u[0] = [10, pr.e("overflow"), 0, 0]; + p.f[0] = [offs[0], offs[1], offs[2], 0.0]; + } + "ec.color.cckernel" => { + let k = ["k1", "k2", "k3", "k4", "k5", "k6", "k7", "k8", "k9"].map(f); + let (scale, offset) = (f("scale"), f("offset")); + let identity = k.iter().enumerate().all(|(i, v)| if i == 4 { *v == 1.0 } else { *v == 0.0 }); + if identity && scale == 1.0 && offset == 0.0 { + return Some(b); + } + p.u[0] = [11, pr.b("absolute") as u32, 0, 0]; + p.f[0] = [k[0], k[1], k[2], k[3]]; + p.f[1] = [k[4], k[5], k[6], k[7]]; + p.f[2] = [k[8], scale, offset, 0.0]; + } + "ec.color.psarbitrarymap" => { + let map = parse_map(pr.s("map")); + p.u[0] = [12, pr.b("applyPhaseMapToAlpha") as u32, map.len() as u32, 0]; + p.f[0][0] = (pr.f("phase") * (map.len() - 1) as f64 / 255.0) as f32; + data = Some(map); + } + "ec.color.videolimiter" => { + const CLIP_LEVELS: [f32; 5] = [0.9, 0.95, 1.0, 1.05, 1.09]; + let level = CLIP_LEVELS[(pr.e("clipLevel") as usize).min(4)]; + let comp = [0.0, 0.03, 0.05, 0.1, 0.2][(pr.e("compressionBeforeClipping") as usize).min(4)]; + p.u[0] = [13, pr.e("clipMethod"), pr.b("gamutWarning") as u32, 0]; + p.f[0] = [level, level * (1.0 - comp), 0.0, 0.0]; + p.f[1] = rgb(pr.color("gamutWarningColor")); + } + "ec.color.colorstabilizer" => { + // The sample means are measured on the CPU (the frame is read back). + if ctx.env.host.is_none() { + return Some(b); + } + let img = e.download(&b.img)?; + let cpu = effectcraft_effects::Buf { img, offset: b.offset, scale: b.scale }; + let Some(maps) = effectcraft_effects::color_stabilizer_maps(ctx, &cpu) else { return Some(b) }; + let mut d = vec![]; + for m in &maps { + d.push(m.len() as f32); + for i in 0..5 { + let (x, y) = m.get(i).copied().unwrap_or((0.0, 0.0)); + d.extend([x as f32, y as f32]); + } + } + p.u[0][0] = 24; + data = Some(d); + } + "ec.color.cccolorneutralizer" => { + let pairs = [("shadowsUnbalance", "shadowsBalance"), ("midtonesUnbalance", "midtonesBalance"), ("highlightsUnbalance", "highlightsBalance")]; + for (i, (u, bal)) in pairs.iter().enumerate() { + let (u, v) = (pr.color(u), pr.color(bal)); + p.f[i] = [v[0] - u[0], v[1] - u[1], v[2] - u[2], 0.0]; + } + p.u[0][0] = 23; + p.f[3] = [f("pinning") / 100.0, f("blendWOriginal") / 100.0, f("contrast") / 100.0, 0.0]; + p.f[4] = [f("darks") / 100.0, f("brights") / 100.0, 0.0, 0.0]; + } + "ec.color.autolevels" | "ec.color.autocontrast" | "ec.color.autocolor" | "ec.color.equalize" => { + // The histograms are measured on the CPU. + let img = e.download(&b.img)?; + if id == "ec.color.equalize" { + let amt = (pr.f("amount") / 100.0).clamp(0.0, 1.0) as f32; + if amt <= 0.0 { + return Some(b); + } + let style = pr.e("style"); + let tables = effectcraft_effects::equalize_tables(&img, style); + p.u[0] = [17, style, tables.first().map_or(0, |t| t.len()) as u32, 0]; + p.f[0][0] = amt; + data = Some(tables.concat()); + } else { + let kind = match id { + "ec.color.autolevels" => 0, + "ec.color.autocontrast" => 1, + _ => 2, + }; + let (ranges, gam) = effectcraft_effects::auto_correct_settings(ctx, &img, kind); + p.u[0][0] = 16; + p.f[0] = [ranges[0].0, ranges[1].0, ranges[2].0, 0.0]; + p.f[1] = [ranges[0].1, ranges[1].1, ranges[2].1, 0.0]; + p.f[2] = [gam[0], gam[1], gam[2], (pr.f("blend") / 100.0).clamp(0.0, 1.0) as f32]; + } + } + _ => return None, + } + run(e, &p, b, data) +} diff --git a/crates/gpu/src/lib.rs b/crates/gpu/src/lib.rs index 0e3eb0e09..559393bc7 100644 --- a/crates/gpu/src/lib.rs +++ b/crates/gpu/src/lib.rs @@ -16,7 +16,10 @@ //! //! 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 -//! uploaded and read back once. +//! uploaded and read back once. Each family lives in its own module with its own WGSL file +//! (`fx_color`, `fx_distort`, `fx_generate`, `fx_key`, `fx_noise`, `fx_stylize`, `fx_tone`); +//! settings a kernel cannot match fall back to the CPU (`catalog::gpu_supported`, or `None` +//! from the family's `apply`). //! //! GPU particles (`particles`): the stepped particle effects hand their simulation to //! [`effectcraft_effects::psim::ParticleSim`], implemented here with one invocation per particle @@ -35,6 +38,10 @@ mod effects; mod fx_color; mod fx_distort; mod fx_generate; +mod fx_key; +mod fx_noise; +mod fx_stylize; +mod fx_tone; mod ops; mod particles; mod walk; @@ -174,4 +181,12 @@ mod tests_fx_distort; #[cfg(test)] mod tests_fx_generate; #[cfg(test)] +mod tests_fx_key; +#[cfg(test)] +mod tests_fx_noise; +#[cfg(test)] +mod tests_fx_stylize; +#[cfg(test)] +mod tests_fx_tone; +#[cfg(test)] mod tests_particles; diff --git a/crates/gpu/src/shaders/fx_key.wgsl b/crates/gpu/src/shaders/fx_key.wgsl new file mode 100644 index 000000000..261e8aca7 --- /dev/null +++ b/crates/gpu/src/shaders/fx_key.wgsl @@ -0,0 +1,867 @@ +// GPU effects (keying, matte and channel family): see src/fx_key.rs. Each entry point mirrors +// the CPU effect in effectcraft-effects operation for operation. Every name here is prefixed +// `fxk_` (the family files share one module). + +// util::unpremul: (straight colour, alpha); colour 0 when alpha ≤ 1e-6. +fn fxk_straight(px: vec4) -> vec3 { + if (px.w > 1e-6) { + return px.xyz / px.w; + } + return vec3(0.0); +} + +fn fxk_premul(c: vec3, a: f32) -> vec4 { + return vec4(c * a, a); +} + +// Pixel `p` of the third image, copied into `data` as RGBA f32 rows of P.u[3].x pixels. +fn fxk_rows(p: vec2) -> vec4 { + let i = (u32(p.y) * P.u[3].x + u32(p.x)) * 4u; + return vec4(data[i], data[i + 1u], data[i + 2u], data[i + 3u]); +} + +// keying::to_lab. +fn fxk_cbrt(t: f32) -> f32 { + return sign(t) * pow(abs(t), 1.0 / 3.0); +} + +fn fxk_lab_f(t: f32) -> f32 { + if (t > 0.008856) { + return fxk_cbrt(t); + } + return 7.787 * t + 16.0 / 116.0; +} + +// keying::range_components. +fn fxk_range_components(space: u32, c: vec3) -> vec3 { + if (space == 1u) { + let y = 0.299 * c.x + 0.587 * c.y + 0.114 * c.z; + let u = -0.14713 * c.x - 0.28886 * c.y + 0.436 * c.z; + let v = 0.615 * c.x - 0.51499 * c.y - 0.10001 * c.z; + return vec3(y * 255.0, u * 255.0 + 128.0, v * 255.0 + 128.0); + } + if (space == 2u) { + return c * 255.0; + } + let cl = clamp(c, vec3(0.0), vec3(1.0)); + let l = vec3(srgb_to_linear(cl.x), srgb_to_linear(cl.y), srgb_to_linear(cl.z)); + let x = (0.4124 * l.x + 0.3576 * l.y + 0.1805 * l.z) / 0.95047; + let yy = 0.2126 * l.x + 0.7152 * l.y + 0.0722 * l.z; + let z = (0.0193 * l.x + 0.1192 * l.y + 0.9505 * l.z) / 1.08883; + let fx = fxk_lab_f(x); + let fy = fxk_lab_f(yy); + let fz = fxk_lab_f(z); + return vec3((116.0 * fy - 16.0) * 2.55, 500.0 * (fx - fy) + 128.0, 200.0 * (fy - fz) + 128.0); +} + +// keying::matte_levels. +fn fxk_matte_levels(v: f32, in_b: f32, in_w: f32, gamma: f32, out_b: f32, out_w: f32) -> f32 { + var t = clamp((v - in_b / 255.0) / max((in_w - in_b) / 255.0, 1e-4), 0.0, 1.0); + t = powz(t, 1.0 / max(gamma, 0.01)); + return clamp(out_b / 255.0 + t * (out_w - out_b) / 255.0, 0.0, 1.0); +} + +// keying::secondary. +fn fxk_secondary(c: vec3, o1: u32, o2: u32, bal: f32) -> f32 { + var hi = c[o2]; + var lo = c[o1]; + if (c[o1] > c[o2]) { + hi = c[o1]; + lo = c[o2]; + } + return bal * hi + (1.0 - bal) * lo; +} + +// channel::arithmetic's 8-bit quantisation. +fn fxk_q(v: f32) -> u32 { + return u32(round_away(clamp(v, 0.0, 1.0) * 255.0)); +} + +// channel2::extract. +fn fxk_extract(px: vec4, ch: u32, inv: bool) -> vec4 { + let c = fxk_straight(px); + let a = max(px.w, 0.0); + var g: f32; + switch ch { + case 0u: { + if (inv) { + return fxk_premul(1.0 - c, a); + } + return px; + } + case 1u: { g = luminance(c) * a; } + case 2u: { g = c.x * a; } + case 3u: { g = c.y * a; } + case 4u: { g = c.z * a; } + default: { g = a; } + } + if (inv) { + g = 1.0 - g; + } + return vec4(g, g, g, 1.0); +} + +// channel2::arith. +fn fxk_arith(op: u32, a: f32, b: f32) -> f32 { + switch op { + case 0u: { return b; } + case 1u: { return a + b; } + case 2u: { return a - b; } + case 3u: { return a * b; } + case 4u: { return abs(a - b); } + case 5u: { return f32(fxk_q(a) & fxk_q(b)) / 255.0; } + case 6u: { return f32(fxk_q(a) | fxk_q(b)) / 255.0; } + case 7u: { return f32(fxk_q(a) ^ fxk_q(b)) / 255.0; } + case 8u, 11u: { return max(a, b); } + case 9u, 10u: { return min(a, b); } + case 12u: { return a + b - a * b; } + case 13u: { + if (a < 0.5) { + return 2.0 * a * b; + } + return 1.0 - 2.0 * (1.0 - a) * (1.0 - b); + } + default: { + if (b < 0.5) { + return 2.0 * a * b; + } + return 1.0 - 2.0 * (1.0 - a) * (1.0 - b); + } + } +} + +// channel2::overflow. +fn fxk_overflow(op: u32, v: f32, how: u32) -> f32 { + if (how == 0u) { + return clamp(v, 0.0, 1.0); + } + if (how == 1u) { + let m = 1.0001; + let r = v - m * floor(v / m); + return min(select(r, 0.0, r >= m || r < 0.0), 1.0); + } + if (op == 1u) { + return v * 0.5; + } + if (op == 2u) { + return (v + 1.0) * 0.5; + } + return clamp(v, 0.0, 1.0); +} + +// Per-pixel keying / matte / channel operations. u[0].x = op (see src/fx_key.rs for each +// effect's parameter layout): +// 1 Color Key matte 2 Luma Key matte 3 Color Range matte 4 Extract matte +// 5 apply a matte (aux.x; u[0].y = 0 final, 2 matte view, 3 Difference Matte's matte view, +// 4 Difference Matte's output) +// 6 Difference Matte matte (src and aux blurred) +// 7 Spill Suppressor 8 Advanced Spill Suppressor (aux = 1×1 excess sums when u[1].w) +// 9 Color Difference Key 10 Unmult 11 route channels (u[1] = sources) +// 12 Remove Color Matting 13 Arithmetic 14 Solid Composite 15 Set Matte +// 16 Channel Combiner 17 Blend 18 Calculations 19 Compound Arithmetic +// 20 Set Channels: channel u[0].y of the value image from aux; 21 Set Channels: premultiply +// 22 Screen Key matte 23 Screen Key output 24 alpha plane 25 Key Cleaner output +// 26 Matte Choker ramp 27 util::set_alpha (aux.x = alpha, data = fill) +// 28 matte view (alpha as opaque grey) +// Refine Soft / Hard Matte: 29 guide pack (luminance, alpha, products); 30 guided-filter +// coefficients from the box means (f[0].x = eps); 31 refined alpha (src = boxed coefficients, +// aux = pack, data = edge band); 32 edge band (src.y − aux.y > 1e-3); 33 choke / contrast / +// invert; 34 View Edge Region; 35 decontamination (aux.x = refined alpha, data = colour estimate) +@compute @workgroup_size(16, 16) +fn fxk_point(@builtin(global_invocation_id) gid: vec3) { + let dims = out_dims(); + let p = vec2(gid.xy); + if (p.x >= dims.x || p.y >= dims.y) { + return; + } + let px = textureLoad(src, p, 0); + let c = fxk_straight(px); + let a = max(px.w, 0.0); + let u0 = P.u[0]; + let u1 = P.u[1]; + let f0 = P.f[0]; + var o = px; + switch u0.x { + case 1u: { + let k = f0.xyz; + let d = max(max(abs(c.x - k.x), abs(c.y - k.y)), abs(c.z - k.z)); + o = vec4(select(1.0, 0.0, d <= f0.w)); + } + case 2u: { + let l = luminance(c); + let thr = f0.x; + let tol = f0.y; + var m: f32; + switch u0.y { + case 0u: { + if (tol > 0.0) { + m = clamp((thr + tol - l) / tol, 0.0, 1.0); + } else { + m = select(1.0, 0.0, l > thr); + } + } + case 2u: { m = select(1.0, 0.0, abs(l - thr) <= tol); } + case 3u: { m = select(0.0, 1.0, abs(l - thr) <= tol); } + default: { + if (tol > 0.0) { + m = clamp((l - (thr - tol)) / tol, 0.0, 1.0); + } else { + m = select(1.0, 0.0, l < thr); + } + } + } + o = vec4(m); + } + case 3u: { + let v = fxk_range_components(u0.y, c); + var out_d = 0.0; + for (var i = 0u; i < 3u; i++) { + out_d = max(max(out_d, P.f[0][i] - v[i]), v[i] - P.f[1][i]); + } + let fuzz = P.f[2].x; + var m = 1.0; + if (out_d <= 0.0) { + m = 0.0; + } else if (fuzz > 0.0) { + m = clamp(out_d / fuzz, 0.0, 1.0); + } + o = vec4(m); + } + case 4u: { + var v: f32; + switch u0.y { + case 1u: { v = c.x; } + case 2u: { v = c.y; } + case 3u: { v = c.z; } + case 4u: { v = a; } + default: { v = luminance(c); } + } + v = 255.0 * v; + let bp = f0.x; + let wp = f0.y; + let bs = f0.z; + let ws = f0.w; + var k = 1.0; + if (v < bp) { + k = select(0.0, clamp(1.0 - (bp - v) / bs, 0.0, 1.0), bs > 0.0); + } else if (v > wp) { + k = select(0.0, clamp(1.0 - (v - wp) / ws, 0.0, 1.0), ws > 0.0); + } + if (u0.z != 0u) { + k = 1.0 - k; + } + o = vec4(k); + } + case 5u: { + let m = textureLoad(aux, p, 0).x; + switch u0.y { + case 2u: { + let k = clamp(m, 0.0, 1.0); + let v = px.w * k; + o = vec4(v, v, v, 1.0); + } + case 3u: { + let v = m * px.w; + o = vec4(v, v, v, px.w); + } + case 4u: { o = px * m; } + default: { o = px * clamp(m, 0.0, 1.0); } + } + } + case 6u: { + let q = textureLoad(aux, p, 0); + let oc = fxk_straight(q); + let oa = max(q.w, 0.0); + let d = max(max(max(abs(c.x - oc.x), abs(c.y - oc.y)), abs(c.z - oc.z)), abs(a - oa)); + var m: f32; + if (d <= f0.x) { + m = 0.0; + } else if (f0.y <= 1e-6) { + m = 1.0; + } else { + m = clamp((d - f0.x) / f0.y, 0.0, 1.0); + } + o = vec4(m); + } + case 7u: { + if (px.w <= 0.0) { + textureStore(out, p, px); + return; + } + var s = px.xyz / px.w; + if (u0.y == 1u) { + let g = (s.x + s.y + s.z) / 3.0; + let dir = f0.xyz; + let proj = (s.x - g) * dir.x + (s.y - g) * dir.y + (s.z - g) * dir.z; + if (proj > 0.0) { + s = s - proj * dir * f0.w; + } + } else { + let spill = max(s[u1.x] - max(s[u1.y], s[u1.z]), 0.0); + s[u1.x] = s[u1.x] - spill * f0.w; + } + o = vec4(s * px.w, px.w); + } + case 8u: { + if (px.w <= 0.0) { + textureStore(out, p, px); + return; + } + var pi = u1.x; + var o1 = u1.y; + var o2 = u1.z; + if (u1.w != 0u) { + let ex = textureLoad(aux, vec2(0, 0), 0); + if (ex.y >= ex.x && ex.y >= ex.z) { + pi = 1u; + o1 = 0u; + o2 = 2u; + } else if (ex.z >= ex.x) { + pi = 2u; + o1 = 0u; + o2 = 1u; + } else { + pi = 0u; + o1 = 1u; + o2 = 2u; + } + } + var s = px.xyz / px.w; + let amt = f0.x; + let spill_range = f0.y; + let luma = f0.z; + let tol = f0.w; + let desat = P.f[1].x; + let neutral = P.f[1].y; + let key_hue = P.f[1].z; + var w = 1.0; + if (tol < 1.0) { + let h = rgb_to_hsl(s).x; + let d0 = abs(h - key_hue); + let d = min(d0, 1.0 - d0); + w = clamp(1.0 - d / max(tol * 0.5, 1e-4), 0.0, 1.0); + } + let mx = max(s[o1], s[o2]); + let avg = 0.5 * (s[o1] + s[o2]); + let lim = mx + (avg - mx) * spill_range; + let spill = max(s[pi] - lim, 0.0) * amt * w; + s[pi] = s[pi] - spill; + let lw = vec3(0.2126, 0.7152, 0.0722); + let back = spill * lw[pi] * luma + spill * neutral / 3.0; + s = s + back; + if (desat > 0.0 && spill > 0.0) { + let l = luminance(s); + let k = min(desat * min(spill * 4.0, 1.0), 1.0); + s = s + (vec3(l) - s) * k; + } + o = vec4(s * px.w, px.w); + } + case 9u: { + var cc = c; + if (u0.z != 0u) { + let cl = clamp(cc, vec3(0.0), vec3(1.0)); + cc = vec3(srgb_to_linear(cl.x), srgb_to_linear(cl.y), srgb_to_linear(cl.z)); + } + let pa = P.f[1]; + let pb = P.f[3]; + let ua = 1.0 - clamp((cc[u1.x] - cc[u1.y]) / f0.x, 0.0, 1.0); + let ub = 1.0 - clamp((cc[u1.x] - cc[u1.z]) / f0.y, 0.0, 1.0); + let ca = fxk_matte_levels(ua, pa.x, pa.y, pa.z, pa.w, P.f[2].x); + let cb = fxk_matte_levels(ub, pb.x, pb.y, pb.z, pb.w, P.f[2].y); + let um = max(ua, ub); + let cm = fxk_matte_levels(max(ca, cb), f0.z, f0.w, P.f[2].z, 0.0, 255.0); + var view = u0.y; + if (view == 8u) { + let right = p.x * 2 >= dims.x; + let bottom = p.y * 2 >= dims.y; + if (!right && !bottom) { + view = 2u; + } else if (right && !bottom) { + view = 4u; + } else if (!right && bottom) { + view = 6u; + } else { + view = 7u; + } + } + var g = -1.0; + switch view { + case 1u: { g = ua; } + case 2u: { g = ca; } + case 3u: { g = ub; } + case 4u: { g = cb; } + case 5u: { g = um; } + case 6u: { g = cm; } + default: {} + } + if (g >= 0.0) { + let m = g * a; + o = vec4(m, m, m, 1.0); + } else { + o = px * cm; + } + } + case 10u: { + let white = u0.y != 0u; + var d: f32; + var start: f32; + if (white) { + d = 1.0 - min(min(c.x, c.y), c.z); + start = 1.0 - f0.x; + } else { + d = max(max(c.x, c.y), c.z); + start = f0.x; + } + d = clamp(d, 0.0, 1.0); + let width = f0.y * (1.0 - start); + var m: f32; + if (width > 1e-6) { + m = clamp((d - start) / width, 0.0, 1.0); + } else { + m = select(0.0, 1.0, d > start); + } + if (m <= 1e-6) { + o = vec4(0.0); + } else { + let bg = select(0.0, 1.0, white); + var oc = c; + if (u0.z != 0u) { + oc = (c - bg * (1.0 - m)) / m; + } + if (u0.w != 0u) { + oc = clamp(oc, vec3(0.0), vec3(1.0)); + } + o = fxk_premul(oc, a * m); + } + } + case 11u: { + let v = vec4(dst_pick(u1.x, c, a), dst_pick(u1.y, c, a), dst_pick(u1.z, c, a), dst_pick(u1.w, c, a)); + o = fxk_premul(v.xyz, clamp(v.w, 0.0, 1.0)); + } + case 12u: { + if (px.w <= 1e-6) { + o = vec4(0.0); + } else { + for (var i = 0; i < 3; i++) { + let s = px[i] / px.w; + var v = max(s - f0[i] * (1.0 - px.w), 0.0); + if (u0.y != 0u) { + v = min(v, px.w); + } + o[i] = v; + } + } + } + case 13u: { + if (px.w <= 0.0) { + textureStore(out, p, px); + return; + } + let s = px.xyz / px.w; + var r = vec3(0.0); + for (var i = 0u; i < 3u; i++) { + let v = s[i]; + let kv = f0[i]; + let iv = fxk_q(v); + let ik = u32(round_away(kv * 255.0)); + var x: f32; + switch u0.y { + case 0u: { x = f32(iv & ik) / 255.0; } + case 1u: { x = f32(iv | ik) / 255.0; } + case 2u: { x = f32(iv ^ ik) / 255.0; } + case 3u: { x = v + kv; } + case 4u: { x = v - kv; } + case 5u: { x = abs(v - kv); } + case 6u: { x = min(v, kv); } + case 7u: { x = max(v, kv); } + case 8u: { x = select(v, 0.0, v > kv); } + case 9u: { x = select(v, 0.0, v < kv); } + case 10u: { x = select(0.0, 1.0, v >= kv); } + case 11u: { x = v * kv; } + default: { x = 1.0 - (1.0 - v) * (1.0 - kv); } + } + if (u0.z != 0u) { + r[i] = clamp(x, 0.0, 1.0); + } else { + r[i] = max(x, 0.0); + } + } + o = vec4(r * px.w, px.w); + } + case 14u: { + var b = blend_pixel(u0.y, f0, px * P.f[1].x, 0.5); + b.w = clamp(b.w, 0.0, 1.0); + o = vec4(max(b.xyz, vec3(0.0)), b.w); + } + case 15u: { + var mc = c; + var ma = a; + if ((u0.z & 8u) == 0u) { + let q = textureLoad(aux, p, 0); + mc = fxk_straight(q); + ma = max(q.w, 0.0); + } + var m = dst_pick(u0.y, mc, ma); + if ((u0.z & 4u) != 0u) { + m = m * ma; + } + m = clamp(m, 0.0, 1.0); + let inv = (u0.z & 1u) != 0u; + if (inv) { + m = 1.0 - m; + } + let own_m = (u0.z & 8u) != 0u; + let is_alpha = (u0.z & 16u) != 0u; + var na = m; + if ((u0.z & 2u) != 0u && (!own_m || !is_alpha || inv)) { + na = a * m; + } + o = fxk_premul(c, clamp(na, 0.0, 1.0)); + } + case 16u: { + let ch_from = u0.y; + let ch_to = u0.z; + let inv = (u0.w & 1u) != 0u; + let solid = (u0.w & 2u) != 0u; + let second = (u0.w & 4u) != 0u; + var sc = c; + var sa = a; + if (second) { + let q = textureLoad(aux, p, 0); + sc = fxk_straight(q); + sa = max(q.w, 0.0); + } + if (sa <= 0.0 && ch_from != 7u && !second) { + if (solid) { + o = fxk_premul(c, 1.0); + } + textureStore(out, p, o); + return; + } + var na = a; + var r: vec3; + if (ch_from <= 3u) { + switch ch_from { + case 0u: { + let hsl = rgb_to_hsl(sc); + r = vec3(hsl.x, hsl.z, hsl.y); + } + case 1u: { r = hsl_to_rgb(sc.x, sc.z, sc.y); } + case 2u: { + let y = 0.299 * sc.x + 0.587 * sc.y + 0.114 * sc.z; + r = vec3(y, 0.492 * (sc.z - y) + 0.5, 0.877 * (sc.x - y) + 0.5); + } + default: { + let y = sc.x; + let uu = sc.y - 0.5; + let vv = sc.z - 0.5; + let rr = y + vv / 0.877; + let bb = y + uu / 0.492; + let gg = (y - 0.299 * rr - 0.114 * bb) / 0.587; + r = vec3(rr, gg, bb); + } + } + if (inv) { + r = 1.0 - r; + } + } else { + var v: f32; + switch ch_from { + case 4u: { v = sc.x; } + case 5u: { v = sc.y; } + case 6u: { v = sc.z; } + case 7u: { v = sa; } + case 8u: { v = rgb_to_hsl(sc).z; } + case 9u: { v = rgb_to_hsl(sc).x; } + case 10u: { v = rgb_to_hsl(sc).y; } + case 11u: { v = luminance(sc); } + case 12u: { v = max(max(sc.x, sc.y), sc.z); } + default: { v = min(min(sc.x, sc.y), sc.z); } + } + if (inv) { + v = 1.0 - v; + } + switch ch_to { + case 0u: { r = vec3(v, 0.0, 0.0); } + case 1u: { r = vec3(0.0, v, 0.0); } + case 2u: { r = vec3(0.0, 0.0, v); } + case 3u: { + na = clamp(v, 0.0, 1.0); + r = vec3(1.0); + } + case 5u: { r = hsl_to_rgb(v, 1.0, 0.5); } + case 6u: { r = hsl_to_rgb(0.0, clamp(v, 0.0, 1.0), 0.5); } + case 8u: { + let hsl = rgb_to_hsl(c); + r = hsl_to_rgb(hsl.x, clamp(hsl.y * v, 0.0, 1.0), hsl.z); + } + default: { r = vec3(v); } + } + } + if (solid) { + na = 1.0; + } + o = fxk_premul(max(r, vec3(0.0)), na); + } + case 17u: { + let q = textureLoad(aux, p, 0); + var bl: vec4; + switch u0.y { + case 0u: { bl = q; } + case 1u, 2u: { + let oa = q.w; + if (px.w <= 0.0 || oa <= 0.0) { + bl = px; + } else { + let hsl = rgb_to_hsl(c); + let ohsl = rgb_to_hsl(fxk_straight(q)); + var h2 = ohsl.x; + var s2 = ohsl.y; + if (u0.y == 2u) { + if (hsl.y > 1e-4) { + s2 = hsl.y; + } else { + h2 = 0.0; + s2 = 0.0; + } + } + let rgb = hsl_to_rgb(h2, s2, hsl.z); + bl = px + (fxk_premul(rgb, a) - px) * oa; + } + } + case 3u: { bl = vec4(min(px.xyz, q.xyz), px.w); } + default: { bl = max(px, q); } + } + o = bl + (px - bl) * f0.x; + } + case 18u: { + let flags = u1.x; + var r = fxk_extract(px, u0.y, (flags & 1u) != 0u); + if ((flags & 8u) != 0u) { + let s = fxk_extract(textureLoad(aux, p, 0), u0.z, (flags & 2u) != 0u) * f0.x; + r = blend_pixel(u0.w, r, s, 0.5); + } + if ((flags & 4u) != 0u && r.w > 1e-6) { + r = fxk_premul(fxk_straight(r), px.w); + } + r.w = clamp(r.w, 0.0, 1.0); + o = r; + } + case 19u: { + let q = textureLoad(aux, p, 0); + let oc = fxk_straight(q); + let oa = max(q.w, 0.0); + var rc = c; + var ra = a; + if (u0.z != 2u) { + for (var i = 0u; i < 3u; i++) { + rc[i] = fxk_overflow(u0.y, fxk_arith(u0.y, c[i], oc[i]), u0.w); + } + } + if (u0.z != 0u) { + ra = fxk_overflow(u0.y, fxk_arith(u0.y, a, oa), u0.w); + } + let res = fxk_premul(rc, ra); + o = res + (px - res) * f0.x; + } + case 20u: { + let q = textureLoad(aux, p, 0); + o = px; + o[u0.y] = dst_pick(u0.z, fxk_straight(q), max(q.w, 0.0)); + } + case 21u: { + o = fxk_premul(px.xyz, clamp(px.w, 0.0, 1.0)); + } + case 22u: { + let d = c[u1.x] - fxk_secondary(c, u1.y, u1.z, f0.z); + let t = clamp(d / f0.x * f0.y, 0.0, 1.0); + o = vec4(clamp((1.0 - t - f0.w) / max(P.f[1].x - f0.w, 1e-3), 0.0, 1.0)); + } + case 23u: { + let k = clamp(textureLoad(aux, p, 0).x, 0.0, 1.0); + let alpha = a * k; + if (u0.y == 1u) { + o = vec4(alpha, alpha, alpha, 1.0); + } else if (u0.y == 2u) { + var v = 0.5; + if (alpha <= 1e-3) { + v = 0.0; + } else if (alpha >= 0.999) { + v = 1.0; + } + o = vec4(v, v, v, 1.0); + } else { + let screen = f0.xyz; + var fg = c; + if (!(u0.z != 0u || k <= 1e-4)) { + let s = 1.0 - k; + for (var i = 0; i < 3; i++) { + fg[i] = min(max(c[i] - s * screen[i], 0.0) / k, max(c[i], 1.0)); + } + } + let spill = max(fg[u1.x] - fxk_secondary(fg, u1.y, u1.z, f0.w), 0.0); + fg[u1.x] = fg[u1.x] - spill * P.f[1].x; + o = fxk_premul(fg, alpha); + } + } + case 24u: { + o = vec4(px.w); + } + case 25u: { + var na = textureLoad(aux, p, 0).x; + if (u0.y != 0u) { + na = clamp((na - 0.5) * 1.5 + 0.5, 0.0, 1.0); + } + var cc = c; + let a0 = a; + if (P.u[3].y != 0u && a0 > 1e-4 && a0 < 0.999) { + let q = fxk_rows(p); + if (q.w > 1e-6) { + let ea = q.w; + if (ea > 1e-4) { + let t = f0.y * (1.0 - a0); + cc = cc + (q.xyz / ea - cc) * t; + } + } + } + na = clamp((na - 0.5) * f0.x + 0.5, 0.0, 1.0); + if (a0 <= 1e-6) { + na = 0.0; + } + o = fxk_premul(cc, na); + } + case 26u: { + o = vec4(clamp((px.x - f0.x) / f0.y + 0.5, 0.0, 1.0)); + } + case 27u: { + let na = clamp(textureLoad(aux, p, 0).x, 0.0, 1.0); + var cc = c; + if (px.w <= 1e-4) { + cc = fxk_straight(fxk_rows(p)); + } + o = fxk_premul(cc, na); + } + case 29u: { + let g = luminance(px.xyz); + o = vec4(g, px.w, g * px.w, g * g); + } + case 30u: { + let var_g = px.w - px.x * px.x; + let cov = px.z - px.x * px.y; + let ca = cov / (var_g + f0.x); + o = vec4(ca, px.y - ca * px.x, 0.0, 0.0); + } + case 31u: { + let q = textureLoad(aux, p, 0); + if (fxk_rows(p).x != 0.0) { + o = vec4(clamp(px.x * q.x + px.y, 0.0, 1.0)); + } else { + o = vec4(q.y); + } + } + case 32u: { + o = vec4(select(0.0, 1.0, px.y - textureLoad(aux, p, 0).y > 1e-3)); + } + case 33u: { + var v = px.x; + let choke = f0.x; + if (choke > 0.0) { + v = (v - choke) / (1.0 - choke); + } else if (choke < 0.0) { + v = v / (1.0 + choke); + } + if (abs(f0.y - 1.0) > 1e-6) { + v = (v - 0.5) * f0.y + 0.5; + } + v = clamp(v, 0.0, 1.0); + if (u0.y != 0u) { + v = 1.0 - v; + } + o = vec4(v); + } + case 34u: { + let v = px.x; + if (textureLoad(aux, p, 0).x != 0.0) { + o = vec4(0.5 + 0.5 * v, 0.5 * v, 0.5 * v, 1.0); + } else { + o = vec4(v, v, v, 1.0); + } + } + case 35u: { + let n = textureLoad(aux, p, 0).x; + let a0 = px.w; + var t = 0.0; + if (a0 > 1e-4 && a0 < 0.999) { + t = 1.0 - a0; + } else if (u0.w != 0u && n > 1e-4 && n < 0.999) { + t = 1.0 - n; + } + let strength = f0.x * t; + if (u0.y != 0u) { + let v = select(0.0, strength, u0.z != 0u); + o = vec4(v, v, v, 1.0); + } else if (P.u[3].y != 0u && strength > 0.0 && a0 > 1e-4) { + let q = fxk_rows(p); + if (q.w > 1e-4) { + let cc = c + (q.xyz / q.w - c) * strength; + o = fxk_premul(cc, a0); + } + } + } + case 28u: { + let v = clamp(px.w, 0.0, 1.0); + o = vec4(v, v, v, 1.0); + } + default: {} + } + textureStore(out, p, o); +} + +// util::fit_layer: bilinear sample of the other layer (src) at the buffer coordinates in data +// (one per column, then one per row). +@compute @workgroup_size(16, 16) +fn fxk_fit(@builtin(global_invocation_id) gid: vec3) { + let dims = out_dims(); + let p = vec2(gid.xy); + if (p.x >= dims.x || p.y >= dims.y) { + return; + } + let x = data[u32(p.x)]; + let y = data[u32(dims.x) + u32(p.y)]; + textureStore(out, p, sample_bilinear(src, x, y)); +} + +// Sum of 16×16 blocks; u[0].x = first pass: sum Advanced Spill Suppressor's per-channel excess +// of each primary over the other two (straight colour × alpha), else sum the values. +@compute @workgroup_size(16, 16) +fn fxk_reduce(@builtin(global_invocation_id) gid: vec3) { + let dims = out_dims(); + let p = vec2(gid.xy); + if (p.x >= dims.x || p.y >= dims.y) { + return; + } + let sd = vec2(textureDimensions(src)); + var acc = vec4(0.0); + for (var j = 0; j < 16; j++) { + for (var i = 0; i < 16; i++) { + let q = vec2(p.x * 16 + i, p.y * 16 + j); + if (q.x >= sd.x || q.y >= sd.y) { + continue; + } + let v = textureLoad(src, q, 0); + if (P.u[0].x != 0u) { + let c = fxk_straight(v); + let a = max(v.w, 0.0); + acc += vec4( + max(c.x - max(c.y, c.z), 0.0) * a, + max(c.y - max(c.x, c.z), 0.0) * a, + max(c.z - max(c.x, c.y), 0.0) * a, + 0.0, + ); + } else { + acc += v; + } + } + } + textureStore(out, p, acc); +} diff --git a/crates/gpu/src/shaders/fx_noise.wgsl b/crates/gpu/src/shaders/fx_noise.wgsl new file mode 100644 index 000000000..56281158b --- /dev/null +++ b/crates/gpu/src/shaders/fx_noise.wgsl @@ -0,0 +1,825 @@ +// GPU effects (noise, blur and time family): see src/fx_noise.rs. Each entry point mirrors the CPU effect in +// effectcraft-effects operation for operation. Every name here is prefixed `fxn_` (the +// family files share one module). + +const FXN_MAX: f32 = 3.40282347e38; + +fn fxn_pixel(gid: vec3) -> vec2 { + let dims = out_dims(); + let p = vec2(gid.xy); + if (p.x >= dims.x || p.y >= dims.y) { + return vec2(-1); + } + return p; +} + +// a / b rounded like the CPU's IEEE division (GPU division may be approximate): one +// residual correction with fma. Used where a threshold follows the quotient. +fn fxn_div4(a: vec4, b: vec4) -> vec4 { + let q = a / b; + let r = fma(-q, b, a); + return fma(r, 1.0 / b, q); +} + +// util::unpremul: (straight colour, alpha). +fn fxn_unpremul(p: vec4) -> vec4 { + if (p.w > 1e-6) { + return vec4(p.xyz / p.w, p.w); + } + return vec4(0.0, 0.0, 0.0, max(p.w, 0.0)); +} + +// ---------------------------------------------------------------- Median family (noise::median_image) + +// Quantise to the median's 512 levels (0..511). u[0].x = 1: straight colour with alpha 1 +// (Median (Legacy) colour only). +@compute @workgroup_size(16, 16) +fn fxn_quant(@builtin(global_invocation_id) gid: vec3) { + let p = fxn_pixel(gid); + if (p.x < 0) { + return; + } + var v = textureLoad(src, p, 0); + if (P.u[0].x == 1u) { + v = vec4(fxn_unpremul(v).xyz, 1.0); + } + let q = clamp(v, vec4(0.0), vec4(1.0)) * 511.0; + textureStore(out, p, vec4(round_away(q.x), round_away(q.y), round_away(q.z), round_away(q.w))); +} + +// Per-channel median of the quantised levels over a (2r+1)² window (edges repeated): the +// smallest level m with more than half the window at or below m. u[0].x = r. +@compute @workgroup_size(16, 16) +fn fxn_median(@builtin(global_invocation_id) gid: vec3) { + let p = fxn_pixel(gid); + if (p.x < 0) { + return; + } + let r = i32(P.u[0].x); + let n = (2 * r + 1) * (2 * r + 1); + let half = f32(n / 2); + var lo = vec4(0.0); + var hi = vec4(511.0); + for (var it = 0; it < 9; it++) { + let mid = floor((lo + hi) * 0.5); + var cnt = vec4(0.0); + for (var dy = -r; dy <= r; dy++) { + for (var dx = -r; dx <= r; dx++) { + let q = tex_get_clamped(src, p.x + dx, p.y + dy); + cnt += select(vec4(0.0), vec4(1.0), q <= mid); + } + } + let above = cnt > vec4(half); + hi = select(hi, mid, above); + lo = select(mid + 1.0, lo, above); + } + textureStore(out, p, fxn_div4(lo, vec4(511.0))); +} + +// noise::median_px. +fn fxn_median_px(o: vec4, m: vec4, on_alpha: bool) -> vec4 { + if (on_alpha) { + let a = clamp(m.w, 0.0, 1.0); + return vec4(min(m.xyz, vec3(a)), a); + } + if (m.w <= 1e-4) { + return o; + } + return vec4(min(fxn_div4(m, vec4(m.w)).xyz, vec3(1.0)) * o.w, o.w); +} + +// src = the layer, aux = the median. u[0] = (mode, operate on alpha): 0 Median, 1 Dust & +// Scratches (f[0].x = threshold), 2 Median (Legacy) with alpha, 3 Median (Legacy) colour only. +@compute @workgroup_size(16, 16) +fn fxn_median_out(@builtin(global_invocation_id) gid: vec3) { + let p = fxn_pixel(gid); + if (p.x < 0) { + return; + } + let o = textureLoad(src, p, 0); + let m = textureLoad(aux, p, 0); + let on_alpha = P.u[0].y != 0u; + var r = o; + switch P.u[0].x { + case 0u: { + r = fxn_median_px(o, m, on_alpha); + } + case 1u: { + let cand = fxn_median_px(o, m, on_alpha); + let d = abs(cand - o); + if (max(max(d.x, d.y), max(d.z, d.w)) > P.f[0].x) { + r = cand; + } + } + case 2u: { + r = m; + } + default: { + r = vec4(m.xyz * o.w, o.w); + } + } + textureStore(out, p, r); +} + +// ---------------------------------------------------------------- Bilateral / Smart Blur (blur2.rs) + +// blur2::edge_preserving. u[0] = (ceil r, stride, hard, largest d²); u[1].x = grey (Bilateral +// with Colorize off); f[0] = (2σs², 2σr², range). +@compute @workgroup_size(16, 16) +fn fxn_edge(@builtin(global_invocation_id) gid: vec3) { + let p = fxn_pixel(gid); + if (p.x < 0) { + return; + } + let ri = i32(P.u[0].x); + let stride = i32(P.u[0].y); + let hard = P.u[0].z != 0u; + let d2max = i32(P.u[0].w); + let sig_s2 = P.f[0].x; + let sig_r2 = P.f[0].y; + let range = P.f[0].z; + let c = textureLoad(src, p, 0); + let cl = luminance(c.xyz); + var acc = vec4(0.0); + var wsum = 0.0; + for (var dy = -ri; dy <= ri; dy += stride) { + for (var dx = -ri; dx <= ri; dx += stride) { + let d2 = dx * dx + dy * dy; + if (d2 > d2max) { + continue; + } + let q = tex_get_clamped(src, p.x + dx, p.y + dy); + var w = 0.0; + if (hard) { + w = select(0.0, 1.0, abs(luminance(q.xyz) - cl) <= range); + } else { + let d = q - c; + let dc = d.x * d.x + d.y * d.y + d.z * d.z + d.w * d.w; + w = exp(-f32(d2) / sig_s2 - dc / sig_r2); + } + if (w > 0.0) { + acc += q * w; + wsum += w; + } + } + } + var o = c; + if (wsum > 0.0) { + o = acc / wsum; + } + if (P.u[1].x != 0u && o.w > 0.0) { + let l = luminance(o.xyz / o.w); + o = vec4(vec3(l * o.w), o.w); + } + textureStore(out, p, o); +} + +// Smart Blur's Edge Only / Overlay Edge (blur2::sobel on the blurred layer's premultiplied +// luminance). u[0].x = mode; f[0].x = edge threshold. +@compute @workgroup_size(16, 16) +fn fxn_smart_edges(@builtin(global_invocation_id) gid: vec3) { + let p = fxn_pixel(gid); + if (p.x < 0) { + return; + } + var l: array; + for (var j = 0; j < 3; j++) { + for (var i = 0; i < 3; i++) { + l[j * 3 + i] = luminance(tex_get_clamped(src, p.x + i - 1, p.y + j - 1).xyz); + } + } + let gx = l[2] + 2.0 * l[5] + l[8] - l[0] - 2.0 * l[3] - l[6]; + let gy = l[6] + 2.0 * l[7] + l[8] - l[0] - 2.0 * l[1] - l[2]; + let e = sqrt(gx * gx + gy * gy) * 0.25; + let px = textureLoad(src, p, 0); + let on = e > P.f[0].x; + var o = px; + if (P.u[0].x == 1u) { + o = vec4(vec3(select(0.0, 1.0, on) * px.w), px.w); + } else if (on) { + o = vec4(vec3(px.w), px.w); + } + textureStore(out, p, o); +} + +// ---------------------------------------------------------------- Sharpen / Unsharp Mask (misc::unsharp_core) + +// src = the layer, aux = its blur. f[0] = (amount, threshold in 0..255). +@compute @workgroup_size(16, 16) +fn fxn_unsharp(@builtin(global_invocation_id) gid: vec3) { + let p = fxn_pixel(gid); + if (p.x < 0) { + return; + } + var o = textureLoad(src, p, 0); + let bl = textureLoad(aux, p, 0); + for (var c = 0; c < 3; c++) { + let d = o[c] - bl[c]; + if (abs(d) * 255.0 >= P.f[0].y) { + o[c] = max(o[c] + d * P.f[0].x, 0.0); + } + } + textureStore(out, p, o); +} + +// ---------------------------------------------------------------- planes + +// Channel select: channel c of `src` becomes aux[c] (u[0].x = 0) or aux's alpha (1) where +// u[1][c] != 0. +@compute @workgroup_size(16, 16) +fn fxn_pick(@builtin(global_invocation_id) gid: vec3) { + let p = fxn_pixel(gid); + if (p.x < 0) { + return; + } + var o = textureLoad(src, p, 0); + let a = textureLoad(aux, p, 0); + for (var c = 0; c < 4; c++) { + if (P.u[1][c] != 0u) { + o[c] = select(a[c], a.w, P.u[0].x == 1u); + } + } + textureStore(out, p, o); +} + +// Channel Blur's result: src = blurred premultiplied channels, aux = the alpha blurred like +// each channel (alpha = the Alpha Blurriness result). +@compute @workgroup_size(16, 16) +fn fxn_cblur_out(@builtin(global_invocation_id) gid: vec3) { + let p = fxn_pixel(gid); + if (p.x < 0) { + return; + } + let v = textureLoad(src, p, 0); + let ba = textureLoad(aux, p, 0); + var s = vec3(0.0); + for (var c = 0; c < 3; c++) { + if (ba[c] > 1e-6) { + s[c] = v[c] / ba[c]; + } + } + let a = clamp(ba.w, 0.0, 1.0); + textureStore(out, p, vec4(s * a, a)); +} + +// Straight colour planes with the raw alpha (Minimax). +@compute @workgroup_size(16, 16) +fn fxn_straight(@builtin(global_invocation_id) gid: vec3) { + let p = fxn_pixel(gid); + if (p.x < 0) { + return; + } + let v = textureLoad(src, p, 0); + textureStore(out, p, vec4(fxn_unpremul(v).xyz, v.w)); +} + +// Premultiply straight planes by the clamped alpha. +@compute @workgroup_size(16, 16) +fn fxn_premul(@builtin(global_invocation_id) gid: vec3) { + let p = fxn_pixel(gid); + if (p.x < 0) { + return; + } + let v = textureLoad(src, p, 0); + let a = clamp(v.w, 0.0, 1.0); + textureStore(out, p, vec4(v.xyz * a, a)); +} + +// util::morph_plane along one axis (edges repeated). u[0] = (r, vertical); u[1] = per-channel +// operation (0 min, 1 max, 2 keep). +@compute @workgroup_size(16, 16) +fn fxn_morph(@builtin(global_invocation_id) gid: vec3) { + let p = fxn_pixel(gid); + if (p.x < 0) { + return; + } + let r = i32(P.u[0].x); + let vertical = P.u[0].y != 0u; + let centre = textureLoad(src, p, 0); + var lo = centre; + var hi = centre; + for (var k = -r; k <= r; k++) { + var v: vec4; + if (vertical) { + v = tex_get_clamped(src, p.x, p.y + k); + } else { + v = tex_get_clamped(src, p.x + k, p.y); + } + lo = min(lo, v); + hi = max(hi, v); + } + var o = centre; + for (var c = 0; c < 4; c++) { + if (P.u[1][c] == 0u) { + o[c] = lo[c]; + } else if (P.u[1][c] == 1u) { + o[c] = hi[c]; + } + } + textureStore(out, p, o); +} + +// Crop: out(p) = src(p + u[0].xy). +@compute @workgroup_size(16, 16) +fn fxn_crop(@builtin(global_invocation_id) gid: vec3) { + let p = fxn_pixel(gid); + if (p.x < 0) { + return; + } + textureStore(out, p, textureLoad(src, p + vec2(P.u[0].xy), 0)); +} + +// ---------------------------------------------------------------- Compound Blur (blur2::compound_blur) + +// The blur map (straight luminance × alpha, inverted with u[0].y) in every channel. u[0].x = 1: +// `src` is the Blur Layer sampled at the per-column / per-row coordinates in `data` (u[1].x = +// width); 0: `src` is the layer itself. +@compute @workgroup_size(16, 16) +fn fxn_cb_map(@builtin(global_invocation_id) gid: vec3) { + let p = fxn_pixel(gid); + if (p.x < 0) { + return; + } + var v: vec4; + if (P.u[0].x == 1u) { + let w = P.u[1].x; + v = sample_bilinear(src, data[u32(p.x)], data[w + u32(p.y)]); + } else { + v = textureLoad(src, p, 0); + } + let s = fxn_unpremul(v); + var l = luminance(s.xyz) * s.w; + if (P.u[0].y != 0u) { + l = 1.0 - l; + } + textureStore(out, p, vec4(l)); +} + +// Writes the pixels whose blur level falls between levels k (src) and k + 1 (aux). The map is +// `data` (RGBA f32 rows of u[0].y pixels); u[0].x = k. +@compute @workgroup_size(16, 16) +fn fxn_cb_level(@builtin(global_invocation_id) gid: vec3) { + let p = fxn_pixel(gid); + if (p.x < 0) { + return; + } + let m = data[(u32(p.y) * P.u[0].y + u32(p.x)) * 4u]; + let t = clamp(m, 0.0, 1.0) * 4.0; + let k = min(u32(floor(t)), 3u); + if (k != P.u[0].x) { + return; + } + let f = t - f32(k); + let a = textureLoad(src, p, 0); + let c = textureLoad(aux, p, 0); + textureStore(out, p, a + (c - a) * f); +} + +// ---------------------------------------------------------------- Echo (time_fx::echo) + +// acc (src) ⊕ frame (aux) × f[0].x. u[0] = (operator, first frame). +@compute @workgroup_size(16, 16) +fn fxn_echo(@builtin(global_invocation_id) gid: vec3) { + let p = fxn_pixel(gid); + if (p.x < 0) { + return; + } + let op = P.u[0].x; + var acc = textureLoad(src, p, 0); + if (P.u[0].y != 0u) { + acc = select(vec4(0.0), vec4(FXN_MAX), op == 2u); + } + let k = P.f[0].x; + let q = textureLoad(aux, p, 0) * k; + switch op { + case 1u: { + acc = max(acc, q); + } + case 2u: { + acc = min(acc, q); + } + case 3u: { + acc = acc + q - acc * q; + } + case 4u: { + acc += q * (1.0 - acc.w); + } + case 5u: { + acc = q + acc * (1.0 - q.w); + } + default: { + acc += q; + } + } + textureStore(out, p, acc); +} + +// Echo's finish. u[0].x = operator; f[0].x = number of frames. +@compute @workgroup_size(16, 16) +fn fxn_echo_fin(@builtin(global_invocation_id) gid: vec3) { + let p = fxn_pixel(gid); + if (p.x < 0) { + return; + } + let op = P.u[0].x; + var acc = textureLoad(src, p, 0); + if (op == 6u) { + acc = acc / P.f[0].x; + } + if (op == 2u && acc.x == FXN_MAX) { + acc = vec4(0.0); + } + textureStore(out, p, vec4(max(acc.xyz, vec3(0.0)), clamp(acc.w, 0.0, 1.0))); +} + +// ---------------------------------------------------------------- Fractal / Turbulent Noise (noise3.rs) + +fn fxn_catmull(p0: f32, p1: f32, p2: f32, p3: f32, t: f32) -> f32 { + return 0.5 * (2.0 * p1 + (p2 - p0) * t + (2.0 * p0 - 5.0 * p1 + 4.0 * p2 - p3) * t * t + (3.0 * p1 - p0 - 3.0 * p2 + p3) * t * t * t); +} + +fn fxn_slice(ix: i32, iy: i32, iz: i32, tx: f32, ty: f32, seed: u32, nt: u32) -> f32 { + if (nt == 0u) { + return lattice(ix, iy, iz, seed); + } + if (nt == 1u) { + let a = lattice(ix, iy, iz, seed); + let b = lattice(ix + 1, iy, iz, seed); + let c = lattice(ix, iy + 1, iz, seed); + let d = lattice(ix + 1, iy + 1, iz, seed); + let top = a + (b - a) * tx; + let bot = c + (d - c) * tx; + return top + (bot - top) * ty; + } + var rows: array; + for (var j = 0; j < 4; j++) { + let y = iy + j - 1; + rows[j] = fxn_catmull(lattice(ix - 1, y, iz, seed), lattice(ix, y, iz, seed), lattice(ix + 1, y, iz, seed), lattice(ix + 2, y, iz, seed), tx); + } + return fxn_catmull(rows[0], rows[1], rows[2], rows[3], ty); +} + +// noise3::typed_noise. +fn fxn_typed_noise(x: f32, y: f32, z: f32, seed: u32, nt: u32) -> f32 { + if (nt == 2u) { + return value_noise(x, y, z, seed); + } + let x0 = floor(x); + let y0 = floor(y); + let z0 = floor(z); + let tx = x - x0; + let ty = y - y0; + let fz = fade(z - z0); + let a = fxn_slice(i32(x0), i32(y0), i32(z0), tx, ty, seed, nt); + let b = fxn_slice(i32(x0), i32(y0), i32(z0) + 1, tx, ty, seed, nt); + return a + (b - a) * fz; +} + +// noise3::shape. +fn fxn_shape(kind: u32, n: f32) -> f32 { + let s = n * 2.0 - 1.0; + switch kind { + case 1u: { + return 1.0 - s * s; + } + case 2u: { + return 1.0 - abs(s); + } + case 3u: { + let t = 1.0 - abs(s); + return t * t; + } + case 8u: { + let t = clamp(abs(s) / 0.12, 0.0, 1.0); + return 1.0 - t * t; + } + default: { + return n; + } + } +} + +// Fractal::layer. `o` = octave data base index. +fn fxn_layer(u: f32, v: f32, o: u32, z: f32, seed: u32) -> f32 { + let nt = P.u[0].y; + if (P.u[1].y != 0u) { + let t = P.f[2].x; + let c = P.f[2].y; + let w = P.f[2].z; + let a = fxn_typed_noise(u, v, t + z, seed, nt); + let b = fxn_typed_noise(u, v, t - c + z, seed, nt); + return a * (1.0 - w) + b * w; + } + return fxn_typed_noise(u, v, data[o + 7u] + z, seed, nt); +} + +// u[0] = (kind, noise type, octaves, seed); u[1] = (sub rotation, cycle, invert, overflow); +// u[2].x = blend mode (BlendMode::ALL index, 255 = None); f[0] = (sin, cos, size x, size y); +// f[1] = (contrast, brightness, opacity, blend with original); f[2] = (cycle t, cycle, t / cycle, +// norm); data = 10 floats per octave (noise3::FractalGpu). +@compute @workgroup_size(16, 16) +fn fxn_fractal(@builtin(global_invocation_id) gid: vec3) { + let p = fxn_pixel(gid); + if (p.x < 0) { + return; + } + let kind = P.u[0].x; + let n_oct = P.u[0].z; + let sr = P.f[0].x; + let cr = P.f[0].y; + let x = f32(p.x) + 0.5; + let y = f32(p.y) + 0.5; + var sum = 0.0; + var best = 0.0; + for (var oi = 0u; oi < n_oct; oi++) { + let o = oi * 10u; + let dx = x - data[o]; + let dy = y - data[o + 1u]; + var u = (dx * cr + dy * sr) / P.f[0].z; + var v = (-dx * sr + dy * cr) / P.f[0].w; + if (P.u[1].x != 0u) { + let s = data[o + 2u]; + let c = data[o + 3u]; + let u2 = u * c + v * s; + v = -u * s + v * c; + u = u2; + } + let f = data[o + 4u]; + u = u * f; + v = v * f; + let z = data[o + 8u]; + let seed = P.u[0].w + oi; + if (kind >= 4u && kind <= 6u) { + let k = data[o + 9u]; + let a = fxn_layer(u * 0.5, v * 0.5, o, z + 3.1, seed ^ 0x55u) - 0.5; + let bb = fxn_layer(u * 0.5, v * 0.5, o, z + 5.7, seed ^ 0xaau) - 0.5; + if (kind == 5u) { + let s = sin(a * 3.0); + let c = cos(a * 3.0); + let u2 = u * c - v * s; + v = u * s + v * c; + u = u2; + } else { + u += a * 1.5 * k; + v += bb * 1.5 * k; + } + } + let n = fxn_shape(kind, fxn_layer(u, v, o, z, seed)); + let amp = data[o + 5u]; + if (kind == 7u) { + best = max(best, n * amp); + } + sum += n * amp * data[o + 6u]; + } + var raw = sum / max(P.f[2].w, 1e-6); + if (kind == 7u) { + raw = best; + } + var val = (raw - 0.5) * P.f[1].x + 0.5 + P.f[1].y; + let ov = P.u[1].w; + if (ov == 0u) { + val = clamp(val, 0.0, 1.0); + } else if (ov == 1u) { + val = clamp(0.5 + 0.5 * tanh(2.0 * (val - 0.5)) / tanh(1.0), 0.0, 1.0); + } else if (ov == 2u) { + let t = val - 2.0 * floor(val / 2.0); + val = select(t, 2.0 - t, t > 1.0); + } + if (P.u[1].z != 0u) { + val = 1.0 - val; + } + let op = P.f[1].z; + let s = vec4(vec3(val * op), op); + let orig = textureLoad(src, p, 0); + var r = s; + if (P.u[2].x != 255u) { + r = blend_pixel(P.u[2].x, orig, s, 0.5); + } + let bo = P.f[1].w; + textureStore(out, p, orig * bo + r * (1.0 - bo)); +} + +// ---------------------------------------------------------------- Noise Alpha / Noise HLS (noise.rs) + +// noise::phased_cycle with the phase's two seeds and smoothed fraction resolved on the CPU. +fn fxn_phased(x: u32, y: u32, sa: u32, sb: u32, t: f32) -> f32 { + let a = hash_noise(x, y, sa); + let b = hash_noise(x, y, sb); + return a + (b - a) * t; +} + +// u[0] = (squared, original alpha mode, overflow, seed a); u[1].x = seed b; f[0] = (amount, t). +@compute @workgroup_size(16, 16) +fn fxn_noise_alpha(@builtin(global_invocation_id) gid: vec3) { + let p = fxn_pixel(gid); + if (p.x < 0) { + return; + } + let k = P.f[0].x; + var n = fxn_phased(u32(p.x), u32(p.y), P.u[0].w, P.u[1].x, P.f[0].y); + if (P.u[0].x != 0u) { + n = n * n; + } + let s = fxn_unpremul(textureLoad(src, p, 0)); + let a = s.w; + var na: f32; + switch P.u[0].y { + case 0u: { + na = a + n * k; + } + case 1u: { + na = select(0.0, a + n * k, a > 0.0); + } + case 2u: { + na = a * (1.0 + (2.0 * n - 1.0) * k); + } + default: { + na = a + (2.0 * n - 1.0) * k * 4.0 * a * (1.0 - a); + } + } + if (P.u[0].z == 1u) { + let t = na - 2.0 * floor(na / 2.0); + na = select(t, 2.0 - t, t > 1.0); + } else if (P.u[0].z == 2u && !(na >= 0.0 && na <= 1.0)) { + na = na - floor(na); + } + na = clamp(na, 0.0, 1.0); + textureStore(out, p, vec4(s.xyz * na, na)); +} + +// Noise HLS: u[0] = (noise kind, seed); u[1], u[2].xy = per channel (hue, lightness, +// saturation) the phase's two seeds; f[0] = (hue, lightness, saturation, grain size); f[1].x = +// phase (Grain), f[1].y = smoothed fraction. +fn fxn_hls_seed(i: u32) -> u32 { + if (i < 4u) { + return P.u[1][i]; + } + return P.u[2][i - 4u]; +} + +fn fxn_hls_n(x: u32, y: u32, k: u32) -> f32 { + let s = P.u[0].y + k * 7919u; + switch P.u[0].x { + case 2u: { + let gs = P.f[0].w; + return clamp((value_noise(f32(x) / gs, f32(y) / gs, P.f[1].x, s) - 0.5) * 3.0, -1.0, 1.0); + } + case 1u: { + let v = fxn_phased(x, y, fxn_hls_seed(2u * k), fxn_hls_seed(2u * k + 1u), P.f[1].y) * 2.0 - 1.0; + return v * abs(v); + } + default: { + return fxn_phased(x, y, fxn_hls_seed(2u * k), fxn_hls_seed(2u * k + 1u), P.f[1].y) * 2.0 - 1.0; + } + } +} + +@compute @workgroup_size(16, 16) +fn fxn_noise_hls(@builtin(global_invocation_id) gid: vec3) { + let p = fxn_pixel(gid); + if (p.x < 0) { + return; + } + let px = textureLoad(src, p, 0); + let s0 = fxn_unpremul(px); + let a = s0.w; + if (a <= 0.0) { + textureStore(out, p, px); + return; + } + let x = u32(p.x); + let y = u32(p.y); + var hsl = rgb_to_hsl(s0.xyz); + if (P.f[0].x != 0.0) { + hsl.x = fract_euclid(hsl.x + fxn_hls_n(x, y, 0u) * P.f[0].x * 0.5); + } + if (P.f[0].y != 0.0) { + hsl.z = clamp(hsl.z + fxn_hls_n(x, y, 1u) * P.f[0].y, 0.0, 1.0); + } + if (P.f[0].z != 0.0) { + hsl.y = clamp(hsl.y + fxn_hls_n(x, y, 2u) * P.f[0].z, 0.0, 1.0); + } + let c = hsl_to_rgb(hsl.x, hsl.y, hsl.z); + textureStore(out, p, vec4(c * a, a)); +} + +// ---------------------------------------------------------------- Remove Grain (noise::remove_grain) + +// The guide (util::guided_filter's g): the luminance of the premultiplied channels in every +// channel (Multichannel), or the channels themselves (u[0].x, Single Channel). +@compute @workgroup_size(16, 16) +fn fxn_gf_guide(@builtin(global_invocation_id) gid: vec3) { + let p = fxn_pixel(gid); + if (p.x < 0) { + return; + } + let v = textureLoad(src, p, 0); + if (P.u[0].x != 0u) { + textureStore(out, p, v); + } else { + textureStore(out, p, vec4(luminance(v.xyz))); + } +} + +// Guided filter arithmetic, S = src, A = aux, D = `data` (RGBA f32 rows of u[0].y pixels): +// u[0].x = 0: S − A·D, 1: S − A·A, 2: S / (A + f[0].x), 3: A − S·D, 4: S·D + A, 5: S·A, 6: S·S. +@compute @workgroup_size(16, 16) +fn fxn_gf(@builtin(global_invocation_id) gid: vec3) { + let p = fxn_pixel(gid); + if (p.x < 0) { + return; + } + let s = textureLoad(src, p, 0); + let a = textureLoad(aux, p, 0); + var d = vec4(0.0); + let mode = P.u[0].x; + if (mode == 0u || mode == 3u || mode == 4u) { + let i = (u32(p.y) * P.u[0].y + u32(p.x)) * 4u; + d = vec4(data[i], data[i + 1u], data[i + 2u], data[i + 3u]); + } + var o: vec4; + switch mode { + case 0u: { + o = s - a * d; + } + case 1u: { + o = s - a * a; + } + case 2u: { + o = s / (a + P.f[0].x); + } + case 3u: { + o = a - s * d; + } + case 4u: { + o = s * d + a; + } + case 5u: { + o = s * a; + } + default: { + o = s * s; + } + } + textureStore(out, p, o); +} + +// Texture: f + (o − f) × f[0].x on colour (src = filtered, aux = original). +@compute @workgroup_size(16, 16) +fn fxn_rg_texture(@builtin(global_invocation_id) gid: vec3) { + let p = fxn_pixel(gid); + if (p.x < 0) { + return; + } + let f = textureLoad(src, p, 0); + let o = textureLoad(aux, p, 0); + textureStore(out, p, vec4(f.xyz + (o.xyz - f.xyz) * P.f[0].x, f.w)); +} + +// Unsharp Mask on the planes: src = channels, aux = their blur; f[0] = (amount, threshold). +@compute @workgroup_size(16, 16) +fn fxn_rg_unsharp(@builtin(global_invocation_id) gid: vec3) { + let p = fxn_pixel(gid); + if (p.x < 0) { + return; + } + var v = textureLoad(src, p, 0); + let l = textureLoad(aux, p, 0); + for (var c = 0; c < 3; c++) { + if (abs(v[c] - l[c]) > P.f[0].y) { + v[c] = v[c] + (v[c] - l[c]) * P.f[0].x; + } + } + textureStore(out, p, v); +} + +// Remove Grain's result: colour clamped to 0..alpha (alpha from aux = the original); with +// u[0].x the Preview viewing mode (noise::preview_compose): the original (aux) outside the +// region f[0] = (x0, x1, y0, y1), the box outline in f[1] where u[0].y. +@compute @workgroup_size(16, 16) +fn fxn_rg_out(@builtin(global_invocation_id) gid: vec3) { + let p = fxn_pixel(gid); + if (p.x < 0) { + return; + } + let o = textureLoad(aux, p, 0); + let a = o.w; + var v = vec4(clamp(textureLoad(src, p, 0).xyz, vec3(0.0), vec3(max(a, 0.0))), a); + if (P.u[0].x != 0u) { + let x = f32(p.x) + 0.5; + let y = f32(p.y) + 0.5; + let r = P.f[0]; + let inside = x >= r.x && x < r.y && y >= r.z && y < r.w; + if (!inside) { + v = o; + } + if (P.u[0].y != 0u) { + let nx = abs(x - r.x) < 0.75 || abs(x - r.y) < 0.75; + let ny = abs(y - r.z) < 0.75 || abs(y - r.w) < 0.75; + if ((nx && y >= r.z - 0.75 && y < r.w + 0.75) || (ny && x >= r.x - 0.75 && x < r.y + 0.75)) { + v = vec4(P.f[1].xyz, 1.0); + } + } + } + textureStore(out, p, v); +} diff --git a/crates/gpu/src/shaders/fx_stylize.wgsl b/crates/gpu/src/shaders/fx_stylize.wgsl new file mode 100644 index 000000000..bc119e8cd --- /dev/null +++ b/crates/gpu/src/shaders/fx_stylize.wgsl @@ -0,0 +1,925 @@ +// GPU effects (stylize and distort family): see src/fx_stylize.rs. Each entry point mirrors the CPU effect in +// effectcraft-effects operation for operation. Every name here is prefixed `fxs_` (the +// family files share one module). Coordinates are pixel centres (x + 0.5, y + 0.5) in buffer +// pixels, as in util::remap. Per-column / per-row tables in `data` hold coordinates the CPU +// computes in f64 (anything with a floor or a wrap), so tile and wrap boundaries match exactly. + +const FXS_TAU: f32 = 6.2831855; +const FXS_PI: f32 = 3.1415927; +const FXS_HALF_PI: f32 = 1.5707964; + +fn fxs_inside(p: vec2) -> bool { + let d = out_dims(); + return p.x < d.x && p.y < d.y; +} + +fn fxs_centre(p: vec2) -> vec2 { + return vec2(f32(p.x) + 0.5, f32(p.y) + 0.5); +} + +// util::unpremul: straight colour (zero below alpha 1e-6) and alpha (≥ 0). +fn fxs_unpremul(px: vec4) -> vec4 { + let a = px.w; + if (a > 1e-6) { + return vec4(fxs_qdiv(px.xyz, a), a); + } + return vec4(0.0, 0.0, 0.0, max(a, 0.0)); +} + +// util::smoothstep. +fn fxs_smoothstep(e0: f32, e1: f32, x: f32) -> f32 { + if (abs(e1 - e0) < 1e-9) { + return select(1.0, 0.0, x < e0); + } + let t = clamp((x - e0) / (e1 - e0), 0.0, 1.0); + return t * t * (3.0 - 2.0 * t); +} + +// x / a rounded like the CPU's IEEE division (GPU division may be off by an ulp, which flips +// thresholds on 8 / 16 bpc colours that sit exactly on them): one residual correction. +fn fxs_qdiv(x: vec3, a: f32) -> vec3 { + let q = x / a; + let r = fma(-q, vec3(a), x); + return q + r / a; +} + +fn fxs_rem2(i: i32) -> i32 { + return ((i % 2) + 2) % 2; +} + +// ---------------------------------------------------------------- per-pixel colour effects +// u[0].x = mode: +// 1 Posterize (misc::posterize): f[0].x = levels − 1 +// 2 Threshold (misc::threshold): f[0].x = level +// 3 CC Threshold: u[0].yz = (channel, invert); f[0].xy = (threshold, blend) +// 4 CC Threshold RGB: u[0].yzw = invert R, G, B; f[0] = (thresholds, blend) +// 5 Strobe Light on colour: u[0].y = operator; f[0] = (strobe colour, blend) +// 6 Strobe Light transparent (Image::scale_alpha): f[0].x = factor +// 7 CC Vignette: f[0] = (cx, cy, distance, amount); f[1].x = pin highlights + +fn fxs_bin(v: f32, t: f32, inv: bool) -> f32 { + return select(0.0, 1.0, (v >= t) != inv); +} + +fn fxs_strobe_op(op: u32, v: f32, k: f32) -> f32 { + switch op { + case 1u: { return v + k; } + case 2u: { return max(v - k, 0.0); } + case 3u: { return v * k; } + case 4u: { return abs(v - k); } + case 5u: { return 1.0 - (1.0 - v) * (1.0 - k); } + case 6u: { return max(v, k); } + case 7u: { return min(v, k); } + default: { return k; } + } +} + +@compute @workgroup_size(16, 16) +fn fxs_point(@builtin(global_invocation_id) gid: vec3) { + let p = vec2(gid.xy); + if (!fxs_inside(p)) { + return; + } + let px = textureLoad(src, p, 0); + let mode = P.u[0].x; + let a = px.w; + if (mode == 6u) { + textureStore(out, p, px * P.f[0].x); + return; + } + if (a <= 0.0) { + textureStore(out, p, px); + return; + } + switch mode { + case 1u: { + let lv = P.f[0].x; + let c = clamp(fxs_qdiv(px.xyz, a), vec3(0.0), vec3(1.0)) * lv; + let o = vec3(round_away(c.x), round_away(c.y), round_away(c.z)) / lv; + textureStore(out, p, vec4(o * a, a)); + } + case 2u: { + let v = select(0.0, 1.0, luminance(fxs_qdiv(px.xyz, a)) >= P.f[0].x); + textureStore(out, p, vec4(vec3(v) * a, a)); + } + case 3u: { + let u = fxs_unpremul(px); + let c = u.xyz; + let t = P.f[0].x; + let blend = P.f[0].y; + let inv = P.u[0].z != 0u; + var o = c; + var na = u.w; + switch P.u[0].y { + case 1u: { + o = vec3(fxs_bin(c.x, t, inv), fxs_bin(c.y, t, inv), fxs_bin(c.z, t, inv)); + } + case 2u: { + o = vec3(fxs_bin(rgb_to_hsl(c).y, t, inv)); + } + case 3u: { + na = fxs_bin(u.w, t, inv); + } + default: { + o = vec3(fxs_bin(luminance(c), t, inv)); + } + } + o = o + (c - o) * blend; + na = na + (u.w - na) * blend; + textureStore(out, p, vec4(o * na, na)); + } + case 4u: { + let c = fxs_qdiv(px.xyz, a); + let t = P.f[0].xyz; + let o = vec3(fxs_bin(c.x, t.x, P.u[0].y != 0u), fxs_bin(c.y, t.y, P.u[0].z != 0u), fxs_bin(c.z, t.z, P.u[0].w != 0u)); + let r = o + (c - o) * P.f[0].w; + textureStore(out, p, vec4(r * a, a)); + } + case 5u: { + let c = px.xyz / a; + let k = P.f[0].xyz; + let op = P.u[0].y; + let o = vec3(fxs_strobe_op(op, c.x, k.x), fxs_strobe_op(op, c.y, k.y), fxs_strobe_op(op, c.z, k.z)); + let r = o + (c - o) * P.f[0].w; + textureStore(out, p, vec4(r * a, a)); + } + case 7u: { + let u = fxs_unpremul(px); + let q = fxs_centre(p); + let dx = q.x - P.f[0].x; + let dy = q.y - P.f[0].y; + let r = sqrt(dx * dx + dy * dy) / P.f[0].z; + let f0 = 1.0 / (1.0 + r * r); + let f = f0 * f0; + var k = 1.0 - P.f[0].w * (1.0 - f); + let pin = P.f[1].x; + if (pin > 0.0) { + let l = clamp(luminance(u.xyz), 0.0, 1.0); + k += (1.0 - k) * pin * l; + } + textureStore(out, p, vec4(max(u.xyz * k, vec3(0.0)) * u.w, u.w)); + } + default: { + textureStore(out, p, px); + } + } +} + +// ---------------------------------------------------------------- inverse-mapped warps +// u[0].x = mode (all sample bilinear, transparent outside, unless noted): +// 1 Mirror (distort::mirror): f[0] = (cx, cy, nx, ny) +// 2 Spherize (distort::spherize): f[0] = (cx, cy, radius) +// 3 Polar Coordinates (distort::polar, edges repeated): u[0].y = to polar; +// f[0] = (w, h, cx, cy); f[1] = (rmax, interpolation) +// 4 CC Slant (distort3::cc_slant): f[0] = (floor x, floor y, 1 / height, slant); f[1].x = x scale +// 5 CC Smear (distort3::cc_smear): f[0] = (from x, from y, dx, dy); f[1] = (len², reach, radius) +// 6 CC Split (distort3::split_impl): f[0] = (ax, ay, ux, uy); f[1] = (len, split 1, split 2, reach) +// 7 Optics Compensation (distort2::optics_compensation): u[0].y = reverse; +// f[0] = (cx, cy, fit, f); f[1] = (reference radius, half FOV) +// 8 CC Kaleida (stylize2::kaleida, edges repeated): u[0].y = mirror; +// f[0] = (cx, cy, size, rotation); f[1].x = segment angle +// 9 Liquify (distort4::liquify): u[0].xy = (_, mesh nx, mesh ny); f[0] = (cell, mesh offset x, y, +// percentage); f[1] = (scale, buffer offset x, y); data = mesh offsets (x, y) per node +// 10 Twirl (Legacy) (distort3::twirl_legacy): f[0] = (cx, cy, radius, angle) +// 11 CC Ripple Pulse (distort3::cc_ripple_pulse): u[0].y = render bump; f[0] = (cx, cy, front, +// speed); f[1] = (time span, wavelength, height) +// 12 CC Power Pin, full perspective (distort3::cc_power_pin): f[2..5] = inverse of the +// square → quad matrix (rows); f[5] = source rect in layer pixels (x0, y0, w, h); +// f[6] = (scale, buffer offset x, y) + +fn fxs_mesh_d(i: u32, j: u32) -> vec2 { + let k = (j * P.u[0].y + i) * 2u; + return vec2(data[k], data[k + 1u]); +} + +// distort3::cc_ripple_pulse's wave. +fn fxs_pulse(r: f32) -> f32 { + let front = P.f[0].z; + let speed = P.f[0].w; + let span = P.f[1].x; + let age = (front - r) / max(speed, 1e-9); + if (age < 0.0 || age > span) { + return 0.0; + } + let fade = 1.0 - age / span; + return sin(FXS_TAU * (r - front) / P.f[1].y) * fade; +} + +// distort4::Mesh::at. +fn fxs_mesh_at(x: f32, y: f32) -> vec2 { + let cell = P.f[0].x; + let nx = P.u[0].y; + let ny = P.u[0].z; + let fx = x / cell; + let fy = y / cell; + if (fx < 0.0 || fy < 0.0 || fx > f32(nx - 1u) || fy > f32(ny - 1u)) { + return vec2(0.0); + } + let x0 = min(u32(floor(fx)), nx - 2u); + let y0 = min(u32(floor(fy)), ny - 2u); + let x1 = min(x0 + 1u, nx - 1u); + let y1 = min(y0 + 1u, ny - 1u); + let tx = fx - f32(x0); + let ty = fy - f32(y0); + let a = fxs_mesh_d(x0, y0); + let b = fxs_mesh_d(x1, y0); + let c = fxs_mesh_d(x0, y1); + let d = fxs_mesh_d(x1, y1); + let top = a + (b - a) * tx; + let bot = c + (d - c) * tx; + return top + (bot - top) * ty; +} + +@compute @workgroup_size(16, 16) +fn fxs_warp(@builtin(global_invocation_id) gid: vec3) { + let p = vec2(gid.xy); + if (!fxs_inside(p)) { + return; + } + let q = fxs_centre(p); + let x = q.x; + let y = q.y; + var s = q; + var clamped = false; + switch P.u[0].x { + case 1u: { + let n = P.f[0].zw; + let d = (x - P.f[0].x) * n.x + (y - P.f[0].y) * n.y; + if (d > 0.0) { + s = vec2(x - 2.0 * d * n.x, y - 2.0 * d * n.y); + } + } + case 2u: { + let c = P.f[0].xy; + let r = P.f[0].z; + let dx = x - c.x; + let dy = y - c.y; + let d = sqrt(dx * dx + dy * dy); + if (d < r && d != 0.0) { + let nd = d / r; + let k = (asin(nd) / FXS_HALF_PI) / nd; + s = vec2(c.x + dx * k, c.y + dy * k); + } + } + case 3u: { + clamped = true; + let w = P.f[0].x; + let h = P.f[0].y; + let cx = P.f[0].z; + let cy = P.f[0].w; + let rmax = P.f[1].x; + var pp: vec2; + if (P.u[0].y != 0u) { + let dx = x - cx; + let dy = y - cy; + let a0 = atan2(dx, -dy); + let a = a0 - floor(a0 / FXS_TAU) * FXS_TAU; + let r = sqrt(dx * dx + dy * dy) / rmax; + pp = vec2(a / FXS_TAU * w, h - r * h); + } else { + let a = x / w * FXS_TAU; + let r = (h - y) / h * rmax; + pp = vec2(cx + r * sin(a), cy - r * cos(a)); + } + let amt = P.f[1].y; + s = vec2(x + (pp.x - x) * amt, y + (pp.y - y) * amt); + } + case 4u: { + let fl = P.f[0].xy; + s = vec2(fl.x + (x - fl.x - (fl.y - y) * P.f[0].w) / P.f[1].x, fl.y + (y - fl.y) * P.f[0].z); + } + case 5u: { + let fr = P.f[0].xy; + let dx = P.f[0].z; + let dy = P.f[0].w; + let t = clamp(((x - fr.x) * dx + (y - fr.y) * dy) / P.f[1].x, 0.0, 1.0); + let px = fr.x + dx * t; + let py = fr.y + dy * t; + let d = sqrt((x - px) * (x - px) + (y - py) * (y - py)); + let w0 = max(1.0 - d / P.f[1].z, 0.0); + let w = w0 * w0 * (3.0 - 2.0 * w0); + let k = P.f[1].y * w * t; + s = vec2(x - dx * k, y - dy * k); + } + case 6u: { + let a = P.f[0].xy; + let ux = P.f[0].z; + let uy = P.f[0].w; + let nx = -uy; + let ny = ux; + let len = P.f[1].x; + let reach = P.f[1].w; + let rx = x - a.x; + let ry = y - a.y; + let t = (rx * ux + ry * uy) / len; + let sd = rx * nx + ry * ny; + if (t >= 0.0 && t <= 1.0) { + let amt = select(P.f[1].z, P.f[1].y, sd >= 0.0); + let g = amt * reach * sin(FXS_PI * t); + let ad = abs(sd); + if (ad < g) { + textureStore(out, p, vec4(0.0)); + return; + } + let push = g * max(1.0 - (ad - g) / reach, 0.0); + let ns = select(-1.0, 1.0, sd >= 0.0) * (ad - push); + s = vec2(x + nx * (ns - sd), y + ny * (ns - sd)); + } + } + case 7u: { + let c = P.f[0].xy; + let fit = P.f[0].z; + let f = P.f[0].w; + let r_ref = P.f[1].x; + let half = P.f[1].y; + let dx = x - c.x; + let dy = y - c.y; + let r = sqrt(dx * dx + dy * dy) * fit; + if (r >= 1e-9) { + var rs: f32; + if (P.u[0].y != 0u) { + rs = atan(r / f) / half * r_ref; + } else { + let th = r / r_ref * half; + if (th >= FXS_HALF_PI - 1e-4) { + textureStore(out, p, vec4(0.0)); + return; + } + rs = f * tan(th); + } + let k = rs / r * fit; + s = vec2(c.x + dx * k, c.y + dy * k); + } + } + case 8u: { + clamped = true; + let c = P.f[0].xy; + let rot = P.f[0].w; + let seg = P.f[1].x; + let dx = x - c.x; + let dy = y - c.y; + let r = sqrt(dx * dx + dy * dy) / P.f[0].z; + let a0 = atan2(dy, dx) - rot; + var a = a0 - floor(a0 / seg) * seg; + if (P.u[0].y != 0u && a > seg * 0.5) { + a = seg - a; + } + s = vec2(c.x + r * cos(a + rot), c.y + r * sin(a + rot)); + } + case 9u: { + let scale = P.f[1].x; + let o = P.f[1].yz; + let lx = (x - o.x) / scale; + let ly = (y - o.y) / scale; + let d = fxs_mesh_at(lx - P.f[0].y, ly - P.f[0].z); + if (d.x == 0.0 && d.y == 0.0) { + textureStore(out, p, textureLoad(src, p, 0)); + return; + } + let pct = P.f[0].w; + s = vec2((lx + d.x * pct) * scale + o.x, (ly + d.y * pct) * scale + o.y); + } + case 10u: { + let c = P.f[0].xy; + let r = P.f[0].z; + let dx = x - c.x; + let dy = y - c.y; + let d = sqrt(dx * dx + dy * dy); + if (d < r) { + let a = -P.f[0].w * (1.0 - d / r); + let sn = sin(a); + let co = cos(a); + s = vec2(c.x + dx * co - dy * sn, c.y + dx * sn + dy * co); + } + } + case 11u: { + let c = P.f[0].xy; + let dx = x - c.x; + let dy = y - c.y; + let r = sqrt(dx * dx + dy * dy); + let height = P.f[1].z; + let d = fxs_pulse(r) * height; + var u = vec2(0.0); + if (r > 1e-9) { + u = vec2(dx / r, dy / r); + } + var o = sample_bilinear(src, x - u.x * d, y - u.y * d); + if (P.u[0].y != 0u && d != 0.0) { + let slope = (fxs_pulse(r + 0.5) - fxs_pulse(r - 0.5)) * height; + let k = clamp(1.0 - slope * 0.5, 0.0, 2.0); + o = vec4(o.xyz * k, o.w); + } + textureStore(out, p, o); + return; + } + case 12u: { + let r0 = P.f[2]; + let r1 = P.f[3]; + let r2 = P.f[4]; + var u = r0.x * x + r0.y * y + r0.z; + var v = r1.x * x + r1.y * y + r1.z; + let w = r2.x * x + r2.y * y + r2.z; + if (w != 1.0 && w != 0.0) { + u = u / w; + v = v / w; + } + if (u < -1e-9 || u > 1.0 + 1e-9 || v < -1e-9 || v > 1.0 + 1e-9) { + textureStore(out, p, vec4(0.0)); + return; + } + let rc = P.f[5]; + let lx = rc.x + u * rc.z; + let ly = rc.y + v * rc.w; + let o = P.f[6]; + s = vec2(lx * o.x + o.y, ly * o.x + o.z); + } + default: {} + } + if (clamped) { + textureStore(out, p, sample_bilinear_clamped(src, s.x, s.y)); + } else { + textureStore(out, p, sample_bilinear(src, s.x, s.y)); + } +} + +// ---------------------------------------------------------------- CC Flo Motion (distort4::flo_motion) + +// One knot's pull at layer point q: f[k] = (knot x, knot y, amount, _). +fn fxs_flo_knot(q: vec2, k: vec4) -> vec2 { + if (k.z == 0.0) { + return q; + } + let d = q - k.xy; + let r = sqrt(d.x * d.x + d.y * d.y) / P.f[2].y; + let w = powz(1.0 - min(r, 1.0), P.f[2].x); + let s = max(1.0 - clamp(k.z, -4.0, 0.95) * w, 0.02); + return k.xy + d * s; +} + +// u[0] = (supersampling, tile edges); f[0], f[1] = knots; f[2] = (falloff, radius, layer w, +// layer h); f[3] = (scale, buffer offset x, y) +@compute @workgroup_size(16, 16) +fn fxs_flomotion(@builtin(global_invocation_id) gid: vec3) { + let p = vec2(gid.xy); + if (!fxs_inside(p)) { + return; + } + let ss = P.u[0].x; + let scale = P.f[3].x; + let off = P.f[3].yz; + let lw = P.f[2].z; + let lh = P.f[2].w; + var acc = vec4(0.0); + for (var sy = 0u; sy < ss; sy++) { + for (var sx = 0u; sx < ss; sx++) { + let px = f32(p.x) + (f32(sx) + 0.5) / f32(ss); + let py = f32(p.y) + (f32(sy) + 0.5) / f32(ss); + var q = vec2((px - off.x) / scale, (py - off.y) / scale); + q = fxs_flo_knot(q, P.f[0]); + q = fxs_flo_knot(q, P.f[1]); + if (P.u[0].y != 0u) { + q = vec2(q.x - floor(q.x / lw) * lw, q.y - floor(q.y / lh) * lh); + } + acc += sample_bilinear(src, q.x * scale + off.x, q.y * scale + off.y); + } + } + textureStore(out, p, acc / f32(ss * ss)); +} + +// ---------------------------------------------------------------- separable table warps +// The source point is (data[x], data[w + y]) (computed in f64 per column and per row), then +// mixed with the original: out = sample + (orig − sample) · blend (Offset, CC Tiler). +// u[0] = (buffer width, repeat edges); f[0].x = blend +@compute @workgroup_size(16, 16) +fn fxs_table(@builtin(global_invocation_id) gid: vec3) { + let p = vec2(gid.xy); + if (!fxs_inside(p)) { + return; + } + let w = P.u[0].x; + let sx = data[u32(p.x)]; + let sy = data[w + u32(p.y)]; + var s: vec4; + if (P.u[0].y != 0u) { + s = sample_bilinear_clamped(src, sx, sy); + } else { + s = sample_bilinear(src, sx, sy); + } + let o = textureLoad(src, p, 0); + textureStore(out, p, s + (o - s) * P.f[0].x); +} + +// CC Griddler (distort2::cc_griddler). data: per column (tile centre x, local x), per row +// (tile centre y, local y). u[0] = (buffer width, cut tiles); f[0] = (cos, sin, h scale, v scale); +// f[1].x = cut half size (0.45 · tile) +@compute @workgroup_size(16, 16) +fn fxs_griddler(@builtin(global_invocation_id) gid: vec3) { + let p = vec2(gid.xy); + if (!fxs_inside(p)) { + return; + } + let w = P.u[0].x; + let x = u32(p.x); + let y = u32(p.y); + let tcx = data[2u * x]; + let lx = data[2u * x + 1u]; + let tcy = data[2u * w + 2u * y]; + let ly = data[2u * w + 2u * y + 1u]; + if (P.u[0].y != 0u && max(abs(lx), abs(ly)) > P.f[1].x) { + textureStore(out, p, vec4(0.0)); + return; + } + let cr = P.f[0].x; + let sr = P.f[0].y; + let rx = lx * cr + ly * sr; + let ry = -lx * sr + ly * cr; + textureStore(out, p, sample_bilinear(src, tcx + rx / P.f[0].z, tcy + ry / P.f[0].w)); +} + +// Motion Tile (stylize2::motion_tile) into a new output size. data: per output column (tile +// index, fraction) of tx, then per row of ty. u[0] = (output width, horizontal phase shift, +// mirror edges); f[0] = (phase, layer w · scale, layer h · scale); f[1].xy = source buffer offset +@compute @workgroup_size(16, 16) +fn fxs_motiontile(@builtin(global_invocation_id) gid: vec3) { + let p = vec2(gid.xy); + if (!fxs_inside(p)) { + return; + } + let w = P.u[0].x; + let x = u32(p.x); + let y = u32(p.y); + let ixc = data[2u * x]; + let fxc = data[2u * x + 1u]; + let iyr = data[2u * w + 2u * y]; + let fyr = data[2u * w + 2u * y + 1u]; + let phase = P.f[0].x; + var i = ixc; + var fx = fxc; + var j = iyr; + var fy = fyr; + if (P.u[0].y != 0u) { + let t = fxc + phase * iyr; + i = ixc + floor(t); + fx = t - floor(t); + } else { + let t = fyr + phase * ixc; + j = iyr + floor(t); + fy = t - floor(t); + } + if (P.u[0].z != 0u) { + if (fxs_rem2(i32(i)) == 1) { + fx = 1.0 - fx; + } + if (fxs_rem2(i32(j)) == 1) { + fy = 1.0 - fy; + } + } + textureStore(out, p, sample_bilinear(src, fx * P.f[0].y + P.f[1].x, fy * P.f[0].z + P.f[1].y)); +} + +// CC RepeTile (stylize2::repetile) into a new output size. data: per output column (tile index, +// position in the tile), then per row. u[0] = (output width, tiling); f[0] = layer rect (x0, y0, w, h) +@compute @workgroup_size(16, 16) +fn fxs_repetile(@builtin(global_invocation_id) gid: vec3) { + let p = vec2(gid.xy); + if (!fxs_inside(p)) { + return; + } + let w = P.u[0].x; + let x = u32(p.x); + let y = u32(p.y); + let ix = i32(data[2u * x]); + var fx = data[2u * x + 1u]; + let iy = i32(data[2u * w + 2u * y]); + var fy = data[2u * w + 2u * y + 1u]; + let odd = fxs_rem2(ix + iy) == 1; + var flip_x = false; + var flip_y = false; + switch P.u[0].y { + case 1u: { + flip_x = fxs_rem2(ix) == 1; + flip_y = fxs_rem2(iy) == 1; + } + case 2u: { + flip_x = odd; + } + case 3u: { + flip_y = odd; + } + case 4u: { + flip_x = odd; + flip_y = odd; + } + default: {} + } + let r = P.f[0]; + if (flip_x) { + fx = r.z - fx; + } + if (flip_y) { + fy = r.w - fy; + } + textureStore(out, p, sample_bilinear(src, r.x + fx, r.y + fy)); +} + +// ---------------------------------------------------------------- Magnify (distort2::magnify) +// data: per column (dx, sx, floor(sx) + 0.5), then per row (dy, sy, floor(sy) + 0.5). +// u[0] = (buffer width, square, scaling, blend: 0 none, 1 normal, 2 blend mode); +// u[1] = (blend mode id, seed); f[0] = (size, feather, opacity, magnification); f[1].x = size − feather +@compute @workgroup_size(16, 16) +fn fxs_magnify(@builtin(global_invocation_id) gid: vec3) { + let p = vec2(gid.xy); + if (!fxs_inside(p)) { + return; + } + let bw = P.u[0].x; + let x = u32(p.x); + let y = u32(p.y); + let dx = data[3u * x]; + let sx = data[3u * x + 1u]; + let nx = data[3u * x + 2u]; + let dy = data[3u * bw + 3u * y]; + let sy = data[3u * bw + 3u * y + 1u]; + let ny = data[3u * bw + 3u * y + 2u]; + var d: f32; + if (P.u[0].y != 0u) { + d = max(abs(dx), abs(dy)); + } else { + d = sqrt(dx * dx + dy * dy); + } + let size = P.f[0].x; + let feather = P.f[0].y; + var w: f32; + if (feather > 0.01) { + w = 1.0 - fxs_smoothstep(P.f[1].x, size, d); + } else { + w = clamp(size - d + 0.5, 0.0, 1.0); + } + w = w * P.f[0].z; + let px = textureLoad(src, p, 0); + let blend = P.u[0].w; + if (w <= 0.0) { + if (blend == 0u) { + textureStore(out, p, vec4(0.0)); + } else { + textureStore(out, p, px); + } + return; + } + var m: vec4; + switch P.u[0].z { + case 1u: { + m = sample_bicubic(src, sx, sy); + } + case 2u: { + let seed = P.u[1].y; + let k = max(1.0 - 1.0 / P.f[0].w, 0.0); + let j1 = (hash_noise(x, y ^ (1u << 20u), seed) - 0.5) * k; + let j2 = (hash_noise(x, y ^ (2u << 20u), seed) - 0.5) * k; + m = sample_bilinear(src, nx + j1, ny + j2); + } + default: { + m = sample_bilinear(src, nx, ny); + } + } + if (blend == 0u) { + textureStore(out, p, m * w); + } else if (blend == 1u) { + textureStore(out, p, px + (m - px) * w); + } else { + textureStore(out, p, blend_pixel(P.u[1].x, px, m * w, 0.5)); + } +} + +// ---------------------------------------------------------------- Scatter (stylize2::scatter) +// u[0] = (grain, seed); f[0].x = amount +@compute @workgroup_size(16, 16) +fn fxs_scatter(@builtin(global_invocation_id) gid: vec3) { + let p = vec2(gid.xy); + if (!fxs_inside(p)) { + return; + } + let grain = P.u[0].x; + let seed = P.u[0].y; + let amt = P.f[0].x; + let x = u32(p.x); + let y = u32(p.y); + var dx = 0.0; + var dy = 0.0; + if (grain != 2u) { + dx = (hash_noise(x, y, seed) - 0.5) * 2.0 * amt; + } + if (grain != 1u) { + dy = (hash_noise(x, y, seed ^ 0x5bd1u) - 0.5) * 2.0 * amt; + } + textureStore(out, p, tex_get(src, i32(round_away(f32(p.x) + dx)), i32(round_away(f32(p.y) + dy)))); +} + +// ---------------------------------------------------------------- Brush Strokes (stylize2::brush_strokes) +// data: cell column per x (w), cell row per y (h), then per cell (dx, dy, length, jitter, samples, +// painted). u[0] = (buffer width, cell columns, paint surface, buffer height); f[0].x = blend +@compute @workgroup_size(16, 16) +fn fxs_brush(@builtin(global_invocation_id) gid: vec3) { + let p = vec2(gid.xy); + if (!fxs_inside(p)) { + return; + } + let w = P.u[0].x; + let h = P.u[0].w; + let cx = u32(data[u32(p.x)]); + let cy = u32(data[w + u32(p.y)]); + let base = w + h + (cy * P.u[0].y + cx) * 6u; + let dx = data[base]; + let dy = data[base + 1u]; + let l = data[base + 2u]; + let jit = data[base + 3u]; + let n = u32(data[base + 4u]); + let m = data[base + 5u]; + let q = fxs_centre(p); + let bx = q.x - dy * jit; + let by = q.y + dx * jit; + var acc = vec4(0.0); + for (var i = 0u; i < n; i++) { + var t = 0.0; + if (n != 1u) { + t = (f32(i) / f32(n - 1u) - 0.5) * l; + } + acc += sample_bilinear_clamped(src, bx + dx * t, by + dy * t); + } + let stroke = acc / f32(n); + let orig = textureLoad(src, p, 0); + let surface = P.u[0].z; + var base_px = orig; + if (surface == 1u) { + base_px = vec4(0.0); + } else if (surface == 2u) { + base_px = vec4(1.0); + } else if (surface == 3u) { + base_px = vec4(0.0, 0.0, 0.0, 1.0); + } + var painted: vec4; + if (surface == 2u || surface == 3u) { + let k = 1.0 - stroke.w * m; + painted = stroke * m + base_px * k; + } else { + painted = base_px + (stroke - base_px) * m; + } + textureStore(out, p, painted + (orig - painted) * P.f[0].x); +} + +// ---------------------------------------------------------------- Roughen Edges (stylize2::roughen_edges) + +// noise::fbm. +fn fxs_fbm(u: f32, v: f32, z: f32, seed: u32, octaves0: f32) -> f32 { + let octaves = clamp(octaves0, 1.0, 20.0); + let n = u32(ceil(octaves)); + let frac = octaves - floor(octaves); + var sum = 0.0; + var norm = 0.0; + var amp = 1.0; + var f = 1.0; + for (var o = 0u; o < n; o++) { + var w = 1.0; + if (o + 1u == n && frac > 0.0) { + w = frac; + } + sum += value_noise(u * f, v * f, z + f32(o) * 7.31, seed + o) * amp * w; + norm += amp * w; + amp *= 0.5; + f *= 2.0; + } + return sum / max(norm, 1e-6); +} + +// aux = the (blurred) alpha in .w. u[0] = (seed, cycle evolution, coloured); +// f[0] = (offset x, offset y, scale · stretch x, scale · stretch y); f[1] = (frequency, evolution, +// cycle, cycle weight); f[2] = (octaves, influence, influence multiplier, sharpness); +// f[3] = (edge colour, sharpness multiplier) +@compute @workgroup_size(16, 16) +fn fxs_roughen(@builtin(global_invocation_id) gid: vec3) { + let p = vec2(gid.xy); + if (!fxs_inside(p)) { + return; + } + let px = textureLoad(src, p, 0); + if (px.w <= 0.0) { + textureStore(out, p, px); + return; + } + let dv = textureLoad(aux, p, 0).w; + let fm = P.f[1].x; + let nx = (f32(p.x) + 0.5 - P.f[0].x) / P.f[0].z * fm; + let ny = (f32(p.y) + 0.5 - P.f[0].y) / P.f[0].w * fm; + let seed = P.u[0].x; + let evo = P.f[1].y; + let oct = P.f[2].x; + var n = fxs_fbm(nx, ny, evo, seed, oct); + if (P.u[0].y != 0u) { + n += (fxs_fbm(nx, ny, evo - P.f[1].z, seed, oct) - n) * P.f[1].w; + } + let infl = P.f[2].y; + let s = dv - 0.5 + (n - 0.5) * infl * P.f[2].z * 0.5; + let m = clamp(s * P.f[2].w * P.f[3].w * 4.0 + 0.5, 0.0, 1.0); + let u = fxs_unpremul(px); + var c = u.xyz; + if (P.u[0].z != 0u) { + let band = 1.0 - fxs_smoothstep(0.5, 0.9, dv + (n - 0.5) * infl * 0.5); + c = c + (P.f[3].xyz - c) * band; + } + let a = u.w * m; + textureStore(out, p, vec4(c * a, a)); +} + +// ---------------------------------------------------------------- Texturize (stylize2::texturize) + +// The texture layer placed into the buffer (transition::place_layer) as luminance +// (Plane::luma). aux = the texture layer's buffer; data: per column x, then per row y in its +// pixels. u[0].x = buffer width +@compute @workgroup_size(16, 16) +fn fxs_tex_luma(@builtin(global_invocation_id) gid: vec3) { + let p = vec2(gid.xy); + if (!fxs_inside(p)) { + return; + } + let w = P.u[0].x; + let t = sample_bilinear(aux, data[u32(p.x)], data[w + u32(p.y)]); + let l = luminance(fxs_unpremul(t).xyz); + textureStore(out, p, vec4(l)); +} + +// Relief shading. aux = the blurred texture luminance; f[0] = (light x, light y, contrast) +@compute @workgroup_size(16, 16) +fn fxs_texturize(@builtin(global_invocation_id) gid: vec3) { + let p = vec2(gid.xy); + if (!fxs_inside(p)) { + return; + } + let gx = (tex_get_clamped(aux, p.x + 1, p.y).x - tex_get_clamped(aux, p.x - 1, p.y).x) * 0.5; + let gy = (tex_get_clamped(aux, p.x, p.y + 1).x - tex_get_clamped(aux, p.x, p.y - 1).x) * 0.5; + let shade = max(1.0 - P.f[0].z * 4.0 * (gx * P.f[0].x + gy * P.f[0].y), 0.0); + let px = textureLoad(src, p, 0); + textureStore(out, p, vec4(px.xyz * shade, px.w)); +} + +// ---------------------------------------------------------------- Glow (misc::glow) extras + +// Bright pass with the Arbitrary Map: as `glow_bright` (same f[0] / u[0] layout, see +// kernels.wgsl) with the red, green and blue curves at data offsets f[1].xyz (−1 = identity). +@compute @workgroup_size(16, 16) +fn fxs_glow_map(@builtin(global_invocation_id) gid: vec3) { + let p = vec2(gid.xy); + if (!fxs_inside(p)) { + return; + } + let px = textureLoad(src, p, 0); + let a = px.w; + if (a <= 0.0) { + textureStore(out, p, vec4(0.0)); + return; + } + let thr = P.f[0].x; + var l = luminance(px.xyz / a); + if (P.u[0].y != 0u) { + l = a; + } + let k = clamp((l - thr) / max(1.0 - thr, 1e-3), 0.0, 1.0); + let t = glow_ab_t(l); + let c = vec3(curve_eval(P.f[1].x, t), curve_eval(P.f[1].y, t), curve_eval(P.f[1].z, t)); + textureStore(out, p, vec4(c * k * a, k * a)); +} + +// On Top with another Glow Operation: blend_pixel(mode, original, glow · intensity, 0). +// aux = the blurred glow; u[0].x = blend mode id; f[0].x = intensity +@compute @workgroup_size(16, 16) +fn fxs_glow_op(@builtin(global_invocation_id) gid: vec3) { + let p = vec2(gid.xy); + if (!fxs_inside(p)) { + return; + } + let g = textureLoad(aux, p, 0) * P.f[0].x; + var o = blend_pixel(P.u[0].x, textureLoad(src, p, 0), g, 0.0); + o.w = clamp(o.w, 0.0, 1.0); + textureStore(out, p, o); +} + +// ---------------------------------------------------------------- accumulation (Transform motion blur) + +// out = src + aux +@compute @workgroup_size(16, 16) +fn fxs_add(@builtin(global_invocation_id) gid: vec3) { + let p = vec2(gid.xy); + if (!fxs_inside(p)) { + return; + } + textureStore(out, p, textureLoad(src, p, 0) + textureLoad(aux, p, 0)); +} + +// out = src / f[0].x +@compute @workgroup_size(16, 16) +fn fxs_div(@builtin(global_invocation_id) gid: vec3) { + let p = vec2(gid.xy); + if (!fxs_inside(p)) { + return; + } + textureStore(out, p, textureLoad(src, p, 0) / P.f[0].x); +} diff --git a/crates/gpu/src/shaders/fx_tone.wgsl b/crates/gpu/src/shaders/fx_tone.wgsl new file mode 100644 index 000000000..39db34588 --- /dev/null +++ b/crates/gpu/src/shaders/fx_tone.wgsl @@ -0,0 +1,729 @@ +// GPU effects (tonal and colour correction family): see src/fx_tone.rs. Each operation mirrors +// the CPU effect in effectcraft-effects operation for operation. Every name here is prefixed +// `fxt_` (the family files share one module). + +fn fxt_straight(px: vec4) -> vec3 { + if (px.w > 1e-6) { + return px.xyz / px.w; + } + return vec3(0.0); +} + +fn fxt_mix(a: vec3, b: vec3, t: f32) -> vec3 { + return a + (b - a) * t; +} + +// f32::rem_euclid(360.0). +fn fxt_rem360(x: f32) -> f32 { + let r = x - 360.0 * floor(x / 360.0); + return select(r, 0.0, r >= 360.0); +} + +// Levels (Individual Controls) channel `i` (data: 5 × (ib, iw, g, ob, ow, identity)). +fn fxt_lv(i: u32, v: f32, clip_b: bool, clip_w: bool) -> f32 { + let o = i * 6u; + if (data[o + 5u] != 0.0) { + return v; + } + var t = (v - data[o]) / max(data[o + 1u] - data[o], 1e-6); + if (clip_b) { + t = max(t, 0.0); + } + if (clip_w) { + t = min(t, 1.0); + } + return data[o + 3u] + (data[o + 4u] - data[o + 3u]) * powz(max(t, 0.0), 1.0 / max(data[o + 2u], 0.01)); +} + +// color_fx::level (data offset o: ib, iw, g, ob, ow, clip black, clip white). +fn fxt_level(o: u32, v: f32) -> f32 { + var t = (v - data[o]) / max(data[o + 1u] - data[o], 1e-6); + if (data[o + 5u] != 0.0) { + t = max(t, 0.0); + } + if (data[o + 6u] != 0.0) { + t = min(t, 1.0); + } + t = powz(max(t, 0.0), 1.0 / data[o + 2u]); + return data[o + 3u] + (data[o + 4u] - data[o + 3u]) * t; +} + +fn fxt_level_ident(o: u32) -> bool { + return data[o] == 0.0 && data[o + 1u] == 1.0 && data[o + 2u] == 1.0 && data[o + 3u] == 0.0 && data[o + 4u] == 1.0; +} + +fn fxt_adjust_sat(s: f32, sat: f32) -> f32 { + if (sat >= 0.0) { + return s + (1.0 - s) * sat * min(s, 1.0); + } + return s * (1.0 + sat); +} + +fn fxt_adjust_light(l: f32, light: f32) -> f32 { + if (light >= 0.0) { + return l + (1.0 - l) * light; + } + return l * (1.0 + light); +} + +// HueRange::weight for range `r` (data: start, end, fall start, fall end, hue, sat, light). +fn fxt_range_weight(r: u32, h: f32) -> f32 { + let o = r * 7u; + let width = fxt_rem360(data[o + 1u] - data[o]); + let d = fxt_rem360(h - data[o]); + if (d <= width) { + return 1.0; + } + let after = d - width; + let before = 360.0 - d; + var wa = 0.0; + if (data[o + 3u] > 0.0) { + wa = 1.0 - after / data[o + 3u]; + } + var wb = 0.0; + if (data[o + 2u] > 0.0) { + wb = 1.0 - before / data[o + 2u]; + } + return clamp(max(wa, wb), 0.0, 1.0); +} + +// color3::colour_match. +fn fxt_colour_match(c: vec3, key: vec3, key_hue: f32, mode: u32, tol: f32, soft: f32) -> f32 { + var d: f32; + if (mode == 0u) { + d = sqrt(dot(c - key, c - key)) / sqrt(3.0); + } else if (mode == 1u) { + let hs = rgb_to_hsl(c); + let dh0 = abs(hs.x - key_hue); + let dh = min(dh0, 1.0 - dh0) * 2.0; + d = select(dh, 1.0, hs.y < 0.02); + } else { + let sa = max(c.x + c.y + c.z, 1e-6); + let sk = max(key.x + key.y + key.z, 1e-6); + let a = vec2(c.x / sa, c.y / sa); + let k = vec2(key.x / sk, key.y / sk); + d = sqrt(dot(a - k, a - k)) * 1.5; + } + if (d <= tol) { + return 1.0; + } + if (soft <= 1e-6) { + return 0.0; + } + return clamp(1.0 - (d - tol) / soft, 0.0, 1.0); +} + +// color3::knee. +fn fxt_knee(v: f32, start: f32, limit: f32) -> f32 { + if (v <= start || limit <= start) { + return min(v, limit); + } + let r = limit - start; + return start + r * (1.0 - exp(-(v - start) / r)); +} + +// PS Arbitrary Map lookup (map in data, n entries; f[0].x = phase offset in entries). +fn fxt_lookup(v: f32, n: u32) -> f32 { + let nf = f32(n); + var x = clamp(v, 0.0, 1.0) * (nf - 1.0) + P.f[0].x; + x = x - nf * floor(x / nf); + let fl = floor(x); + let i0 = u32(fl) % n; + let i1 = select(i0, i0 + 1u, i0 + 1u < n); + let t = x - fl; + return data[i0] + (data[i1] - data[i0]) * t; +} + +fn fxt_eq_look(table: u32, n: u32, v: f32) -> f32 { + let i = min(u32(round_away(clamp(v, 0.0, 1.0) * f32(n - 1u))), n - 1u); + return data[table * n + i]; +} + +// Per-pixel colour corrections. u[0].x = op (layouts in src/fx_tone.rs): +// 1 Levels (Individual Controls) 2 Gamma/Pedestal/Gain 3 Photo Filter 4 Change to Color +// 5 Leave Color 6 Change Color 7 Broadcast Colors 8 Color Balance (HLS) 9 CC Toner +// 10 CC Color Offset 11 CC Kernel 12 PS Arbitrary Map 13 Video Limiter +// 14 Hue/Saturation with colour ranges 15 Levels with channel controls +// 16 Auto Levels / Contrast / Color apply 17 Equalize apply 18 Invert (HLS / YIQ channels) +// 19 luminance plane 20 Shadow/Highlight (aux = (shadow base, highlight base)) +// 21 pack src.x, aux.x 22 Shadow/Highlight clip remap 23 CC Color Neutralizer +// 24 Color Stabilizer (data: per channel n, then 5 × (x, y)) +@compute @workgroup_size(16, 16) +fn fxt_point(@builtin(global_invocation_id) gid: vec3) { + let dims = out_dims(); + let p = vec2(gid.xy); + if (p.x >= dims.x || p.y >= dims.y) { + return; + } + let px = textureLoad(src, p, 0); + let a = px.w; + let u0 = P.u[0]; + let f0 = P.f[0]; + // Image::map_straight: transparent pixels stay as they are. + let straight_skip = a <= 0.0; + let c = fxt_straight(px); + var o = px; + switch u0.x { + case 1u: { + let cb = u0.y != 0u; + let cw = u0.z != 0u; + // color2::map_ca. + if (a <= 0.0 && px.x == 0.0 && px.y == 0.0 && px.z == 0.0) { + let na = clamp(fxt_lv(4u, 0.0, cb, cw), 0.0, 1.0); + if (na > 0.0) { + o = vec4(0.0, 0.0, 0.0, clamp(na, 0.0, 1.0)); + } + } else { + let aa = max(a, 0.0); + var nc: vec3; + for (var i = 0u; i < 3u; i++) { + nc[i] = fxt_lv(i + 1u, fxt_lv(0u, c[i], cb, cw), cb, cw); + } + let na = clamp(clamp(fxt_lv(4u, aa, cb, cw), 0.0, 1.0), 0.0, 1.0); + o = vec4(nc * na, na); + } + } + case 2u: { + if (straight_skip) { + break; + } + let stretch = P.f[3].x; + var r: vec3; + for (var i = 0; i < 3; i++) { + var v = c[i]; + if (stretch > 0.0 && v > 0.0 && v < 1.0) { + let q = 1.0 - v; + v = v + stretch * 0.25 * v * (q * q * q * q); + } + let g = P.f[i]; + r[i] = max(g.y + (g.z - g.y) * powz(max(v, 0.0), g.x), 0.0); + } + o = vec4(r * a, a); + } + case 3u: { + if (straight_skip) { + break; + } + var r = fxt_mix(c, c * f0.xyz, f0.w); + if (u0.y != 0u) { + let l0 = luminance(c); + let l1 = max(luminance(r), 1e-6); + r = r * l0 / l1; + } + o = vec4(r * a, a); + } + case 4u: { + if (straight_skip) { + break; + } + let fr = f0.xyz; + let tc = P.f[1].xyz; + let tol = P.f[2].xyz; + let soft = f0.w; + let hsl = rgb_to_hsl(c); + let d = vec3(abs(fract_euclid(hsl.x - fr.x + 0.5) - 0.5) * 2.0, abs(hsl.z - fr.z), abs(hsl.y - fr.y)); + var k = 1.0; + for (var i = 0; i < 3; i++) { + let over = d[i] - tol[i]; + if (over > 0.0) { + k = min(k, select(clamp(1.0 - over / soft, 0.0, 1.0), 0.0, soft <= 1e-6)); + } + } + let ch_l = (u0.y & 1u) != 0u; + let ch_s = (u0.y & 2u) != 0u; + var r = c; + if ((u0.y & 8u) != 0u) { + r = vec3(k); + } else if (k > 0.0) { + var nh: f32; + var ns = hsl.y; + var nl = hsl.z; + if ((u0.y & 4u) != 0u) { + let dh = (fract_euclid(tc.x - fr.x + 0.5) - 0.5) * k; + nh = fract_euclid(hsl.x + dh); + if (ch_s) { + ns = hsl.y + (tc.y - fr.y) * k; + } + if (ch_l) { + nl = hsl.z + (tc.z - fr.z) * k; + } + } else { + let dh = (fract_euclid(tc.x - hsl.x + 0.5) - 0.5) * k; + nh = fract_euclid(hsl.x + dh); + if (ch_s) { + ns = hsl.y + (tc.y - hsl.y) * k; + } + if (ch_l) { + nl = hsl.z + (tc.z - hsl.z) * k; + } + } + r = hsl_to_rgb(nh, clamp(ns, 0.0, 1.0), clamp(nl, 0.0, 1.0)); + } + o = vec4(r * a, a); + } + case 5u: { + if (straight_skip) { + break; + } + let keep = f0.xyz; + let amt = P.f[1].x; + let tol = P.f[1].y; + let soft = P.f[1].z; + var d: f32; + if (u0.y != 0u) { + let hsl = rgb_to_hsl(c); + d = select(abs(fract_euclid(hsl.x - f0.w + 0.5) - 0.5) * 2.0, 1.0, hsl.y < 0.02); + } else { + d = sqrt(dot(c - keep, c - keep)) / sqrt(3.0); + } + var keepk: f32; + if (tol >= 1.0 || d <= tol) { + keepk = 1.0; + } else if (soft <= 1e-6) { + keepk = 0.0; + } else { + keepk = clamp(1.0 - (d - tol) / soft, 0.0, 1.0); + } + let g = luminance(c); + let r = fxt_mix(c, vec3(g), amt * (1.0 - keepk)); + o = vec4(r * a, a); + } + case 6u: { + if (a <= 0.0) { + break; + } + var m = fxt_colour_match(c, f0.xyz, f0.w, u0.y, P.f[2].x, P.f[2].y); + if ((u0.z & 2u) != 0u) { + m = 1.0 - m; + } + var r = c; + if ((u0.z & 1u) != 0u) { + r = vec3(m); + } else if (m > 0.0) { + let hsl = rgb_to_hsl(c); + let dh = P.f[1].x; + let dl = P.f[1].y; + let ds = P.f[1].z; + let h2 = fract_euclid(hsl.x + dh); + let s2 = clamp(hsl.y + ds * select(hsl.y, 1.0 - hsl.y, ds > 0.0), 0.0, 1.0); + let l2 = clamp(hsl.z + dl * select(hsl.z, 1.0 - hsl.z, dl > 0.0), 0.0, 1.0); + r = fxt_mix(c, hsl_to_rgb(h2, s2, l2), m); + } + o = vec4(r * a, a); + } + case 7u: { + if (a <= 0.0) { + break; + } + let pal = u0.y != 0u; + let y = 0.299 * c.x + 0.587 * c.y + 0.114 * c.z; + var ch: f32; + if (pal) { + let uu = 0.492 * (c.z - y); + let vv = 0.877 * (c.x - y); + ch = sqrt(uu * uu + vv * vv); + } else { + let ii = 0.596 * c.x - 0.274 * c.y - 0.322 * c.z; + let qq = 0.211 * c.x - 0.523 * c.y + 0.312 * c.z; + ch = sqrt(ii * ii + qq * qq); + } + let max_norm = f0.x; + let unsafe_px = y + ch > max_norm + 1e-5; + switch u0.z { + case 2u: { + if (unsafe_px) { + o = vec4(0.0); + } + } + case 3u: { + if (!unsafe_px) { + o = vec4(0.0); + } + } + case 1u: { + if (unsafe_px) { + let target_v = max(max_norm - y, 0.0); + let s = select(1.0, target_v / ch, ch > 1e-6); + var r = y + (c - y) * s; + if (y > max_norm) { + r = r * max_norm / y; + } + o = vec4(r * a, a); + } + } + default: { + if (unsafe_px) { + let k = max_norm / (y + ch); + o = vec4(c * k * a, a); + } + } + } + } + case 8u: { + if (straight_skip) { + break; + } + let hsl = rgb_to_hsl(c); + let dh = f0.x; + let dl = f0.y; + let ds = f0.z; + var l = hsl.z; + if (dl > 0.0) { + l = l + (1.0 - l) * dl; + } else { + l = l + l * dl; + } + var s = hsl.y; + if (ds > 0.0) { + s = s + (1.0 - s) * ds; + } else { + s = s + s * ds; + } + let r = hsl_to_rgb(hsl.x + dh, clamp(s, 0.0, 1.0), clamp(l, 0.0, 1.0)); + o = vec4(r * a, a); + } + case 9u: { + if (straight_skip) { + break; + } + let n = u0.y; + let t = clamp(luminance(c), 0.0, 1.0) * f32(n); + let k = min(u32(floor(t)), n - 1u); + let tone = fxt_mix(P.f[1u + k].xyz, P.f[2u + k].xyz, t - f32(k)); + let r = fxt_mix(tone, c, f0.x); + o = vec4(r * a, a); + } + case 10u: { + if (straight_skip) { + break; + } + var r = c; + for (var k = 0; k < 3; k++) { + if (f0[k] == 0.0) { + continue; + } + let x = c[k] + f0[k]; + if (x <= 1.0) { + r[k] = x; + } else if (u0.y == 1u) { + r[k] = max(2.0 - x, 0.0); + } else if (u0.y == 2u) { + r[k] = select(1.0, 0.0, x - 1.0 < 0.5); + } else { + r[k] = fract_euclid(x); + } + } + o = vec4(r * a, a); + } + case 11u: { + if (a <= 0.0) { + break; + } + var acc = vec3(0.0); + for (var j = 0; j < 3; j++) { + for (var i = 0; i < 3; i++) { + let idx = j * 3 + i; + let w = P.f[idx / 4][idx % 4]; + if (w == 0.0) { + continue; + } + let q = tex_get_clamped(src, p.x + i - 1, p.y + j - 1); + acc += fxt_straight(q) * w; + } + } + var v = acc * P.f[2].y + P.f[2].z; + if (u0.y != 0u) { + v = abs(v); + } + o = vec4(max(v, vec3(0.0)) * a, a); + } + case 12u: { + let n = u0.z; + let aa = max(a, 0.0); + let r = vec3(fxt_lookup(c.x, n), fxt_lookup(c.y, n), fxt_lookup(c.z, n)); + var na = aa; + if (u0.y != 0u) { + na = fxt_lookup(aa, n); + } + o = vec4(r * na, na); + } + case 13u: { + if (a <= 0.0) { + break; + } + let level = f0.x; + let start = f0.y; + let y = luminance(c); + var r = c; + if (u0.y != 1u) { + let y2 = fxt_knee(max(y, 0.0), start, level); + r = r - y + y2; + } + if (u0.y != 0u) { + let yl = luminance(r); + var t = 1.0; + for (var i = 0; i < 3; i++) { + let v = r[i]; + if (v > level && v - yl > 1e-6) { + t = min(t, (level - yl) / (v - yl)); + } + if (v < 0.0 && yl - v > 1e-6) { + t = min(t, yl / (yl - v)); + } + } + t = clamp(t, 0.0, 1.0); + r = yl + (r - yl) * t; + } + r = clamp(r, vec3(0.0), vec3(level)); + if (u0.z != 0u && any(abs(c - r) > vec3(1e-4))) { + r = P.f[1].xyz; + } + o = vec4(r * a, a); + } + case 14u: { + if (straight_skip) { + break; + } + let hsl = rgb_to_hsl(c); + var h = hsl.x; + var s = hsl.y; + var l = hsl.z; + if (u0.y != 0u) { + let cl = P.f[1].z; + h = P.f[1].x; + s = P.f[1].y; + l = clamp(l + cl * select(l, 1.0 - l, cl > 0.0), 0.0, 1.0); + } else { + var s2 = fxt_adjust_sat(s, f0.y); + var l2 = fxt_adjust_light(l, f0.z); + var h2 = h + f0.x; + for (var r = 0u; r < u0.z; r++) { + let w = fxt_range_weight(r, hsl.x * 360.0) * min(hsl.y * 20.0, 1.0); + if (w > 0.0) { + let ro = r * 7u; + h2 += data[ro + 4u] * w; + s2 = fxt_adjust_sat(clamp(s2, 0.0, 1.0), data[ro + 5u] * w); + l2 = fxt_adjust_light(l2, data[ro + 6u] * w); + } + } + h = fract_euclid(h2); + s = clamp(s2, 0.0, 1.0); + l = l2; + } + o = vec4(hsl_to_rgb(h, s, l) * a, a); + } + case 15u: { + var r = c; + if (!straight_skip) { + for (var i = 0u; i < 3u; i++) { + r[i] = fxt_level(0u, c[i]); + } + } + // The master pass writes premultiplied values; the channel pass reads them back. + var q = px; + if (!straight_skip) { + q = vec4(r * a, a); + } + let qc = fxt_straight(q); + let qa = max(q.w, 0.0); + var any_ch = false; + for (var k = 0u; k < 4u; k++) { + if (!fxt_level_ident(7u + 7u * k)) { + any_ch = true; + } + } + if (!any_ch) { + o = q; + break; + } + var oc = qc; + for (var i = 0u; i < 3u; i++) { + let off = 7u + 7u * i; + if (!fxt_level_ident(off)) { + oc[i] = fxt_level(off, qc[i]); + } + } + var na = qa; + if (!fxt_level_ident(28u)) { + na = clamp(fxt_level(28u, qa), 0.0, 1.0); + } + o = vec4(oc * na, na); + } + case 16u: { + if (a <= 0.0 && px.x == 0.0 && px.y == 0.0 && px.z == 0.0) { + break; + } + let lo = f0.xyz; + let hi = P.f[1].xyz; + let g = P.f[2]; + var r: vec3; + for (var k = 0; k < 3; k++) { + let st = clamp((c[k] - lo[k]) / (hi[k] - lo[k]), 0.0, 1.0); + r[k] = powz(st, g[k]); + } + r = fxt_mix(r, c, g.w); + let na = clamp(max(a, 0.0), 0.0, 1.0); + o = vec4(r * na, na); + } + case 17u: { + if (a <= 0.0 && px.x == 0.0 && px.y == 0.0 && px.z == 0.0) { + break; + } + let n = u0.z; + let amt = f0.x; + var r: vec3; + if (u0.y == 1u) { + let l = luminance(c); + let nl = fxt_eq_look(0u, n, l); + if (l > 1e-5) { + r = c * nl / l; + } else { + r = vec3(nl); + } + } else if (u0.y == 2u) { + r = vec3(fxt_eq_look(0u, n, c.x), fxt_eq_look(0u, n, c.y), fxt_eq_look(0u, n, c.z)); + } else { + r = vec3(fxt_eq_look(0u, n, c.x), fxt_eq_look(1u, n, c.y), fxt_eq_look(2u, n, c.z)); + } + r = fxt_mix(c, r, amt); + let na = clamp(max(a, 0.0), 0.0, 1.0); + o = vec4(r * na, na); + } + case 18u: { + if (a <= 0.0) { + break; + } + let ch = u0.y; + var r: vec3; + if (ch <= 7u) { + var hsl = rgb_to_hsl(c); + if (ch == 4u || ch == 5u) { + hsl.x = 1.0 - hsl.x; + } + if (ch == 4u || ch == 6u) { + hsl.z = 1.0 - hsl.z; + } + if (ch == 4u || ch == 7u) { + hsl.y = 1.0 - hsl.y; + } + r = hsl_to_rgb(hsl.x, hsl.y, hsl.z); + } else { + var q = vec3( + 0.299 * c.x + 0.587 * c.y + 0.114 * c.z, + 0.596 * c.x - 0.274 * c.y - 0.322 * c.z, + 0.211 * c.x - 0.523 * c.y + 0.312 * c.z, + ); + if (ch == 8u || ch == 9u) { + q.x = 1.0 - q.x; + } + if (ch == 8u || ch == 10u) { + q.y = -q.y; + } + if (ch == 8u || ch == 11u) { + q.z = -q.z; + } + r = max( + vec3(q.x + 0.956 * q.y + 0.621 * q.z, q.x - 0.272 * q.y - 0.647 * q.z, q.x - 1.106 * q.y + 1.703 * q.z), + vec3(0.0), + ); + } + o = vec4((c + (r - c) * f0.x) * a, a); + } + case 19u: { + o = vec4(luminance(c)); + } + case 20u: { + if (a <= 0.0) { + break; + } + let bases = textureLoad(aux, p, 0); + let s_amt = f0.x; + let h_amt = f0.y; + let ws0 = clamp(1.0 - bases.x / f0.z, 0.0, 1.0); + let wh0 = clamp((bases.y - (1.0 - f0.w)) / f0.w, 0.0, 1.0); + let ws = ws0 * ws0; + let wh = wh0 * wh0; + let gain = (1.0 + s_amt * ws * 2.0) * (1.0 - h_amt * wh * 0.5); + let l = luminance(c); + let nl = l * gain; + let sat = pow(max(gain, 1e-3), P.f[1].x); + var r = nl + (c - l) * sat; + let mc = P.f[1].y; + if (mc != 0.0) { + let shift = (0.5 + (nl - 0.5) * (1.0 + mc)) - nl; + r = r + shift; + } + r = fxt_mix(max(r, vec3(0.0)), c, P.f[1].z); + o = vec4(r * a, a); + } + case 21u: { + o = vec4(px.x, textureLoad(aux, p, 0).x, 0.0, 0.0); + } + case 22u: { + if (a <= 0.0 && px.x == 0.0 && px.y == 0.0 && px.z == 0.0) { + break; + } + let r = max(f0.x + (c - f0.y) * f0.z, vec3(0.0)); + let na = clamp(max(a, 0.0), 0.0, 1.0); + o = vec4(r * na, na); + } + case 23u: { + if (a <= 0.0) { + break; + } + let l = clamp(luminance(c), 0.0, 1.0); + let ws = 1.0 - fxt_smoothstep(0.0, 0.5, l); + let wh = fxt_smoothstep(0.5, 1.0, l); + let wm = 1.0 - ws - wh; + let pin = P.f[3].x; + let keep = 1.0 - pin * clamp(1.0 - 4.0 * l * (1.0 - l), 0.0, 1.0); + let contrast = P.f[3].z; + let darks = P.f[4].x; + let brights = P.f[4].y; + var r: vec3; + for (var k = 0; k < 3; k++) { + let d = P.f[0][k] * ws + P.f[1][k] * wm + P.f[2][k] * wh; + var v = c[k] + d * keep; + v = 0.5 + (v - 0.5) * (1.0 + contrast); + v += darks * 0.25 * (1.0 - l) * (1.0 - l) + brights * 0.25 * l * l; + r[k] = max(v, 0.0); + } + r = fxt_mix(r, c, P.f[3].y); + o = vec4(r * a, a); + } + case 24u: { + if (a <= 0.0) { + break; + } + var r: vec3; + for (var k = 0u; k < 3u; k++) { + let base = k * 11u; + let n = u32(data[base]); + let v = c[k]; + // slice::partition_point(x < v), clamped to 1..n-1. + var i = 0u; + while (i < n && data[base + 1u + 2u * i] < v) { + i++; + } + i = clamp(i, 1u, n - 1u); + let ax = data[base + 1u + 2u * (i - 1u)]; + let ay = data[base + 2u + 2u * (i - 1u)]; + let cx = data[base + 1u + 2u * i]; + let cy = data[base + 2u + 2u * i]; + r[k] = ay + (cy - ay) * (v - ax) / max(cx - ax, 1e-9); + } + o = vec4(r * a, a); + } + default: {} + } + textureStore(out, p, o); +} + +// util::smoothstep. +fn fxt_smoothstep(e0: f32, e1: f32, x: f32) -> f32 { + if (abs(e1 - e0) < 1e-9) { + return select(1.0, 0.0, x < e0); + } + let t = clamp((x - e0) / (e1 - e0), 0.0, 1.0); + return t * t * (3.0 - 2.0 * t); +} diff --git a/crates/gpu/src/tests.rs b/crates/gpu/src/tests.rs index f795b58d2..7a3a1712a 100644 --- a/crates/gpu/src/tests.rs +++ b/crates/gpu/src/tests.rs @@ -511,7 +511,7 @@ fn effect_chains_and_mixed_stacks() { let mut l = s.footage(80, 50); s.effect(&mut l, "ec.color.exposure", &[("master/exposure", n(0.7))]); s.effect(&mut l, "ec.blur.gaussian", &[("blurriness", n(5.0))]); - s.effect(&mut l, "ec.stylize.posterize", &[]); + s.effect(&mut l, "ec.stylize.coloremboss", &[]); s.effect(&mut l, "ec.stylize.glow", &[]); s.effect(&mut l, "ec.channel.invert", &[("blend", n(50.0))]); s.push(l); diff --git a/crates/gpu/src/tests_adjust.rs b/crates/gpu/src/tests_adjust.rs index ae7f88da3..bc6be0cf7 100644 --- a/crates/gpu/src/tests_adjust.rs +++ b/crates/gpu/src/tests_adjust.rs @@ -57,7 +57,7 @@ fn gpu_adjustment_chains_match_the_cpu() { #[test] fn mixed_adjustment_chains_fall_back_per_effect() { let mut stack = gpu_stack(); - stack.insert(1, ("ec.stylize.posterize", vec![])); + stack.insert(1, ("ec.stylize.coloremboss", vec![])); stack.push(("ec.stylize.glow", vec![("threshold", n(30.0))])); for depth in [BitDepth::Bpc8, BitDepth::Bpc32] { let s = adjustment_scene(depth, &stack, true); @@ -93,7 +93,7 @@ impl FxTarget for Probe { fn adjustment_stacks_split_into_gpu_runs_and_cpu_steps() { let Some(g) = gpu() else { return }; let mut stack = gpu_stack(); - stack.insert(1, ("ec.stylize.posterize", vec![])); + stack.insert(1, ("ec.stylize.coloremboss", vec![])); let s = adjustment_scene(BitDepth::Bpc32, &stack, false); let mut r = Renderer::new(&s.p, &Pattern, RenderOpts { backend: Backend::Gpu, ..opts() }); r.accel = Some(g); diff --git a/crates/gpu/src/tests_fx_key.rs b/crates/gpu/src/tests_fx_key.rs new file mode 100644 index 000000000..76c7e831e --- /dev/null +++ b/crates/gpu/src/tests_fx_key.rs @@ -0,0 +1,230 @@ +//! GPU keying, matte and channel family effects vs the CPU effects (the oracle): direct on a +//! buffer at full and half resolution, as adjustment, and composited at 8 and 32 bpc. + +use effectcraft_keyframe::Value; +use effectcraft_project::BitDepth; +use effectcraft_render::RenderOpts; +use effectcraft_time::Tick; + +use crate::tests::{Scene, c, check, compare_at, effect_case, n, opts, set}; + +fn e(v: u32) -> Value { + Value::Enum(v) +} + +fn on() -> Value { + Value::Bool(true) +} + +fn off() -> Value { + Value::Bool(false) +} + +/// [`effect_case`] for hard thresholds (8-bit quantisation, bitwise operators): the composited +/// renders may flip a few pixels sitting on the threshold after the layer transform's float +/// rounding, so up to 0.1 % of them may differ by any amount. +fn discontinuous_case(id: &str, vals: &[(&str, Value)]) { + crate::tests::effect_direct(id, vals, false); + crate::tests::effect_direct(id, vals, true); + for depth in [BitDepth::Bpc8, BitDepth::Bpc32] { + let mut s = Scene::new(depth); + let bg = s.solid([0.15, 0.1, 0.2], 97, 61); + s.push(bg); + let mut l = s.footage(70, 44); + s.effect(&mut l, id, vals); + set(&mut l, "transform/rotation", Value::Scalar(8.0)); + s.push(l); + check(&format!("{id} {vals:?} {depth:?}"), compare_at(&s, opts(), Tick::ZERO), 0.001); + check(&format!("{id} {vals:?} {depth:?} half"), compare_at(&s, RenderOpts { scale: 0.5, ..opts() }, Tick::ZERO), 0.001); + } +} + +#[test] +fn colour_luma_range_extract_keys() { + effect_case("ec.key.colorkey", &[("keyColor", c(0.7, 0.6, 0.5)), ("colorTolerance", n(60.0))]); + effect_case("ec.key.colorkey", &[("keyColor", c(0.5, 0.6, 0.5)), ("colorTolerance", n(70.0)), ("edgeThin", n(1.5)), ("edgeFeather", n(4.0))]); + effect_case("ec.key.colorkey", &[("keyColor", c(0.5, 0.6, 0.5)), ("colorTolerance", n(70.0)), ("edgeThin", n(-2.0))]); + effect_case("ec.key.luma", &[("threshold", n(120.0)), ("tolerance", n(40.0))]); + effect_case("ec.key.luma", &[("keyType", e(0)), ("threshold", n(140.0)), ("tolerance", n(30.0)), ("edgeFeather", n(3.0))]); + effect_case("ec.key.luma", &[("keyType", e(2)), ("threshold", n(128.0)), ("tolerance", n(25.0)), ("edgeThin", n(1.0))]); + effect_case("ec.key.luma", &[("keyType", e(3)), ("threshold", n(100.0)), ("tolerance", n(50.0))]); + effect_case("ec.key.colorrange", &[("fuzziness", n(20.0))]); + effect_case("ec.key.colorrange", &[("colorSpace", e(1)), ("minL", n(60.0)), ("maxA", n(150.0)), ("minB", n(90.0)), ("fuzziness", n(15.0))]); + effect_case("ec.key.colorrange", &[("colorSpace", e(2)), ("minL", n(80.0)), ("maxA", n(180.0)), ("minB", n(40.0)), ("fuzziness", n(30.0))]); + effect_case("ec.key.extract", &[("blackPoint", n(60.0)), ("whitePoint", n(200.0)), ("blackSoftness", n(30.0)), ("whiteSoftness", n(20.0))]); + discontinuous_case("ec.key.extract", &[("channel", e(1)), ("blackPoint", n(90.0)), ("invert", on())]); + effect_case("ec.key.extract", &[("channel", e(4)), ("whitePoint", n(200.0)), ("whiteSoftness", n(40.0))]); +} + +#[test] +fn difference_colour_difference_and_screen_keys() { + effect_case("ec.key.differencematte", &[]); + effect_case("ec.key.differencematte", &[("view", e(2)), ("matchingTolerance", n(0.0)), ("blurBeforeDifference", n(3.0))]); + effect_case("ec.key.colordifference", &[("keyColor", c(0.2, 0.3, 0.8))]); + effect_case( + "ec.key.colordifference", + &[ + ("keyColor", c(0.3, 0.8, 0.2)), + ("colorMatchingAccuracy", e(1)), + ("partialAInBlack", n(20.0)), + ("partialAGamma", n(1.4)), + ("partialBOutWhite", n(200.0)), + ("blackLevel", n(10.0)), + ("gamma", n(0.8)), + ], + ); + for view in [1, 2, 3, 4, 5, 6, 8] { + effect_case("ec.key.colordifference", &[("keyColor", c(0.2, 0.3, 0.8)), ("view", e(view)), ("partialBInWhite", n(180.0))]); + } + effect_case("ec.key.screen", &[("screenColor", c(0.4, 0.75, 0.3))]); + effect_case( + "ec.key.screen", + &[ + ("screenColor", c(0.3, 0.4, 0.8)), + ("screenGain", n(130.0)), + ("screenBalance", n(30.0)), + ("despill", n(60.0)), + ("clipBlack", n(10.0)), + ("clipWhite", n(85.0)), + ("screenPreblur", n(2.0)), + ("screenShrinkGrow", n(-1.5)), + ("screenSoftness", n(3.0)), + ], + ); + effect_case("ec.key.screen", &[("screenColor", c(0.8, 0.4, 0.3)), ("screenShrinkGrow", n(2.0)), ("edgeColor", e(1))]); + for view in [1, 2] { + effect_case("ec.key.screen", &[("screenColor", c(0.4, 0.75, 0.3)), ("view", e(view))]); + } +} + +#[test] +fn spill_unmult_and_key_cleaner() { + effect_case("ec.key.spill", &[("colorToSuppress", c(0.2, 0.9, 0.1))]); + effect_case("ec.key.spill", &[("colorToSuppress", c(0.9, 0.8, 0.1)), ("colorAccuracy", e(1)), ("suppression", n(150.0))]); + effect_case("ec.key.advancedspill", &[]); + effect_case("ec.key.advancedspill", &[("suppression", n(80.0)), ("ultraSettings/spillRange", n(20.0)), ("ultraSettings/lumaCorrection", n(50.0))]); + effect_case( + "ec.key.advancedspill", + &[ + ("method", e(1)), + ("ultraSettings/keyColor", c(0.9, 0.2, 0.3)), + ("ultraSettings/tolerance", n(40.0)), + ("ultraSettings/desaturate", n(50.0)), + ("ultraSettings/spillColorCorrection", n(30.0)), + ], + ); + effect_case("ec.key.unmult", &[]); + effect_case("ec.key.unmult", &[("backgroundColor", e(1)), ("whiteLevel", n(90.0)), ("softness", n(40.0)), ("clipHdrResults", off())]); + effect_case("ec.key.unmult", &[("blackLevel", n(30.0)), ("softness", n(0.0)), ("removeColorMatting", off())]); + effect_case("ec.key.keycleaner", &[]); + effect_case("ec.key.keycleaner", &[("additionalEdgeRadius", n(6.0)), ("reduceChatter", on()), ("alphaContrast", n(250.0)), ("strength", n(60.0))]); +} + +#[test] +fn chokers() { + effect_case("ec.matte.simplechoker", &[("chokeMatte", n(1.5))]); + effect_case("ec.matte.simplechoker", &[("chokeMatte", n(-2.5))]); + effect_case("ec.matte.simplechoker", &[("chokeMatte", n(-1.0)), ("view", e(1))]); + effect_case("ec.matte.mattechoker", &[]); + effect_case( + "ec.matte.mattechoker", + &[ + ("geometricSoftness1", n(2.0)), + ("choke1", n(-40.0)), + ("geometricSoftness2", n(3.0)), + ("choke2", n(30.0)), + ("grayLevelSoftness2", n(40.0)), + ("iterations", n(2.0)), + ], + ); +} + +#[test] +fn refine_mattes() { + effect_case("ec.matte.refinesoft", &[]); + effect_case("ec.matte.refinesoft", &[("radius", n(4.0)), ("smooth", n(40.0)), ("feather", n(30.0)), ("contrast", n(20.0)), ("shiftEdge", n(-25.0))]); + effect_case("ec.matte.refinesoft", &[("calculateEdgeDetails", off()), ("smooth", n(50.0)), ("shiftEdge", n(30.0))]); + effect_case("ec.matte.refinesoft", &[("radius", n(3.0)), ("viewEdgeRegion", on())]); + effect_case("ec.matte.refinesoft", &[("decontamination/viewDecontaminationMap", on()), ("decontamination/extendWhereSmoothed", off())]); + effect_case("ec.matte.refinesoft", &[("decontamination/increaseDecontaminationRadius", n(4.0)), ("decontamination/decontaminationAmount", n(60.0))]); + effect_case("ec.matte.refinehard", &[]); + effect_case("ec.matte.refinehard", &[("choke", n(20.0)), ("feather", n(20.0)), ("decontaminate", off())]); +} + +#[test] +fn channel_effects() { + effect_case("ec.channel.setchannels", &[("setRedTo", e(2)), ("setGreenTo", e(4)), ("setBlueTo", e(5)), ("setAlphaTo", e(6))]); + effect_case("ec.channel.shiftchannels", &[("takeRedFrom", e(3)), ("takeGreenFrom", e(7)), ("takeAlphaFrom", e(4))]); + effect_case("ec.channel.removecolormatting", &[("backgroundColor", c(0.3, 0.5, 0.7))]); + effect_case("ec.channel.removecolormatting", &[("backgroundColor", c(0.8, 0.2, 0.1)), ("clipHdr", off())]); + for op in [0, 2, 3, 4, 8, 10, 12] { + discontinuous_case("ec.channel.arithmetic", &[("operator", e(op)), ("redValue", n(120.0)), ("greenValue", n(60.0)), ("blueValue", n(200.0))]); + } + effect_case("ec.channel.arithmetic", &[("operator", e(3)), ("redValue", n(200.0)), ("clip", off())]); + for mode in [0, 2, 4, 9, 11] { + effect_case("ec.channel.solidcomposite", &[("blendingMode", e(mode)), ("color", c(0.8, 0.3, 0.2)), ("opacity", n(70.0)), ("sourceOpacity", n(80.0))]); + } + effect_case("ec.channel.setmatte", &[("takeMatteFrom", e(4))]); + effect_case("ec.channel.setmatte", &[("takeMatteFrom", e(3)), ("invertMatte", on())]); + effect_case("ec.channel.setmatte", &[("takeMatteFrom", e(1)), ("compositeMatteWithOriginal", off()), ("premultiplyMatteLayer", off())]); + for (from, to) in [(0, 0), (1, 0), (2, 0), (3, 0), (8, 4), (9, 5), (10, 6), (11, 3), (12, 8), (13, 7), (7, 1)] { + effect_case("ec.channel.combiner", &[("from", e(from)), ("to", e(to))]); + } + effect_case("ec.channel.combiner", &[("from", e(4)), ("to", e(2)), ("invert", on()), ("solidAlpha", on())]); + effect_case("ec.channel.blend", &[("mode", e(1)), ("blendWithOriginal", n(30.0))]); + effect_case("ec.channel.calculations", &[("inputChannel", e(1))]); + effect_case("ec.channel.calculations", &[("inputChannel", e(0)), ("invertInput", on())]); + effect_case("ec.channel.compoundarithmetic", &[("operator", e(3)), ("operateOnChannels", e(1))]); +} + +/// The channel and keying effects that read another layer, composited. +#[test] +fn layer_inputs() { + let cases: Vec<(&str, Vec<(&str, Value)>)> = vec![ + ("ec.key.differencematte", vec![("matchingTolerance", n(10.0)), ("matchingSoftness", n(20.0))]), + ("ec.key.differencematte", vec![("ifLayerSizesDiffer", e(1)), ("blurBeforeDifference", n(2.0)), ("view", e(2))]), + ("ec.channel.setmatte", vec![("takeMatteFrom", e(4)), ("stretchMatteToFit", off())]), + ("ec.channel.setmatte", vec![("takeMatteFrom", e(3))]), + ("ec.channel.setchannels", vec![("setGreenTo", e(0)), ("setAlphaTo", e(4))]), + ("ec.channel.combiner", vec![("useSecondLayer", on()), ("from", e(11)), ("to", e(7))]), + ("ec.channel.blend", vec![("mode", e(0)), ("blendWithOriginal", n(40.0))]), + ("ec.channel.blend", vec![("mode", e(2))]), + ("ec.channel.blend", vec![("mode", e(3)), ("ifLayerSizesDiffer", e(1))]), + ("ec.channel.blend", vec![("mode", e(4))]), + ("ec.channel.calculations", vec![("secondLayerChannel", e(2)), ("blendingMode", e(2)), ("secondLayerOpacity", n(70.0))]), + ("ec.channel.calculations", vec![("inputChannel", e(4)), ("blendingMode", e(10)), ("preserveTransparency", on()), ("invertSecondLayer", on())]), + ("ec.channel.compoundarithmetic", vec![("operator", e(1)), ("overflowBehavior", e(2))]), + ("ec.channel.compoundarithmetic", vec![("operator", e(4)), ("operateOnChannels", e(1)), ("blendWithOriginal", n(25.0))]), + ("ec.channel.compoundarithmetic", vec![("operator", e(13)), ("operateOnChannels", e(2))]), + ]; + let layer_param = |id: &str| match id { + "ec.key.differencematte" => vec!["differenceLayer"], + "ec.channel.setmatte" => vec!["takeMatteFromLayer"], + "ec.channel.setchannels" => vec!["sourceLayer2", "sourceLayer4"], + "ec.channel.combiner" => vec!["sourceLayer"], + "ec.channel.blend" => vec!["blendWithLayer"], + "ec.channel.calculations" => vec!["secondLayer"], + _ => vec!["secondSource"], + }; + for depth in [BitDepth::Bpc8, BitDepth::Bpc32] { + for (id, vals) in &cases { + let mut s = Scene::new(depth); + let bg = s.solid([0.15, 0.1, 0.2], 97, 61); + s.push(bg); + let mut other = s.footage(50, 36); + other.switches.video = false; + let oid = s.push(other); + let mut l = s.footage(70, 44); + let mut vals = vals.clone(); + for p in layer_param(id) { + vals.push((p, Value::Layer(Some(oid.0)))); + } + s.effect(&mut l, id, &vals); + set(&mut l, "transform/rotation", Value::Scalar(8.0)); + s.push(l); + let label = format!("{id} {vals:?} {depth:?}"); + check(&label, compare_at(&s, opts(), Tick::ZERO), 0.0); + check(&format!("{label} half"), compare_at(&s, RenderOpts { scale: 0.5, ..opts() }, Tick::ZERO), 0.0); + } + } +} diff --git a/crates/gpu/src/tests_fx_noise.rs b/crates/gpu/src/tests_fx_noise.rs new file mode 100644 index 000000000..70f893e82 --- /dev/null +++ b/crates/gpu/src/tests_fx_noise.rs @@ -0,0 +1,193 @@ +//! GPU noise, blur and time family effects vs the CPU effects (the oracle): direct on a buffer at full and half +//! resolution, as adjustment, and composited at 8 and 32 bpc. + +use effectcraft_color::Label; +use effectcraft_keyframe::{Keyframe, Value}; +use effectcraft_project::build; +use effectcraft_project::{BitDepth, Comp, ItemKind, LayerSource}; +use effectcraft_render::RenderOpts; +use effectcraft_time::{FrameRate, Tick}; + +use crate::tests::{Scene, c, check, compare_at, effect_case, n, opts, set}; + +fn e(v: u32) -> Value { + Value::Enum(v) +} + +fn on() -> Value { + Value::Bool(true) +} + +fn pt(x: f64, y: f64) -> Value { + Value::Vec2([x, y]) +} + +#[test] +fn medians() { + effect_case("ec.noise.median", &[("radius", n(2.0))]); + effect_case("ec.noise.median", &[("radius", n(5.0)), ("operateOnAlpha", on())]); + effect_case("ec.noise.medianlegacy", &[("radius", n(3.0))]); + effect_case("ec.noise.medianlegacy", &[("radius", n(2.0)), ("operateOnAlphaChannel", on())]); + effect_case("ec.noise.dustscratches", &[("radius", n(3.0)), ("threshold", n(10.0))]); + effect_case("ec.noise.dustscratches", &[("radius", n(2.0)), ("threshold", n(0.0)), ("operateOnAlpha", on())]); +} + +#[test] +fn edge_preserving_blurs() { + effect_case("ec.blur.bilateral", &[]); + effect_case("ec.blur.bilateral", &[("radius", n(9.0)), ("threshold", n(30.0)), ("colorize", Value::Bool(false))]); + effect_case("ec.blur.bilateral", &[("radius", n(14.0)), ("threshold", n(5.0))]); + effect_case("ec.blur.smart", &[]); + effect_case("ec.blur.smart", &[("radius", n(6.0)), ("threshold", n(15.0)), ("mode", e(1))]); + effect_case("ec.blur.smart", &[("radius", n(4.0)), ("threshold", n(40.0)), ("mode", e(2))]); +} + +#[test] +fn sharpen_and_unsharp_mask() { + effect_case("ec.blur.sharpen", &[("amount", n(60.0))]); + effect_case("ec.blur.sharpen", &[("amount", n(250.0))]); + effect_case("ec.blur.unsharp", &[("amount", n(120.0)), ("radius", n(4.0))]); + effect_case("ec.blur.unsharp", &[("amount", n(80.0)), ("radius", n(2.5)), ("threshold", n(12.0))]); +} + +#[test] +fn channel_and_compound_blur() { + effect_case("ec.blur.channel", &[("redBlurriness", n(8.0)), ("blueBlurriness", n(3.0))]); + effect_case("ec.blur.channel", &[("greenBlurriness", n(6.0)), ("alphaBlurriness", n(6.0)), ("repeatEdge", on())]); + effect_case("ec.blur.channel", &[("redBlurriness", n(5.0)), ("alphaBlurriness", n(10.0)), ("dimensions", e(1))]); + effect_case("ec.blur.compound", &[]); + effect_case("ec.blur.compound", &[("maximumBlur", n(12.0)), ("invertBlur", on())]); +} + +/// Compound Blur reading another layer (stretched and centred), composited. +#[test] +fn compound_blur_layer() { + for depth in [BitDepth::Bpc8, BitDepth::Bpc32] { + for stretch in [true, false] { + let mut s = Scene::new(depth); + let bg = s.solid([0.15, 0.1, 0.2], 97, 61); + s.push(bg); + let mut map = s.footage(50, 36); + map.switches.video = false; + let mid = s.push(map); + let mut l = s.footage(70, 44); + s.effect( + &mut l, + "ec.blur.compound", + &[("blurLayer", Value::Layer(Some(mid.0))), ("maximumBlur", n(10.0)), ("stretchMapToFit", Value::Bool(stretch))], + ); + set(&mut l, "transform/rotation", Value::Scalar(8.0)); + s.push(l); + let label = format!("compound blur layer {stretch} {depth:?}"); + check(&label, compare_at(&s, opts(), Tick::ZERO), 0.0); + check(&format!("{label} half"), compare_at(&s, RenderOpts { scale: 0.5, ..opts() }, Tick::ZERO), 0.0); + } + } +} + +#[test] +fn minimax() { + effect_case("ec.channel.minimax", &[("radius", n(3.0))]); + effect_case("ec.channel.minimax", &[("radius", n(2.0)), ("operation", e(0)), ("channel", e(2))]); + effect_case("ec.channel.minimax", &[("radius", n(4.0)), ("operation", e(2)), ("channel", e(1)), ("direction", e(1))]); + effect_case("ec.channel.minimax", &[("radius", n(3.0)), ("operation", e(3)), ("channel", e(4)), ("dontShrinkEdges", on()), ("direction", e(2))]); +} + +#[test] +fn turbulent_noise() { + effect_case("ec.noise.turbulent", &[]); + effect_case( + "ec.noise.turbulent", + &[("fractalType", e(3)), ("noiseType", e(3)), ("evolution", n(120.0)), ("evolutionOptions/turbulenceFactor", n(1.5)), ("blendingMode", e(5))], + ); + effect_case( + "ec.noise.turbulent", + &[("fractalType", e(5)), ("noiseType", e(1)), ("complexity", n(3.5)), ("transform/scale", n(40.0)), ("overflow", e(1)), ("blendingMode", e(0))], + ); + effect_case("ec.noise.turbulent", &[("fractalType", e(7)), ("noiseType", e(0)), ("transform/perspectiveOffset", on()), ("opacity", n(70.0))]); +} + +#[test] +fn fractal_noise_other_modes() { + effect_case("ec.noise.fractal", &[("noiseType", e(3)), ("fractalType", e(4)), ("blendingMode", e(3))]); + effect_case( + "ec.noise.fractal", + &[ + ("fractalType", e(6)), + ("evolutionOptions/cycleEvolution", on()), + ("evolutionOptions/cycle", n(2.0)), + ("evolution", n(500.0)), + ("subSettings/subRotation", n(25.0)), + ("subSettings/subOffset", pt(4.0, -3.0)), + ], + ); + effect_case("ec.noise.fractal", &[("fractalType", e(8)), ("noiseType", e(1)), ("blendingMode", e(12)), ("contrast", n(160.0)), ("invert", on())]); +} + +#[test] +fn noise_alpha_and_hls() { + effect_case("ec.noise.noisealpha", &[("amount", n(40.0))]); + effect_case("ec.noise.noisealpha", &[("amount", n(70.0)), ("noise", e(3)), ("originalAlpha", e(2)), ("overflow", e(1)), ("noisePhase", n(100.0))]); + effect_case( + "ec.noise.noisealpha", + &[("amount", n(60.0)), ("noise", e(2)), ("originalAlpha", e(3)), ("overflow", e(2)), ("noiseOptions/cycleNoise", on()), ("randomSeed", n(9.0))], + ); + effect_case("ec.noise.noisehls", &[("hue", n(30.0)), ("lightness", n(20.0)), ("saturation", n(40.0))]); + effect_case("ec.noise.noisehls", &[("noise", e(1)), ("lightness", n(50.0)), ("noisePhase", n(200.0))]); + effect_case("ec.noise.noisehls", &[("noise", e(2)), ("hue", n(60.0)), ("saturation", n(25.0)), ("grainSize", n(3.0))]); + effect_case("ec.noise.noisehlsauto", &[("noise", e(0)), ("hue", n(20.0)), ("lightness", n(30.0)), ("noiseAnimationSpeed", n(7.0))]); +} + +#[test] +fn remove_grain() { + effect_case("ec.noise.removegrain", &[]); + effect_case( + "ec.noise.removegrain", + &[ + ("noiseReductionSettings/noiseReduction", n(2.5)), + ("noiseReductionSettings/passes", n(2.0)), + ("noiseReductionSettings/mode", e(1)), + ("fineTuning/texture", n(0.3)), + ], + ); + effect_case("ec.noise.removegrain", &[("unsharpMask/amount", n(80.0)), ("unsharpMask/radius", n(2.0)), ("unsharpMask/threshold", n(4.0))]); + effect_case("ec.noise.removegrain", &[("viewingMode", e(0)), ("previewRegion/showBox", on()), ("previewRegion/boxColor", c(1.0, 0.2, 0.1))]); +} + +/// Echo and Posterize Time fetch other frames through the effect host: a layer whose rotation +/// is keyframed, at a nonzero time (the direct cases have no host and pass the layer through). +#[test] +fn echo_and_posterize_time() { + effect_case("ec.time.echo", &[]); + effect_case("ec.time.posterizetime", &[]); + for depth in [BitDepth::Bpc8, BitDepth::Bpc32] { + for (id, vals) in [ + ("ec.time.echo", vec![("numberOfEchoes", n(3.0)), ("decay", n(0.7)), ("echoTime", n(-0.1))]), + ("ec.time.echo", vec![("numberOfEchoes", n(4.0)), ("echoOperator", e(2)), ("echoTime", n(0.05))]), + ("ec.time.echo", vec![("numberOfEchoes", n(2.0)), ("echoOperator", e(4)), ("startingIntensity", n(0.8))]), + ("ec.time.echo", vec![("numberOfEchoes", n(5.0)), ("echoOperator", e(6))]), + ("ec.time.posterizetime", vec![("frameRate", n(4.0))]), + ] { + let mut s = Scene::new(depth); + let bg = s.solid([0.15, 0.1, 0.2], 97, 61); + s.push(bg); + // A precomp whose content moves, so the frames differ. + let inner = Comp::new(60, 40, FrameRate::FPS_30, Tick::from_seconds_f64(2.0)); + let inner_id = s.p.add_item("Inner", Label::Sandstone, None, ItemKind::Comp(inner.into())); + let mut a = s.footage(30, 24); + let pos = a.props.prop_mut("transform/position").unwrap(); + pos.keys = vec![Keyframe::new(Tick::ZERO, crate::tests::v3(10.0, 12.0)), Keyframe::new(Tick::from_seconds_f64(1.0), crate::tests::v3(50.0, 30.0))]; + let rot = a.props.prop_mut("transform/rotation").unwrap(); + rot.keys = vec![Keyframe::new(Tick::ZERO, Value::Scalar(0.0)), Keyframe::new(Tick::from_seconds_f64(1.0), Value::Scalar(90.0))]; + s.p.comp_mut(inner_id).unwrap().layers = vec![a]; + let mut l = build::layer(&mut s.p, &s.comp, "Inner", LayerSource::Comp { item: inner_id }, (60, 40), None); + s.effect(&mut l, id, &vals); + set(&mut l, "transform/rotation", Value::Scalar(8.0)); + s.push(l); + let t = Tick::from_seconds_f64(0.4); + let label = format!("{id} {vals:?} {depth:?}"); + check(&label, compare_at(&s, opts(), t), 0.0); + check(&format!("{label} half"), compare_at(&s, RenderOpts { scale: 0.5, ..opts() }, t), 0.0); + } + } +} diff --git a/crates/gpu/src/tests_fx_stylize.rs b/crates/gpu/src/tests_fx_stylize.rs new file mode 100644 index 000000000..35e3e99ba --- /dev/null +++ b/crates/gpu/src/tests_fx_stylize.rs @@ -0,0 +1,223 @@ +//! GPU stylize and distort family effects vs the CPU effects (the oracle): direct on a buffer at full and half +//! resolution, as adjustment, and composited at 8 and 32 bpc. + +use effectcraft_keyframe::{Keyframe, Value}; +use effectcraft_project::BitDepth; +use effectcraft_render::RenderOpts; +use effectcraft_time::Tick; + +use crate::tests::{Scene, c, check, compare_at, effect_case, n, opts, set}; + +fn e(v: u32) -> Value { + Value::Enum(v) +} + +fn on() -> Value { + Value::Bool(true) +} + +fn pt(x: f64, y: f64) -> Value { + Value::Vec2([x, y]) +} + +#[test] +fn posterize_thresholds_strobe_vignette() { + effect_case("ec.stylize.posterize", &[("level", n(4.0))]); + effect_case("ec.stylize.posterize", &[("level", n(9.0))]); + effect_case("ec.stylize.threshold", &[("level", n(100.0))]); + effect_case("ec.stylize.threshold", &[("level", n(181.0))]); + effect_case("ec.stylize.ccthreshold", &[]); + effect_case("ec.stylize.ccthreshold", &[("channel", e(1)), ("invert", on()), ("threshold", n(90.0))]); + effect_case("ec.stylize.ccthreshold", &[("channel", e(2)), ("blend", n(30.0))]); + effect_case("ec.stylize.ccthreshold", &[("channel", e(3)), ("threshold", n(60.0))]); + effect_case( + "ec.stylize.ccthresholdrgb", + &[("redThreshold", n(70.0)), ("greenThreshold", n(150.0)), ("blueThreshold", n(110.0)), ("invertGreen", on()), ("blend", n(25.0))], + ); + effect_case( + "ec.stylize.strobe", + &[("strobeDuration", n(0.5)), ("strobeOperator", e(1)), ("strobeColor", c(0.2, 0.5, 0.9)), ("blendWithOriginal", n(30.0))], + ); + effect_case("ec.stylize.strobe", &[("strobeDuration", n(0.5)), ("strobeOperator", e(4)), ("strobeColor", c(0.6, 0.1, 0.3))]); + effect_case("ec.stylize.strobe", &[("strobeDuration", n(0.5)), ("strobe", e(1)), ("blendWithOriginal", n(40.0))]); + effect_case("ec.stylize.strobe", &[("strobeDuration", n(0.1)), ("strobePeriod", n(0.2)), ("randomStrobeProbability", n(50.0))]); + effect_case("ec.stylize.ccvignette", &[("amount", n(80.0))]); + effect_case("ec.stylize.ccvignette", &[("amount", n(-60.0)), ("pinHighlights", n(50.0)), ("angleOfView", n(120.0)), ("center", pt(20.0, 15.0))]); +} + +#[test] +fn scatter_brush_strokes() { + effect_case("ec.stylize.scatter", &[("amount", n(5.0))]); + effect_case("ec.stylize.scatter", &[("amount", n(3.0)), ("grain", e(1))]); + effect_case("ec.stylize.scatter", &[("amount", n(7.0)), ("grain", e(2)), ("randomizeEveryFrame", on())]); + effect_case("ec.stylize.brushstrokes", &[]); + effect_case( + "ec.stylize.brushstrokes", + &[("strokeAngle", n(40.0)), ("brushSize", n(3.0)), ("strokeLength", n(10.0)), ("paintSurface", e(2)), ("blendWithOriginal", n(20.0))], + ); + effect_case("ec.stylize.brushstrokes", &[("paintSurface", e(1)), ("strokeDensity", n(0.6)), ("strokeRandomness", n(2.0)), ("brushSize", n(1.3))]); + effect_case("ec.stylize.brushstrokes", &[("paintSurface", e(3)), ("strokeLength", n(25.0))]); +} + +#[test] +fn roughen_edges() { + effect_case("ec.stylize.roughenedges", &[]); + effect_case("ec.stylize.roughenedges", &[("edgeType", e(1)), ("border", n(4.0)), ("edgeColor", c(0.1, 0.6, 0.9))]); + effect_case( + "ec.stylize.roughenedges", + &[ + ("edgeType", e(5)), + ("evolutionOptions/cycleEvolution", on()), + ("evolutionOptions/cycle", n(2.0)), + ("evolution", n(500.0)), + ("stretch", n(2.0)), + ("complexity", n(3.0)), + ], + ); + effect_case("ec.stylize.roughenedges", &[("edgeType", e(7)), ("border", n(0.0)), ("scale", n(60.0)), ("evolutionOptions/randomSeed", n(9.0))]); +} + +#[test] +fn tiles_kaleida_repetile() { + effect_case("ec.stylize.motiontile", &[("tileWidth", n(50.0)), ("tileHeight", n(40.0))]); + effect_case( + "ec.stylize.motiontile", + &[("outputWidth", n(150.0)), ("outputHeight", n(130.0)), ("mirrorEdges", on()), ("phase", n(70.0)), ("tileWidth", n(45.0))], + ); + effect_case( + "ec.stylize.motiontile", + &[("horizontalPhaseShift", on()), ("phase", n(33.0)), ("tileCenter", pt(30.0, 17.0)), ("tileHeight", n(55.0)), ("outputWidth", n(80.0))], + ); + effect_case("ec.stylize.cckaleida", &[]); + effect_case("ec.stylize.cckaleida", &[("mirroring", e(2)), ("rotation", n(20.0)), ("size", n(150.0))]); + effect_case("ec.stylize.ccrepetile", &[("expandRight", n(10.0)), ("expandUp", n(4.0))]); + effect_case("ec.stylize.ccrepetile", &[("expandLeft", n(12.0)), ("expandDown", n(9.0)), ("tiling", e(1))]); + effect_case("ec.stylize.ccrepetile", &[("expandLeft", n(30.0)), ("expandRight", n(30.0)), ("expandUp", n(20.0)), ("tiling", e(4))]); + effect_case("ec.distort.cctiler", &[]); + effect_case("ec.distort.cctiler", &[("scale", n(70.0)), ("blendWithOriginal", n(30.0)), ("center", pt(30.0, 18.0))]); + effect_case("ec.distort.ccgriddler", &[("rotation", n(20.0))]); + effect_case("ec.distort.ccgriddler", &[("cutTiles", on()), ("horizontalScale", n(120.0)), ("verticalScale", n(80.0)), ("tileSize", n(17.0))]); +} + +#[test] +fn mirror_offset_polar_spherize_corner_pin() { + effect_case("ec.distort.mirror", &[("angle", n(30.0))]); + effect_case("ec.distort.mirror", &[("angle", n(200.0)), ("center", pt(27.0, 19.0))]); + effect_case("ec.distort.offset", &[("shift", pt(10.0, 5.0))]); + effect_case("ec.distort.offset", &[("shift", pt(50.3, 20.7)), ("blend", n(40.0))]); + effect_case("ec.distort.polar", &[("interpolation", n(100.0))]); + effect_case("ec.distort.polar", &[("interpolation", n(60.0)), ("conversion", e(1))]); + effect_case("ec.distort.spherize", &[("radius", n(20.0))]); + effect_case("ec.distort.spherize", &[("radius", n(35.0)), ("center", pt(30.0, 25.0))]); + effect_case("ec.distort.cornerpin", &[("ul", pt(5.0, 3.0)), ("ur", pt(60.0, 8.0)), ("lr", pt(66.0, 40.0)), ("ll", pt(2.0, 44.0))]); + effect_case("ec.distort.cornerpin", &[("ul", pt(-8.0, 6.0)), ("ur", pt(75.0, -4.0)), ("lr", pt(50.0, 30.0)), ("ll", pt(12.0, 50.0))]); +} + +#[test] +fn optics_magnify() { + effect_case("ec.distort.opticscompensation", &[("fieldOfView", n(60.0))]); + effect_case("ec.distort.opticscompensation", &[("fieldOfView", n(90.0)), ("reverseLensDistortion", on()), ("fovOrientation", e(2))]); + effect_case( + "ec.distort.opticscompensation", + &[("fieldOfView", n(110.0)), ("reverseLensDistortion", on()), ("resize", e(1)), ("viewCenter", pt(30.0, 20.0))], + ); + effect_case("ec.distort.opticscompensation", &[("fieldOfView", n(120.0)), ("optimalPixels", on()), ("fovOrientation", e(1))]); + effect_case("ec.distort.magnify", &[("size", n(15.0))]); + effect_case( + "ec.distort.magnify", + &[("shape", e(1)), ("feather", n(6.0)), ("size", n(18.0)), ("scaling", e(1)), ("blendingMode", e(3)), ("magnification", n(270.0))], + ); + effect_case("ec.distort.magnify", &[("scaling", e(2)), ("blendingMode", e(0)), ("size", n(20.0)), ("opacity", n(70.0))]); + effect_case("ec.distort.magnify", &[("size", n(25.0)), ("center", pt(60.0, 10.0)), ("resizeLayer", on()), ("blendingMode", e(5))]); +} + +#[test] +fn cc_slant_smear_split() { + effect_case("ec.distort.ccslant", &[("slant", n(30.0))]); + effect_case("ec.distort.ccslant", &[("slant", n(-20.0)), ("stretching", on()), ("height", n(120.0))]); + effect_case("ec.distort.ccsmear", &[]); + effect_case("ec.distort.ccsmear", &[("reach", n(-40.0)), ("radius", n(20.0)), ("to", pt(40.0, 10.0))]); + effect_case("ec.distort.ccsplit", &[]); + effect_case("ec.distort.ccsplit2", &[("split1", n(30.0)), ("split2", n(80.0)), ("pointB", pt(60.0, 30.0))]); +} + +#[test] +fn twirl_legacy_ripple_pulse_power_pin() { + effect_case("ec.distort.twirllegacy", &[("angle", n(90.0))]); + effect_case("ec.distort.twirllegacy", &[("angle", n(-250.0)), ("radius", n(80.0)), ("center", pt(30.0, 18.0))]); + effect_case("ec.distort.ccripplepulse", &[("pulseLevel", n(80.0)), ("timeSpan", n(0.5))]); + effect_case("ec.distort.ccripplepulse", &[("pulseLevel", n(-60.0)), ("amplitude", n(150.0)), ("renderBump", on()), ("center", pt(25.0, 20.0))]); + effect_case("ec.distort.ccpowerpin", &[("topLeft", pt(5.0, 3.0)), ("bottomRight", pt(62.0, 38.0))]); + effect_case("ec.distort.ccpowerpin", &[("topRight", pt(80.0, -6.0)), ("bottomLeft", pt(-4.0, 50.0)), ("expandLeft", n(20.0)), ("expandBottom", n(-10.0))]); +} + +#[test] +fn flo_motion() { + effect_case("ec.distort.ccflomotion", &[]); + effect_case("ec.distort.ccflomotion", &[("antialiasing", e(2)), ("tileEdges", on()), ("amount1", n(-150.0)), ("falloff", n(0.7))]); + effect_case("ec.distort.ccflomotion", &[("antialiasing", e(0)), ("amount2", n(0.0)), ("knot1", pt(20.0, 30.0))]); +} + +#[test] +fn liquify_render() { + let mesh = "warp 20 50 0 0 0 10,10 20,15 30,20\ntwirlClockwise 24 80 0 0 0 40,25 42,27"; + effect_case("ec.distort.liquify", &[("distortionMesh", Value::Str(mesh.into()))]); + effect_case( + "ec.distort.liquify", + &[("distortionMesh", Value::Str(mesh.into())), ("distortionPercentage", n(60.0)), ("distortionMeshOffset", pt(3.0, -2.0))], + ); +} + +#[test] +fn glow_operations_and_arbitrary_map() { + effect_case("ec.stylize.glow", &[("glowOperation", e(2)), ("threshold", n(30.0)), ("radius", n(8.0))]); + effect_case("ec.stylize.glow", &[("glowOperation", e(3)), ("colors", e(1)), ("intensity", n(2.0))]); + effect_case("ec.stylize.glow", &[("colors", e(2)), ("arbitraryMap", Value::Str("0,0 0.5,0.8 1,1|0,0.2 1,0.6|".into())), ("threshold", n(20.0))]); + effect_case("ec.stylize.glow", &[("colors", e(2)), ("arbitraryMap", Value::Str("0,1 1,0||0,0 1,1".into())), ("operation", e(1)), ("glowOperation", e(4))]); +} + +/// Texturize with a texture layer (Tile / Center / Stretch), composited. +#[test] +fn texturize_layer() { + for depth in [BitDepth::Bpc8, BitDepth::Bpc32] { + for placement in 0..3 { + let mut s = Scene::new(depth); + let bg = s.solid([0.15, 0.1, 0.2], 97, 61); + s.push(bg); + let mut tex = s.footage(50, 36); + tex.switches.video = false; + let tid = s.push(tex); + let mut l = s.footage(70, 44); + s.effect( + &mut l, + "ec.stylize.texturize", + &[("textureLayer", Value::Layer(Some(tid.0))), ("texturePlacement", e(placement)), ("textureContrast", n(1.5)), ("lightDirection", n(60.0))], + ); + set(&mut l, "transform/rotation", Value::Scalar(8.0)); + s.push(l); + let label = format!("texturize {placement} {depth:?}"); + check(&label, compare_at(&s, opts(), Tick::ZERO), 0.0); + check(&format!("{label} half"), compare_at(&s, RenderOpts { scale: 0.5, ..opts() }, Tick::ZERO), 0.0); + } + } +} + +/// Transform's own shutter angle: the effect's rotation and position animate. +#[test] +fn transform_motion_blur() { + for depth in [BitDepth::Bpc8, BitDepth::Bpc32] { + let mut s = Scene::new(depth); + let bg = s.solid([0.15, 0.1, 0.2], 97, 61); + s.push(bg); + let mut l = s.footage(70, 44); + s.effect(&mut l, "ec.distort.transform", &[("useCompositionShutterAngle", Value::Bool(false)), ("shutterAngle", n(180.0))]); + let rot = l.props.prop_mut("effects/#1/rotation").expect("transform rotation"); + rot.keys = vec![Keyframe::new(Tick::ZERO, Value::Scalar(0.0)), Keyframe::new(Tick::from_seconds_f64(1.0), Value::Scalar(120.0))]; + let pos = l.props.prop_mut("effects/#1/position").expect("transform position"); + pos.keys = vec![Keyframe::new(Tick::ZERO, pt(35.0, 22.0)), Keyframe::new(Tick::from_seconds_f64(1.0), pt(95.0, 40.0))]; + s.push(l); + let label = format!("transform motion blur {depth:?}"); + check(&label, compare_at(&s, opts(), Tick::from_seconds_f64(0.3)), 0.0); + check(&format!("{label} half"), compare_at(&s, RenderOpts { scale: 0.5, ..opts() }, Tick::from_seconds_f64(0.3)), 0.0); + } +} diff --git a/crates/gpu/src/tests_fx_tone.rs b/crates/gpu/src/tests_fx_tone.rs new file mode 100644 index 000000000..61c977d18 --- /dev/null +++ b/crates/gpu/src/tests_fx_tone.rs @@ -0,0 +1,185 @@ +//! GPU colour-correction family effects vs the CPU effects (the oracle): direct on a buffer at +//! full and half resolution, as adjustment, and composited at 8 and 32 bpc. + +use effectcraft_color::Label; +use effectcraft_keyframe::{Keyframe, Value}; +use effectcraft_project::{BitDepth, Comp, ItemKind, LayerSource, build}; +use effectcraft_render::RenderOpts; +use effectcraft_time::{FrameRate, Tick}; + +use crate::tests::{Scene, c, check, compare_at, effect_case, n, opts, set}; + +fn e(v: u32) -> Value { + Value::Enum(v) +} + +fn on() -> Value { + Value::Bool(true) +} + +fn off() -> Value { + Value::Bool(false) +} + +#[test] +fn levels_and_gamma() { + effect_case( + "ec.color.levelsic", + &[("rgb/rgbInBlack", n(0.1)), ("rgb/rgbGamma", n(1.3)), ("red/redOutWhite", n(0.8)), ("blue/blueInWhite", n(0.7)), ("alpha/alphaGamma", n(0.7))], + ); + effect_case("ec.color.levelsic", &[("green/greenInBlack", n(0.2)), ("green/greenInWhite", n(0.6)), ("clipToOutputWhite", e(0))]); + effect_case("ec.color.levelsic", &[("alpha/alphaOutBlack", n(0.3))]); + effect_case("ec.color.levels", &[("redInBlack", n(0.1)), ("greenGamma", n(1.5)), ("blueOutWhite", n(0.7)), ("inWhite", n(0.9))]); + effect_case("ec.color.levels", &[("alphaInWhite", n(0.6)), ("redInWhite", n(0.5)), ("redClipToOutputWhite", e(0))]); + effect_case("ec.color.gammapedestalgain", &[("redGamma", n(1.4)), ("greenPedestal", n(0.1)), ("blueGain", n(0.8))]); + effect_case("ec.color.gammapedestalgain", &[("blackStretch", n(2.0)), ("redGain", n(1.2))]); +} + +#[test] +fn hue_saturation_ranges() { + effect_case("ec.color.huesaturation", &[("redsHue", n(40.0)), ("bluesSaturation", n(-60.0)), ("greensLightness", n(30.0))]); + effect_case( + "ec.color.huesaturation", + &[ + ("hue", n(20.0)), + ("saturation", n(15.0)), + ("yellowsSaturation", n(50.0)), + ("yellowsRangeStart", n(30.0)), + ("yellowsStartFalloff", n(8.0)), + ("cyansLightness", n(-40.0)), + ], + ); +} + +#[test] +fn colour_replacement() { + effect_case("ec.color.photofilter", &[]); + effect_case( + "ec.color.photofilter", + &[("filter", e(effectcraft_effects::PHOTO_FILTER_CUSTOM)), ("color", c(0.2, 0.7, 0.4)), ("density", n(60.0)), ("preserveLuminosity", off())], + ); + effect_case("ec.color.changetocolor", &[("from", c(0.8, 0.4, 0.3)), ("to", c(0.2, 0.5, 0.9)), ("toleranceGroup/hue", n(15.0))]); + effect_case( + "ec.color.changetocolor", + &[("from", c(0.6, 0.6, 0.3)), ("to", c(0.9, 0.2, 0.6)), ("change", e(3)), ("changeBy", e(1)), ("toleranceGroup/hue", n(25.0)), ("softness", n(30.0))], + ); + effect_case("ec.color.changetocolor", &[("from", c(0.6, 0.6, 0.3)), ("change", e(2)), ("toleranceGroup/hue", n(25.0)), ("viewCorrectionMatte", on())]); + effect_case("ec.color.leavecolor", &[("amount", n(80.0)), ("color", c(0.7, 0.5, 0.4)), ("tolerance", n(10.0)), ("edgeSoftness", n(20.0))]); + effect_case( + "ec.color.leavecolor", + &[("amount", n(100.0)), ("color", c(0.8, 0.4, 0.3)), ("matchColors", e(1)), ("tolerance", n(5.0)), ("edgeSoftness", n(30.0))], + ); + effect_case("ec.color.changecolor", &[("hueTransform", n(90.0)), ("colorToChange", c(0.7, 0.5, 0.4)), ("matchingSoftness", n(20.0))]); + effect_case( + "ec.color.changecolor", + &[ + ("lightnessTransform", n(-30.0)), + ("saturationTransform", n(40.0)), + ("matchColors", e(0)), + ("matchingTolerance", n(20.0)), + ("matchingSoftness", n(15.0)), + ], + ); + effect_case("ec.color.changecolor", &[("view", e(1)), ("matchColors", e(2)), ("matchingSoftness", n(25.0)), ("invertColorCorrectionMask", on())]); +} + +#[test] +fn broadcast_balance_limiter() { + effect_case("ec.color.broadcast", &[("maxSignal", n(95.0))]); + effect_case("ec.color.broadcast", &[("locale", e(1)), ("howToMakeSafe", e(1)), ("maxSignal", n(90.0))]); + effect_case("ec.color.colorbalancehls", &[("hue", n(40.0)), ("lightness", n(-20.0)), ("saturation", n(30.0))]); + effect_case("ec.color.colorbalancehls", &[("lightness", n(25.0)), ("saturation", n(-50.0))]); + effect_case("ec.color.videolimiter", &[("clipLevel", e(0))]); + effect_case("ec.color.videolimiter", &[("clipLevel", e(1)), ("clipMethod", e(1)), ("compressionBeforeClipping", e(4))]); + effect_case("ec.color.videolimiter", &[("clipLevel", e(0)), ("clipMethod", e(0)), ("gamutWarning", on())]); +} + +#[test] +fn cc_colour_and_arbitrary_map() { + effect_case("ec.color.cctoner", &[]); + effect_case("ec.color.cctoner", &[("tones", e(2)), ("blend", n(30.0))]); + effect_case("ec.color.cctoner", &[("tones", e(0)), ("shadows", c(0.1, 0.0, 0.3)), ("highlights", c(1.0, 0.9, 0.6))]); + effect_case("ec.color.cccoloroffset", &[("redPhase", n(90.0)), ("bluePhase", n(200.0))]); + effect_case("ec.color.cccoloroffset", &[("greenPhase", n(120.0)), ("overflow", e(1))]); + effect_case("ec.color.cckernel", &[("k1", n(-1.0)), ("k5", n(5.0)), ("k9", n(-1.0)), ("scale", n(0.5)), ("offset", n(0.05))]); + effect_case("ec.color.cckernel", &[("k2", n(1.0)), ("k5", n(0.0)), ("k8", n(-1.0)), ("absolute", on())]); + let map: String = (0..256).map(|i| format!("{}", ((i as f64 / 255.0).powf(0.6) * 255.0).round())).collect::>().join(","); + effect_case("ec.color.psarbitrarymap", &[("map", Value::Str(map.clone()))]); + effect_case("ec.color.psarbitrarymap", &[("map", Value::Str(map)), ("phase", n(40.0)), ("applyPhaseMapToAlpha", on())]); +} + +#[test] +fn automatic_corrections() { + effect_case("ec.color.autolevels", &[]); + effect_case("ec.color.autolevels", &[("blackClip", n(2.0)), ("whiteClip", n(3.0)), ("blend", n(20.0))]); + effect_case("ec.color.autocontrast", &[("blackClip", n(1.0))]); + effect_case("ec.color.autocolor", &[]); + effect_case("ec.color.autocolor", &[("snapNeutralMidtones", on()), ("whiteClip", n(2.0))]); + effect_case("ec.color.equalize", &[]); + effect_case("ec.color.equalize", &[("style", e(1)), ("amount", n(70.0))]); + effect_case("ec.color.equalize", &[("style", e(2))]); +} + +#[test] +fn shadow_highlight_neutralizer_invert() { + effect_case("ec.color.shadowhighlight", &[]); + effect_case( + "ec.color.shadowhighlight", + &[ + ("autoAmounts", off()), + ("shadowAmount", n(70.0)), + ("highlightAmount", n(40.0)), + ("moreOptions/shadowRadius", n(8.0)), + ("moreOptions/highlightRadius", n(15.0)), + ("moreOptions/midtoneContrast", n(30.0)), + ("moreOptions/blackClip", n(2.0)), + ("blend", n(20.0)), + ], + ); + effect_case("ec.color.shadowhighlight", &[("moreOptions/blackClip", n(0.0)), ("moreOptions/whiteClip", n(0.0)), ("moreOptions/colorCorrection", n(-40.0))]); + effect_case( + "ec.color.cccolorneutralizer", + &[("shadowsBalance", c(0.1, 0.0, 0.05)), ("midtonesUnbalance", c(0.6, 0.5, 0.4)), ("pinning", n(40.0)), ("contrast", n(20.0)), ("darks", n(-30.0))], + ); + effect_case("ec.color.cccolorneutralizer", &[("highlightsBalance", c(0.9, 1.0, 0.8)), ("brights", n(40.0)), ("blendWOriginal", n(25.0))]); + for ch in 4..=11 { + effect_case("ec.channel.invert", &[("channel", e(ch)), ("blend", n(20.0))]); + } +} + +/// Color Stabilizer reads its reference frame through the effect host: a precomp whose content +/// moves, at a nonzero time (the direct cases have no host and pass the layer through). +#[test] +fn color_stabilizer() { + effect_case("ec.color.colorstabilizer", &[]); + for depth in [BitDepth::Bpc8, BitDepth::Bpc32] { + for mode in [0, 1, 2] { + let mut s = Scene::new(depth); + let bg = s.solid([0.15, 0.1, 0.2], 97, 61); + s.push(bg); + let inner = Comp::new(60, 40, FrameRate::FPS_30, Tick::from_seconds_f64(2.0)); + let inner_id = s.p.add_item("Inner", Label::Sandstone, None, ItemKind::Comp(inner.into())); + let mut a = s.footage(50, 34); + let pos = a.props.prop_mut("transform/position").unwrap(); + pos.keys = vec![Keyframe::new(Tick::ZERO, crate::tests::v3(22.0, 18.0)), Keyframe::new(Tick::from_seconds_f64(1.0), crate::tests::v3(38.0, 24.0))]; + let op = a.props.prop_mut("transform/opacity").unwrap(); + op.keys = vec![Keyframe::new(Tick::ZERO, Value::Scalar(100.0)), Keyframe::new(Tick::from_seconds_f64(1.0), Value::Scalar(40.0))]; + s.p.comp_mut(inner_id).unwrap().layers = vec![a]; + let mut l = build::layer(&mut s.p, &s.comp, "Inner", LayerSource::Comp { item: inner_id }, (60, 40), None); + let vals = [ + ("stabilize", e(mode)), + ("blackPoint", Value::Vec2([18.0, 20.0])), + ("midPoint", Value::Vec2([30.0, 18.0])), + ("whitePoint", Value::Vec2([42.0, 22.0])), + ("sampleSize", n(4.0)), + ]; + s.effect(&mut l, "ec.color.colorstabilizer", &vals); + set(&mut l, "transform/rotation", Value::Scalar(8.0)); + s.push(l); + let t = Tick::from_seconds_f64(0.5); + let label = format!("color stabilizer {mode} {depth:?}"); + check(&label, compare_at(&s, opts(), t), 0.0); + check(&format!("{label} half"), compare_at(&s, RenderOpts { scale: 0.5, ..opts() }, t), 0.0); + } + } +} diff --git a/crates/hevcenc/Cargo.toml b/crates/hevcenc/Cargo.toml new file mode 100644 index 000000000..136611ff2 --- /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 000000000..5c4e5debe --- /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 000000000..27ed3063e --- /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 000000000..220b999cd --- /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 000000000..6f5d17b87 --- /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 000000000..2cbeb4f3d --- /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 000000000..e363871b5 --- /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 000000000..f1fe9e4cb --- /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 000000000..3a5b9b479 --- /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 000000000..5f4cb2ce3 --- /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 000000000..f1b89f731 --- /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 000000000..7c2fc4470 --- /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 000000000..cffea75c2 --- /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 000000000..5d9c147ea --- /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 000000000..20d7c319c --- /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 000000000..e69de29bb 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 000000000..e69de29bb 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 000000000..e69de29bb 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 000000000..427290bff --- /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 000000000..a72a5bb42 --- /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 000000000..ec3cb8bfd 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 000000000..e00328da5 --- /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 000000000..ec3cb8bfd 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 000000000..e00328da5 --- /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 000000000..9162efb38 --- /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 000000000..0e08d299b --- /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 000000000..dc01c242e --- /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 000000000..b86b2c74e --- /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 000000000..891735595 --- /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 000000000..aed09422b --- /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 000000000..ec3cb8bfd 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 000000000..e00328da5 --- /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 000000000..3a26abadd --- /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 000000000..714a1d1e2 --- /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 000000000..ec3cb8bfd 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 000000000..e00328da5 --- /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 000000000..7cf5e27ba --- /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 000000000..c133fb7ac --- /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 000000000..1790b2249 --- /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 000000000..69147ada1 --- /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 000000000..ec3cb8bfd 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 000000000..e00328da5 --- /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 000000000..ec3cb8bfd 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 000000000..e00328da5 --- /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 000000000..e425ee190 --- /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 000000000..bc49c4059 --- /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 000000000..c31310a1b --- /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 000000000..2b02b78b7 --- /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 000000000..73f42b52f 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 000000000..e00328da5 --- /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 000000000..5f22e76e5 --- /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 000000000..e2674a325 --- /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 000000000..13bf633db 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 000000000..e00328da5 --- /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 000000000..14901e191 --- /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 000000000..461b185a9 --- /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 000000000..ec3cb8bfd 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 000000000..e00328da5 --- /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 000000000..464bb7994 --- /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 000000000..51e6f5f4d --- /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 000000000..ec3cb8bfd 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 000000000..e00328da5 --- /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 000000000..51b08d802 --- /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 000000000..8f4313648 --- /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 000000000..ec3cb8bfd 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 000000000..e00328da5 --- /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 000000000..05871b3fe --- /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 000000000..75da585b0 --- /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 000000000..ec3cb8bfd 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 000000000..e00328da5 --- /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 000000000..7f0dfbf3f --- /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 000000000..269e2805f --- /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 000000000..6a76c0403 --- /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 000000000..01850cf9d --- /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 000000000..20c7065d3 --- /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 000000000..0a8e349cd --- /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 000000000..ec3cb8bfd 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 000000000..e00328da5 --- /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 000000000..ec3cb8bfd 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 000000000..e00328da5 --- /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 000000000..b4b6ba8b4 --- /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 000000000..631fa7660 --- /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 000000000..01fcc6df2 --- /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 000000000..4e1f49266 --- /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 000000000..ec3cb8bfd 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 000000000..e00328da5 --- /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 000000000..33392f0e0 --- /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 000000000..2c5faf033 --- /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 000000000..ec3cb8bfd 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 000000000..e00328da5 --- /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 000000000..5108ffda1 --- /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 000000000..dd274fe62 --- /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 000000000..60eca5301 --- /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 000000000..ae4bfa9a2 --- /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 000000000..4958ae8bf --- /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 000000000..c2e1f0f86 --- /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 000000000..ec3cb8bfd 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 000000000..e00328da5 --- /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 000000000..ec3cb8bfd 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 000000000..e00328da5 --- /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 000000000..c20ab603c --- /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 000000000..33f3e03a2 --- /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 000000000..ec3cb8bfd 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 000000000..e00328da5 --- /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 000000000..fe54c7c42 --- /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 000000000..6c15c911b --- /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 000000000..e6c51cac9 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 000000000..e00328da5 --- /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 000000000..a75b31b5b --- /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 000000000..02c09d11c --- /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 000000000..b5966d710 --- /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 000000000..366699bc6 --- /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 000000000..ec3cb8bfd 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 000000000..e00328da5 --- /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 000000000..47d0a4fa9 --- /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 000000000..28a46706e --- /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 000000000..ec3cb8bfd 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 000000000..e00328da5 --- /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 000000000..c9a733129 --- /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 000000000..d013eade8 --- /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 000000000..ec3cb8bfd 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 000000000..e00328da5 --- /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 000000000..aece5c19d --- /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 000000000..08cf61b24 --- /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 000000000..eb48decde 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 000000000..eb48decde 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 000000000..01540bc15 --- /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 000000000..e00328da5 --- /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 000000000..57fc28546 --- /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 000000000..91782756f --- /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 000000000..e69de29bb 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 000000000..986aecc0a 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 000000000..986aecc0a 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 000000000..e2181c8f5 --- /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 000000000..e00328da5 --- /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 000000000..57fc28546 --- /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 000000000..bb48420d5 --- /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 000000000..e69de29bb 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 000000000..c1b1362d5 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 000000000..c1b1362d5 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 000000000..55e492807 --- /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 000000000..e00328da5 --- /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 000000000..d0bad9fd3 --- /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 000000000..27a892392 --- /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 000000000..e69de29bb 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 000000000..e00328da5 --- /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 000000000..d3d235a54 --- /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 000000000..545de2684 --- /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 000000000..e69de29bb 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 000000000..0b58654c8 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 000000000..0b58654c8 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 000000000..2a84a7399 --- /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 000000000..8ff963107 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 000000000..8ff963107 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 000000000..5e8a8fd81 --- /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 000000000..e00328da5 --- /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 000000000..6d81eca24 --- /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 000000000..323737b36 --- /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 000000000..e69de29bb 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 000000000..e00328da5 --- /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 000000000..d15ba9ab7 --- /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 000000000..4de1f8ae2 --- /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 000000000..e69de29bb 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 000000000..73a8ef6f9 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 000000000..73a8ef6f9 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 000000000..ff779b97c --- /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 000000000..3ff8ed41d 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 000000000..3ff8ed41d 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 000000000..3171e69ce --- /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 000000000..e00328da5 --- /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 000000000..036d1e647 --- /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 000000000..f62a8d10f --- /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 000000000..374068c0c --- /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 000000000..e69de29bb 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 000000000..7b2610621 --- /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 000000000..93d1b1390 --- /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 000000000..2574a6127 --- /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 000000000..f618c9caa --- /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 000000000..20801a0d5 --- /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 000000000..63971aefe --- /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 000000000..e6fcfb029 --- /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 000000000..0b2cd1e6b 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 000000000..9f91813c7 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 000000000..1deb140e7 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 000000000..f969e16e0 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 000000000..f1da5d813 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 000000000..ca4238600 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 000000000..5e5e2f50f 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 000000000..03d053ffc 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 000000000..c8e72a48b 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 000000000..6430cb842 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 000000000..495842e6d 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 000000000..3185389e6 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 000000000..cb28bbc21 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 000000000..39c9f55ae 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 000000000..01bf22102 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 000000000..adb9c9606 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 000000000..247a152c3 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 000000000..91bcca08a 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 000000000..a84d8dc5d 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 000000000..3775073c7 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 000000000..236051225 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 000000000..a98615671 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 000000000..50a6d6401 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 000000000..082563d99 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 000000000..68125455e 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 000000000..35caf1b2d 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 000000000..ccb90c50f 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 000000000..386642d76 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 000000000..e816378f0 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 000000000..ddbe1a4f7 --- /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 000000000..abc8bb757 --- /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 000000000..899eea5c0 --- /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 000000000..dbdcd0aed --- /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 000000000..93b321a33 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 000000000..f0407a531 --- /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 000000000..f4d68481d --- /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 000000000..a8d25a5c9 --- /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 000000000..781aff084 --- /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 000000000..b07086d81 --- /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 000000000..777bc455b --- /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/opusenc/Cargo.toml b/crates/opusenc/Cargo.toml index 90b0fd026..489c648ea 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 0c76d49f4..999d8a7e9 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 1e7a2cec8..04963abfe 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 000000000..9045cb86d --- /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 000000000..de510433e --- /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 000000000..7846d2973 --- /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 000000000..c7a4b8692 --- /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 000000000..3f1f17ac6 --- /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 000000000..f8cda15a9 --- /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 000000000..8b4727268 --- /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 000000000..9b420f31b --- /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 000000000..672426cc5 --- /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 000000000..843cf3d9f --- /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/project/src/render_queue.rs b/crates/project/src/render_queue.rs index 35aed0bed..c3a5503e6 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 003071e1d..de6ff8df1 100644 --- a/crates/render/src/lib.rs +++ b/crates/render/src/lib.rs @@ -320,7 +320,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, diff --git a/crates/track/src/roto/mod.rs b/crates/track/src/roto/mod.rs index 393f743e0..7103624be 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 bec3f3783..15bd1f68b 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/src/control.rs b/crates/ui-egui/src/control.rs index ff3db561c..3e6bdd10f 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 1fb2ef790..590843d7c 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 61fa5ad63..1f42df2e2 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 fd6283514..880d19285 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 { @@ -465,3 +617,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 5f1d59806..1380553a1 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 02dcb1a9b..b5160410d 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/fx_editors.rs b/crates/ui-egui/src/panels/fx_editors.rs new file mode 100644 index 000000000..e47d0a8df --- /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 4f6fd0ae3..8cc14dccb 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 7b750be7a..6c9bac94d 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 8ebd6ad30..d43b4e793 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 ec0853a1b..cf6c0c710 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/state.rs b/crates/ui-egui/src/state.rs index 4b657f527..574345262 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 9703d6ba6..184bf9ec3 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 000000000..331df8790 --- /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_web_offload.rs b/crates/ui-egui/tests/ui_web_offload.rs new file mode 100644 index 000000000..54ae1a373 --- /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 c15a6cda3..f45aaa3c4 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)) | @@ -174,10 +176,15 @@ against the caster planes (Shadow Diffusion, Light Transmission) and blending. A 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 -effects (`effects::GPU_EFFECTS`, 58 of them: blurs, colour, keying incl. Key Light, distortion, -transitions, generators, noise and grain; see [effects.md](effects.md)) repeat the CPU effect's -steps (padding, box radii, parameters, hashes) as kernels; consecutive GPU effects run as one -chain with one upload and one readback. Tests render scenes on both paths and compare them +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) +as kernels, in one module per family (`gpu::fx_*` with `shaders/fx_*.wgsl`); consecutive GPU +effects run as one chain with one upload and one readback. Statistics that need the whole frame +(Auto Levels / Contrast / Color, Equalize, Shadow/Highlight, Color Stabilizer, Remove Grain's +noise level) are measured on the CPU from one readback and applied on the GPU; effects reading +other frames (Echo, Posterize Time) upload the frames the host renders. Settings a kernel cannot +match render on the CPU (`effects::catalog::gpu_supported`). Tests render scenes on both paths and compare them (≤ 1/255 at 8 bpc, ≤ 1e-3 at 32 bpc); they skip without an adapter. The desktop viewer builds the `Gpu` on egui-wgpu's device and shows frames from GPU textures without reading them back (`ui-egui::frames`); headless renders, the CLI (unless `--gpu`) and CI use the CPU. On the web diff --git a/docs/effects.md b/docs/effects.md index e35ae5f6f..32eabfa15 100644 --- a/docs/effects.md +++ b/docs/effects.md @@ -5,7 +5,7 @@ EffectCraft ships 306 effects with 3009 parameters in total, grouped into the same categories as the Effects & Presets panel. Every effect is our own implementation, written from public behaviour descriptions and standard image-processing literature. Parameter names, order, popup options, units, defaults and ranges follow the reference application so that projects, expressions and muscle memory carry over. -- **GPU**: 59 effects also run on the GPU compositor with identical results. +- **GPU**: 152 effects also run on the GPU compositor with identical results. - **32**: 306 effects process 32-bit float (HDR, overbright) pixels without clamping. - **Status**: 306 implemented in full, 0 partial (what is missing is listed). @@ -46,85 +46,85 @@ Effects are addressed by id (`ec..`) or display name in commands | Effect | Id | Params | GPU | 32 | Status | |---|---|---:|:-:|:-:|---| -| Bilateral Blur | `ec.blur.bilateral` | 3 | | 32 | Implemented | +| Bilateral Blur | `ec.blur.bilateral` | 3 | GPU | 32 | Implemented | | CC Cross Blur | `ec.blur.cccross` | 3 | | 32 | Implemented | | CC Radial Blur | `ec.blur.ccradial` | 4 | | 32 | Implemented | | CC Radial Fast Blur | `ec.blur.ccradialfast` | 3 | GPU | 32 | Implemented | | CC Vector Blur | `ec.blur.ccvector` | 4 | | 32 | Implemented | | Camera Lens Blur | `ec.blur.cameralens` | 16 | GPU | 32 | Implemented | | Camera-Shake Deblur | `ec.blur.camerashakedeblur` | 8 | | 32 | Implemented | -| Channel Blur | `ec.blur.channel` | 6 | | 32 | Implemented | -| Compound Blur | `ec.blur.compound` | 4 | | 32 | Implemented | +| Channel Blur | `ec.blur.channel` | 6 | GPU | 32 | Implemented | +| Compound Blur | `ec.blur.compound` | 4 | GPU | 32 | Implemented | | Directional Blur | `ec.blur.directional` | 2 | GPU | 32 | Implemented | | Fast Box Blur | `ec.blur.fastbox` | 4 | GPU | 32 | Implemented | | Gaussian Blur | `ec.blur.gaussian` | 3 | GPU | 32 | Implemented | | Radial Blur | `ec.blur.radial` | 4 | GPU | 32 | Implemented | -| Sharpen | `ec.blur.sharpen` | 1 | | 32 | Implemented | -| Smart Blur | `ec.blur.smart` | 3 | | 32 | Implemented | -| Unsharp Mask | `ec.blur.unsharp` | 3 | | 32 | Implemented | +| Sharpen | `ec.blur.sharpen` | 1 | GPU | 32 | Implemented | +| Smart Blur | `ec.blur.smart` | 3 | GPU | 32 | Implemented | +| Unsharp Mask | `ec.blur.unsharp` | 3 | GPU | 32 | Implemented | ## Channel | Effect | Id | Params | GPU | 32 | Status | |---|---|---:|:-:|:-:|---| -| Arithmetic | `ec.channel.arithmetic` | 5 | | 32 | Implemented | -| Blend | `ec.channel.blend` | 4 | | 32 | Implemented | +| Arithmetic | `ec.channel.arithmetic` | 5 | GPU | 32 | Implemented | +| Blend | `ec.channel.blend` | 4 | GPU | 32 | Implemented | | CC Composite | `ec.channel.cccomposite` | 3 | | 32 | Implemented | -| Calculations | `ec.channel.calculations` | 9 | | 32 | Implemented | -| Channel Combiner | `ec.channel.combiner` | 6 | | 32 | Implemented | -| Compound Arithmetic | `ec.channel.compoundarithmetic` | 6 | | 32 | Implemented | +| Calculations | `ec.channel.calculations` | 9 | GPU | 32 | Implemented | +| Channel Combiner | `ec.channel.combiner` | 6 | GPU | 32 | Implemented | +| Compound Arithmetic | `ec.channel.compoundarithmetic` | 6 | GPU | 32 | Implemented | | Invert | `ec.channel.invert` | 2 | GPU | 32 | Implemented | -| Minimax | `ec.channel.minimax` | 5 | | 32 | Implemented | -| Remove Color Matting | `ec.channel.removecolormatting` | 2 | | 32 | Implemented | -| Set Channels | `ec.channel.setchannels` | 9 | | 32 | Implemented | -| Set Matte | `ec.channel.setmatte` | 6 | | 32 | Implemented | -| Shift Channels | `ec.channel.shiftchannels` | 4 | | 32 | Implemented | -| Solid Composite | `ec.channel.solidcomposite` | 4 | | 32 | Implemented | +| Minimax | `ec.channel.minimax` | 5 | GPU | 32 | Implemented | +| Remove Color Matting | `ec.channel.removecolormatting` | 2 | GPU | 32 | Implemented | +| Set Channels | `ec.channel.setchannels` | 9 | GPU | 32 | Implemented | +| Set Matte | `ec.channel.setmatte` | 6 | GPU | 32 | Implemented | +| Shift Channels | `ec.channel.shiftchannels` | 4 | GPU | 32 | Implemented | +| Solid Composite | `ec.channel.solidcomposite` | 4 | GPU | 32 | Implemented | ## Color Correction | Effect | Id | Params | GPU | 32 | Status | |---|---|---:|:-:|:-:|---| -| Auto Color | `ec.color.autocolor` | 6 | | 32 | Implemented | -| Auto Contrast | `ec.color.autocontrast` | 5 | | 32 | Implemented | -| Auto Levels | `ec.color.autolevels` | 5 | | 32 | Implemented | +| Auto Color | `ec.color.autocolor` | 6 | GPU | 32 | Implemented | +| Auto Contrast | `ec.color.autocontrast` | 5 | GPU | 32 | Implemented | +| Auto Levels | `ec.color.autolevels` | 5 | GPU | 32 | Implemented | | Black & White | `ec.color.blackwhite` | 8 | GPU | 32 | Implemented | | Brightness & Contrast | `ec.color.brightnesscontrast` | 3 | GPU | 32 | Implemented | -| Broadcast Colors | `ec.color.broadcast` | 3 | | 32 | Implemented | -| CC Color Neutralizer | `ec.color.cccolorneutralizer` | 11 | | 32 | Implemented | -| CC Color Offset | `ec.color.cccoloroffset` | 4 | | 32 | Implemented | -| CC Kernel | `ec.color.cckernel` | 12 | | 32 | Implemented | -| CC Toner | `ec.color.cctoner` | 7 | | 32 | Implemented | -| Change Color | `ec.color.changecolor` | 9 | | 32 | Implemented | -| Change to Color | `ec.color.changetocolor` | 9 | | 32 | Implemented | +| Broadcast Colors | `ec.color.broadcast` | 3 | GPU | 32 | Implemented | +| CC Color Neutralizer | `ec.color.cccolorneutralizer` | 11 | GPU | 32 | Implemented | +| CC Color Offset | `ec.color.cccoloroffset` | 4 | GPU | 32 | Implemented | +| CC Kernel | `ec.color.cckernel` | 12 | GPU | 32 | Implemented | +| CC Toner | `ec.color.cctoner` | 7 | GPU | 32 | Implemented | +| Change Color | `ec.color.changecolor` | 9 | GPU | 32 | Implemented | +| Change to Color | `ec.color.changetocolor` | 9 | GPU | 32 | Implemented | | Channel Mixer | `ec.color.channelmixer` | 13 | GPU | 32 | Implemented | | Color Balance | `ec.color.colorbalance` | 10 | GPU | 32 | Implemented | -| Color Balance (HLS) | `ec.color.colorbalancehls` | 3 | | 32 | Implemented | +| Color Balance (HLS) | `ec.color.colorbalancehls` | 3 | GPU | 32 | Implemented | | Color Link | `ec.color.colorlink` | 6 | | 32 | Implemented | -| Color Stabilizer | `ec.color.colorstabilizer` | 6 | | 32 | Implemented | +| Color Stabilizer | `ec.color.colorstabilizer` | 6 | GPU | 32 | Implemented | | Colorama | `ec.color.colorama` | 20 | GPU | 32 | Implemented | | Curves | `ec.color.curves` | 0 | GPU | 32 | Implemented | -| Equalize | `ec.color.equalize` | 2 | | 32 | Implemented | +| Equalize | `ec.color.equalize` | 2 | GPU | 32 | Implemented | | Exposure | `ec.color.exposure` | 14 | GPU | 32 | Implemented | -| Gamma/Pedestal/Gain | `ec.color.gammapedestalgain` | 10 | | 32 | Implemented | +| Gamma/Pedestal/Gain | `ec.color.gammapedestalgain` | 10 | GPU | 32 | Implemented | | Hue/Saturation | `ec.color.huesaturation` | 50 | GPU | 32 | Implemented | -| Leave Color | `ec.color.leavecolor` | 5 | | 32 | Implemented | +| Leave Color | `ec.color.leavecolor` | 5 | GPU | 32 | Implemented | | Levels | `ec.color.levels` | 36 | GPU | 32 | Implemented | -| Levels (Individual Controls) | `ec.color.levelsic` | 27 | | 32 | Implemented | +| Levels (Individual Controls) | `ec.color.levelsic` | 27 | GPU | 32 | Implemented | | Lumetri Color | `ec.color.lumetri` | 66 | GPU | 32 | Implemented | | OCIO CDL Transform | `ec.color.ociocdl` | 12 | | 32 | Implemented | | OCIO Color Space Transform | `ec.color.ociocolorspace` | 7 | | 32 | Implemented | | OCIO Display Transform | `ec.color.ociodisplay` | 9 | | 32 | Implemented | | OCIO File Transform | `ec.color.ociofile` | 4 | | 32 | Implemented | | OCIO Look Transform | `ec.color.ociolook` | 6 | | 32 | Implemented | -| PS Arbitrary Map | `ec.color.psarbitrarymap` | 2 | | 32 | Implemented | -| Photo Filter | `ec.color.photofilter` | 4 | | 32 | Implemented | +| PS Arbitrary Map | `ec.color.psarbitrarymap` | 2 | GPU | 32 | Implemented | +| Photo Filter | `ec.color.photofilter` | 4 | GPU | 32 | Implemented | | Selective Color | `ec.color.selectivecolor` | 38 | GPU | 32 | Implemented | -| Shadow/Highlight | `ec.color.shadowhighlight` | 14 | | 32 | Implemented | +| Shadow/Highlight | `ec.color.shadowhighlight` | 14 | GPU | 32 | Implemented | | Tint | `ec.color.tint` | 3 | GPU | 32 | Implemented | | Tritone | `ec.color.tritone` | 4 | GPU | 32 | Implemented | | Vibrance | `ec.color.vibrance` | 2 | GPU | 32 | Implemented | -| Video Limiter | `ec.color.videolimiter` | 5 | | 32 | Implemented | +| Video Limiter | `ec.color.videolimiter` | 5 | GPU | 32 | Implemented | ## Distort @@ -135,37 +135,37 @@ Effects are addressed by id (`ec..`) or display name in commands | CC Bend It | `ec.distort.ccbendit` | 4 | | 32 | Implemented | | CC Bender | `ec.distort.ccbender` | 5 | | 32 | Implemented | | CC Blobbylize | `ec.distort.ccblobbylize` | 15 | | 32 | Implemented | -| CC Flo Motion | `ec.distort.ccflomotion` | 7 | | 32 | Implemented | -| CC Griddler | `ec.distort.ccgriddler` | 5 | | 32 | Implemented | +| CC Flo Motion | `ec.distort.ccflomotion` | 7 | GPU | 32 | Implemented | +| CC Griddler | `ec.distort.ccgriddler` | 5 | GPU | 32 | Implemented | | CC Lens | `ec.distort.cclens` | 3 | GPU | 32 | Implemented | | CC Page Turn | `ec.distort.ccpageturn` | 7 | | 32 | Implemented | -| CC Power Pin | `ec.distort.ccpowerpin` | 10 | | 32 | Implemented | -| CC Ripple Pulse | `ec.distort.ccripplepulse` | 5 | | 32 | Implemented | -| CC Slant | `ec.distort.ccslant` | 4 | | 32 | Implemented | -| CC Smear | `ec.distort.ccsmear` | 4 | | 32 | Implemented | -| CC Split | `ec.distort.ccsplit` | 3 | | 32 | Implemented | -| CC Split 2 | `ec.distort.ccsplit2` | 4 | | 32 | Implemented | -| CC Tiler | `ec.distort.cctiler` | 3 | | 32 | Implemented | -| Corner Pin | `ec.distort.cornerpin` | 4 | | 32 | Implemented | +| CC Power Pin | `ec.distort.ccpowerpin` | 10 | GPU | 32 | Implemented | +| CC Ripple Pulse | `ec.distort.ccripplepulse` | 5 | GPU | 32 | Implemented | +| CC Slant | `ec.distort.ccslant` | 4 | GPU | 32 | Implemented | +| CC Smear | `ec.distort.ccsmear` | 4 | GPU | 32 | Implemented | +| CC Split | `ec.distort.ccsplit` | 3 | GPU | 32 | Implemented | +| CC Split 2 | `ec.distort.ccsplit2` | 4 | GPU | 32 | Implemented | +| CC Tiler | `ec.distort.cctiler` | 3 | GPU | 32 | Implemented | +| Corner Pin | `ec.distort.cornerpin` | 4 | GPU | 32 | Implemented | | Detail-preserving Upscale | `ec.distort.upscale` | 4 | | 32 | Implemented | | Displacement Map | `ec.distort.displacementmap` | 8 | GPU | 32 | Implemented | -| Liquify | `ec.distort.liquify` | 12 | | 32 | Implemented | -| Magnify | `ec.distort.magnify` | 10 | | 32 | Implemented | +| Liquify | `ec.distort.liquify` | 12 | GPU | 32 | Implemented | +| Magnify | `ec.distort.magnify` | 10 | GPU | 32 | Implemented | | Mesh Warp | `ec.distort.meshwarp` | 3 | GPU | 32 | Implemented | -| Mirror | `ec.distort.mirror` | 2 | | 32 | Implemented | -| Offset | `ec.distort.offset` | 2 | | 32 | Implemented | -| Optics Compensation | `ec.distort.opticscompensation` | 6 | | 32 | Implemented | -| Polar Coordinates | `ec.distort.polar` | 2 | | 32 | Implemented | +| Mirror | `ec.distort.mirror` | 2 | GPU | 32 | Implemented | +| Offset | `ec.distort.offset` | 2 | GPU | 32 | Implemented | +| Optics Compensation | `ec.distort.opticscompensation` | 6 | GPU | 32 | Implemented | +| Polar Coordinates | `ec.distort.polar` | 2 | GPU | 32 | Implemented | | Puppet | `ec.distort.puppet` | 1 | | 32 | Implemented | | Reshape | `ec.distort.reshape` | 6 | | 32 | Implemented | | Ripple | `ec.distort.ripple` | 7 | GPU | 32 | Implemented | | Rolling Shutter Repair | `ec.distort.rollingshutterrepair` | 5 | | 32 | Implemented | | Smear | `ec.distort.smear` | 8 | | 32 | Implemented | -| Spherize | `ec.distort.spherize` | 2 | | 32 | Implemented | +| Spherize | `ec.distort.spherize` | 2 | GPU | 32 | Implemented | | Transform | `ec.distort.transform` | 12 | GPU | 32 | Implemented | | Turbulent Displace | `ec.distort.turbulentdisplace` | 11 | GPU | 32 | Implemented | | Twirl | `ec.distort.twirl` | 3 | GPU | 32 | Implemented | -| Twirl (Legacy) | `ec.distort.twirllegacy` | 3 | | 32 | Implemented | +| Twirl (Legacy) | `ec.distort.twirllegacy` | 3 | GPU | 32 | Implemented | | Warp | `ec.distort.warp` | 5 | | 32 | Implemented | | Warp Stabilizer | `ec.distort.warpstabilizer` | 20 | | 32 | Implemented | | Wave Warp | `ec.distort.wavewarp` | 7 | GPU | 32 | Implemented | @@ -236,29 +236,29 @@ Effects are addressed by id (`ec..`) or display name in commands | Effect | Id | Params | GPU | 32 | Status | |---|---|---:|:-:|:-:|---| -| Advanced Spill Suppressor | `ec.key.advancedspill` | 8 | | 32 | Implemented | +| Advanced Spill Suppressor | `ec.key.advancedspill` | 8 | GPU | 32 | Implemented | | CC Simple Wire Removal | `ec.key.ccsimplewireremoval` | 7 | | 32 | Implemented | -| Color Difference Key | `ec.key.colordifference` | 16 | | 32 | Implemented | -| Color Range | `ec.key.colorrange` | 8 | | 32 | Implemented | -| Difference Matte | `ec.key.differencematte` | 6 | | 32 | Implemented | -| Extract | `ec.key.extract` | 6 | | 32 | Implemented | +| Color Difference Key | `ec.key.colordifference` | 16 | GPU | 32 | Implemented | +| Color Range | `ec.key.colorrange` | 8 | GPU | 32 | Implemented | +| Difference Matte | `ec.key.differencematte` | 6 | GPU | 32 | Implemented | +| Extract | `ec.key.extract` | 6 | GPU | 32 | Implemented | | Inner/Outer Key | `ec.key.innerouter` | 88 | | 32 | Implemented | -| Key Cleaner | `ec.key.keycleaner` | 4 | | 32 | Implemented | +| Key Cleaner | `ec.key.keycleaner` | 4 | GPU | 32 | Implemented | | Key Light | `ec.keying.keylight` | 56 | GPU | 32 | Implemented | | Linear Color Key | `ec.key.linearcolor` | 6 | GPU | 32 | Implemented | -| Screen Key | `ec.key.screen` | 11 | | 32 | Implemented | -| Unmult | `ec.key.unmult` | 6 | | 32 | Implemented | +| Screen Key | `ec.key.screen` | 11 | GPU | 32 | Implemented | +| Unmult | `ec.key.unmult` | 6 | GPU | 32 | Implemented | ## Matte | Effect | Id | Params | GPU | 32 | Status | |---|---|---:|:-:|:-:|---| -| Matte Choker | `ec.matte.mattechoker` | 7 | | 32 | Implemented | +| Matte Choker | `ec.matte.mattechoker` | 7 | GPU | 32 | Implemented | | Mocha shape | `ec.obsolete.mochashape` | 6 | | 32 | Implemented | -| Refine Hard Matte | `ec.matte.refinehard` | 16 | | 32 | Implemented | -| Refine Soft Matte | `ec.matte.refinesoft` | 18 | | 32 | Implemented | +| Refine Hard Matte | `ec.matte.refinehard` | 16 | GPU | 32 | Implemented | +| Refine Soft Matte | `ec.matte.refinesoft` | 18 | GPU | 32 | Implemented | | Roto Brush & Refine Edge | `ec.matte.rotobrush` | 24 | | 32 | Implemented | -| Simple Choker | `ec.matte.simplechoker` | 2 | | 32 | Implemented | +| Simple Choker | `ec.matte.simplechoker` | 2 | GPU | 32 | Implemented | ## Noise & Grain @@ -266,17 +266,17 @@ Effects are addressed by id (`ec..`) or display name in commands |---|---|---:|:-:|:-:|---| | Add Grain | `ec.noise.addgrain` | 32 | GPU | 32 | Implemented | | Curl Noise | `ec.noise.curlnoise` | 10 | | 32 | Implemented | -| Dust & Scratches | `ec.noise.dustscratches` | 3 | | 32 | Implemented | +| Dust & Scratches | `ec.noise.dustscratches` | 3 | GPU | 32 | Implemented | | Fractal Noise | `ec.noise.fractal` | 26 | GPU | 32 | Implemented | | Match Grain | `ec.noise.matchgrain` | 37 | | 32 | Implemented | -| Median | `ec.noise.median` | 2 | | 32 | Implemented | -| Median (Legacy) | `ec.noise.medianlegacy` | 2 | | 32 | Implemented | +| Median | `ec.noise.median` | 2 | GPU | 32 | Implemented | +| Median (Legacy) | `ec.noise.medianlegacy` | 2 | GPU | 32 | Implemented | | Noise | `ec.noise.noise` | 3 | GPU | 32 | Implemented | -| Noise Alpha | `ec.noise.noisealpha` | 8 | | 32 | Implemented | -| Noise HLS | `ec.noise.noisehls` | 6 | | 32 | Implemented | -| Noise HLS Auto | `ec.noise.noisehlsauto` | 6 | | 32 | Implemented | -| Remove Grain | `ec.noise.removegrain` | 20 | | 32 | Implemented | -| Turbulent Noise | `ec.noise.turbulent` | 22 | | 32 | Implemented | +| Noise Alpha | `ec.noise.noisealpha` | 8 | GPU | 32 | Implemented | +| Noise HLS | `ec.noise.noisehls` | 6 | GPU | 32 | Implemented | +| Noise HLS Auto | `ec.noise.noisehlsauto` | 6 | GPU | 32 | Implemented | +| Remove Grain | `ec.noise.removegrain` | 20 | GPU | 32 | Implemented | +| Turbulent Noise | `ec.noise.turbulent` | 22 | GPU | 32 | Implemented | ## Obsolete @@ -284,13 +284,13 @@ Effects are addressed by id (`ec..`) or display name in commands |---|---|---:|:-:|:-:|---| | Basic 3D | `ec.obsolete.basic3d` | 5 | | 32 | Implemented | | Basic Text | `ec.obsolete.basictext` | 11 | | 32 | Implemented | -| Color Key | `ec.key.colorkey` | 4 | | 32 | Implemented | +| Color Key | `ec.key.colorkey` | 4 | GPU | 32 | Implemented | | Gaussian Blur (Legacy) | `ec.obsolete.gaussianlegacy` | 2 | | 32 | Implemented | | Lightning | `ec.obsolete.lightning` | 25 | | 32 | Implemented | -| Luma Key | `ec.key.luma` | 5 | | 32 | Implemented | +| Luma Key | `ec.key.luma` | 5 | GPU | 32 | Implemented | | Path Text | `ec.obsolete.pathtext` | 32 | | 32 | Implemented | | Reduce Interlace Flicker | `ec.blur.reduceflicker` | 1 | | 32 | Implemented | -| Spill Suppressor | `ec.key.spill` | 3 | | 32 | Implemented | +| Spill Suppressor | `ec.key.spill` | 3 | GPU | 32 | Implemented | ## Paint @@ -340,31 +340,31 @@ Effects are addressed by id (`ec..`) or display name in commands | Effect | Id | Params | GPU | 32 | Status | |---|---|---:|:-:|:-:|---| -| Brush Strokes | `ec.stylize.brushstrokes` | 7 | | 32 | Implemented | +| Brush Strokes | `ec.stylize.brushstrokes` | 7 | GPU | 32 | Implemented | | CC Block Load | `ec.stylize.ccblockload` | 5 | | 32 | Implemented | | CC Burn Film | `ec.stylize.ccburnfilm` | 3 | | 32 | Implemented | | CC Glass | `ec.stylize.ccglass` | 17 | | 32 | Implemented | | CC HexTile | `ec.stylize.cchextile` | 5 | | 32 | Implemented | -| CC Kaleida | `ec.stylize.cckaleida` | 4 | | 32 | Implemented | +| CC Kaleida | `ec.stylize.cckaleida` | 4 | GPU | 32 | Implemented | | CC Mr. Smoothie | `ec.stylize.ccmrsmoothie` | 7 | | 32 | Implemented | | CC Plastic | `ec.stylize.ccplastic` | 18 | | 32 | Implemented | -| CC RepeTile | `ec.stylize.ccrepetile` | 5 | | 32 | Implemented | -| CC Threshold | `ec.stylize.ccthreshold` | 4 | | 32 | Implemented | -| CC Threshold RGB | `ec.stylize.ccthresholdrgb` | 7 | | 32 | Implemented | -| CC Vignette | `ec.stylize.ccvignette` | 4 | | 32 | Implemented | +| CC RepeTile | `ec.stylize.ccrepetile` | 5 | GPU | 32 | Implemented | +| CC Threshold | `ec.stylize.ccthreshold` | 4 | GPU | 32 | Implemented | +| CC Threshold RGB | `ec.stylize.ccthresholdrgb` | 7 | GPU | 32 | Implemented | +| CC Vignette | `ec.stylize.ccvignette` | 4 | GPU | 32 | Implemented | | Cartoon | `ec.stylize.cartoon` | 12 | | 32 | Implemented | | Color Emboss | `ec.stylize.coloremboss` | 4 | | 32 | Implemented | | Emboss | `ec.stylize.emboss` | 4 | GPU | 32 | Implemented | | Find Edges | `ec.stylize.findedges` | 2 | GPU | 32 | Implemented | | Glow | `ec.stylize.glow` | 14 | GPU | 32 | Implemented | | Mosaic | `ec.stylize.mosaic` | 3 | GPU | 32 | Implemented | -| Motion Tile | `ec.stylize.motiontile` | 8 | | 32 | Implemented | -| Posterize | `ec.stylize.posterize` | 1 | | 32 | Implemented | -| Roughen Edges | `ec.stylize.roughenedges` | 13 | | 32 | Implemented | -| Scatter | `ec.stylize.scatter` | 3 | | 32 | Implemented | -| Strobe Light | `ec.stylize.strobe` | 7 | | 32 | Implemented | -| Texturize | `ec.stylize.texturize` | 4 | | 32 | Implemented | -| Threshold | `ec.stylize.threshold` | 1 | | 32 | Implemented | +| Motion Tile | `ec.stylize.motiontile` | 8 | GPU | 32 | Implemented | +| Posterize | `ec.stylize.posterize` | 1 | GPU | 32 | Implemented | +| Roughen Edges | `ec.stylize.roughenedges` | 13 | GPU | 32 | Implemented | +| Scatter | `ec.stylize.scatter` | 3 | GPU | 32 | Implemented | +| Strobe Light | `ec.stylize.strobe` | 7 | GPU | 32 | Implemented | +| Texturize | `ec.stylize.texturize` | 4 | GPU | 32 | Implemented | +| Threshold | `ec.stylize.threshold` | 1 | GPU | 32 | Implemented | ## Text @@ -379,9 +379,9 @@ Effects are addressed by id (`ec..`) or display name in commands |---|---|---:|:-:|:-:|---| | CC Force Motion Blur | `ec.time.ccforcemotionblur` | 4 | | 32 | Implemented | | CC Wide Time | `ec.time.ccwidetime` | 3 | | 32 | Implemented | -| Echo | `ec.time.echo` | 5 | | 32 | Implemented | +| Echo | `ec.time.echo` | 5 | GPU | 32 | Implemented | | Pixel Motion Blur | `ec.time.pixelmotionblur` | 4 | | 32 | Implemented | -| Posterize Time | `ec.time.posterizetime` | 1 | | 32 | Implemented | +| Posterize Time | `ec.time.posterizetime` | 1 | GPU | 32 | Implemented | | Time Difference | `ec.time.timedifference` | 5 | | 32 | Implemented | | Time Displacement | `ec.time.timedisplacement` | 4 | | 32 | Implemented | | Timewarp | `ec.time.timewarp` | 28 | | 32 | Implemented | diff --git a/docs/parity.md b/docs/parity.md index 4c9cf5f34..8e08e002c 100644 --- a/docs/parity.md +++ b/docs/parity.md @@ -10,13 +10,13 @@ How close EffectCraft is to After Effects 2026, feature by feature, and how much | Unweighted | ≈ 94% (half-credit) / 98% (fractions) | | P0 / P1 / P2 | ≈ 96% / 92% / 85% (half-credit); 99% / 97% / 96% (fractions) | | Features done / partial / missing | 80 / 12 / 0 of 92 | -| **Effects** | **298 of 298** After Effects 2026 effects exist; 42 still simplified ([effects.md](effects.md)); 58 run on the GPU | +| **Effects** | **298 of 298** After Effects 2026 effects exist; 42 still simplified ([effects.md](effects.md)); 152 run on the GPU | | Disabled menu entries left | 0 (the stub list is empty) | | 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, @@ -36,7 +36,7 @@ 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; JPX / JBIG2 images in -imported PDF/AI files (mesh shadings and CCITT landed in M13.12) and Illustrator procset EPS; and more codecs. Face tracking and a true Subspace Warp +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,7 +126,7 @@ 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 | 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: 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) | @@ -134,10 +134,10 @@ the disk cache in the formats wave; the 3D Camera Tracker in M12.6.) | 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) | | 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 | ≈ 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 | 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) | +| 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 | -| Effects | ≈ 80% | 4.0 | GPU versions of the remaining effects (58 run on the GPU since M12.7) 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) | +| Effects | ≈ 85% | 3.0 | GPU versions of the remaining effects (152 run on the GPU since M13.9, EFF-5: keying, mattes, channel, colour correction, stylize, distort, noise, blurs and time effects joined the 58 of M12.7; Warp, Bezier Warp, Reshape, Smear, Cartoon, CC Page Turn and others still render on the CPU) and the missing controls listed as partial in [effects.md](effects.md) (every After Effects effect exists since M9.11, M12.5 and M12.6; parameter names, order, twirl-downs, popups, units and defaults were aligned in M9.12) | +| Interface | 74% | 5.0 | more Learn tutorials and pixel-level fidelity of dialogs (the Home ▸ Learn tab with interactive tutorials and a UI fidelity pass landed in M13.5 UI completion; visual editors for Lumetri RGB / hue-saturation curves, Colorama's output cycle wheel, Glow's colour map and Reshape's correspondence points (viewer handles), a scrolling Preview panel and AE-style Composition / Timeline tabs (close, label swatch, viewer lock) landed in M13.10; Timeline outline and Project panel columns scroll horizontally, the Layer Style dialog, ROI resize handles, Pan Behind snapping and 3D Reference Axes landed in M13.5; native macOS menu bar, Timeline columns/search/reveal-add, Home screen with recent projects and all AE workspaces landed; viewer rulers/snapping/channels/snapshots landed in M0.13) | | Project | ≈ 68% | 5.5 | auto-save, folder moves, OCIO displays beyond the built-in tone map (Color Engine with OCIO/ACES working spaces, HDR compand/tone mapping, Rec. 2100 PQ/HLG output, Feet + Frames, display colour management, Simulate Output and the locked viewer landed in M7.7; proxies and Interpret Footage fields / pixel aspect / alpha guess landed: PRJ-8, PRJ-3) | | Masks & roto | 74% | 5.0 | Roto Brush's learned (3.0) segmentation model (variable-width mask feather points with the Mask Feather tool landed in M13.5; mask tracking and Mask Interpolation landed in M6.6; Roto Brush & Refine Edge with graph-cut segmentation, flow propagation, edge matting, decontamination and Freeze in M6.7) | | Preview | 74% | 3.2 | wireframes and Advanced 3D compositing on the GPU (Classic 3D bokeh depth of field — iris shapes, highlights, fringe, progressive blur on tilted planes — runs in WGSL since M13.6: 3D Showcase GPU warm 205 → 54 ms/frame at full size on an M4 Pro; the Preview panel's five shortcuts with their own Include / Loop / Cache Before Playback / Range / Play From / Frame Rate / Skip / Resolution / Full Screen / stop options and `playback.settings.get/set` landed in M13.8; Classic 3D runs and adjustment layers composite on the GPU since M12.7; persistent disk cache with the blue cache bar landed; region of interest, snapshots, exposure and Fast Previews landed in M0.13) | @@ -239,7 +239,7 @@ 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 @@ -273,7 +273,7 @@ Browser in the web app. 2. Effect Controls widgets: angle dial, point crosshair, eyedropper, curves and levels editors. 3. ~~Viewer basics: snapping, rulers, channel view, snapshots, exposure, a drawable region of interest.~~ (M0.13) 4. ~~A GPU (wgpu) compositor, then GPU effects~~ (M12.2: 2D compositing and 16 GPU effects; M12.7: - Classic 3D runs, adjustment layers, 58 GPU effects and GPU particles). + Classic 3D runs, adjustment layers, 58 GPU effects and GPU particles; M13.9: 152 GPU effects). 5. ~~Pen tool for shape paths, shape vertex editing, free transform~~ (M6.5); the Layer viewer. 6. ~~Motion-path handles in the viewer; graph editor transform box and snapping.~~ (M5.8) 7. Point tracking and stabilization (in progress). diff --git a/docs/web.md b/docs/web.md index 72eb75ff6..52d3419ca 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 01cb44b4b..681aee1e7 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),