Merge pull request #2496 from dylanmtaylor/sonar/enforce-clippy-standards

Enforce Rust coding standards via Clippy
This commit is contained in:
Dylan M. Taylor
2026-10-28 06:18:48 -04:00
committed by GitHub
215 changed files with 8307 additions and 5176 deletions
+1
View File
@@ -22,6 +22,7 @@ EffectCraft is a clean-room, open-source, pure-Rust motion graphics and visual e
- **Everything is agent-drivable:** every interactive widget registers an automation id; UI state is serde.
- **Community links** (Help menu, About, header Discord button): https://discord.gg/artcraft, https://getartcraft.com, https://getartcraft.com/apps/effectcraft, https://github.com/storytold/effectcraft.
- **Quality gates** before every commit: `cargo xtask ci` (fmt, clippy -D warnings, tests, layers, assets, wasm).
- **New workspace crates** must include `[lints] workspace = true` in `Cargo.toml` to inherit `[workspace.lints]`. Production crate roots must declare the required crate-level lint attributes before modules or other crate items; crate-level `//!` documentation may precede them.
- **Commits:** one task id per commit (`M6.3: trim paths`). Only green states.
## Running and looking at the app
+3 -1
View File
@@ -135,9 +135,11 @@ sys-locale = "0.3"
unsafe_code = "forbid"
[workspace.lints.clippy]
cognitive_complexity = "deny"
too_many_arguments = "deny"
wildcard_imports = "deny"
dbg_macro = "warn"
todo = "warn"
too_many_arguments = "allow"
needless_range_loop = "allow"
type_complexity = "allow"
+20 -9
View File
@@ -152,6 +152,13 @@ fn essential_graphics_template_via_script_and_commands() {
assert_eq!(image::open(&frames[0]).unwrap().to_rgba8(), edit);
assert_eq!(std::fs::read(dir.join("lt.ectemplate")).unwrap(), original_template);
invalid_template_values_fail_cleanly(&dir);
template_usage_errors(&dir);
rational_template_keeps_exact_rate(&dir, &original_template);
let _ = std::fs::remove_dir_all(&dir);
}
fn invalid_template_values_fail_cleanly(dir: &PathBuf) {
// Invalid values fail before the queue creates an output directory, or frame rendering
// overwrites an existing output. A JSON object and existing control names are required.
for (name, values, expected) in [
@@ -164,25 +171,27 @@ fn essential_graphics_template_via_script_and_commands() {
std::fs::write(dir.join(&values_file), values).unwrap();
let output_dir = dir.join(format!("failed-{name}"));
let out = output_dir.join("frame_[#####].png");
let (code, error) = cli(&dir, &["render", "--template", "lt.ectemplate", "--values", &values_file, "--out", out.to_str().unwrap()]);
let (code, error) = cli(dir, &["render", "--template", "lt.ectemplate", "--values", &values_file, "--out", out.to_str().unwrap()]);
assert_eq!(code, 1, "{name}: {error}");
assert!(error["error"].as_str().unwrap().contains(expected), "{name}: {error}");
assert!(!output_dir.exists(), "{name}: invalid input created render output");
std::fs::write(dir.join("untouched.png"), b"existing output").unwrap();
let (code, error) = cli(&dir, &["render-frame", "--template", "lt.ectemplate", "--values", &values_file, "--out", "untouched.png"]);
let (code, error) = cli(dir, &["render-frame", "--template", "lt.ectemplate", "--values", &values_file, "--out", "untouched.png"]);
assert_eq!(code, 1, "{name}: {error}");
assert_eq!(std::fs::read(dir.join("untouched.png")).unwrap(), b"existing output");
}
let (code, error) = cli(&dir, &["render-frame", "--template", "lt.ectemplate", "--values", "missing-values.json", "--out", "missing.png"]);
let (code, error) = cli(dir, &["render-frame", "--template", "lt.ectemplate", "--values", "missing-values.json", "--out", "missing.png"]);
assert_eq!(code, 1, "{error}");
assert!(error["error"].as_str().unwrap().contains("cannot read values"), "{error}");
assert!(!dir.join("missing.png").exists());
std::fs::write(dir.join("large-values.json"), vec![b' '; 1024 * 1024 + 1]).unwrap();
let (code, error) = cli(&dir, &["render-frame", "--template", "lt.ectemplate", "--values", "large-values.json", "--out", "large.png"]);
let (code, error) = cli(dir, &["render-frame", "--template", "lt.ectemplate", "--values", "large-values.json", "--out", "large.png"]);
assert_eq!(code, 1, "{error}");
assert!(error["error"].as_str().unwrap().contains("exceeds 1 MiB"), "{error}");
assert!(!dir.join("large.png").exists());
}
fn template_usage_errors(dir: &PathBuf) {
for args in [
vec!["render-frame", "--values", "values.json"],
vec!["info", "--template", "lt.ectemplate"],
@@ -191,10 +200,12 @@ fn essential_graphics_template_via_script_and_commands() {
vec!["render", "--template", "lt.ectemplate", "--queue"],
vec!["render-frame", "--template", "lt.ectemplate", "--bridge", "1"],
] {
let output = Command::new(crate::binary()).current_dir(&dir).args(&args).output().unwrap();
let output = Command::new(crate::binary()).current_dir(dir).args(&args).output().unwrap();
assert_eq!(output.status.code(), Some(2), "{args:?}: {}", String::from_utf8_lossy(&output.stderr));
}
}
fn rational_template_keeps_exact_rate(dir: &PathBuf, original_template: &[u8]) {
// File-loaded templates can use exact rational rates that comp.new's float parser rounds.
// Preserve that rate, Tick duration, work area, background and locked source layers.
let mut project = effectcraft_engine::project::Project::from_json(&std::fs::read_to_string(dir.join("t.ecproj")).unwrap()).unwrap();
@@ -207,13 +218,13 @@ fn essential_graphics_template_via_script_and_commands() {
comp.background = [0.08, 0.16, 0.24];
comp.layers.first_mut().unwrap().switches.locked = true;
std::fs::write(dir.join("rational.ecproj"), project.to_json()).unwrap();
ok(&dir, &["exec", "essential.exportTemplate", r#"{"comp":"Lower Third","path":"rational.ectemplate"}"#, "rational.ecproj"]);
ok(dir, &["exec", "essential.exportTemplate", r#"{"comp":"Lower Third","path":"rational.ectemplate"}"#, "rational.ecproj"]);
let rational_template = std::fs::read(dir.join("rational.ectemplate")).unwrap();
let rational = ok(&dir, &["render-frame", "--template", "rational.ectemplate", "--frame", "5", "--out", "rational.png"]);
let rational = ok(dir, &["render-frame", "--template", "rational.ectemplate", "--frame", "5", "--out", "rational.png"]);
assert!((rational["time"].as_f64().unwrap() - 5.0 / rate.as_f64()).abs() < 1e-9, "{rational}");
assert_eq!((rational["width"].as_u64().unwrap(), rational["height"].as_u64().unwrap()), (320, 90));
let output = dir.join("rational-work-area/frame_[#####].png");
ok(&dir, &["render", "--template", "rational.ectemplate", "--work-area", "--out", output.to_str().unwrap(), "--format", "png", "--resolution", "0.05"]);
ok(dir, &["render", "--template", "rational.ectemplate", "--work-area", "--out", output.to_str().unwrap(), "--format", "png", "--resolution", "0.05"]);
let count = std::fs::read_dir(dir.join("rational-work-area"))
.unwrap()
.filter_map(Result::ok)
@@ -222,8 +233,8 @@ fn essential_graphics_template_via_script_and_commands() {
assert_eq!(count, 4, "the template's exact work area contains frames 3 through 6");
assert_eq!(std::fs::read(dir.join("rational.ectemplate")).unwrap(), rational_template);
assert_eq!(std::fs::read(dir.join("lt.ectemplate")).unwrap(), original_template);
let _ = std::fs::remove_dir_all(&dir);
}
fn template_output_alias_fixture(case: &str) -> PathBuf {
let dir = std::env::temp_dir().join(format!("ec-qa-template-output-{case}-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
+3 -2
View File
@@ -311,10 +311,11 @@ pub fn stats() -> Value {
})
}
type PendingFrameCheck = (js_sys::Function, js_sys::Function, (u32, u32, Vec<u8>), f64);
thread_local! {
/// `workerFrameCheck` promises waiting for their frame: id → (resolve, reject, local frame).
#[allow(clippy::type_complexity)]
static CHECKS: RefCell<std::collections::HashMap<u64, (js_sys::Function, js_sys::Function, (u32, u32, Vec<u8>), f64)>> = RefCell::new(Default::default());
static CHECKS: RefCell<std::collections::HashMap<u64, PendingFrameCheck>> = RefCell::new(Default::default());
}
/// `effectcraft.workerFrameCheck({time?, scale?, backend?})`: render the active comp's frame in
+1 -1
View File
@@ -9,8 +9,8 @@
//!
//! `--control <port>` (or `EFFECTCRAFT_CONTROL_PORT`) starts a localhost JSON-lines control server;
//! see `effectcraft_ui_egui::control` for the methods.
#![cfg_attr(all(target_os = "windows", not(debug_assertions)), windows_subsystem = "windows")]
#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)]
#![cfg_attr(all(target_os = "windows", not(debug_assertions)), windows_subsystem = "windows")]
// Built everywhere so its tests run on every platform; only Linux AppImages use it.
mod appearance;
-2
View File
@@ -1,6 +1,4 @@
//! The desktop binary rejects a bad command line before it starts anything (#997).
// Integration tests: unwrapping and panicking on failure is fine here.
#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
use std::path::PathBuf;
use std::process::{Command, Stdio};
+1
View File
@@ -5,3 +5,4 @@ allow-indexing-slicing-in-tests = true
disallowed-types = [
{ path = "egui::ComboBox", reason = "use craft_ui::ComboBox: egui's combo box menu can't grow back while open (egui#5225)" },
]
cognitive-complexity-threshold = 30
+39 -12
View File
@@ -7,6 +7,10 @@ use serde_json::{Value, json};
use crate::harness::{Img, Qa, coverage, luma, mean_diff};
fn lum(i: &Img, x: u32, y: u32) -> f64 {
luma(i.get_pixel(x, y))
}
fn px(img: &Img, x: u32, y: u32) -> [u8; 4] {
img.get_pixel(x, y).0
}
@@ -25,13 +29,12 @@ fn write_sequence(qa: &Qa) -> String {
qa.path("seq/ball_000.png")
}
#[test]
fn compositing_matte_blend_adjustment_3d_and_alpha_renders() {
let mut qa = Qa::new("compositing");
/// Import the sequence (reinterpreted at 12 fps) and a PSD written by the app; returns the
/// sequence item and the PSD's composition.
fn import_footage(qa: &mut Qa) -> (Value, Value) {
// Footage: the sequence, and a layered PSD written by the app itself (Save Frame As ▸
// Photoshop Layers) from a small comp.
let seq = write_sequence(&qa);
let seq = write_sequence(qa);
let imp = qa.exec("file.import", json!({"paths": [seq]}));
let seq_item = imp["items"][0].clone();
let project = qa.tool("get_project", json!({}));
@@ -55,10 +58,14 @@ fn compositing_matte_blend_adjustment_3d_and_alpha_renders() {
let pc = qa.tool("get_comp", json!({"comp": psd_comp.clone()}));
let names: Vec<&str> = pc["layers"].as_array().unwrap().iter().map(|l| l["name"].as_str().unwrap()).collect();
assert_eq!(names, ["Bar", "Gray"], "the PSD's layers, top first: {pc}");
(seq_item, psd_comp)
}
/// The main comp: the sequence plays, then a Screen-blended PSD; returns the Normal-mode frame.
fn main_comp_and_blend(qa: &mut Qa, seq_item: &Value, psd_comp: &Value) -> Img {
// The main comp.
let main = qa.exec("comp.new", json!({"name": "Main", "width": 160, "height": 90, "frameRate": 12, "duration": 1}))["comp"].clone();
qa.exec("layer.addItem", json!({"item": seq_item}));
qa.exec("comp.new", json!({"name": "Main", "width": 160, "height": 90, "frameRate": 12, "duration": 1}));
qa.exec("layer.addItem", json!({"item": seq_item.clone()}));
let a = qa.frame(None, 0.0);
let b = qa.frame(None, 0.5);
assert!(mean_diff(&a, &b) > 1.0, "the sequence plays");
@@ -75,7 +82,10 @@ fn compositing_matte_blend_adjustment_3d_and_alpha_renders() {
for c in 0..3 {
assert!(s[c] > n[c] && s[c] > [32, 64, 96][c], "Screen brightens channel {c}: {s:?}");
}
normal
}
fn track_matte(qa: &mut Qa) {
// Track matte: a magenta solid seen only through text.
qa.exec("layer.newText", json!({"text": "MATTE", "name": "Matte", "size": 40, "fill": "#ffffff"}));
qa.exec("layer.newSolid", json!({"name": "Fill", "color": "#ff00ff", "width": 160, "height": 90}));
@@ -91,7 +101,9 @@ fn compositing_matte_blend_adjustment_3d_and_alpha_renders() {
let fill = info["layers"].as_array().unwrap().iter().find(|l| l["name"] == "Fill").unwrap().clone();
assert_eq!(fill["trackMatte"]["kind"], "Alpha Matte");
qa.exec("layer.setSwitch", json!({"layers": ["Fill", "Matte"], "switch": "video", "value": false}));
}
fn adjustment_layer(qa: &mut Qa, normal: &Img) {
// Adjustment layer: Lumetri exposure, inside a feathered, expanded elliptical mask; then Glow.
qa.exec("layer.newAdjustment", json!({"name": "Grade"}));
let fx = qa.exec("effect.apply", json!({"effect": "Lumetri Color", "layers": ["Grade"]}));
@@ -103,7 +115,6 @@ fn compositing_matte_blend_adjustment_3d_and_alpha_renders() {
let mk = qa.exec("layer.addMask", json!({"layer": "Grade", "shape": "ellipse", "rect": [40, 20, 80, 50]}));
assert!(mk["mask"].is_u64(), "{mk}");
let hard = qa.frame(None, 0.5);
let lum = |i: &Img, x, y| luma(i.get_pixel(x, y));
assert!(lum(&hard, 80, 45) > lum(&before, 80, 45) - 1.0 && lum(&hard, 5, 5) < lum(&before, 5, 5) - 10.0, "the mask limits the grade to the ellipse");
qa.exec("layer.mask.set", json!({"layer": "Grade", "field": "expansion", "value": 8}));
let grown = qa.frame(None, 0.5);
@@ -116,7 +127,7 @@ fn compositing_matte_blend_adjustment_3d_and_alpha_renders() {
}
n
};
assert!(bright(&grown, &normal) > bright(&hard, &normal) + 100, "expansion grows the graded area");
assert!(bright(&grown, normal) > bright(&hard, normal) + 100, "expansion grows the graded area");
qa.exec("layer.mask.set", json!({"layer": "Grade", "field": "feather", "value": 20}));
let soft = qa.frame(None, 0.5);
// At the ellipse's edge (x = 40 − 8): feathered is in between none and full.
@@ -125,7 +136,10 @@ fn compositing_matte_blend_adjustment_3d_and_alpha_renders() {
qa.exec("effect.apply", json!({"effect": "Glow", "layers": ["Grade"]}));
let glow = qa.frame(None, 0.5);
assert!(mean_diff(&soft, &glow) > 0.5, "Glow changes the frame");
}
/// The 3D stage with depth of field and a light; returns the stage comp.
fn stage_3d(qa: &mut Qa) -> Value {
// 3D stage: a near and a far card, a camera with depth of field, a light.
let stage = qa.exec("comp.new", json!({"name": "Stage", "width": 160, "height": 90, "frameRate": 12, "duration": 1}))["comp"].clone();
qa.exec("layer.newSolid", json!({"name": "Near", "color": "#ff4040", "width": 40, "height": 40}));
@@ -155,7 +169,10 @@ fn compositing_matte_blend_adjustment_3d_and_alpha_renders() {
let advanced = qa.frame(None, 0.0);
assert!(mean_diff(&classic, &advanced) > 0.2, "Advanced 3D renders the stage its own way");
assert!(lum(&advanced, 50, 45) > 60.0, "Advanced 3D still lights the card");
stage
}
fn alpha_renders(qa: &mut Qa, stage: &Value) {
// Renders with alpha: the stage has no background layer, so most of it is transparent.
let mov = qa.path("stage.mov");
let webm = qa.path("stage.webm");
@@ -165,7 +182,7 @@ fn compositing_matte_blend_adjustment_3d_and_alpha_renders() {
assert!(done["items"].as_array().unwrap().iter().all(|i| i["status"] == "Done"), "{done}");
let reference = qa.tool("render_frame", json!({"comp": stage.clone(), "time": 0, "transparent": true, "inline": false}));
assert_eq!(reference["width"], 160);
let refa = alpha_frame(&mut qa, stage.clone());
let refa = alpha_frame(qa, stage.clone());
assert!(px(&refa, 2, 2)[3] < 40 && px(&refa, 50, 45)[3] == 255, "render_frame transparent keeps alpha: {:?} {:?}", px(&refa, 2, 2), px(&refa, 50, 45));
// ProRes 4444 carries alpha in the frame; WebM VP9 as a second VP9 stream in BlockAdditions
// (AlphaMode 1), which FilmCraft's Matroska reader decodes into the frame's alpha plane.
@@ -174,14 +191,24 @@ fn compositing_matte_blend_adjustment_3d_and_alpha_renders() {
assert_eq!(r["errors"], json!([]));
assert_eq!(r["unlabeledAlpha"], r["items"], "{path}: the importer sees an alpha channel");
let c = qa.exec("file.newCompFromSelection", json!({}))["comps"][0].clone();
let f = alpha_frame(&mut qa, c.clone());
let f = alpha_frame(qa, c.clone());
// The opaque card decodes to the stage's colours (lossy codecs, so loosely).
let (got, want) = (px(&f, 50, 45), px(&refa, 50, 45));
assert!((0..3).all(|c| got[c].abs_diff(want[c]) < 12), "{path} decodes to the stage: {got:?} vs {want:?}");
assert!(mean_diff(&qa.frame(Some(c.clone()), 0.0), &qa.frame(Some(stage.clone()), 0.0)) < 8.0, "{path} composites like the stage");
assert!(px(&f, 2, 2)[3] < 40 && px(&f, 50, 45)[3] > 250, "{path}: transparent corners, opaque card: {:?} {:?}", px(&f, 2, 2), px(&f, 50, 45));
}
let _ = main;
}
#[test]
fn compositing_matte_blend_adjustment_3d_and_alpha_renders() {
let mut qa = Qa::new("compositing");
let (seq_item, psd_comp) = import_footage(&mut qa);
let normal = main_comp_and_blend(&mut qa, &seq_item, &psd_comp);
track_matte(&mut qa);
adjustment_layer(&mut qa, &normal);
let stage = stage_3d(&mut qa);
alpha_renders(&mut qa, &stage);
}
fn alpha_frame(qa: &mut Qa, comp: Value) -> Img {
+9 -1
View File
@@ -2,7 +2,15 @@
//! 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::*;
use crate::tables::{
DEFAULT_ANGLE_DELTA_CDF, DEFAULT_COEFF_BASE_CDF, DEFAULT_COEFF_BASE_EOB_CDF, DEFAULT_COEFF_BR_CDF, DEFAULT_DC_SIGN_CDF, DEFAULT_DRL_MODE_CDF,
DEFAULT_EOB_EXTRA_CDF, DEFAULT_EOB_PT_16_CDF, DEFAULT_EOB_PT_32_CDF, DEFAULT_EOB_PT_64_CDF, DEFAULT_EOB_PT_128_CDF, DEFAULT_EOB_PT_256_CDF,
DEFAULT_EOB_PT_512_CDF, DEFAULT_EOB_PT_1024_CDF, DEFAULT_INTER_TX_TYPE_SET3_CDF, DEFAULT_INTRA_TX_TYPE_SET2_CDF, DEFAULT_IS_INTER_CDF, DEFAULT_MV_BIT_CDF,
DEFAULT_MV_CLASS_CDF, DEFAULT_MV_CLASS0_BIT_CDF, DEFAULT_MV_CLASS0_FR_CDF, DEFAULT_MV_FR_CDF, DEFAULT_MV_JOINT_CDF, DEFAULT_MV_SIGN_CDF,
DEFAULT_NEW_MV_CDF, DEFAULT_PARTITION_W8_CDF, DEFAULT_PARTITION_W16_CDF, DEFAULT_PARTITION_W32_CDF, DEFAULT_PARTITION_W64_CDF, DEFAULT_REF_MV_CDF,
DEFAULT_SINGLE_REF_CDF, DEFAULT_SKIP_CDF, DEFAULT_TXB_SKIP_CDF, DEFAULT_UV_MODE_CFL_ALLOWED_CDF, DEFAULT_UV_MODE_CFL_NOT_ALLOWED_CDF, DEFAULT_Y_MODE_CDF,
DEFAULT_ZERO_MV_CDF,
};
#[derive(Clone)]
pub(crate) struct Cdfs {
+30 -9
View File
@@ -3,8 +3,12 @@
//! 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::pred::{INTRA_MODES_USED, InterBlock, IntraEdge, Plane, edges, predict_inter, predict_intra, subpel_plane};
use crate::tables::{
AC_QLOOKUP, BLOCK_8X8, BLOCK_64X64, DC_PRED, DC_QLOOKUP, DCT_DCT, DEFAULT_SCAN_4X4, DEFAULT_SCAN_8X8, DEFAULT_SCAN_16X16, DEFAULT_SCAN_32X32, MODE_TO_TXFM,
NEWMV, NUM_4X4_BLOCKS_WIDE, PARTITION_SPLIT, PARTITION_SUBSIZE, TX_4X4, TX_8X8, TX_16X16, TX_32X32, TX_64X64, TX_SET_DCTONLY, TX_SET_INTER_3,
TX_SET_INTRA_2, TX_SIZE_SQR_UP, TX_TYPE_IN_SET_INTER, TX_TYPE_IN_SET_INTRA,
};
use crate::transform::{forward_2d, inverse_2d};
/// Decisions for one coded block.
@@ -100,6 +104,17 @@ pub(crate) fn intra_uv_tx_type(uv_mode: usize, uv_tx: usize) -> usize {
if tx_in_set(tx_set(uv_tx, false), false, t) { t } else { DCT_DCT }
}
/// Geometry and coding mode of one square transform block.
#[derive(Clone, Copy)]
struct TransformBlock {
plane: usize,
x: usize,
y: usize,
log2: u32,
tx_type: usize,
intra: bool,
}
struct TxResult {
levels: Vec<i32>,
nonzero: bool,
@@ -362,7 +377,7 @@ impl<'a> Decider<'a> {
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 (res, recon) = self.try_tx(TransformBlock { plane: 0, x: c * 4, y: r * 4, log2, tx_type: t, intra: true }, &pred[..n * n]);
let type_bits = if !res.nonzero || tx_set(tx_y, false) == TX_SET_DCTONLY {
0.0
} else if t == DCT_DCT {
@@ -422,7 +437,7 @@ impl<'a> Decider<'a> {
let uv_type = intra_uv_tx_type(uv_mode, uv_tx);
let mut code_uv = |i: usize| {
predict_intra(&ced[i], &ce[i], uv_mode, &mut pred);
let t = self.code_tx(1 + i, c * 2, r * 2, clog2, uv_type, &pred[..cn * cn], true);
let t = self.code_tx(TransformBlock { plane: 1 + i, x: c * 2, y: r * 2, log2: clog2, tx_type: uv_type, intra: true }, &pred[..cn * cn]);
self.mark_decoded(1 + i, r, c, n4);
t
};
@@ -450,13 +465,17 @@ impl<'a> Decider<'a> {
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]);
predict_inter(
&refr.planes[p],
&InterBlock { last_x: vw as i32 - 1, last_y: vh as i32 - 1, x, y, width: pn, height: pn, mv, subsampling: s, bit_depth: 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);
let t = self.code_tx(TransformBlock { plane: p, x, y, log2: log2 - s as u32, tx_type: DCT_DCT, intra: false }, &pred[..pn * pn]);
dist += t.dist;
tx.push(t);
}
@@ -771,8 +790,9 @@ impl<'a> Decider<'a> {
/// 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);
fn code_tx(&mut self, block: TransformBlock, pred: &[u16]) -> TxResult {
let TransformBlock { plane: p, x, y, log2, .. } = block;
let (res, recon) = self.try_tx(block, pred);
let n = 1usize << log2;
self.put(p, x, y, n, recon.as_deref().unwrap_or(pred));
res
@@ -780,7 +800,8 @@ impl<'a> Decider<'a> {
/// 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<Vec<u16>>) {
fn try_tx(&self, block: TransformBlock, pred: &[u16]) -> (TxResult, Option<Vec<u16>>) {
let TransformBlock { plane: p, x, y, log2, tx_type, intra } = block;
let n = 1usize << log2;
let tx_sz = tx_size_for(log2);
let bd = self.bd;
+17 -5
View File
@@ -3,7 +3,7 @@
//! is 0 in the frames this encoder writes.
use crate::pred::Plane;
use crate::tables::*;
use crate::tables::{MAX_LOOP_FILTER, NUM_4X4_BLOCKS_HIGH, NUM_4X4_BLOCKS_WIDE, SUBSAMPLED_SIZE, TX_HEIGHT, TX_WIDTH};
/// 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`).
@@ -39,7 +39,7 @@ pub(crate) fn loop_filter(planes: &mut [Plane; 3], info: &LfInfo, level: [u32; 4
}
let mut col = 0;
while col < info.mi_cols {
edge(pl, info, level, bit_depth, plane, pass, row, col);
edge(pl, info, level, bit_depth, plane, pass, (row, col));
col += step;
}
row += step;
@@ -48,7 +48,8 @@ pub(crate) fn loop_filter(planes: &mut [Plane; 3], info: &LfInfo, level: [u32; 4
}
}
fn edge(pl: &mut Plane, info: &LfInfo, level: [u32; 4], bd: u32, plane: usize, pass: usize, row: usize, col: usize) {
fn edge(pl: &mut Plane, info: &LfInfo, level: [u32; 4], bd: u32, plane: usize, pass: usize, position: (usize, usize)) {
let (row, col) = position;
let s = (plane > 0) as usize;
let (dx, dy) = if pass == 0 { (1usize, 0usize) } else { (0, 1) };
let x = col * 4;
@@ -88,13 +89,24 @@ fn edge(pl: &mut Plane, info: &LfInfo, level: [u32; 4], bd: u32, plane: usize, p
let limit = lvl.max(1);
let blimit = 2 * (lvl + 2) + limit;
let thresh = lvl >> 4;
let strength = FilterStrength { limit, blimit, thresh };
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);
sample_filter(pl, (xp + dy * k, yp + dx * k), plane, &strength, (dx as isize, dy as isize), filter_size, bd);
}
}
/// Thresholds derived from the loop-filter level (spec 7.14.4).
struct FilterStrength {
limit: i32,
blimit: i32,
thresh: i32,
}
#[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) {
fn sample_filter(pl: &mut Plane, position: (usize, usize), plane: usize, strength: &FilterStrength, direction: (isize, isize), filter_size: usize, bd: u32) {
let (x, y) = position;
let (dx, dy) = direction;
let FilterStrength { limit, blimit, thresh } = *strength;
let stride = pl.stride as isize;
let step = dy * stride + dx;
let base = y as isize * stride + x as isize;
+23 -15
View File
@@ -2,7 +2,10 @@
//! 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::tables::{
DC_PRED, EIGHTTAP, H_PRED, PAETH_PRED, SM_WEIGHTS_TX_4X4, SM_WEIGHTS_TX_8X8, SM_WEIGHTS_TX_16X16, SM_WEIGHTS_TX_32X32, SMOOTH_H_PRED, SMOOTH_PRED,
SMOOTH_V_PRED, SUBPEL_FILTERS, V_PRED,
};
use crate::transform::round2;
/// A plane buffer (padded to whole superblocks).
@@ -184,19 +187,20 @@ pub(crate) fn predict_intra(ed: &Edges, e: &IntraEdge, mode: usize, pred: &mut [
/// 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],
) {
pub(crate) struct InterBlock {
pub last_x: i32,
pub last_y: i32,
pub x: usize,
pub y: usize,
pub width: usize,
pub height: usize,
pub mv: [i32; 2],
pub subsampling: usize,
pub bit_depth: u32,
}
pub(crate) fn predict_inter(refp: &Plane, block: &InterBlock, pred: &mut [u16]) {
let InterBlock { last_x, last_y, x, y, width: w, height: h, mv, subsampling: ss, bit_depth } = *block;
// 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);
@@ -340,7 +344,11 @@ mod tests {
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);
predict_inter(
&p,
&InterBlock { last_x: w as i32 - 1, last_y: h as i32 - 1, x: 0, y: 0, width: 8, height: 8, mv, subsampling: 0, bit_depth: 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));
+12 -1
View File
@@ -4,7 +4,7 @@
use std::sync::OnceLock;
use crate::tables::*;
use crate::tables::{ADST_ADST, ADST_DCT, COS128_LOOKUP, DCT_ADST, DCT_DCT, TRANSFORM_ROW_SHIFT, TX_WIDTH_LOG2};
#[inline(always)]
fn round2_64(x: i64, n: u32) -> i32 {
@@ -71,6 +71,12 @@ fn inverse_dct(t: &mut [i32], n: u32, r: u32) {
for i in 0..len {
t[i] = copy[brev(n, i)];
}
inverse_dct_initial_stages(t, n, r);
inverse_dct_final_stages(t, n, r);
}
/// Rotate the high-frequency branches before the four-point core.
fn inverse_dct_initial_stages(t: &mut [i32], n: u32, r: u32) {
if n >= 5 {
for i in 0..8 {
bf(t, 16 + i, 31 - i, 6 + ((brev(3, 7 - i) as i32) << 3), false);
@@ -103,6 +109,10 @@ fn inverse_dct(t: &mut [i32], n: u32, r: u32) {
}
}
}
}
/// Evaluate the four-point core and merge progressively larger branches.
fn inverse_dct_final_stages(t: &mut [i32], n: u32, r: u32) {
for i in 0..2 {
bf(t, 2 * i, 2 * i + 1, 32 + 16 * i as i32, i == 0);
}
@@ -454,6 +464,7 @@ pub(crate) fn forward_2d(residual: &[i32], out: &mut [f32], n: usize, tx_type: u
#[cfg(test)]
mod tests {
use super::*;
use crate::tables::{TX_4X4, TX_8X8, TX_16X16, TX_32X32};
/// 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.
+30 -18
View File
@@ -8,7 +8,15 @@ 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::*;
use crate::tables::{
ADST_ADST, ADST_DCT, ALTREF_FRAME, ALTREF2_FRAME, BLOCK_8X8, BLOCK_64X64, BR_CDF_SIZE, BWDREF_FRAME, CLASS0_SIZE, COEFF_BASE_CTX_OFFSET, COEFF_BASE_RANGE,
D67_PRED, DC_PRED, DCT_ADST, DCT_DCT, DEFAULT_INTRA_FRAME_Y_MODE_CDF, GLOBALMV, GOLDEN_FRAME, INTRA_FRAME, INTRA_MODE_CONTEXT, LAST_FRAME, LAST2_FRAME,
LAST3_FRAME, MAG_REF_OFFSET_WITH_TX_CLASS, MAX_ANGLE_DELTA, MAX_REF_MV_STACK_SIZE, MI_HEIGHT_LOG2, MI_WIDTH_LOG2, MV_BORDER, MV_CLASS_0, NEARESTMV, NEARMV,
NEWMV, NUM_4X4_BLOCKS_HIGH, NUM_4X4_BLOCKS_WIDE, NUM_BASE_LEVELS, PARTITION_HORZ, PARTITION_HORZ_4, PARTITION_HORZ_A, PARTITION_HORZ_B, PARTITION_NONE,
PARTITION_SPLIT, PARTITION_SUBSIZE, PARTITION_VERT, PARTITION_VERT_4, PARTITION_VERT_A, PARTITION_VERT_B, REF_CAT_LEVEL, SIG_REF_DIFF_OFFSET, SIZE_GROUP,
SUBSAMPLED_SIZE, TX_32X32, TX_CLASS_2D, TX_HEIGHT, TX_HEIGHT_LOG2, TX_SIZE_SQR, TX_SIZE_SQR_UP, TX_TYPE_INTER_INV_SET3, TX_TYPE_INTRA_INV_SET2, TX_WIDTH,
TX_WIDTH_LOG2, V_PRED,
};
type MvP = [i32; 2];
@@ -455,23 +463,7 @@ impl<'a> TileWriter<'a> {
}
} 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 {
// The decision only picks types of this set (DCT_DCT otherwise).
let s = TX_TYPE_INTER_INV_SET3.iter().position(|&v| v as usize == t).unwrap_or_default();
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).unwrap_or_default();
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);
self.write_transform_type(x4, y4, tx_sz, b);
}
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));
@@ -571,6 +563,26 @@ impl<'a> TileWriter<'a> {
}
}
fn write_transform_type(&mut self, x4: usize, y4: usize, tx_sz: usize, b: &Leaf) {
// 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 {
// The decision only picks types of this set (DCT_DCT otherwise).
let s = TX_TYPE_INTER_INV_SET3.iter().position(|&v| v as usize == t).unwrap_or_default();
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).unwrap_or_default();
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);
}
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) {
+28 -13
View File
@@ -692,18 +692,33 @@ impl BoltCfg<'_> {
}
}
/// Grow a bolt from `a` to `z` by midpoint displacement (`depth` levels), forking at random.
/// Segments go to `out` with their intensity; `mains` marks the main core's segments.
#[allow(clippy::too_many_arguments)]
fn bolt(cfg: &BoltCfg, a: (f64, f64), z: (f64, f64), depth: u32, inten: f32, main: bool, rng: &mut Rng, out: &mut Vec<Seg>, mains: &mut Vec<bool>) {
if out.len() > 20_000 {
/// One branch's geometry and recursive growth state.
struct BoltBranch {
start: (f64, f64),
end: (f64, f64),
depth: u32,
intensity: f32,
main: bool,
}
/// Segments and their matching main-core flags, kept in the same traversal order.
#[derive(Default)]
struct BoltOutput {
segments: Vec<Seg>,
mains: Vec<bool>,
}
/// Grow a branch by midpoint displacement, recursively appending its segments and forks.
fn bolt(cfg: &BoltCfg, branch: BoltBranch, rng: &mut Rng, output: &mut BoltOutput) {
let BoltBranch { start: a, end: z, depth, intensity: inten, main } = branch;
if output.segments.len() > 20_000 {
return;
}
let (dx, dy) = (z.0 - a.0, z.1 - a.1);
let len = (dx * dx + dy * dy).sqrt();
if depth == 0 || len < 1.0 {
out.push((a, z, inten));
mains.push(main);
output.segments.push((a, z, inten));
output.mains.push(main);
return;
}
let (nx, ny) = (-dy / len.max(1e-9), dx / len.max(1e-9));
@@ -725,8 +740,8 @@ fn bolt(cfg: &BoltCfg, a: (f64, f64), z: (f64, f64), depth: u32, inten: f32, mai
if cfg.obstacle > 0.0 && !main && cfg.alpha_at(m) > 0.5 && rng.f() < cfg.obstacle / 10.0 {
return;
}
bolt(cfg, a, m, depth - 1, inten, main, rng, out, mains);
bolt(cfg, m, z, depth - 1, inten, main, rng, out, mains);
bolt(cfg, BoltBranch { start: a, end: m, depth: depth - 1, intensity: inten, main }, rng, output);
bolt(cfg, BoltBranch { start: m, end: z, depth: depth - 1, intensity: inten, main }, rng, output);
if rng.f() < cfg.fork && inten > cfg.term && len >= cfg.min_fork {
let ang = rng.s() * 0.7;
let (s, c) = ang.sin_cos();
@@ -734,7 +749,7 @@ fn bolt(cfg: &BoltCfg, a: (f64, f64), z: (f64, f64), depth: u32, inten: f32, mai
let k = 1.2;
// Fork Strength 50 % keeps the decayed intensity; Fork Variation randomises it.
let fi = inten * (1.0 - cfg.decay) * cfg.fork_strength * 2.0 * if cfg.fork_var > 0.0 { 1.0 - cfg.fork_var * rng.f() as f32 } else { 1.0 };
bolt(cfg, m, (m.0 + fx * k, m.1 + fy * k), depth.saturating_sub(1), fi.min(1.0), false, rng, out, mains);
bolt(cfg, BoltBranch { start: m, end: (m.0 + fx * k, m.1 + fy * k), depth: depth.saturating_sub(1), intensity: fi.min(1.0), main: false }, rng, output);
}
}
@@ -772,10 +787,9 @@ pub fn lightning_segments(ctx: &EffectCtx, b: &Buf) -> Vec<Seg> {
fork_strength: (g("expertSettings/forkStrength", 50.0) / 100.0).clamp(0.0, 1.0) as f32,
fork_var: (g("expertSettings/forkVariation", 0.0) / 100.0).clamp(0.0, 1.0) as f32,
};
let mut segs = Vec::new();
let mut mains = Vec::new();
let mut output = BoltOutput::default();
let (h, w) = (b.img.height as f64, b.img.width as f64);
let mut go = |a, z, depth, cfg: &BoltCfg, rng: &mut Rng| bolt(cfg, a, z, depth, 1.0, true, rng, &mut segs, &mut mains);
let mut go = |a, z, depth, cfg: &BoltCfg, rng: &mut Rng| bolt(cfg, BoltBranch { start: a, end: z, depth, intensity: 1.0, main: true }, rng, &mut output);
// LIGHTNING_TYPES order.
match kind {
// Direction: travels toward the Direction point and beyond.
@@ -811,6 +825,7 @@ pub fn lightning_segments(ctx: &EffectCtx, b: &Buf) -> Vec<Seg> {
// Strike.
_ => go(o, d, depth, &cfg, &mut rng),
}
let BoltOutput { segments: mut segs, mains } = output;
let dist = |p: (f64, f64)| ((p.0 - o.0).powi(2) + (p.1 - o.1).powi(2)).sqrt();
let far = segs.iter().map(|(_, z, _)| dist(*z)).fold(1e-9, f64::max);
// Decay Main Core: the whole bolt fades with distance from the origin.
+1 -1
View File
@@ -1,5 +1,5 @@
use super::raster::{Seg, mask_px, over, poly_segs, raster_segs};
use super::*;
use super::{Buf, EffectCtx, IndexedParallelIterator, IntoParallelRefMutIterator, PI, ParallelIterator, poly_length, premul};
// ---------------------------------------------------------------- Audio Spectrum / Waveform
+1 -1
View File
@@ -1,4 +1,4 @@
use super::*;
use super::{EffectSpec, ParamUi, Value, slider};
pub(super) fn spec(id: &'static str, name: &'static str, params: Vec<crate::ParamSpec>, render: crate::RenderFn) -> EffectSpec {
EffectSpec { id, name, category: "Generate", params, render, gpu: false, float: true }
+4 -1
View File
@@ -1,5 +1,8 @@
use super::raster::over;
use super::*;
use super::{
Buf, EffectCtx, IndexedParallelIterator, IntoParallelRefIterator, IntoParallelRefMutIterator, PI, ParallelIterator, Plane, Px, gauss_plane, hash1,
hsl_to_rgb, morph_frac, premul, rgb_to_hsl, smoothstep, unpremul,
};
pub(super) fn glue_gun(ctx: &EffectCtx, mut b: Buf) -> Buf {
let pr = ctx.params;
+1 -1
View File
@@ -1,5 +1,5 @@
use super::raster::over;
use super::*;
use super::{Buf, EffectCtx, IndexedParallelIterator, ParallelIterator, ParallelSliceMut, Px, hsl_to_rgb};
// ---------------------------------------------------------------- Fractal
+4 -1
View File
@@ -1,5 +1,8 @@
use super::raster::{Seg, all_masks_px, chaikin, mask_px, over, poly_segs, raster_convex, raster_segs, scan_runs, stroke_outline, trim_poly};
use super::*;
use super::{
Buf, EffectCtx, Image, IndexedParallelIterator, IntoParallelRefIterator, IntoParallelRefMutIterator, ParallelIterator, Params, Plane, gauss_plane, hash1,
layer_or_self, luminance, poly_length, smoothstep, unpremul,
};
/// How painted coverage meets the layer: 0 On Original Image, 1 On Transparent, 2 Reveal
/// Original Image.
+1 -2
View File
@@ -1,4 +1,4 @@
use super::*;
use super::{Buf, EffectCtx, IndexedParallelIterator, ParallelIterator, ParallelSliceMut, Plane, Px, smoothstep};
// ---------------------------------------------------------------- shared rasterisation
@@ -147,7 +147,6 @@ pub(super) fn chaikin(pts: &[[f64; 2]], iters: usize) -> Vec<[f64; 2]> {
// ---------------------------------------------------------------- Scribble
/// Scan runs (start, end along direction `d`) of the fill region on the line `n·p = s`.
#[allow(clippy::too_many_arguments)]
pub(super) fn scan_runs(polys: &[(Vec<[f64; 2]>, bool, bool)], fill: u32, ew: f64, d: [f64; 2], n: [f64; 2], s: f64, out: &mut Vec<(f64, f64)>) {
out.clear();
for (pts, _closed, inverted) in polys {
+1 -1
View File
@@ -4,7 +4,7 @@ use super::fill::{eyedropper_fill, glue_gun, paint_bucket, threads};
use super::fractal::{FRACTAL_OVERSAMPLE, FRACTAL_PALETTES, FRACTAL_SETS, fractal};
use super::paths::{scribble, stroke, vegas, write_on};
use super::raster::{END_CAPS, JOINS};
use super::*;
use super::{EffectSpec, ParamUi, Value, col, num, p, popup, slider};
// ---------------------------------------------------------------- registry
+25 -12
View File
@@ -358,20 +358,24 @@ pub enum FrameMatte {
Outside,
}
/// The matte of layer frame `f` on the layer grid of `img` (the effect's input on that frame at
/// `scale`), computing what is missing. `frame(g)`: the input on frame g on the same grid.
#[allow(clippy::too_many_arguments)]
/// The layer grid and timing used to compute one object matte.
pub struct MatteFrame<'a> {
pub layer_size: [f64; 2],
pub fps: f64,
pub frame: i64,
pub scale: f64,
pub img: &'a Image,
}
/// Compute the object's alpha for one layer frame, loading other frames on the same grid as needed.
pub fn frame_matte(
params: &Params,
layer_size: [f64; 2],
fps: f64,
f: i64,
scale: f64,
img: &Image,
input: &MatteFrame<'_>,
model: Option<Arc<dyn MaskModel>>,
frame: &mut dyn FnMut(i64) -> Option<Arc<Image>>,
cancel: &dyn Fn() -> bool,
) -> FrameMatte {
let MatteFrame { layer_size, fps, frame: f, scale, img } = *input;
let Some(chain) = chain(params, layer_size, fps, f, scale, model) else { return FrameMatte::NoPrompt };
if !chain.data.in_span(f) {
return FrameMatte::Outside;
@@ -408,7 +412,7 @@ fn render(ctx: &EffectCtx, buf: Buf) -> Buf {
if let Some(a) = frozen_alpha(ctx.params, layer_size, fps, f, w, h, model.as_deref()) {
return compose(&buf, Some(&a), w, h, b(ctx.params, "invert"), param(ctx.params, "view").map(Value::as_enum).unwrap_or(0) == 1);
}
let alpha = match frame_matte(ctx.params, layer_size, fps, f, sc, &img, model, &mut frame, &|| false) {
let alpha = match frame_matte(ctx.params, &MatteFrame { layer_size, fps, frame: f, scale: sc, img: &img }, model, &mut frame, &|| false) {
FrameMatte::Alpha(a) if a.len() == w * h => Some(a),
FrameMatte::NoPrompt => return buf,
_ => None,
@@ -608,7 +612,11 @@ mod tests {
assert_ne!(seed(&p, [100.0, 80.0], 25.0, Some(stub.as_ref())), seed(&p, [100.0, 80.0], 25.0, None));
let img = frame_at(0);
let mut frame = |g: i64| Some(Arc::new(frame_at(g)));
let FrameMatte::Alpha(a) = frame_matte(&p, [100.0, 80.0], 25.0, 0, 1.0, &img, Some(stub.clone()), &mut frame, &|| false) else { panic!("no matte") };
let FrameMatte::Alpha(a) =
frame_matte(&p, &MatteFrame { layer_size: [100.0, 80.0], fps: 25.0, frame: 0, scale: 1.0, img: &img }, Some(stub.clone()), &mut frame, &|| false)
else {
panic!("no matte")
};
assert!(a[40 * 100 + 40] > 0.5 && a[5 * 100 + 5] < 0.5);
assert_eq!(stub.calls.load(std::sync::atomic::Ordering::Relaxed), 1);
assert_eq!(*stub.bbox.lock().unwrap(), None);
@@ -634,7 +642,11 @@ mod tests {
let stub = Arc::new(Stub::default());
let img = frame_at(0);
let mut frame = |g: i64| Some(Arc::new(frame_at(g)));
let FrameMatte::Alpha(a) = frame_matte(&p, [100.0, 80.0], 25.0, 0, 1.0, &img, Some(stub.clone()), &mut frame, &|| false) else { panic!("no matte") };
let FrameMatte::Alpha(a) =
frame_matte(&p, &MatteFrame { layer_size: [100.0, 80.0], fps: 25.0, frame: 0, scale: 1.0, img: &img }, Some(stub.clone()), &mut frame, &|| false)
else {
panic!("no matte")
};
assert_eq!(*stub.bbox.lock().unwrap(), Some([74.0, 54.0, 100.0, 80.0]));
assert!(a[70 * 100 + 90] > 0.5 && a[40 * 100 + 40] < 0.5);
// At half resolution the box is scaled with the frame (another Input Key, so the
@@ -643,7 +655,8 @@ mod tests {
p.values.insert(INPUT_KEY.into(), Value::Str("half".into()));
let half = effectcraft_raster::resample(&img, 50, 40);
let mut frame = |g: i64| Some(Arc::new(effectcraft_raster::resample(&frame_at(g), 50, 40)));
let _ = frame_matte(&p, [100.0, 80.0], 25.0, 0, 0.5, &half, Some(stub.clone()), &mut frame, &|| false);
let _ =
frame_matte(&p, &MatteFrame { layer_size: [100.0, 80.0], fps: 25.0, frame: 0, scale: 0.5, img: &half }, Some(stub.clone()), &mut frame, &|| false);
assert_eq!(*stub.bbox.lock().unwrap(), Some([37.0, 27.0, 50.0, 40.0]));
}
+1 -1
View File
@@ -1,4 +1,4 @@
use super::*;
use super::{ParamUi, slider};
// ---------------------------------------------------------------- ASC CDL
+1 -1
View File
@@ -1,7 +1,7 @@
use super::cdl::Cdl;
use super::lut::load_file;
use super::space::{ACES_CCT, ACES_CG, ACES2065_1, D65, IDENTITY, P3, REC709, REC2020, SPACES, Space, Tf, Xform, matrix, sp, space_names};
use super::*;
use super::{Buf, ColorSpace, EffectCtx, EffectSpec, IntoParallelRefMutIterator, ParallelIterator, Value, invert, mul, mul_vec, p, popup, unpremul};
// ---------------------------------------------------------------- displays, views and looks
+1 -1
View File
@@ -1,5 +1,5 @@
use super::cdl::{Cdl, parse_cdl_xml};
use super::*;
use super::{Arc, HashMap, Lut, Mutex, OnceLock, parse_cube};
// ---------------------------------------------------------------- LUT files
+1 -1
View File
@@ -2,7 +2,7 @@ use super::cdl::Cdl;
use super::effects::{DISPLAYS, cdl_from_params, custom_xf, looks, parse_looks, profile, space_at};
use super::lut::{FileXform, load_file};
use super::space::{ACES_CCT, D65, IDENTITY, Space, Tf, Xform, matrix};
use super::*;
use super::{Arc, EffectCtx, Lut, invert, mul};
// ---- Colour programs (the GPU compositor's colour-management kernels)
+1 -1
View File
@@ -4,7 +4,7 @@ use super::effects::{
space_param, spec,
};
use super::space::SPACES;
use super::*;
use super::{EffectSpec, ParamUi, Value, num, p, popup, slider};
pub fn specs() -> Vec<EffectSpec> {
let cc = "Color Correction";
+1 -1
View File
@@ -1,4 +1,4 @@
use super::*;
use super::{invert, mul, mul_vec, rgb_to_xyz};
pub(super) type M3 = [[f64; 3]; 3];
+1 -1
View File
@@ -634,7 +634,7 @@ fn render(ctx: &EffectCtx, buf: Buf) -> Buf {
let (w, h) = (img.width as usize, img.height as usize);
let mp = matte_params(ctx.params);
let mut rgb = rb::rgb_of(&img);
let map = refine::decontaminate(&mut rgb, &alpha, &band, w, h, &mp, rr * sc, sc);
let map = refine::decontaminate(&mut rgb, &alpha, &band, (w, h), &mp, rr * sc, sc);
let view_map = b(ctx.params, "refineEdgeMatte/decontamination/viewDecontaminationMap");
let (ox, oy) = (buf.offset[0].round() as i64, buf.offset[1].round() as i64);
for y in 0..h {
+1 -1
View File
@@ -1,4 +1,4 @@
use super::*;
use super::{EffectSpec, ParamUi, Value, hash1, slider};
pub(crate) fn spec(id: &'static str, name: &'static str, params: Vec<crate::ParamSpec>, render: crate::RenderFn) -> EffectSpec {
EffectSpec { id, name, category: "Simulation", params, render, gpu: false, float: true }
+3 -2
View File
@@ -1,7 +1,7 @@
use super::common::{h, hs};
use super::particle_fx::{PS2_CACHE, PW_CACHE, ps2_phys, pw_phys};
use super::sprites::{Shape, Sprite};
use super::*;
use super::{Buf, EffectCtx, Image, IntoParallelRefMutIterator, ParallelIterator, SimCache, params_key_except, value_noise};
// ------------------------------------------------------------------ particle engine
@@ -263,7 +263,8 @@ pub(super) fn particle_look(q: &Particle, birth: [f32; 4], death: [f32; 4], bsiz
(c, size.max(0.0))
}
pub(super) fn type_sprite(kind: u32, x: f32, y: f32, r: f32, c: [f32; 4], vx: f32, vy: f32, rot: f32) -> Sprite {
pub(super) fn type_sprite(kind: u32, x: f32, y: f32, r: f32, c: [f32; 4], velocity: [f32; 2], rot: f32) -> Sprite {
let [vx, vy] = velocity;
let vl = (vx * vx + vy * vy).sqrt().max(1e-6);
let shape = match kind {
0 => Shape::Line { dx: -vx / vl * r * 2.0, dy: -vy / vl * r * 2.0 },
+3 -3
View File
@@ -1,6 +1,6 @@
use super::engine::{PState, PW_EFFECT_CAMERA, Phys, accelerated, norm3, particle_look, sim_key, simulate, type_sprite};
use super::sprites::{Acc, Post, Sprite, SpritePlan, raster, splat};
use super::*;
use super::{Buf, EffectCtx, Image, IntoParallelRefIterator, ParallelIterator, SimCache, Value, params_key, unpremul};
// ------------------------------------------------------------------ CC Particle World
@@ -169,7 +169,7 @@ pub(super) fn particle_world_plan(ctx: &EffectCtx, b: &Buf) -> Option<SpritePlan
let PwSeen { depth, x, y, k, v } = cam.see(q.p, q.v, lw, lh, b)?;
let (c, size) = particle_look(q, birth, death, bs, ds, svar, max_op);
let r = size * 0.5 * lw * k * s * 0.25;
let sp = type_sprite(kind, x, y, r, c, v[0] * lw * k * s, v[1] * lw * k * s, (q.id % 628) as f32 * 0.01 + q.age * 2.0);
let sp = type_sprite(kind, x, y, r, c, [v[0] * lw * k * s, v[1] * lw * k * s], (q.id % 628) as f32 * 0.01 + q.age * 2.0);
Some((depth, sp))
})
.collect();
@@ -242,7 +242,7 @@ pub(super) fn particle_systems2_plan(ctx: &EffectCtx, b: &Buf) -> SpritePlan {
let (bx, by) = b.to_px([(q.p[0] * unit) as f64, (q.p[1] * unit) as f64]);
let (c, size) = particle_look(q, birth, death, bs, ds, svar, max_op);
let r = size * unit * 0.02 * s;
type_sprite(kind, bx as f32, by as f32, r, c, q.v[0] * unit * s, q.v[1] * unit * s, (q.id % 628) as f32 * 0.01 + q.age * 2.0)
type_sprite(kind, bx as f32, by as f32, r, c, [q.v[0] * unit * s, q.v[1] * unit * s], (q.id % 628) as f32 * 0.01 + q.age * 2.0)
})
.collect();
particle_sprites(sprites, pr.e("particle/transferMode"))
+1 -1
View File
@@ -3,7 +3,7 @@ use super::engine::{PARTICLE_TYPES, PW_ANIMS};
use super::particle_fx::{PW_FOV, mr_mercury, particle_systems2, particle_systems2_plan, particle_world, particle_world_plan};
use super::sprites::SpritePlan;
use super::weather::{bubbles, drizzle, rainfall, rainfall_plan, snowfall, snowfall_plan, star_burst, star_burst_plan};
use super::*;
use super::{Buf, EffectCtx, EffectSpec, ParamUi, Value, col, num, p, popup, slider};
/// The sprite plan of a sprite-drawing simulation effect for buffer geometry `b` (its pixels
/// are not read), `None` for other effects or settings drawn otherwise (the GPU compositor's
+1 -1
View File
@@ -1,4 +1,4 @@
use super::*;
use super::{Buf, Image, IndexedParallelIterator, IntoParallelRefIterator, IntoParallelRefMutIterator, ParallelIterator, ParallelSliceMut, Px, unpremul};
// ------------------------------------------------------------------ band rasteriser
+1 -1
View File
@@ -1,6 +1,6 @@
use super::common::{h, hs};
use super::sprites::{Acc, Post, Shape, Sprite, SpritePlan, Tint, aa, raster};
use super::*;
use super::{Buf, EffectCtx, Image, IntoParallelIterator, ParallelIterator, unpremul};
// ------------------------------------------------------------------ CC Rainfall / CC Snowfall
+4 -1
View File
@@ -1,4 +1,7 @@
use super::*;
use super::{
Acc, Buf, EffectCtx, Image, IndexedParallelIterator, IntoParallelRefIterator, IntoParallelRefMutIterator, ParallelIterator, Plane, Px, Shape, SimCache,
Sprite, Value, hs, params_key, splat, unpremul, value_noise,
};
// ------------------------------------------------------------------ Foam
+1 -1
View File
@@ -1,5 +1,5 @@
use super::shatter::{ShatterPlan, card_dance_plan, shatter_impl};
use super::*;
use super::{Acc, Buf, EffectCtx, Image, Lighting, Proj, raster, unpremul};
// ------------------------------------------------------------------ 3D pieces
+1 -1
View File
@@ -1,6 +1,6 @@
use super::pieces::{Cam, Piece, PiecePlan, PieceTex, rot_axis, sort_far_first};
use super::shatter::{ShatterPlan, shatter_impl};
use super::*;
use super::{Acc, Buf, EffectCtx, IntoParallelIterator, ParallelIterator, Post, Shape, Sprite, SpritePlan, h, hs, unpremul};
// ------------------------------------------------------------------ CC Ball Action
+1 -1
View File
@@ -2,7 +2,7 @@ use super::foam::{FOAM_TEXTURES, foam};
use super::pixels::{ball_action, pixel_polly, scatterize};
use super::shatter::{CD_PROPS, CD_SOURCES, SHATTER_PATTERNS, SHATTER_TEXTURE_MODES, card_dance, shatter};
use super::surfaces::{caustics, wave_world};
use super::*;
use super::{EffectSpec, ParamUi, Value, col, num, p, pct, popup, pt, slider, spec};
// ------------------------------------------------------------------ specs
+4 -1
View File
@@ -1,5 +1,8 @@
use super::pieces::{M3, Piece, PiecePlan, PieceTex, inv3, rot_axis, rot_xyz, sort_far_first};
use super::*;
use super::{
Acc, Buf, EffectCtx, Image, IndexedParallelIterator, IntoParallelIterator, IntoParallelRefIterator, Lighting, ParallelIterator, Post, Shape, Sprite,
SpritePlan, Value, h, hs, layer_or_self, unpremul,
};
// ------------------------------------------------------------------ Card Dance
+1 -1
View File
@@ -1,4 +1,4 @@
use super::*;
use super::{Acc, Buf, EffectCtx, Image, IntoParallelIterator, ParallelIterator, Plane, Px, Shape, SimCache, Sprite, Value, gauss_plane, params_key, unpremul};
// ------------------------------------------------------------------ Caustics
+3 -2
View File
@@ -575,7 +575,8 @@ fn mapper_from(ctx: &EffectCtx, group: &str, ephemeral: bool) -> Option<Mapper>
/// Draw a map layer frame as a particle: centred on (x, y) buffer px, rotated `angle` degrees
/// and scaled, over `out`.
fn blit_layer(out: &mut Image, lp: &crate::LayerPixels, x: f64, y: f64, angle: f64, scale: [f64; 2], buf_scale: f64, alpha: f32) {
fn blit_layer(out: &mut Image, lp: &crate::LayerPixels, position: [f64; 2], angle: f64, scale: [f64; 2], buf_scale: f64, alpha: f32) {
let [x, y] = position;
let (w, h) = (lp.size[0] * scale[0].abs() * buf_scale, lp.size[1] * scale[1].abs() * buf_scale);
let r = (w * w + h * h).sqrt() * 0.5 + 1.0;
let (x0, x1) = ((x - r).floor().max(0.0) as i64, (x + r).ceil().min(out.width as f64) as i64);
@@ -925,7 +926,7 @@ impl PgPlan {
pub fn finish(&self, mut b: Buf) -> Buf {
let mut fx = splat(b.img.width, b.img.height, &self.sprites.sprites, self.sprites.acc);
for q in &self.blits {
blit_layer(&mut fx, &self.frames[q.frame], q.x, q.y, q.angle, q.scale, b.scale, q.alpha);
blit_layer(&mut fx, &self.frames[q.frame], [q.x, q.y], q.angle, q.scale, b.scale, q.alpha);
}
// Particles replace the layer's own pixels.
b.img = combine(&b.img, &fx, 5);
+11 -14
View File
@@ -453,7 +453,7 @@ fn render(ctx: &EffectCtx, buf: Buf) -> Buf {
});
let need = need_m.into_inner().unwrap_or_else(std::sync::PoisonError::into_inner);
if synth && need.iter().any(|w| *w > 0.0) {
synthesize(ctx, &a, k, &src_of, &crop, sc, &mut out, &need);
synthesize(ctx, &a, k, &SynthesisGrid { src_of: &src_of, crop: &crop, scale: sc }, &mut out, &need);
}
if b(ctx.params, "advanced/showTrackPoints") {
let r = (2.5 * f(ctx.params, "advanced/trackPointSize") / 100.0 * sc).max(0.5);
@@ -465,19 +465,16 @@ fn render(ctx: &EffectCtx, buf: Buf) -> Buf {
Buf { img: out, offset: [0.0; 2], scale: sc }
}
/// Fill the pixels flagged in `need` (weight = how much synthesized content they take) from
/// neighbouring frames, nearest first.
#[allow(clippy::too_many_arguments)]
fn synthesize(
ctx: &EffectCtx,
a: &WarpAnalysis,
k: usize,
src_of: &(dyn Fn([f64; 2]) -> [f64; 2] + Sync),
crop: &[f64; 4],
sc: f64,
out: &mut Image,
need: &[f32],
) {
/// Maps an output sample into the uncropped source grid shared by neighbouring frames.
struct SynthesisGrid<'a> {
src_of: &'a (dyn Fn([f64; 2]) -> [f64; 2] + Sync),
crop: &'a [f64; 4],
scale: f64,
}
/// Fill the pixels flagged in `need` from neighbouring frames, nearest first.
fn synthesize(ctx: &EffectCtx, a: &WarpAnalysis, k: usize, grid: &SynthesisGrid<'_>, out: &mut Image, need: &[f32]) {
let SynthesisGrid { src_of, crop, scale: sc } = *grid;
let Some(host) = ctx.env.host else { return };
let n = a.frames.len();
let range = ((f(ctx.params, "advanced/synthesizeInputRange").max(0.0) / a.frame_duration.max(1e-6)).round() as usize).min(60);
+12 -1
View File
@@ -450,7 +450,18 @@ async fn run_work(w: Work, shared: &RotoShared) {
(RotoTask::Propagate, true) => om::ensure(&w.params, w.size, w.fps, *f, model.clone(), &mut get, &cancel).is_some(),
(RotoTask::Freeze, true) => {
let img = get(*f);
match img.map(|img| (om::frame_matte(&w.params, w.size, w.fps, *f, 1.0, &img, model.clone(), &mut get, &cancel), img)) {
match img.map(|img| {
(
om::frame_matte(
&w.params,
&om::MatteFrame { layer_size: w.size, fps: w.fps, frame: *f, scale: 1.0, img: &img },
model.clone(),
&mut get,
&cancel,
),
img,
)
}) {
Some((om::FrameMatte::Alpha(a), img)) => {
frozen.size = [img.width as usize, img.height as usize];
frozen.frames.insert(*f, fx::freeze_frame(&a, &[]));
+1 -1
View File
@@ -156,7 +156,7 @@ mod enabled;
mod params;
mod schema;
use super::*;
use super::commands;
pub use enabled::always;
pub(crate) use enabled::{has_comp, has_keys, has_layers, has_project_selection, not_yet, not_yet_run};
pub use params::match_path_of;
+1 -1
View File
@@ -1,4 +1,4 @@
use super::*;
use super::{EngineError, Result, Session, Value};
// ---------- enablement ----------
+4 -1
View File
@@ -1,5 +1,8 @@
use super::create::{apply_text_params, text_range_p};
use super::*;
use super::{
EngineError, GroupKind, Ids, ItemId, KV, LayerId, MaskMode, Node, Project, PropGroup, Result, Session, TextDoc, Value, b_p, bad, build, json, layer_mut,
layer_p, merge_p, str_p,
};
pub(super) fn add_mask(s: &mut Session, p: &Value) -> Result<Value> {
let (cid, lid) = layer_p(s, p, "layer.addMask")?;
+4 -1
View File
@@ -1,4 +1,7 @@
use super::*;
use super::{
EngineError, Ids, ItemId, ItemKind, Justify, KV, Label, Layer, LayerId, LayerSource, Project, Result, Session, Solid, TextDoc, Tick, Value, b_p, bad,
build, comp_id, f_p, json, limits, str_p,
};
pub(crate) fn color_p(p: &Value, k: &str) -> Option<[f32; 3]> {
match p.get(k)? {
+2 -1
View File
@@ -1,4 +1,4 @@
use super::*;
use super::{Comp, EngineError, ItemId, KV, Layer, LayerId, PropGroup, Result, Session, Tick, Value, b_p, bad, layers_p, resolve_layer};
/// Re-express a layer in its new parent space without moving its anchor.
pub(crate) struct ParentFix {
@@ -336,6 +336,7 @@ pub(super) fn set_parent(s: &mut Session, p: &Value) -> Result<Value> {
#[cfg(test)]
mod parent_chain_tests {
use super::super::json;
use super::*;
use std::sync::Arc;
+4 -1
View File
@@ -1,4 +1,7 @@
use super::*;
use super::{
EngineError, FootageKind, Ids, ItemId, ItemKind, Layer, LayerSource, Project, Result, Session, Value, bad, build, has_comp, json, layer_mut, layers_p,
unlocked,
};
/// A layer whose source is an item (footage, precomposition or solid), as in After Effects'
/// Replace Source. Text, shape, null, camera, light and 3D model layers have no AVItem source to
+1 -1
View File
@@ -1,4 +1,4 @@
use super::*;
use super::{EngineError, LayerId, Result, Session, Value, b_p, comp_id, json, resolve_layer};
pub(super) fn select(s: &mut Session, p: &Value) -> Result<Value> {
let cid = comp_id(s, p)?;
+4 -1
View File
@@ -1,4 +1,7 @@
use super::*;
use super::{
BlendMode, EngineError, FrameBlend, ItemId, KV, Layer, LayerId, MatteKind, Quality, Result, Session, TrackMatte, Value, b_p, bad, json, layer_mut, layer_p,
layers_p, merge_p, resolve_layer, str_p,
};
pub(super) fn set_switch(s: &mut Session, p: &Value) -> Result<Value> {
let (cid, ids) = layers_p(s, p)?;
+4 -1
View File
@@ -1,4 +1,7 @@
use super::*;
use super::{
EngineError, ItemKind, Label, Layer, LayerId, LayerSource, Result, Session, Tick, Value, b_p, bad, build, f_p, json, layer_mut, layers_p, merge_p,
resolve_layer, str_p, unlocked, with,
};
pub(super) fn timing(s: &mut Session, p: &Value) -> Result<Value> {
let (cid, ids) = layers_p(s, p)?;
+4 -1
View File
@@ -1,5 +1,8 @@
use super::create::{color_p, pixel_aspect_p};
use super::*;
use super::{
EngineError, ItemKind, KV, LayerId, LayerSource, PropGroup, Result, Session, Tick, Value, bad, has_layers, json, layer_mut, layer_p, layers_p, limits,
str_p, unlocked,
};
/// Move Position by `(dx, dy)`, including every key. Separated dimensions move X and Y as well
/// as the combined property the channels were split from.
+1 -1
View File
@@ -1,4 +1,4 @@
use super::*;
use super::{Comp, EngineError, ItemId, Layer, LayerId, Result, Session, Tick, Value};
/// Hand a frontend-only command to the UI ([`crate::Event::Frontend`]).
pub(crate) fn frontend(s: &mut Session, id: &str, p: &Value) -> Result<Value> {
+10 -10
View File
@@ -121,16 +121,16 @@ fn parametric_path(g: &PropGroup, lt: effectcraft_time::Tick) -> Option<ShapePat
let path = match g.match_id.as_str() {
"rect" => effectcraft_path::rect(v2("size", [100.0; 2]), v2("position", [0.0; 2]), f("roundness", 0.0)),
"ellipse" => effectcraft_path::ellipse(v2("size", [100.0; 2]), v2("position", [0.0; 2])),
"star" => effectcraft_path::polystar(
g.prop("type").map_or(0, |pr| pr.value_at(lt).as_enum()) == 0,
f("points", 5.0),
v2("position", [0.0; 2]),
f("rotation", 0.0),
f("innerRadius", 50.0),
f("outerRadius", 100.0),
f("innerRoundness", 0.0),
f("outerRoundness", 0.0),
),
"star" => effectcraft_path::polystar(effectcraft_path::Polystar {
star: g.prop("type").map_or(0, |pr| pr.value_at(lt).as_enum()) == 0,
points: f("points", 5.0),
pos: v2("position", [0.0; 2]),
rotation: f("rotation", 0.0),
inner: f("innerRadius", 50.0),
outer: f("outerRadius", 100.0),
inner_round: f("innerRoundness", 0.0),
outer_round: f("outerRoundness", 0.0),
}),
_ => return None,
};
// A closed parametric shape is one subpath; anything else would not be this shape.
+50 -32
View File
@@ -205,23 +205,8 @@ fn apply_om_template(om: &mut OutputModule, t: &OutputModule) {
/// The `format` parameter's values.
const FORMATS: &str = "format: h264|hevc|av1|prores|uncompressed|webm|png|jpeg|tiff|exr|gif|wav|aiff";
/// Apply Output Module parameters (`template` first). `roi`: the viewer's region of interest
/// (captured by `crop.useRoi`).
fn apply_output(om: &mut OutputModule, templates: &RenderTemplates, roi: Option<[f64; 4]>, p: &Value, cmd: &str) -> Result<bool> {
let mut any = false;
if let Some(t) = str_p(p, "template") {
let t = templates.output_module(t).ok_or_else(|| bad(cmd, format!("no Output Module template “{t}”")))?;
apply_om_template(om, &t);
any = true;
}
let before = om.clone();
if let Some(f) = str_p(p, "format") {
let f = OutputFormat::from_name(f).ok_or_else(|| bad(cmd, FORMATS))?;
om.set_format(f);
}
// Before Channels: the WebM codec decides whether alpha can be written, so an explicit
// `channels: rgba` with `webmCodec: av1` is refused rather than dropped (#166).
any |= apply_codec_options(om, p, cmd)?;
/// `channels` (refusing RGBA where the format has no alpha) and `color`/`alphaMode`.
fn apply_channels_and_alpha(om: &mut OutputModule, p: &Value, cmd: &str) -> Result<()> {
if let Some(c) = str_p(p, "channels") {
om.channels = match c.to_ascii_lowercase().replace([' ', '+'], "").as_str() {
"rgb" => Channels::Rgb,
@@ -244,21 +229,11 @@ fn apply_output(om: &mut OutputModule, templates: &RenderTemplates, roi: Option<
_ => return Err(bad(cmd, "color: straight|premultiplied")),
};
}
if let Some(q) = f_p(p, "quality") {
om.quality = q.clamp(1.0, 100.0) as u8;
}
if let Some(b) = f_p(p, "bitrate") {
om.bitrate_kbps = b.clamp(50.0, 500_000.0) as u32;
}
if let Some(b) = b_p(p, "webmBitrate") {
om.webm_bitrate = b;
}
if let Some(k) = p.get("keyframeInterval").and_then(Value::as_u64) {
om.keyframe_interval = k.min(100_000) as u32;
}
if let Some(pp) = str_p(p, "proresProfile") {
om.prores_profile = ProResProfile::from_name(pp).ok_or_else(|| bad(cmd, "proresProfile: proxy|lt|standard|hq|4444|4444xq"))?;
}
Ok(())
}
/// `crop` plus the flat dialog keys (`cropTop`… and `resizeWidth`/`resizeHeight`).
fn apply_crop(om: &mut OutputModule, roi: Option<[f64; 4]>, p: &Value, cmd: &str) -> Result<()> {
match p.get("crop") {
Some(Value::Bool(b)) => om.crop.enabled = *b,
Some(Value::Object(c)) => {
@@ -298,6 +273,11 @@ fn apply_output(om: &mut OutputModule, templates: &RenderTemplates, roi: Option<
om.resize.enabled = true;
}
}
Ok(())
}
/// `resize` (bool or object).
fn apply_resize(om: &mut OutputModule, p: &Value, cmd: &str) -> Result<()> {
match p.get("resize") {
Some(Value::Bool(b)) => om.resize.enabled = *b,
Some(Value::Object(r)) => {
@@ -328,6 +308,44 @@ fn apply_output(om: &mut OutputModule, templates: &RenderTemplates, roi: Option<
Some(Value::Null) | None => {}
Some(_) => return Err(bad(cmd, "resize: bool or {enabled?, preset?, width?, height?, lockAspect?, quality?: low|high}")),
}
Ok(())
}
/// Apply Output Module parameters (`template` first). `roi`: the viewer's region of interest
/// (captured by `crop.useRoi`).
fn apply_output(om: &mut OutputModule, templates: &RenderTemplates, roi: Option<[f64; 4]>, p: &Value, cmd: &str) -> Result<bool> {
let mut any = false;
if let Some(t) = str_p(p, "template") {
let t = templates.output_module(t).ok_or_else(|| bad(cmd, format!("no Output Module template “{t}”")))?;
apply_om_template(om, &t);
any = true;
}
let before = om.clone();
if let Some(f) = str_p(p, "format") {
let f = OutputFormat::from_name(f).ok_or_else(|| bad(cmd, FORMATS))?;
om.set_format(f);
}
// Before Channels: the WebM codec decides whether alpha can be written, so an explicit
// `channels: rgba` with `webmCodec: av1` is refused rather than dropped (#166).
any |= apply_codec_options(om, p, cmd)?;
apply_channels_and_alpha(om, p, cmd)?;
if let Some(q) = f_p(p, "quality") {
om.quality = q.clamp(1.0, 100.0) as u8;
}
if let Some(b) = f_p(p, "bitrate") {
om.bitrate_kbps = b.clamp(50.0, 500_000.0) as u32;
}
if let Some(b) = b_p(p, "webmBitrate") {
om.webm_bitrate = b;
}
if let Some(k) = p.get("keyframeInterval").and_then(Value::as_u64) {
om.keyframe_interval = k.min(100_000) as u32;
}
if let Some(pp) = str_p(p, "proresProfile") {
om.prores_profile = ProResProfile::from_name(pp).ok_or_else(|| bad(cmd, "proresProfile: proxy|lt|standard|hq|4444|4444xq"))?;
}
apply_crop(om, roi, p, cmd)?;
apply_resize(om, p, cmd)?;
if let Some(a) = p.get("audio") {
om.audio = match a {
Value::Bool(true) => AudioOutput::On,
+1 -1
View File
@@ -1,5 +1,5 @@
use super::params::bad;
use super::*;
use super::{CommandSpec, Result, Value};
// ---------- parameter validation (agents) ----------
+10 -1
View File
@@ -221,7 +221,16 @@ pub fn mask_path(kind: &str, centre: [f64; 2], size: [f64; 2]) -> Option<ShapePa
"rounded" | "roundedRect" => effectcraft_path::rect(size, centre, roundness(size)),
k => {
let (star, points, outer, inner) = polystar_of(k, size)?;
effectcraft_path::polystar(star, points, centre, 0.0, inner, outer, 0.0, 0.0)
effectcraft_path::polystar(effectcraft_path::Polystar {
star,
points,
pos: centre,
rotation: 0.0,
inner,
outer,
inner_round: 0.0,
outer_round: 0.0,
})
}
};
effectcraft_path::from_kurbo(&bez).into_iter().next()
+1 -1
View File
@@ -1,4 +1,4 @@
use super::*;
use super::{Deserialize, ItemId, Layer, LayerId, Serialize, Tick, Uid, commands, mask_track, media_browser, roto, viewer};
/// A keyframe reference: layer, property uid, key time (layer time).
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
+1 -1
View File
@@ -1,4 +1,4 @@
use super::*;
use super::{ItemId, Serialize, Value};
/// Events for frontends (drained each frame).
#[derive(Clone, Debug, PartialEq, Serialize)]
+1 -1
View File
@@ -1,4 +1,4 @@
use super::*;
use super::prefs;
pub fn text_families() -> Vec<String> {
effectcraft_text::families().into_iter().map(|(f, _)| f).collect()
+17 -5
View File
@@ -130,8 +130,20 @@ fn solid_sized(p: &mut Project, comp: &Comp, folder: ItemId, name: &str, color:
build::layer(p, comp, name, LayerSource::Solid { item: sid }, (w, h), None)
}
/// What `card` draws: name, gradient end colors (hex), size, position and Y rotation.
#[derive(Clone, Copy)]
struct CardSpec<'a> {
name: &'a str,
a: &'a str,
b: &'a str,
size: (u32, u32),
pos: [f64; 3],
rot_y: f64,
}
/// A 3D card: a solid with a diagonal gradient and a thin border grid.
fn card(p: &mut Project, comp: &Comp, folder: ItemId, name: &str, a: &str, b: &str, size: (u32, u32), pos: [f64; 3], rot_y: f64) -> Layer {
fn card(p: &mut Project, comp: &Comp, folder: ItemId, spec: &CardSpec) -> Layer {
let CardSpec { name, a, b, size, pos, rot_y } = *spec;
let mut l = solid_sized(p, comp, folder, name, [1.0, 1.0, 1.0], size.0, size.1);
let (w, h) = (size.0 as f64, size.1 as f64);
effect(
@@ -192,11 +204,11 @@ fn showcase_3d(p: &mut Project, solids: ItemId) -> Comp {
set(&mut floor, "materialOptions/specularIntensity", Value::Scalar(10.0));
// Cards at different depths and angles; the centre pair intersects.
let left = card(p, &c, solids, "Card Blue", "#2E7BF0", "#7A4DFF", (520, 340), [520.0, 560.0, 120.0], 28.0);
let right = card(p, &c, solids, "Card Coral", "#FF6A5C", "#FFB347", (520, 340), [1420.0, 560.0, 260.0], -32.0);
let mut x1 = card(p, &c, solids, "Cross A", "#1FD1A5", "#2E7BF0", (460, 460), [960.0, 470.0, 520.0], 40.0);
let left = card(p, &c, solids, &CardSpec { name: "Card Blue", a: "#2E7BF0", b: "#7A4DFF", size: (520, 340), pos: [520.0, 560.0, 120.0], rot_y: 28.0 });
let right = card(p, &c, solids, &CardSpec { name: "Card Coral", a: "#FF6A5C", b: "#FFB347", size: (520, 340), pos: [1420.0, 560.0, 260.0], rot_y: -32.0 });
let mut x1 = card(p, &c, solids, &CardSpec { name: "Cross A", a: "#1FD1A5", b: "#2E7BF0", size: (460, 460), pos: [960.0, 470.0, 520.0], rot_y: 40.0 });
set(&mut x1, "materialOptions/lightTransmission", Value::Scalar(30.0));
let x2 = card(p, &c, solids, "Cross B", "#F0E14A", "#FF6A5C", (460, 460), [960.0, 470.0, 520.0], -40.0);
let x2 = card(p, &c, solids, &CardSpec { name: "Cross B", a: "#F0E14A", b: "#FF6A5C", size: (460, 460), pos: [960.0, 470.0, 520.0], rot_y: -40.0 });
// Hero title in 3D space, gently turning.
let mut title = text_layer(
p,
+1 -1
View File
@@ -1,4 +1,4 @@
use super::*;
use super::config;
/// Host services (file access) injected by the frontend.
pub trait Services: Send + Sync {
+3 -1
View File
@@ -1,7 +1,9 @@
use super::doc_state::{CompSelection, EditorState};
use super::error::{EngineError, Result};
use super::event::Event;
use super::*;
use super::{
Arc, Comp, History, Image, ItemId, LayerId, Project, RenderOpts, Renderer, Session, ThumbnailJob, Tick, Value, camera_track, commands, guard, roto, warp,
};
mod comps;
mod dispatch;
+1 -1
View File
@@ -1,4 +1,4 @@
use super::*;
use super::{Comp, CompSelection, Event, ItemId, LayerId, Result, Session, Tick, Value, commands};
/// `t` as a current time of `comp`: a frame within its duration.
fn comp_frame_at(comp: &Comp, t: Tick) -> Tick {
+1 -1
View File
@@ -1,4 +1,4 @@
use super::*;
use super::{EngineError, Event, Result, Session, Value, commands, guard};
impl Session {
pub fn new() -> Session {
+1 -1
View File
@@ -1,4 +1,4 @@
use super::*;
use super::{Arc, EditorState, EngineError, Event, History, ItemId, Project, Result, Session, camera_track, commands, roto, warp};
impl Session {
/// Apply an undoable edit. `merge`: consecutive edits with the same key fold into one step.
+1 -1
View File
@@ -1,4 +1,4 @@
use super::*;
use super::{EngineError, Image, ItemId, RenderOpts, Renderer, Result, Session, ThumbnailJob, Tick};
impl Session {
/// Render a comp frame with the session's footage + expression hosts.
+1 -1
View File
@@ -1,5 +1,5 @@
use super::choices::{APPEARANCE_MODES, DARK_THEMES, LANGUAGES, LIGHT_THEMES, WINDOW_GRAPHICS};
use super::*;
use super::{Value, json};
// ---------------------------------------------------------------- schema (Settings dialog)
+1 -1
View File
@@ -1,7 +1,7 @@
use super::choices::{APPEARANCE_MODES, DARK_THEMES, LANGUAGES, LIGHT_THEMES, THEMES, WINDOW_GRAPHICS, choices, is_choice, theme_family};
use super::recent::MAX_EFFECTS_FAVORITES;
use super::sections::{CompPreset, default_labels, parse_hex};
use super::*;
use super::{Appearance, General, Map, PREFS_VERSION, Prefs, Startup, Value, json};
/// Bring an older settings document up to [`PREFS_VERSION`].
///
+1 -1
View File
@@ -1,5 +1,5 @@
use super::sections::parse_hex;
use super::*;
use super::{Label, Prefs, Serialize, Tick};
impl Prefs {
/// Display name of a label (Labels settings).
+1 -1
View File
@@ -1,4 +1,4 @@
use super::*;
use super::Prefs;
impl Prefs {
/// Remember a project in File ▸ Open Recent.
+1 -1
View File
@@ -1,5 +1,5 @@
use super::choices::theme_family;
use super::*;
use super::{BTreeMap, Deserialize, Label, Serialize, Value};
/// Unknown keys inside a page, kept so a newer version's settings survive a round trip.
pub(super) type Extra = BTreeMap<String, Value>;
+71 -45
View File
@@ -3,7 +3,7 @@
use effectcraft_effects::camera_tracker as ct;
use effectcraft_geom::vec3;
use effectcraft_project::LayerId;
use effectcraft_project::{Comp, Layer, LayerId, PropGroup};
use effectcraft_raster::Image;
use effectcraft_render::EvalCtx;
use effectcraft_render::three_d::camera::active_camera;
@@ -11,6 +11,7 @@ use effectcraft_track::camtrack::linalg::{self, M3, V3};
use serde_json::{Value, json};
use crate::Session;
use crate::camera_track::Placed;
use crate::tests::track::{H, W, frame_time, setup, texture};
const F: f64 = 260.0;
@@ -77,10 +78,9 @@ fn status(s: &mut Session) -> Value {
s.execute("camera.solveStatus", json!({})).unwrap()
}
#[test]
fn track_camera_solves_and_creates_layers_on_the_plane() {
let (mut s, clip) = tracked(json!({}));
let st = status(&mut s);
/// Solved with a sane error and focal length; returns the Camera Tracker effect group.
fn assert_solved(s: &mut Session, clip: LayerId) -> PropGroup {
let st = status(s);
assert_eq!(st["solved"], true, "{st}");
let err = st["averageError"].as_f64().unwrap();
assert!(err < 0.5, "average error {err}");
@@ -90,9 +90,12 @@ fn track_camera_solves_and_creates_layers_on_the_plane() {
// Method Used / Average Error are reported in the effect.
let g = s.active_comp().unwrap().layer(clip).unwrap().effects().unwrap().groups().next().unwrap().clone();
assert!(matches!(g.prop(ct::METHOD_USED).map(|p| &p.value), Some(effectcraft_keyframe::Value::Str(m)) if !m.is_empty()));
g
}
// Pick floor points on frame 12 (their 3D positions lie on y = 1.5 in the true scene: the
// ones seen below the horizon at the bottom of the frame).
/// Pick floor points on frame 12 (their 3D positions lie on y = 1.5 in the true scene: the
/// ones seen below the horizon at the bottom of the frame).
fn select_floor_points(s: &mut Session) -> Vec<u64> {
s.execute("time.set", json!({"time": frame_time(12).seconds()})).unwrap();
let pts = s.execute("camera.points", json!({})).unwrap();
let list = pts["points"].as_array().unwrap();
@@ -101,32 +104,19 @@ fn track_camera_solves_and_creates_layers_on_the_plane() {
assert!(floor.len() >= 3, "{floor:?}");
s.execute("camera.selectPoints", json!({"points": floor})).unwrap();
assert_eq!(s.state.camera_points.len(), floor.len());
floor
}
// Create Solid and Camera: one undo step, camera + solid.
let r = s.execute_checked("camera.createFromSolve", json!({"kind": "solid"})).unwrap();
let cam = LayerId(r["camera"].as_u64().unwrap());
let solid = LayerId(r["layers"][0].as_u64().unwrap());
let comp = s.active_comp().unwrap().clone();
assert!(comp.layer(cam).unwrap().is_camera());
assert_eq!(comp.layer(cam).unwrap().name, "3D Tracker Camera");
assert_eq!(comp.layers[0].id, cam);
let sl = comp.layer(solid).unwrap();
assert!(sl.switches.three_d);
assert!(sl.name.starts_with("Track Solid"));
// The camera is keyed on every frame.
assert_eq!(comp.layer(cam).unwrap().props.prop("transform/position").unwrap().keys.len(), 25);
// Every solved point projects through the created camera exactly where the solve puts it,
// and onto its tracked position (the footage fills the comp, so layer pixels are comp
// pixels): within 1 px for 95 % of the observations (the rest are the solve's outliers:
// features on occlusion edges), 0.5 px on average.
/// Every solved point projects through the created camera exactly where the solve puts it,
/// and onto its tracked position (the footage fills the comp, so layer pixels are comp
/// pixels): within 1 px for 95 % of the observations (the rest are the solve's outliers:
/// features on occlusion edges), 0.5 px on average.
fn assert_reprojection(s: &Session, comp: &Comp, pl: &Placed, g: &PropGroup) {
let cid = s.active_comp_id().unwrap();
let uid = g.uid;
let pl = crate::camera_track::placed(&s.project, cid, clip, uid).unwrap();
let tracks = ct::tracks(&crate::camera_track::static_params(&g)).unwrap();
let tracks = ct::tracks(&crate::camera_track::static_params(g)).unwrap();
let mut errs = vec![];
for k in [0u32, 6, 12, 18, 24] {
let ctx = EvalCtx::new(&s.project, cid, &comp, frame_time(k));
let ctx = EvalCtx::new(&s.project, cid, comp, frame_time(k));
let camst = active_camera(&ctx);
for sp in pl.solve.visible(k as usize) {
let Some(obs) = tracks.track(sp.id).and_then(|t| t.at(k)) else { continue };
@@ -142,34 +132,31 @@ fn track_camera_solves_and_creates_layers_on_the_plane() {
let p95 = errs[errs.len() * 95 / 100];
let mean = errs.iter().sum::<f64>() / errs.len() as f64;
assert!(p95 < 1.0 && mean < 0.5, "p95 {p95} px, mean {mean} px");
}
// The solid lies on the selected points' plane.
/// The solid lies on the selected points' plane and faces the camera.
fn assert_solid_on_plane(s: &Session, comp: &Comp, sl: &Layer, pl: &Placed, size: f64) {
let cid = s.active_comp_id().unwrap();
let tr = sl.props.sub("transform").unwrap();
let pos = tr.get("position").unwrap().value.as_vec3();
let ori = tr.get("orientation").unwrap().value.as_vec3();
let normal = linalg::mv(&linalg::from_euler_xyz(ori), [0.0, 0.0, -1.0]);
let size = r["target"]["size"].as_f64().unwrap();
for id in &s.state.camera_points {
let w = pl.to_world(pl.solve.point(*id).unwrap().pos);
let d = linalg::dot(linalg::sub(w, pos), normal).abs();
assert!(d < 0.05 * size.max(1.0) + 1.0, "point {id} is {d} px off the plane");
}
// The solid faces the camera.
let ctx = EvalCtx::new(&s.project, cid, &comp, frame_time(12));
let ctx = EvalCtx::new(&s.project, cid, comp, frame_time(12));
let eye = active_camera(&ctx).eye;
assert!(linalg::dot(normal, linalg::sub([eye.x, eye.y, eye.z], pos)) > 0.0);
}
// Undo removes both layers; redo brings them back.
let before = comp.layers.len();
assert!(s.undo());
assert_eq!(s.active_comp().unwrap().layers.len(), before - 2);
assert!(s.redo());
assert_eq!(s.active_comp().unwrap().layers.len(), before);
// A second create reuses the camera: Create Multiple Nulls and Camera.
/// A second create reuses the camera (Create Multiple Nulls and Camera), then Text and
/// Shadow Catcher (+ light).
fn assert_create_kinds(s: &mut Session, cam: LayerId, n_floor: usize) {
let r = s.execute_checked("camera.createFromSolve", json!({"kind": "null", "multiple": true})).unwrap();
assert_eq!(r["camera"].as_u64().unwrap(), cam.0);
assert_eq!(r["layers"].as_array().unwrap().len(), floor.len());
assert_eq!(r["layers"].as_array().unwrap().len(), n_floor);
// Text and Shadow Catcher (+ light).
let r = s.execute_checked("camera.createFromSolve", json!({"kind": "text"})).unwrap();
assert_eq!(r["layers"].as_array().unwrap().len(), 1);
@@ -180,11 +167,12 @@ fn track_camera_solves_and_creates_layers_on_the_plane() {
assert_eq!(comp.layer(ids[0]).unwrap().props.prop("materialOptions/acceptsShadows").unwrap().value.as_enum(), 2);
assert!(comp.layer(ids[1]).unwrap().is_light());
assert!(s.execute_checked("camera.createFromSolve", json!({"kind": "teapot"})).is_err());
}
// Set Ground Plane and Origin: the target becomes the origin; a null created there sits at
// (0, 0, 0) lying on the X-Z plane (its front facing up, −y).
/// Set Ground Plane and Origin, then an explicit target.
fn assert_ground_and_target(s: &mut Session) {
s.execute_checked("camera.setGroundPlane", json!({})).unwrap();
assert_eq!(status(&mut s)["groundPlane"], true);
assert_eq!(status(s)["groundPlane"], true);
let r = s.execute_checked("camera.createFromSolve", json!({"kind": "null"})).unwrap();
let null = LayerId(r["layers"][0].as_u64().unwrap());
let tr = s.active_comp().unwrap().layer(null).unwrap().props.sub("transform").unwrap().clone();
@@ -203,6 +191,44 @@ fn track_camera_solves_and_creates_layers_on_the_plane() {
assert!(linalg::norm(linalg::sub(pos, [10.0, 0.0, 20.0])) < 1e-6, "{pos:?}");
let front = linalg::mv(&linalg::from_euler_xyz(tr.get("orientation").unwrap().value.as_vec3()), [0.0, 0.0, -1.0]);
assert!(linalg::norm(linalg::sub(front, [0.0, 0.0, -1.0])) < 1e-6, "{front:?}");
}
#[test]
fn track_camera_solves_and_creates_layers_on_the_plane() {
let (mut s, clip) = tracked(json!({}));
let g = assert_solved(&mut s, clip);
let floor = select_floor_points(&mut s);
// Create Solid and Camera: one undo step, camera + solid.
let r = s.execute_checked("camera.createFromSolve", json!({"kind": "solid"})).unwrap();
let cam = LayerId(r["camera"].as_u64().unwrap());
let solid = LayerId(r["layers"][0].as_u64().unwrap());
let comp = s.active_comp().unwrap().clone();
assert!(comp.layer(cam).unwrap().is_camera());
assert_eq!(comp.layer(cam).unwrap().name, "3D Tracker Camera");
assert_eq!(comp.layers[0].id, cam);
let sl = comp.layer(solid).unwrap();
assert!(sl.switches.three_d);
assert!(sl.name.starts_with("Track Solid"));
// The camera is keyed on every frame.
assert_eq!(comp.layer(cam).unwrap().props.prop("transform/position").unwrap().keys.len(), 25);
let cid = s.active_comp_id().unwrap();
let uid = g.uid;
let pl = crate::camera_track::placed(&s.project, cid, clip, uid).unwrap();
assert_reprojection(&s, &comp, &pl, &g);
assert_solid_on_plane(&s, &comp, sl, &pl, r["target"]["size"].as_f64().unwrap());
// Undo removes both layers; redo brings them back.
let before = comp.layers.len();
assert!(s.undo());
assert_eq!(s.active_comp().unwrap().layers.len(), before - 2);
assert!(s.redo());
assert_eq!(s.active_comp().unwrap().layers.len(), before);
assert_create_kinds(&mut s, cam, floor.len());
assert_ground_and_target(&mut s);
// Serde keeps the analysis.
let back = effectcraft_project::Project::from_json(&s.project.to_json()).unwrap();
+23 -19
View File
@@ -239,6 +239,28 @@ fn set_param(s: &mut Session, layer: LayerId, path: &str, v: serde_json::Value)
s.execute("prop.set", json!({"layer": layer.0, "path": format!("effects/#1/{path}"), "value": v})).unwrap();
}
/// Stabilize, Crop, Auto-scale (the default) hides the borders; Crop leaves an opaque centre.
fn assert_crop_framing(s: &mut Session, clip: LayerId) {
set_param(s, clip, "borders/framing", json!(2));
let st = s.execute("warp.status", json!({})).unwrap();
assert!(st["autoScale"].as_f64().unwrap() > 100.5, "{st}");
for f in 0..25 {
assert_eq!(transparent(&render(s, f)), 0, "frame {f}");
}
set_param(s, clip, "borders/framing", json!(1));
let st = s.execute("warp.status", json!({})).unwrap();
let crop: [f64; 4] = serde_json::from_value(st["crop"].clone()).unwrap();
for f in [0, 9, 24] {
let img = render(s, f);
for y in (crop[1].ceil() as u32 + 1)..(crop[3].floor() as u32 - 1) {
for x in (crop[0].ceil() as u32 + 1)..(crop[2].floor() as u32 - 1) {
assert!(img.get(x as i64, y as i64)[3] > 0.99, "frame {f} ({x}, {y})");
}
}
assert!(img.get(0, 0)[3] < 0.01);
}
}
#[test]
fn warp_stabilizer_analyzes_and_stabilizes() {
let (mut s, clip, solid) = setup(shaky);
@@ -289,25 +311,7 @@ fn warp_stabilizer_analyzes_and_stabilizes() {
}
set_param(&mut s, clip, "stabilization/result", json!(0));
// Stabilize, Crop, Auto-scale (the default) hides the borders; Crop leaves an opaque centre.
set_param(&mut s, clip, "borders/framing", json!(2));
let st = s.execute("warp.status", json!({})).unwrap();
assert!(st["autoScale"].as_f64().unwrap() > 100.5, "{st}");
for f in 0..25 {
assert_eq!(transparent(&render(&s, f)), 0, "frame {f}");
}
set_param(&mut s, clip, "borders/framing", json!(1));
let st = s.execute("warp.status", json!({})).unwrap();
let crop: [f64; 4] = serde_json::from_value(st["crop"].clone()).unwrap();
for f in [0, 9, 24] {
let img = render(&s, f);
for y in (crop[1].ceil() as u32 + 1)..(crop[3].floor() as u32 - 1) {
for x in (crop[0].ceil() as u32 + 1)..(crop[2].floor() as u32 - 1) {
assert!(img.get(x as i64, y as i64)[3] > 0.99, "frame {f} ({x}, {y})");
}
}
assert!(img.get(0, 0)[3] < 0.01);
}
assert_crop_framing(&mut s, clip);
// Synthesize Edges fills most of the borders from neighbouring frames.
set_param(&mut s, clip, "borders/framing", json!(3));
let synth: usize = (0..25).map(|f| transparent(&render(&s, f))).sum();
+43 -33
View File
@@ -49,45 +49,38 @@ fn camera(s: &mut Session) -> u64 {
s.execute("layer.newCamera", json!({"name": "Cam", "position": [320, 180, -800], "poi": [320, 180, 0], "zoom": 900})).unwrap()["layer"].as_u64().unwrap()
}
#[test]
fn stereo_rig_builds_eye_comps_and_glasses() {
let mut s = session();
let src = cid(&s);
let cam = camera(&mut s);
let items_before = s.project.items.len();
let r = s.execute("camera.stereoRig", json!({"sceneDepth": 4})).unwrap();
assert_eq!(s.project.items.len(), items_before + 3, "left, right and stereo comps");
assert_eq!(r["master"].as_u64(), Some(cam));
// Controls on a hidden null in the source comp.
let ctl = layer_in(&s, src, r["controls"].as_u64().unwrap());
/// Controls on a hidden null in the source comp.
fn assert_stereo_controls(ctl: &Layer) {
assert_eq!(ctl.name, "Stereo 3D Controls");
assert!(!ctl.switches.video);
let fx = ctl.effects().unwrap().groups().next().unwrap().clone();
assert!(matches!(&fx.kind, GroupKind::Effect { effect } if effect == effectcraft_effects::controls::STEREO_CONTROLS));
assert_eq!(fx.get("sceneDepth").unwrap().value, KV::Scalar(4.0));
// Eye comps: an eye camera (two-node, linked by expressions) and the collapsed source comp.
let d = 0.04 * 640.0;
for (key, name, x) in [("leftComp", "Scene Left Eye", 320.0 - d / 2.0), ("rightComp", "Scene Right Eye", 320.0 + d / 2.0)] {
let ec = ItemId(r[key].as_u64().unwrap());
assert_eq!(s.project.item(ec).unwrap().name, name);
let c = s.project.comp(ec).unwrap();
assert_eq!((c.width, c.height), (640, 360));
assert_eq!(c.layers.len(), 2);
let cam = &c.layers[0];
assert!(cam.is_camera() && effectcraft_render::three_d::camera::is_two_node(cam));
assert!(close3(v3(cam, "transform/position"), [x, 180.0, -800.0], 1e-6), "{:?}", v3(cam, "transform/position"));
// Parallel eyes (no convergence): each looks straight ahead.
assert!(close3(v3(cam, "transform/poi"), [x, 180.0, 0.0], 1e-6), "{:?}", v3(cam, "transform/poi"));
assert_eq!(cam.props.prop("cameraOptions/zoom").unwrap().value, KV::Scalar(900.0));
for p in ["transform/position", "transform/poi", "cameraOptions/zoom"] {
let e = cam.props.prop(p).unwrap().expr.clone().unwrap();
assert!(e.text.contains("comp(\"Scene\")") && e.text.contains("Cam"), "{p}: {}", e.text);
}
let nest = &c.layers[1];
assert_eq!(nest.source, LayerSource::Comp { item: src });
assert!(nest.switches.collapse && nest.switches.three_d);
}
/// An eye comp: an eye camera (two-node, linked by expressions) at `x` and the collapsed source comp.
fn assert_eye_comp(s: &Session, src: ItemId, ec: ItemId, name: &str, x: f64) {
assert_eq!(s.project.item(ec).unwrap().name, name);
let c = s.project.comp(ec).unwrap();
assert_eq!((c.width, c.height), (640, 360));
assert_eq!(c.layers.len(), 2);
let cam = &c.layers[0];
assert!(cam.is_camera() && effectcraft_render::three_d::camera::is_two_node(cam));
assert!(close3(v3(cam, "transform/position"), [x, 180.0, -800.0], 1e-6), "{:?}", v3(cam, "transform/position"));
// Parallel eyes (no convergence): each looks straight ahead.
assert!(close3(v3(cam, "transform/poi"), [x, 180.0, 0.0], 1e-6), "{:?}", v3(cam, "transform/poi"));
assert_eq!(cam.props.prop("cameraOptions/zoom").unwrap().value, KV::Scalar(900.0));
for p in ["transform/position", "transform/poi", "cameraOptions/zoom"] {
let e = cam.props.prop(p).unwrap().expr.clone().unwrap();
assert!(e.text.contains("comp(\"Scene\")") && e.text.contains("Cam"), "{p}: {}", e.text);
}
// The stereo comp: left eye on top with 3D Glasses fed by both eyes; right eye hidden.
let nest = &c.layers[1];
assert_eq!(nest.source, LayerSource::Comp { item: src });
assert!(nest.switches.collapse && nest.switches.three_d);
}
/// The stereo comp: left eye on top with 3D Glasses fed by both eyes; right eye hidden.
fn assert_stereo_comp(s: &Session, r: &serde_json::Value) {
let oc = ItemId(r["stereoComp"].as_u64().unwrap());
let out = s.project.comp(oc).unwrap();
assert_eq!(s.project.item(oc).unwrap().name, "Scene Stereo 3D");
@@ -98,6 +91,23 @@ fn stereo_rig_builds_eye_comps_and_glasses() {
assert!(matches!(&g.kind, GroupKind::Effect { effect } if effect == "ec.perspective.3dglasses"));
assert_eq!(g.get("leftView").unwrap().value, KV::Layer(Some(l)));
assert_eq!(g.get("rightView").unwrap().value, KV::Layer(Some(rr)));
}
#[test]
fn stereo_rig_builds_eye_comps_and_glasses() {
let mut s = session();
let src = cid(&s);
let cam = camera(&mut s);
let items_before = s.project.items.len();
let r = s.execute("camera.stereoRig", json!({"sceneDepth": 4})).unwrap();
assert_eq!(s.project.items.len(), items_before + 3, "left, right and stereo comps");
assert_eq!(r["master"].as_u64(), Some(cam));
assert_stereo_controls(&layer_in(&s, src, r["controls"].as_u64().unwrap()));
let d = 0.04 * 640.0;
for (key, name, x) in [("leftComp", "Scene Left Eye", 320.0 - d / 2.0), ("rightComp", "Scene Right Eye", 320.0 + d / 2.0)] {
assert_eye_comp(&s, src, ItemId(r[key].as_u64().unwrap()), name, x);
}
assert_stereo_comp(&s, &r);
// One undo step; redo; serde.
let json = serde_json::to_string(&*s.project).unwrap();
let back: Project = serde_json::from_str(&json).unwrap();
+40 -25
View File
@@ -2,7 +2,7 @@
use effectcraft_color::BlendMode;
use effectcraft_keyframe::Value as KV;
use effectcraft_project::{ItemKind, LayerSource, MaskMode};
use effectcraft_project::{Comp, ItemKind, Layer, LayerSource, MaskMode};
use effectcraft_psd::write::*;
use effectcraft_psd::{DValue, Descriptor, Rect};
use serde_json::json;
@@ -74,22 +74,8 @@ pub(crate) fn sample_psd() -> Vec<u8> {
write(&d)
}
#[test]
fn psd_imports_as_composition() {
let path = tmp("sample.psd");
std::fs::write(&path, sample_psd()).unwrap();
let mut s = Session::default();
let r = s.execute_checked("file.import", json!({"paths": [path], "importAs": "composition"})).unwrap();
assert_eq!(r["errors"], json!([]), "{r}");
let cid = effectcraft_project::ItemId(r["comps"][0].as_u64().unwrap());
assert_eq!(s.state.active_comp, Some(cid));
assert_eq!(s.project.item(cid).unwrap().name, "sample");
let c = s.project.comp(cid).unwrap().clone();
assert_eq!((c.width, c.height), (120, 80));
let names: Vec<&str> = c.layers.iter().map(|l| l.name.as_str()).collect();
assert_eq!(names, ["Title", "Hue Shift", "Vector Masked", "Clipped", "Base", "Group A", "Background"]);
// Text layer: editable text, positioned at the type origin.
let title = &c.layers[0];
/// Text layer: editable text, positioned at the type origin.
fn assert_title_layer(title: &Layer) {
assert!(matches!(title.source, LayerSource::Text));
let Some(KV::Text(doc)) = title.props.prop("text/sourceText").map(|p| p.value.clone()) else { panic!("text") };
assert_eq!(doc.text, "Hello PSD");
@@ -97,14 +83,18 @@ fn psd_imports_as_composition() {
assert_eq!(doc.font, "Inter");
assert_eq!(doc.style, "Bold");
assert_eq!(title.props.prop("transform/position").unwrap().value.as_vec2(), [8.0, 70.0]);
// Adjustment layer with Hue/Saturation.
let hue = &c.layers[1];
}
/// Adjustment layer with Hue/Saturation.
fn assert_hue_layer(hue: &Layer) {
assert!(hue.switches.adjustment);
let fx = hue.effects().unwrap().groups().next().unwrap();
assert_eq!(fx.get("hue").unwrap().value.as_f64(), 40.0);
assert_eq!(fx.get("saturation").unwrap().value.as_f64(), 10.0);
// Vector mask → mask; Stroke style.
let vm = &c.layers[2];
}
/// Vector mask → mask; Stroke style.
fn assert_vector_mask_layer(vm: &Layer) {
let m = vm.masks().unwrap().groups().next().unwrap();
assert!(matches!(m.kind, effectcraft_project::GroupKind::Mask { mode: MaskMode::Add, .. }));
let Some(KV::Path(p)) = m.get("path").map(|p| p.value.clone()) else { panic!("mask path") };
@@ -113,19 +103,23 @@ fn psd_imports_as_composition() {
let st = vm.props.group("layerStyles/stroke").unwrap();
assert_eq!(st.get("size").unwrap().value.as_f64(), 2.0);
assert_eq!(st.get("position").unwrap().value, KV::Enum(1));
// Clipped → preserve transparency; opacity and blend mode.
}
/// Clipped → preserve transparency (opacity, blend mode); drop shadow.
fn assert_clipped_and_shadow(c: &Comp) {
let clipped = &c.layers[3];
assert!(clipped.preserve_transparency);
assert_eq!(clipped.blend_mode, BlendMode::Screen);
assert!((clipped.props.prop("transform/opacity").unwrap().value.as_f64() - 128.0 / 255.0 * 100.0).abs() < 1e-9);
// Drop shadow.
let ds = c.layers[4].props.group("layerStyles/dropShadow").unwrap();
assert_eq!(ds.get("distance").unwrap().value.as_f64(), 9.0);
assert_eq!(ds.get("angle").unwrap().value.as_f64(), 45.0);
assert_eq!(ds.get("useGlobalLight").unwrap().value, KV::Bool(false));
assert_eq!(ds.get("opacity").unwrap().value.as_f64(), 60.0);
// Group → collapsed precomp (pass through) with its layers.
let g = &c.layers[5];
}
/// Group → collapsed precomp (pass through) with its layers; footage items name their Photoshop layer.
fn assert_group_precomp(s: &Session, g: &Layer) {
assert!(g.switches.collapse);
let LayerSource::Comp { item } = g.source else { panic!("precomp") };
let inner = s.project.comp(item).unwrap();
@@ -140,6 +134,27 @@ fn psd_imports_as_composition() {
assert_eq!((sl.name.as_str(), sl.layer_size), ("Masked Square", false));
assert_eq!((f.width, f.height), (120, 80));
assert_eq!(s.project.item(item).unwrap().name, "Masked Square/sample.psd");
}
#[test]
fn psd_imports_as_composition() {
let path = tmp("sample.psd");
std::fs::write(&path, sample_psd()).unwrap();
let mut s = Session::default();
let r = s.execute_checked("file.import", json!({"paths": [path], "importAs": "composition"})).unwrap();
assert_eq!(r["errors"], json!([]), "{r}");
let cid = effectcraft_project::ItemId(r["comps"][0].as_u64().unwrap());
assert_eq!(s.state.active_comp, Some(cid));
assert_eq!(s.project.item(cid).unwrap().name, "sample");
let c = s.project.comp(cid).unwrap().clone();
assert_eq!((c.width, c.height), (120, 80));
let names: Vec<&str> = c.layers.iter().map(|l| l.name.as_str()).collect();
assert_eq!(names, ["Title", "Hue Shift", "Vector Masked", "Clipped", "Base", "Group A", "Background"]);
assert_title_layer(&c.layers[0]);
assert_hue_layer(&c.layers[1]);
assert_vector_mask_layer(&c.layers[2]);
assert_clipped_and_shadow(&c);
assert_group_precomp(&s, &c.layers[5]);
// One undo step removes everything.
s.execute("edit.undo", json!({})).unwrap();
assert!(s.project.comp(cid).is_none());
+8 -11
View File
@@ -270,7 +270,7 @@ async fn sequence(cx: &Cx<'_>, comp: &Comp, w: u32, h: u32, st: &mut State<'_>)
let path = sequence_path(cx.path, f0 + k as i64);
// Encode in memory first: the storage hook decides where it fits.
let mut buf = std::io::Cursor::new(Vec::new());
write_still(cx, &mut buf, fmt, &img, comp, channels, w, h, cx.output.quality)?;
write_still(cx, &mut buf, &img, comp, StillOutput { format: fmt, channels, size: (w, h), quality: cx.output.quality })?;
let data = buf.into_inner();
let path = cx.place(&path, data.len() as u64);
let mut f = cx.create(&path)?;
@@ -287,18 +287,15 @@ async fn sequence(cx: &Cx<'_>, comp: &Comp, w: u32, h: u32, st: &mut State<'_>)
Ok(Report { path: sequence_path(cx.path, f0), frames: 0, width: w, height: h, seconds: 0.0, bytes, audio: false, log: None, overflow: vec![] })
}
#[allow(clippy::too_many_arguments)]
fn write_still<W: std::io::Write + std::io::Seek>(
cx: &Cx,
file: &mut W,
fmt: OutputFormat,
img: &Image,
comp: &Comp,
struct StillOutput {
format: OutputFormat,
channels: Channels,
w: u32,
h: u32,
size: (u32, u32),
quality: u8,
) -> Result<()> {
}
fn write_still<W: std::io::Write + std::io::Seek>(cx: &Cx, file: &mut W, img: &Image, comp: &Comp, output: StillOutput) -> Result<()> {
let StillOutput { format: fmt, channels, size: (w, h), quality } = output;
use image::{ExtendedColorType, ImageEncoder, ImageFormat};
match fmt {
OutputFormat::ExrSequence => {
+4 -4
View File
@@ -45,7 +45,7 @@ fn custom(start: f64, end: f64) -> RenderSettings {
}
fn run_with(p: &Project, cid: ItemId, s: &RenderSettings, om: &OutputModule, path: &str, options: JobOptions, cancel: bool) -> (Result<Report, String>, Files) {
run_on(p, cid, s, om, path, options, None, cancel)
run_on(p, cid, s, om, path, options, (None, cancel))
}
/// [`run_with`] with an accelerator.
@@ -56,9 +56,9 @@ fn run_on(
om: &OutputModule,
path: &str,
options: JobOptions,
accel: Option<&dyn Accelerator>,
cancel: bool,
execution: (Option<&dyn Accelerator>, bool),
) -> (Result<Report, String>, Files) {
let (accel, cancel) = execution;
let files: Files = Default::default();
let f = files.clone();
let sink: Box<Sink> = Box::new(move |path: &str, data: Vec<u8>| f.lock().unwrap().push((path.to_string(), data)));
@@ -456,7 +456,7 @@ fn a_job_renders_on_one_compositor() {
let om = OutputModule::for_format(OutputFormat::PngSequence);
let s = custom(0.0, 0.4);
let render = |p: &Project, accel: &GreenAccel, path: &str| {
let (r, files) = run_on(p, cid, &s, &om, path, JobOptions::default(), Some(accel), false);
let (r, files) = run_on(p, cid, &s, &om, path, JobOptions::default(), (Some(accel), false));
r.expect("export");
(0..10).map(|i| png(&files, i)).collect::<Vec<_>>()
};
+27 -24
View File
@@ -313,6 +313,32 @@ impl<'a> Resolver<'a> {
self.try_resolve(req).unwrap_or(Resp::Null)
}
/// Resolve an ancestor from its UID path while preserving child indexing.
fn ancestor(&self, comp: u64, layer: u64, path: &str, up: i64) -> Option<Resp> {
let l = self.layer(comp, layer)?;
// The node's full `@uid/@uid/…` path from the layer: its ancestors are its prefixes.
let uid = match self.prop(comp, layer, path) {
Some((_, p)) => p.uid,
None => l.props.node(path)?.uid(),
};
let full = l.props.path_of(uid)?;
let segs: Vec<&str> = full.split('/').collect();
let up = usize::try_from(up).ok().filter(|u| *u >= 1 && *u <= segs.len())?;
if up == segs.len() {
return Some(Resp::Str("layer".into()));
}
let keep = segs.len() - up;
let group_path = segs.get(..keep)?.join("/");
let parent = match keep {
1 => &l.props,
_ => l.props.group(&segs.get(..keep - 1)?.join("/"))?,
};
let uid = segs.get(keep - 1)?.strip_prefix('@')?.parse::<u64>().ok()?;
let index = parent.children.iter().position(|c| c.uid() == uid)?;
let n = parent.children.get(index)?;
Some(node_resp(group_path, n, index + 1))
}
fn try_resolve(&self, req: &Req) -> Option<Resp> {
let project = self.ctx.project;
Some(match req {
@@ -373,30 +399,7 @@ impl<'a> Resolver<'a> {
let path = if group.is_empty() { format!("@{}", n.uid()) } else { format!("{group}/@{}", n.uid()) };
node_resp(path, n, index)
}
Req::Ancestor { comp, layer, path, up } => {
let l = self.layer(*comp, *layer)?;
// The node's full `@uid/@uid/…` path from the layer: its ancestors are its prefixes.
let uid = match self.prop(*comp, *layer, path) {
Some((_, p)) => p.uid,
None => l.props.node(path)?.uid(),
};
let full = l.props.path_of(uid)?;
let segs: Vec<&str> = full.split('/').collect();
let up = usize::try_from(*up).ok().filter(|u| *u >= 1 && *u <= segs.len())?;
if up == segs.len() {
return Some(Resp::Str("layer".into()));
}
let keep = segs.len() - up;
let group_path = segs.get(..keep)?.join("/");
let parent = match keep {
1 => &l.props,
_ => l.props.group(&segs.get(..keep - 1)?.join("/"))?,
};
let uid = segs.get(keep - 1)?.strip_prefix('@')?.parse::<u64>().ok()?;
let index = parent.children.iter().position(|c| c.uid() == uid)?;
let n = parent.children.get(index)?;
node_resp(group_path, n, index + 1)
}
Req::Ancestor { comp, layer, path, up } => return self.ancestor(*comp, *layer, path, *up),
Req::PropInfo { comp, layer, path } => {
let (l, p) = self.prop(*comp, *layer, path)?;
let dims = prop_dims(l, p);
+11 -7
View File
@@ -93,7 +93,7 @@ impl ExprHost for Expressions {
return Ok([v.first().copied().unwrap_or(100.0), v.get(1).copied().unwrap_or(100.0), v.get(2).copied().unwrap_or(100.0)]);
};
let value = prop.value_at(layer.layer_time(ctx.time));
let out = evaluate_out(self, ctx, layer, prop, &value, &e.text, [index as f64 + 1.0, total as f64], selector)?;
let out = evaluate_out(self, ctx, layer, prop, &value, &e.text, TextSelector { index_total: [index as f64 + 1.0, total as f64], value: selector })?;
let num = |o: &runtime::Out| match o {
runtime::Out::Num(x) => Some(*x),
runtime::Out::Bool(b) => Some(if *b { 100.0 } else { 0.0 }),
@@ -124,13 +124,17 @@ impl Drop for FrameGuard {
/// Evaluate `text` as the expression of `prop` on `layer` at `ctx.time`, given its keyframed
/// `value`. Other properties' expressions are evaluated through `host`.
pub fn evaluate(host: &dyn ExprHost, ctx: &EvalCtx, layer: &Layer, prop: &Property, value: &Value, text: &str) -> Result<Value, String> {
let out = evaluate_out(host, ctx, layer, prop, value, text, [1.0, 1.0], [100.0; 3])?;
let out = evaluate_out(host, ctx, layer, prop, value, text, TextSelector { index_total: [1.0, 1.0], value: [100.0; 3] })?;
runtime::to_value(&out, value)
}
/// Run an expression and return the raw script result. `text_sel` / `selector` are the text
/// Expression Selector's `[textIndex, textTotal]` and `selectorValue`.
#[allow(clippy::too_many_arguments)]
/// Expression Selector state: `[textIndex, textTotal]` and `selectorValue`.
struct TextSelector {
index_total: [f64; 2],
value: [f64; 3],
}
/// Run an expression and return the raw script result.
fn evaluate_out(
host: &dyn ExprHost,
ctx: &EvalCtx,
@@ -138,9 +142,9 @@ fn evaluate_out(
prop: &Property,
value: &Value,
text: &str,
text_sel: [f64; 2],
selector: [f64; 3],
text_selector: TextSelector,
) -> Result<runtime::Out, String> {
let TextSelector { index_total: text_sel, value: selector } = text_selector;
let owner = Owner { comp: ctx.comp_id.0, layer: layer.id.0, uid: prop.uid, time: ctx.time, text: text.to_string() };
let (depth, cycle) = FRAMES.with(|f| {
let f = f.borrow();
+1 -1
View File
@@ -26,7 +26,7 @@ pub(crate) fn apply(e: &mut Enc, src: &GpuImage, d: &PlaneDof) -> Option<GpuImag
padded
};
let prefix = e.image(w + 1, h);
e.dispatch("bokeh_prefix", &Params::default(), &boosted, None, &prefix, None, (h.div_ceil(64), 1));
e.dispatch("bokeh_prefix", &Params::default(), crate::context::KernelImages { src: &boosted, aux: None, out: &prefix }, None, (h.div_ceil(64), 1));
// Radius map, then every level's spans.
let mut data: Vec<f32> = d.radius.as_ref().clone();
let mut levels = vec![];
+8 -1
View File
@@ -214,7 +214,14 @@ pub(crate) fn draw_run(e: &mut Enc, run: &Run3d, canvas: &GpuImage) -> Option<Gp
let mut p = Params::default();
p.u[0] = [run.planes.len() as u32, run.lights.len() as u32, run.casters.len() as u32, 0];
let out = e.scratch(w, h);
e.dispatch_ext("classic3d", &p, &atlas, Some(canvas), &out, Some(&mb), (w.div_ceil(16), h.div_ceil(16)), Some([&pb, &gb, &lb, &cb]));
e.dispatch_ext(
"classic3d",
&p,
crate::context::KernelImages { src: &atlas, aux: Some(canvas), out: &out },
Some(&mb),
(w.div_ceil(16), h.div_ceil(16)),
Some([&pb, &gb, &lb, &cb]),
);
Some(out)
}
+12 -16
View File
@@ -1089,6 +1089,13 @@ pub(crate) struct Enc<'g> {
owner: u64,
}
/// Images bound to a compute kernel's source, optional auxiliary and output slots.
pub(crate) struct KernelImages<'a> {
pub src: &'a GpuImage,
pub aux: Option<&'a GpuImage>,
pub out: &'a GpuImage,
}
impl Drop for Enc<'_> {
fn drop(&mut self) {
// Unsubmitted work is discarded, so nothing recorded will read pooled textures.
@@ -1162,32 +1169,21 @@ impl<'g> Enc<'g> {
}
/// Run kernel `entry` writing `out` (dispatched over `groups` workgroups).
pub fn dispatch(
&mut self,
entry: &str,
p: &Params,
src: &GpuImage,
aux: Option<&GpuImage>,
out: &GpuImage,
data: Option<&wgpu::Buffer>,
groups: (u32, u32),
) {
self.dispatch_ext(entry, p, src, aux, out, data, groups, None);
pub fn dispatch(&mut self, entry: &str, p: &Params, images: KernelImages<'_>, data: Option<&wgpu::Buffer>, groups: (u32, u32)) {
self.dispatch_ext(entry, p, images, data, groups, None);
}
/// [`Enc::dispatch`] for the kernels in `EXT_ENTRIES`, with their group 1 storage buffers.
#[allow(clippy::too_many_arguments)]
pub fn dispatch_ext(
&mut self,
entry: &str,
p: &Params,
src: &GpuImage,
aux: Option<&GpuImage>,
out: &GpuImage,
images: KernelImages<'_>,
data: Option<&wgpu::Buffer>,
groups: (u32, u32),
ext: Option<[&wgpu::Buffer; 4]>,
) {
let KernelImages { src, aux, out } = images;
let g = self.g;
let Some(pipe) = g.pipelines.get(entry) else {
if !g.unavailable.iter().any(|(e, _)| *e == entry) {
@@ -1243,7 +1239,7 @@ impl<'g> Enc<'g> {
/// Per-pixel kernel over `out`'s size (16×16 workgroups).
pub fn pixels(&mut self, entry: &str, p: &Params, src: &GpuImage, aux: Option<&GpuImage>, out: &GpuImage, data: Option<&wgpu::Buffer>) {
let groups = (out.width.div_ceil(16), out.height.div_ceil(16));
self.dispatch(entry, p, src, aux, out, data, groups);
self.dispatch(entry, p, KernelImages { src, aux, out }, data, groups);
}
/// Copy `src` into a larger zeroed image at (`x`, `y`) (Buf::pad).
+8 -2
View File
@@ -234,7 +234,13 @@ pub(crate) fn box_passes(e: &mut Enc, img: &GpuImage, rx: &[usize], ry: &[usize]
let mut p = Params::default();
p.u[0] = [r as u32, repeat as u32, block, 0];
let out = e.scratch(cur.width, cur.height);
e.dispatch(if vertical { "box_v" } else { "box_h" }, &p, &cur, None, &out, None, (lines.div_ceil(64), n.div_ceil(block)));
e.dispatch(
if vertical { "box_v" } else { "box_h" },
&p,
crate::context::KernelImages { src: &cur, aux: None, out: &out },
None,
(lines.div_ceil(64), n.div_ceil(block)),
);
cur = out;
}
cur
@@ -390,7 +396,7 @@ fn transform(e: &mut Enc, ctx: &EffectCtx, mut b: GBuf) -> Option<GBuf> {
* Mat3::translate(vec2(-anchor.0, -anchor.1));
let sampling = if ctx.params.e("sampling") == 1 { Sampling::Bicubic } else { Sampling::Bilinear };
let empty = e.image(b.img.width, b.img.height);
b.img = ops::warp(e, &empty, &b.img, &m, sampling, effectcraft_color::BlendMode::Normal, opacity, 0, None);
b.img = ops::warp(e, &empty, &b.img, &m, ops::WarpOptions { sampling, mode: effectcraft_color::BlendMode::Normal, opacity, seed: 0 }, None);
Some(b)
}
+1 -1
View File
@@ -165,7 +165,7 @@ fn camera_lens(e: &mut Enc, ctx: &EffectCtx, mut b: GBuf) -> Option<GBuf> {
return None;
}
let pre = e.image(w + 1, h);
e.dispatch("gen_row_prefix", &Params::default(), &src, None, &pre, None, (h.div_ceil(64), 1));
e.dispatch("gen_row_prefix", &Params::default(), crate::context::KernelImages { src: &src, aux: None, out: &pre }, None, (h.div_ceil(64), 1));
Some(pre)
} else {
None
+1 -1
View File
@@ -214,7 +214,7 @@ pub(crate) fn median_image(e: &mut Enc, img: &GpuImage, r: usize, straight: bool
let seg = (4 * r as u32).clamp(32, 512).min(q.width);
let out = e.scratch(q.width, q.height);
let groups = (q.height.div_ceil(e.g.median_kernel.rows_per_workgroup()), q.width.div_ceil(seg));
e.dispatch("fxn_median_huang", &with_u0([r as u32, seg, 0, 0]), &q, None, &out, Some(&levels), groups);
e.dispatch("fxn_median_huang", &with_u0([r as u32, seg, 0, 0]), crate::context::KernelImages { src: &q, aux: None, out: &out }, Some(&levels), groups);
out
}
+2 -2
View File
@@ -879,7 +879,7 @@ fn corner_pin(e: &mut Enc, ctx: &EffectCtx, mut b: GBuf) -> Option<GBuf> {
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);
b.img = ops::warp(e, &empty, &b.img, &m, ops::WarpOptions { sampling: Sampling::Bilinear, mode: BlendMode::Normal, opacity: 1.0, seed: 0 }, None);
Some(b)
}
@@ -989,7 +989,7 @@ pub(crate) fn transform(e: &mut Enc, ctx: &EffectCtx, mut b: GBuf) -> Option<GBu
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 one = ops::warp(e, &empty, &b.img, &transform_matrix(&pr, &b), ops::WarpOptions { sampling, mode: BlendMode::Normal, opacity: op, seed: 0 }, None);
let sum = e.image(w, h);
e.pixels("fxs_add", &Params::default(), &acc, Some(&one), &sum, None);
acc = sum;
+14 -2
View File
@@ -181,7 +181,13 @@ fn sphere_blur(e: &mut Enc, img: &GpuImage, sigma: f64) -> GpuImage {
let mut p = Params::default();
p.u[0][0] = 256;
let out = e.scratch(cur.width, cur.height);
e.dispatch("fxv_box_h", &p, &cur, None, &out, Some(&buf), (cur.height.div_ceil(64), cur.width.div_ceil(256)));
e.dispatch(
"fxv_box_h",
&p,
crate::context::KernelImages { src: &cur, aux: None, out: &out },
Some(&buf),
(cur.height.div_ceil(64), cur.width.div_ceil(256)),
);
cur = out;
}
for &r in &radii {
@@ -192,7 +198,13 @@ fn sphere_blur(e: &mut Enc, img: &GpuImage, sigma: f64) -> GpuImage {
let mut p = Params::default();
p.u[0] = [block, r as u32, 0, 0];
let out = e.scratch(cur.width, cur.height);
e.dispatch("fxv_box_v", &p, &cur, None, &out, None, (cur.width.div_ceil(64), cur.height.div_ceil(block)));
e.dispatch(
"fxv_box_v",
&p,
crate::context::KernelImages { src: &cur, aux: None, out: &out },
None,
(cur.width.div_ceil(64), cur.height.div_ceil(block)),
);
cur = out;
}
cur
+13 -27
View File
@@ -32,37 +32,23 @@ pub fn half(e: &mut Enc, src: &GpuImage) -> GpuImage {
out
}
/// `raster::composite_warp` (and, with `accumulate = Some(weight)`, `accumulate_warp`): draw
/// `src` transformed by `m` (src pixels → dst pixels) onto `dst`.
pub fn warp(
e: &mut Enc,
dst: &GpuImage,
src: &GpuImage,
m: &Mat3,
sampling: Sampling,
mode: BlendMode,
opacity: f32,
seed: u32,
accumulate: Option<f32>,
) -> GpuImage {
warp_q(e, dst, src, m, sampling, mode, opacity, seed, accumulate, None)
/// Sampling and blending policy for a transformed GPU image.
pub struct WarpOptions {
pub sampling: Sampling,
pub mode: BlendMode,
pub opacity: f32,
pub seed: u32,
}
/// Draw `src` transformed by `m` onto `dst`, or accumulate it with a sample weight.
pub fn warp(e: &mut Enc, dst: &GpuImage, src: &GpuImage, m: &Mat3, opts: WarpOptions, accumulate: Option<f32>) -> GpuImage {
warp_q(e, dst, src, m, opts, accumulate, None)
}
/// [`warp`], clamping and quantising the result to `levels` (8/16 bpc: the comp after a layer,
/// in the same pass).
#[allow(clippy::too_many_arguments)]
pub fn warp_q(
e: &mut Enc,
dst: &GpuImage,
src: &GpuImage,
m: &Mat3,
sampling: Sampling,
mode: BlendMode,
opacity: f32,
seed: u32,
accumulate: Option<f32>,
levels: Option<f32>,
) -> GpuImage {
pub fn warp_q(e: &mut Enc, dst: &GpuImage, src: &GpuImage, m: &Mat3, opts: WarpOptions, accumulate: Option<f32>, levels: Option<f32>) -> GpuImage {
let WarpOptions { sampling, mode, opacity, seed } = opts;
if src.width == 0 || src.height == 0 || opacity <= 0.0 {
return dst.clone();
}
-1
View File
@@ -2,7 +2,6 @@
//! Set EFFECTCRAFT_REQUIRE_GPU=1 for acceptance: unavailable adapters/devices then fail
//! instead of taking the explicitly logged headless-CI skip path.
#![cfg(not(target_arch = "wasm32"))]
#![allow(clippy::expect_used)] // Test helpers require successful setup to exercise rejection.
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
+2 -2
View File
@@ -466,12 +466,12 @@ fn place_src(
) -> Option<GpuImage> {
let pl = r.placement(ctx, layer, buf);
if let [m] = pl.matrices.as_slice() {
return Some(ops::warp_q(e, target, &src, m, pl.sampling, mode, opacity, pl.seed, None, levels));
return Some(ops::warp_q(e, target, &src, m, ops::WarpOptions { sampling: pl.sampling, mode, opacity, seed: pl.seed }, None, levels));
}
let mut acc = e.zeros(target.width, target.height);
let k = 1.0 / pl.matrices.len() as f32;
for m in &pl.matrices {
acc = ops::warp(e, &acc, &src, m, pl.sampling, BlendMode::Normal, 1.0, 0, Some(k));
acc = ops::warp(e, &acc, &src, m, ops::WarpOptions { sampling: pl.sampling, mode: BlendMode::Normal, opacity: 1.0, seed: 0 }, Some(k));
}
Some(ops::blend_full(e, target, &acc, mode, opacity, pl.seed, levels))
}
-1
View File
@@ -78,7 +78,6 @@ pub fn deblock(planes: &mut [Vec<u16>; 3], blk: &[Blk], w: usize, h: usize, qp:
/// 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;
+179 -142
View File
@@ -7,7 +7,10 @@ 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::tables::{
ABS_MVD_GT0, ABS_MVD_GT1, CBF_CHROMA, CBF_LUMA, CODED_SUB_BLOCK, CTX_IDX_MAP, CU_SKIP, GT1, GT2, INTRA_CHROMA, LAST_X_PREFIX, LAST_Y_PREFIX, MERGE_FLAG,
MERGE_IDX, MVP_FLAG, PART_MODE, PRED_MODE, PREV_INTRA_LUMA, RQT_ROOT_CBF, SCAN_4, SIG_COEFF, SPLIT_CU, qpc_420, scan_order,
};
use crate::transform;
pub const F_INTRA: u8 = 1;
@@ -73,6 +76,40 @@ pub enum SliceKind {
P,
}
/// Picture geometry and slice coding parameters shared by the CTU encoder.
pub struct FrameConfig {
pub bit_depth: u32,
pub width: usize,
pub height: usize,
pub kind: SliceKind,
pub slice_qp: i32,
}
/// A square node in the coding or transform quadtree.
#[derive(Clone, Copy)]
struct TreeNode {
x: usize,
y: usize,
log2: u32,
depth: u32,
}
impl TreeNode {
fn children(self) -> impl Iterator<Item = Self> {
let h = 1usize << (self.log2 - 1);
[(0, 0), (h, 0), (0, h), (h, h)].into_iter().map(move |(dx, dy)| Self { x: self.x + dx, y: self.y + dy, log2: self.log2 - 1, depth: self.depth + 1 })
}
}
/// Transform syntax inherited from the parent node and the enclosing CU.
#[derive(Clone, Copy)]
struct TransformState {
blk_idx: u32,
parent: [bool; 2],
intra: bool,
nxn: bool,
}
pub struct FrameCoder<'a> {
pub bd: u32,
pub w: usize,
@@ -114,17 +151,8 @@ fn morton(x: u32, y: u32) -> u32 {
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<u16>; 3],
refp: Option<&'a [Vec<u16>; 3]>,
me: Option<&'a MeRef>,
) -> Self {
pub fn new(config: FrameConfig, src: &'a [Vec<u16>; 3], refp: Option<&'a [Vec<u16>; 3]>, me: Option<&'a MeRef>) -> Self {
let FrameConfig { bit_depth: bd, width: w, height: h, kind, slice_qp } = config;
let off = 6 * (bd as i32 - 8);
let qpi = slice_qp.clamp(-off, 57);
let qp_c = qpc_420(qpi) + off;
@@ -256,7 +284,7 @@ impl<'a> FrameCoder<'a> {
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);
self.write_cq(enc, TreeNode { x, y, log2: LOG2_CTB, depth: 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
@@ -276,7 +304,8 @@ impl<'a> FrameCoder<'a> {
}
/// 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) {
fn write_cq(&mut self, enc: &mut CabacEncoder, node: TreeNode, decs: &[CuDec], idx: &mut usize) {
let TreeNode { x, y, log2, depth } = node;
if x >= self.w || y >= self.h {
return;
}
@@ -294,9 +323,8 @@ impl<'a> FrameCoder<'a> {
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);
for child in node.children() {
self.write_cq(enc, child, decs, idx);
}
}
}
@@ -447,7 +475,7 @@ impl<'a> FrameCoder<'a> {
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);
self.write_tt(s, TreeNode { x, y, log2, depth: 0 }, TransformState { blk_idx: 0, parent: [true, true], intra: true, nxn }, &mut ti);
(mode, self.cu_dist(x, y, n))
}
_ => {
@@ -490,7 +518,12 @@ impl<'a> FrameCoder<'a> {
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);
self.write_tt(
s,
TreeNode { x, y, log2, depth: 0 },
TransformState { blk_idx: 0, parent: [true, true], intra: false, nxn: false },
&mut ti,
);
}
CuMode::Amvp { mv, mvp } => {
s.decision(CU_SKIP + skip_ctx, 0);
@@ -503,7 +536,12 @@ impl<'a> FrameCoder<'a> {
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);
self.write_tt(
s,
TreeNode { x, y, log2, depth: 0 },
TransformState { blk_idx: 0, parent: [true, true], intra: false, nxn: false },
&mut ti,
);
}
}
// Handled by the arm above.
@@ -715,22 +753,9 @@ impl<'a> FrameCoder<'a> {
/// 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<S: Sink>(
&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,
) {
fn write_tt<S: Sink>(&self, s: &mut S, node: TreeNode, state: TransformState, ti: &mut usize) {
let TreeNode { x: x0, y: y0, log2, depth } = node;
let TransformState { blk_idx, parent, intra, nxn } = state;
let n = 1usize << log2;
let split = log2 > LOG2_MAX_TB || (nxn && depth == 0);
let mut cbf_c = [false; 2];
@@ -745,13 +770,11 @@ impl<'a> FrameCoder<'a> {
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);
for (i, child) in node.children().enumerate() {
self.write_tt(s, child, TransformState { blk_idx: i as u32, parent: cbf_c, ..state }, ti);
}
return;
}
let _ = (xb, yb);
let tb = self.tbs[*ti];
*ti += 1;
debug_assert_eq!(tb.c, 0);
@@ -874,9 +897,9 @@ impl<'a> FrameCoder<'a> {
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);
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::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);
}
@@ -1153,8 +1176,119 @@ fn last_prefix(v: u32) -> (u32, u32, u32) {
}
}
/// Context increment for significant_coeff_flag (H.265 9.3.4.2).
fn significance_context(group: (usize, usize), pos: (usize, usize), log2: u32, prev_csbf: u8, c: usize, scan_idx: usize) -> usize {
let (xs, ys) = group;
let (xp, yp) = pos;
let (xc, yc) = (xs * 4 + xp, ys * 4 + yp);
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
}
}
/// Absolute levels, signs and Rice remainders for one significant coefficient group.
fn write_coefficient_levels<S: Sink>(s: &mut S, sig: &[u8], at: impl Fn(usize) -> i32, i: usize, c: usize, prev_g1: &mut Option<u32>) {
let nsig = sig.len();
let abs_at = |k: usize| at(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<usize> = 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(sig[k] as usize) < 0) as u32);
}
if hidden {
let sum: u32 = (0..nsig).map(abs_at).sum();
debug_assert_eq!(at(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);
}
}
}
}
/// residual_coding() (7.3.8.11) of one transform block of levels (row-major, n x n). No transform
/// skip, no sign data hiding.
/// skip; signs use sign data hiding where the coefficient span permits it.
fn residual_coding<S: Sink>(s: &mut S, lv: &[i32], log2: u32, c: usize, scan_idx: usize) {
let n = 1usize << log2;
let log2sb = log2 - 2;
@@ -1233,51 +1367,7 @@ fn residual_coding<S: Sink>(s: &mut S, lv: &[i32], log2: u32, c: usize, scan_idx
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
};
let sig_ctx = significance_context((xs, ys), (xp, yp), log2, prev_csbf, c, scan_idx);
s.decision(SIG_COEFF + c_off_sig + sig_ctx, v as u32);
if v {
sig[nsig] = p as u8;
@@ -1294,60 +1384,7 @@ fn residual_coding<S: Sink>(s: &mut S, lv: &[i32], log2: u32, c: usize, scan_idx
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<usize> = 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);
}
}
}
write_coefficient_levels(s, &sig[..nsig], |p| at(xs, ys, p), i, c, &mut prev_g1);
}
}
+4 -2
View File
@@ -35,7 +35,8 @@ impl PlaneRef<'_> {
/// 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]) {
pub fn mc_luma(f: PlaneRef, bd: u32, x: i32, y: i32, mv: [i16; 2], size: (usize, usize), out: &mut [i16]) {
let (w, h) = size;
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);
@@ -89,7 +90,8 @@ pub fn mc_luma(f: PlaneRef, bd: u32, x: i32, y: i32, mv: [i16; 2], w: usize, h:
}
/// 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]) {
pub fn mc_chroma(f: PlaneRef, bd: u32, x: i32, y: i32, mv: [i16; 2], size: (usize, usize), out: &mut [i16]) {
let (w, h) = size;
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);
+1 -3
View File
@@ -15,8 +15,6 @@
//! ```
#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)]
// 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;
@@ -311,7 +309,7 @@ impl Encoder {
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());
let mut fc = FrameCoder::new(frame::FrameConfig { bit_depth: bd, width: w, height: h, kind, slice_qp: qp }, &src, refp, me.as_ref());
// slice_segment_header() (7.3.6.1)
let mut bw = BitWriter::new();
+35 -4
View File
@@ -103,12 +103,34 @@ pub(crate) fn frame_to_image_in(f: &VideoFrame, op: AlphaOp, mut buf: Vec<Px>) -
PixelData::Yuv8 { planes, chroma, alpha } => {
let yuv = Yuv::new(f.color.matrix, f.color.range, 8);
let amax = 255.0;
convert_yuv(&mut img, w, h, *chroma, [&planes[0][..], &planes[1][..], &planes[2][..]], alpha.as_deref().map(|a| &a[..]), amax, &yuv, op);
convert_yuv(
&mut img,
YuvSource {
size: (w, h),
chroma: *chroma,
planes: [&planes[0][..], &planes[1][..], &planes[2][..]],
alpha: alpha.as_deref().map(|a| &a[..]),
amax,
},
&yuv,
op,
);
}
PixelData::Yuv16 { planes, chroma, bits, alpha } => {
let yuv = Yuv::new(f.color.matrix, f.color.range, *bits);
let amax = ((1u32 << *bits) - 1) as f32;
convert_yuv(&mut img, w, h, *chroma, [&planes[0][..], &planes[1][..], &planes[2][..]], alpha.as_deref().map(|a| &a[..]), amax, &yuv, op);
convert_yuv(
&mut img,
YuvSource {
size: (w, h),
chroma: *chroma,
planes: [&planes[0][..], &planes[1][..], &planes[2][..]],
alpha: alpha.as_deref().map(|a| &a[..]),
amax,
},
&yuv,
op,
);
}
}
img
@@ -280,8 +302,17 @@ fn centred(i: usize, n: usize) -> (usize, usize) {
(c, o)
}
#[allow(clippy::too_many_arguments)]
fn convert_yuv<T: Sample>(img: &mut Image, w: usize, h: usize, chroma: Chroma, planes: [&[T]; 3], alpha: Option<&[T]>, amax: f32, k: &Yuv, op: AlphaOp) {
/// Borrowed YUV planes and the geometry/range needed to interpret their samples.
struct YuvSource<'a, T> {
size: (usize, usize),
chroma: Chroma,
planes: [&'a [T]; 3],
alpha: Option<&'a [T]>,
amax: f32,
}
fn convert_yuv<T: Sample>(img: &mut Image, source: YuvSource<'_, T>, k: &Yuv, op: AlphaOp) {
let YuvSource { size: (w, h), chroma, planes, alpha, amax } = source;
let (sx, sy) = chroma.shifts();
let cw = w.div_ceil(1 << sx);
let ch = h.div_ceil(1 << sy);
+18 -24
View File
@@ -8,7 +8,8 @@
use crate::range::{BITRES, RangeEncoder, ilog};
use crate::rate::{Mode, QTHETA_OFFSET, get_pulses};
use crate::tables::*;
use crate::tables::EBANDS;
use crate::{CeltFrame, rate::Allocation};
pub const SPREAD_NORMAL: usize = 2;
@@ -55,7 +56,7 @@ fn exp_rotation1_inv(x: &mut [f32], len: usize, stride: usize, c: f32, s: f32) {
if len <= stride {
return;
}
if len >= 2 * stride + 1 {
if len > 2 * stride {
for i in 0..=len - 2 * stride - 1 {
let x1 = x[i];
let x2 = x[i + stride];
@@ -217,7 +218,7 @@ impl BandEnc<'_, '_> {
let q = qn as u32;
let half = q >> 1;
let ft = (half + 1) * (half + 1);
let (fl, fs) = if it <= half { (it * (it + 1) >> 1, it + 1) } else { (ft - ((q + 1 - it) * (q + 2 - it) >> 1), q + 1 - it) };
let (fl, fs) = if it <= half { ((it * (it + 1)) >> 1, it + 1) } else { (ft - (((q + 1 - it) * (q + 2 - it)) >> 1), q + 1 - it) };
self.enc.encode(fl, fl + fs, ft);
itheta = ((it * 16384) / q) as i32;
}
@@ -294,26 +295,19 @@ impl BandEnc<'_, '_> {
}
}
/// Codes all band shapes (`quant_all_bands`, encoder side, long blocks). `x` holds `c` channels
/// of `n` normalised MDCT coefficients each.
#[allow(clippy::too_many_arguments)]
pub fn quant_all_bands(
m: &Mode,
start: usize,
end: usize,
x: &[f32],
n_total: usize,
c: usize,
pulses: &[i32; NB_EBANDS],
spread: usize,
dual_stereo: bool,
intensity: usize,
total_bits: i32,
mut balance: i32,
enc: &mut RangeEncoder,
lm: usize,
coded_bands: usize,
) {
/// Shape-coding controls, independent of the selected band allocation.
pub struct BandCoding {
pub spread: usize,
pub total_bits: i32,
}
/// Codes all band shapes (`quant_all_bands`, encoder side, long blocks). `x` holds
/// `frame.channels` channels of `n_total` normalised MDCT coefficients each.
pub fn quant_all_bands(m: &Mode, frame: CeltFrame, x: &[f32], n_total: usize, allocation: &Allocation, coding: BandCoding, enc: &mut RangeEncoder) {
let CeltFrame { start, end, channels: c, lm } = frame;
let Allocation { pulses, dual_stereo, intensity, coded_bands, balance, .. } = allocation;
let (dual_stereo, intensity, coded_bands, mut balance) = (*dual_stereo, *intensity, *coded_bands, *balance);
let BandCoding { spread, total_bits } = coding;
let mm = 1usize << lm;
debug_assert!(c == 1 || dual_stereo || intensity <= start, "stereo frames are coded as dual stereo");
let mut ctx = BandEnc { m, enc, i: 0, spread, remaining_bits: 0, pvq_v: Vec::new(), iy: Vec::new(), tmp: Vec::new() };
@@ -362,7 +356,7 @@ mod tests {
x[i + stride] = c * x2 + s * x1;
x[i] = c * x1 + ms * x2;
}
if len >= 2 * stride + 1 {
if len > 2 * stride {
for i in (0..=len - 2 * stride - 1).rev() {
let x1 = x[i];
let x2 = x[i + stride];

Some files were not shown because too many files have changed in this diff Show More