mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-28 20:16:52 -06:00
Merge pull request #2496 from dylanmtaylor/sonar/enforce-clippy-standards
Enforce Rust coding standards via Clippy
This commit is contained in:
@@ -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
@@ -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"
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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));
|
||||
|
||||
@@ -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
@@ -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) {
|
||||
|
||||
@@ -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,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,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 }
|
||||
|
||||
@@ -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,5 +1,5 @@
|
||||
use super::raster::over;
|
||||
use super::*;
|
||||
use super::{Buf, EffectCtx, IndexedParallelIterator, ParallelIterator, ParallelSliceMut, Px, hsl_to_rgb};
|
||||
|
||||
// ---------------------------------------------------------------- Fractal
|
||||
|
||||
|
||||
@@ -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,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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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,4 +1,4 @@
|
||||
use super::*;
|
||||
use super::{ParamUi, slider};
|
||||
|
||||
// ---------------------------------------------------------------- ASC CDL
|
||||
|
||||
|
||||
@@ -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,5 +1,5 @@
|
||||
use super::cdl::{Cdl, parse_cdl_xml};
|
||||
use super::*;
|
||||
use super::{Arc, HashMap, Lut, Mutex, OnceLock, parse_cube};
|
||||
|
||||
// ---------------------------------------------------------------- LUT files
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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,4 +1,4 @@
|
||||
use super::*;
|
||||
use super::{invert, mul, mul_vec, rgb_to_xyz};
|
||||
|
||||
pub(super) type M3 = [[f64; 3]; 3];
|
||||
|
||||
|
||||
@@ -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,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 }
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -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"))
|
||||
|
||||
@@ -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,4 +1,4 @@
|
||||
use super::*;
|
||||
use super::{Buf, Image, IndexedParallelIterator, IntoParallelRefIterator, IntoParallelRefMutIterator, ParallelIterator, ParallelSliceMut, Px, unpremul};
|
||||
|
||||
// ------------------------------------------------------------------ band rasteriser
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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,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,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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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,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
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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, &[]));
|
||||
|
||||
@@ -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,4 +1,4 @@
|
||||
use super::*;
|
||||
use super::{EngineError, Result, Session, Value};
|
||||
|
||||
// ---------- enablement ----------
|
||||
|
||||
|
||||
@@ -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")?;
|
||||
|
||||
@@ -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)? {
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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,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)?;
|
||||
|
||||
@@ -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)?;
|
||||
|
||||
@@ -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)?;
|
||||
|
||||
@@ -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,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> {
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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,5 +1,5 @@
|
||||
use super::params::bad;
|
||||
use super::*;
|
||||
use super::{CommandSpec, Result, Value};
|
||||
|
||||
// ---------- parameter validation (agents) ----------
|
||||
|
||||
|
||||
@@ -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,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,4 +1,4 @@
|
||||
use super::*;
|
||||
use super::{ItemId, Serialize, Value};
|
||||
|
||||
/// Events for frontends (drained each frame).
|
||||
#[derive(Clone, Debug, PartialEq, Serialize)]
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use super::*;
|
||||
use super::prefs;
|
||||
|
||||
pub fn text_families() -> Vec<String> {
|
||||
effectcraft_text::families().into_iter().map(|(f, _)| f).collect()
|
||||
|
||||
@@ -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,4 +1,4 @@
|
||||
use super::*;
|
||||
use super::config;
|
||||
|
||||
/// Host services (file access) injected by the frontend.
|
||||
pub trait Services: Send + Sync {
|
||||
|
||||
@@ -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,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,4 +1,4 @@
|
||||
use super::*;
|
||||
use super::{EngineError, Event, Result, Session, Value, commands, guard};
|
||||
|
||||
impl Session {
|
||||
pub fn new() -> Session {
|
||||
|
||||
@@ -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,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,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,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,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,4 +1,4 @@
|
||||
use super::*;
|
||||
use super::Prefs;
|
||||
|
||||
impl Prefs {
|
||||
/// Remember a project in File ▸ Open Recent.
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
@@ -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
@@ -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
@@ -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();
|
||||
|
||||
@@ -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![];
|
||||
|
||||
@@ -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
@@ -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).
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
@@ -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
Reference in New Issue
Block a user