mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-24 01:57:53 -06:00
Review: tie the colour kernels' literals to the CPU constants; visible GPU skip; B&C wording
WGSL cannot read Rust constants, so the Rec. 601 luma (Tint, Tritone, Luma Key / Extract), Black & White's tint weights and Exposure's 2.2 power stay literals in the kernels; they now name their Rust source (effects::LUMA_601, BW_TINT_LUMA, EXPOSURE_LINEAR_GAMMA, newly exported) and the test shader_constants_match_the_cpu (no GPU needed) fails if a literal drifts. gpu_tint_defaults_match_after_effects_greys says "skipped: no GPU adapter" when it skips (EFFECTCRAFT_REQUIRE_GPU=1 still makes it fail). docs/effects-parity.md: Brightness & Contrast is close to, not the same as, After Effects (up to 6.2/255), and the summary row lists what is measured. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
d9205fa5f4
commit
e559a7ce9a
@@ -22,9 +22,13 @@ fn mix(a: [f32; 3], b: [f32; 3], t: f32) -> [f32; 3] {
|
||||
/// pure red, green and blue keep red (601: 76, 709: 54) and blue (29 / 18) and key out green
|
||||
/// (150 / 182). The GPU kernels (`pointwise` op 1, `fx_color` op 4, `fxk_key_luma`) mirror it.
|
||||
pub(crate) fn luma_601(c: [f32; 3]) -> f32 {
|
||||
0.299 * c[0] + 0.587 * c[1] + 0.114 * c[2]
|
||||
LUMA_601[0] * c[0] + LUMA_601[1] * c[1] + LUMA_601[2] * c[2]
|
||||
}
|
||||
|
||||
/// The Rec. 601 luma weights of [`luma_601`]. The WGSL kernels repeat them as literals; the GPU
|
||||
/// test `shader_constants_match_the_cpu` checks that they are these.
|
||||
pub const LUMA_601: [f32; 3] = [0.299, 0.587, 0.114];
|
||||
|
||||
fn tint(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
let black = ctx.params.color("black");
|
||||
let white = ctx.params.color("white");
|
||||
@@ -337,6 +341,7 @@ pub fn exposure_settings(ctx: &EffectCtx) -> [(f32, f32, f32); 3] {
|
||||
/// The power curve Exposure's own linear-light conversion uses (Bypass Linear Light Conversion
|
||||
/// off). After Effects decodes with a pure 2.2 power, not the piecewise sRGB curve: Offset 0.0005
|
||||
/// on black measures 8/255 there, which 2.2 predicts (8.06) and sRGB does not (1.65).
|
||||
/// `kernels.wgsl` repeats it as a literal; the GPU test `shader_constants_match_the_cpu` checks it.
|
||||
pub const EXPOSURE_LINEAR_GAMMA: f32 = 2.2;
|
||||
|
||||
/// Exposure: per channel, `(value · 2^exposure + offset)^(1 / gamma)`, in linear light unless
|
||||
@@ -379,8 +384,12 @@ pub fn mirror_pow(v: f32, p: f32) -> f32 {
|
||||
/// back toward the gray). After Effects 2026 with Tint Color [1, 0.5, 0]: gray 0.1 (a red solid
|
||||
/// at Reds 10, case 717) → (43, 21, 0), gray 0.6 (yellow at defaults, case 825) → (255, 129, 2),
|
||||
/// white → white; the old hue / saturation / lightness mix gave (51, 26, 0) for the first.
|
||||
/// The luminosity weights of [`bw_tint`] (a Color blend's). `fx_color.wgsl` repeats them; the GPU
|
||||
/// test `shader_constants_match_the_cpu` checks that they are these.
|
||||
pub const BW_TINT_LUMA: [f32; 3] = [0.3, 0.59, 0.11];
|
||||
|
||||
pub fn bw_tint(tint: [f32; 3], gray: f32) -> [f32; 3] {
|
||||
let lum = |c: [f32; 3]| 0.3 * c[0] + 0.59 * c[1] + 0.11 * c[2];
|
||||
let lum = |c: [f32; 3]| BW_TINT_LUMA[0] * c[0] + BW_TINT_LUMA[1] * c[1] + BW_TINT_LUMA[2] * c[2];
|
||||
if !gray.is_finite() || gray <= 0.0 {
|
||||
return [0.0; 3];
|
||||
}
|
||||
|
||||
@@ -70,11 +70,11 @@ use std::collections::HashMap;
|
||||
use std::sync::OnceLock;
|
||||
|
||||
pub use card3d::{CompLight, CompScene};
|
||||
pub use color_fx::{COLORAMA_PRESETS, ColoramaPalette};
|
||||
pub use color_fx::{
|
||||
HUESAT_CHANNELS, HUESAT_RANGES, LEVELS_CHANNELS, PHOTO_FILTER_CUSTOM, PHOTO_FILTERS, exposure_settings, fill_uses_masks, huesat_ranges_identity,
|
||||
levels_channel_ids, levels_channel_settings, levels_channels_identity, levels_clip,
|
||||
BW_TINT_LUMA, EXPOSURE_LINEAR_GAMMA, HUESAT_CHANNELS, HUESAT_RANGES, LEVELS_CHANNELS, LUMA_601, PHOTO_FILTER_CUSTOM, PHOTO_FILTERS, exposure_settings,
|
||||
fill_uses_masks, huesat_ranges_identity, levels_channel_ids, levels_channel_settings, levels_channels_identity, levels_clip,
|
||||
};
|
||||
pub use color_fx::{COLORAMA_PRESETS, ColoramaPalette};
|
||||
pub use color2::{Curve, auto_correct_settings, color_link_mode, color_link_sample, equalize_tables, luma_clip_points, shadow_highlight_amounts};
|
||||
pub use color3::OffsetCurve;
|
||||
pub use distort::transform_shutter;
|
||||
|
||||
@@ -102,6 +102,7 @@ fn fxc_sc_weights(c0: vec3<f32>) -> array<f32, 9> {
|
||||
|
||||
// Black & White's Tint (effects::color_fx::bw_tint): the tint's hue and saturation at the
|
||||
// gray's luminosity (Color blend, weights 0.3 / 0.59 / 0.11, channels clipped toward the gray).
|
||||
// The weights are effects::BW_TINT_LUMA (checked by the test shader_constants_match_the_cpu).
|
||||
fn fxc_bw_tint(tint: vec3<f32>, gray: f32) -> vec3<f32> {
|
||||
if (!(gray > 0.0)) {
|
||||
return vec3<f32>(0.0);
|
||||
@@ -191,7 +192,8 @@ fn fxc_point(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
case 4u: {
|
||||
if (a > 0.0) {
|
||||
let c = px.xyz / a;
|
||||
// Rec. 601 weights, as effects::color_fx::tritone.
|
||||
// Rec. 601 weights, as effects::color_fx::tritone: effects::LUMA_601 (checked by
|
||||
// the test shader_constants_match_the_cpu).
|
||||
let l = clamp(dot(c, vec3<f32>(0.299, 0.587, 0.114)), 0.0, 1.0);
|
||||
var t: vec3<f32>;
|
||||
if (l < 0.5) {
|
||||
|
||||
@@ -62,7 +62,8 @@ fn fxk_range_components(space: u32, c: vec3<f32>) -> vec3<f32> {
|
||||
}
|
||||
|
||||
// keying::matte_levels.
|
||||
// Luma Key / Extract luminance: Rec. 601 (effects::color_fx::luma_601).
|
||||
// Luma Key / Extract luminance: Rec. 601, effects::LUMA_601 (checked by the test
|
||||
// shader_constants_match_the_cpu).
|
||||
fn fxk_key_luma(c: vec3<f32>) -> f32 {
|
||||
return dot(c, vec3<f32>(0.299, 0.587, 0.114));
|
||||
}
|
||||
|
||||
@@ -605,7 +605,8 @@ fn pointwise(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
case 1u: {
|
||||
if (a > 0.0) {
|
||||
let c = straight(px);
|
||||
// Rec. 601 weights, as After Effects (effects crate luma_601).
|
||||
// Rec. 601 weights, as After Effects: effects::LUMA_601 (checked by the test
|
||||
// shader_constants_match_the_cpu).
|
||||
let l = clamp(0.299 * c.x + 0.587 * c.y + 0.114 * c.z, 0.0, 1.0);
|
||||
let blk = P.f[0].xyz;
|
||||
let wht = P.f[1].xyz;
|
||||
@@ -726,7 +727,8 @@ fn pointwise(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
var o = vec3<f32>(0.0);
|
||||
for (var i = 0; i < 3; i++) {
|
||||
// Per-channel gain, offset, gamma; negative values are mirrored. The
|
||||
// linear-light conversion is a pure 2.2 power (effects crate exposure).
|
||||
// linear-light conversion is a pure 2.2 power: effects::EXPOSURE_LINEAR_GAMMA
|
||||
// (checked by the test shader_constants_match_the_cpu).
|
||||
if (P.u[0].y != 0u) {
|
||||
let x = c[i] * P.f[0][i] + P.f[1][i];
|
||||
o[i] = sign(x) * powz(abs(x), 1.0 / P.f[2][i]);
|
||||
|
||||
+28
-1
@@ -855,7 +855,11 @@ fn effects_whose_kernels_failed_render_on_the_cpu() {
|
||||
/// working space, not linearised): Rec. 601 weights, as the CPU (`color_fx::luma_601`).
|
||||
#[test]
|
||||
fn gpu_tint_defaults_match_after_effects_greys() {
|
||||
let Some(g) = gpu() else { return };
|
||||
let Some(g) = gpu() else {
|
||||
// Visible skip (EFFECTCRAFT_REQUIRE_GPU=1 makes a missing adapter fail instead).
|
||||
eprintln!("gpu_tint_defaults_match_after_effects_greys: skipped: no GPU adapter");
|
||||
return;
|
||||
};
|
||||
for (col, want) in [([1.0, 0.0, 0.0], 76.0), ([0.0, 1.0, 0.0], 150.0), ([0.0, 0.0, 1.0], 29.0), ([0.5, 0.5, 0.5], 128.0)] {
|
||||
let mut s = Scene::new(BitDepth::Bpc32);
|
||||
s.p.settings.working_space = Some(ColorSpace::Srgb);
|
||||
@@ -870,6 +874,29 @@ fn gpu_tint_defaults_match_after_effects_greys() {
|
||||
}
|
||||
}
|
||||
|
||||
/// WGSL has no way to read the CPU's constants, so the kernels repeat them as literals; this
|
||||
/// ties each literal to its Rust source (runs without a GPU): Rec. 601 luma for Tint (`pointwise`
|
||||
/// op 1), Tritone and Luma Key / Extract, Black & White's tint weights and Exposure's 2.2 power.
|
||||
#[test]
|
||||
fn shader_constants_match_the_cpu() {
|
||||
use effectcraft_effects::{BW_TINT_LUMA, EXPOSURE_LINEAR_GAMMA, LUMA_601};
|
||||
let kernels = include_str!("shaders/kernels.wgsl");
|
||||
let color = include_str!("shaders/fx_color.wgsl");
|
||||
let key = include_str!("shaders/fx_key.wgsl");
|
||||
let [r, g, b] = LUMA_601;
|
||||
let dot = format!("dot(c, vec3<f32>({r}, {g}, {b}))");
|
||||
assert!(kernels.contains(&format!("clamp({r} * c.x + {g} * c.y + {b} * c.z, 0.0, 1.0)")), "Tint luma in kernels.wgsl is not {LUMA_601:?}");
|
||||
assert!(color.contains(&format!("clamp({dot}, 0.0, 1.0)")), "Tritone luma in fx_color.wgsl is not {LUMA_601:?}");
|
||||
assert!(key.contains(&format!("return {dot};")), "fxk_key_luma in fx_key.wgsl is not {LUMA_601:?}");
|
||||
let [r, g, b] = BW_TINT_LUMA;
|
||||
assert!(color.contains(&format!("let w = vec3<f32>({r}, {g}, {b});")), "fxc_bw_tint weights are not {BW_TINT_LUMA:?}");
|
||||
let gamma = EXPOSURE_LINEAR_GAMMA;
|
||||
assert!(
|
||||
kernels.contains(&format!("powz(abs(c[i]), {gamma})")) && kernels.contains(&format!("1.0 / {gamma})")),
|
||||
"Exposure's power in kernels.wgsl is not {gamma}"
|
||||
);
|
||||
}
|
||||
|
||||
/// A layer whose bounds change every frame reads back at a new size each time: the staging
|
||||
/// buffers kept for reuse stay within budget instead of growing for the whole render (#2081).
|
||||
#[test]
|
||||
|
||||
@@ -15,7 +15,7 @@ After Effects', and does the preset library exist. The per-effect catalogue is t
|
||||
| Parameter names, order, popups, units, defaults | aligned in M9.12 | Agent-checked against After Effects' documentation, not against a live dump |
|
||||
| On the GPU | **280** (92%) | Measured: registry flags; GPU vs CPU agreement tested per effect |
|
||||
| 32-bit float, no clamping | 306 | Measured: registry flags |
|
||||
| **Rendered output matches After Effects** | **unmeasured** | Only Tint is checked against After Effects renders (four solids, within 1/255). `effectcraft_effects::fidelity::compare_frames` exists for the corpus ([fidelity/README.md](fidelity/README.md)); simulation effects are pinned by golden hashes of our own output (M13.27) |
|
||||
| **Rendered output matches After Effects** | **partly measured** | Some colour effects are checked against After Effects 2026 renders of our own test projects: Tint, Tritone, Black & White, Exposure, Levels, Curves, Luma Key, Extract, 4-Color Gradient and Fill within 1–3/255, Brightness & Contrast up to 6.2/255 (Color Correction, Generate and Keying rows below); every other effect is unmeasured. `effectcraft_effects::fidelity::compare_frames` exists for the corpus ([fidelity/README.md](fidelity/README.md)); simulation effects are pinned by golden hashes of our own output (M13.27) |
|
||||
| Animation presets | **54** original presets (8 text-animator, 23 Transitions, 23 Backgrounds) against After Effects' **621** `.ffx` presets | Measured: `crates/engine/presets/text_animators.json`, `crates/engine/presets/transitions.json`, `crates/engine/presets/backgrounds.json`; the After Effects `Presets` folder |
|
||||
| 26.x additions | Curl Noise (26.3) present (#463 open); effect colour labels (26.5) present in Effect Controls only, not in the Timeline; the rebuilt Effect Controls (26.5) missing | Release notes |
|
||||
|
||||
@@ -30,7 +30,7 @@ corpus compares renders, each effect is assumed to differ somewhere, and users k
|
||||
| Audio | 13 | 0 | CPU by design (applied in the mixdown) |
|
||||
| Blur & Sharpen | 16 | 15 | Gaussian Blur interaction and output to be measured first ([fidelity/ae-2026-acceptance.md](fidelity/ae-2026-acceptance.md)) |
|
||||
| Channel | 13 | 13 | — |
|
||||
| Color Correction | 40 | 40 | Tint, Tritone and Black & White (sliders and Tint) match After Effects 2026 renders (Rec. 601 luminance); Levels (Individual Controls) matches After Effects 2026 in 32 bpc (channel controls before RGB, "Off for 32 bpc Color" unclipped; plain Levels' channel order not measured); Brightness & Contrast (non-legacy) within about 6/255 of After Effects 2026 ramps (negative settings invert the positive curves); Curves interpolates a natural cubic spline through its points, as After Effects 2026 (exact on its grey steps); Tint has Swap Colors (#963); ACES 2.0 / OCIO 2.5 (26.5); Apply Color LUT (#514) and 32-bpc blending (#494) are closed |
|
||||
| Color Correction | 40 | 40 | Tint, Tritone and Black & White (sliders and Tint) match After Effects 2026 renders (Rec. 601 luminance); Levels (Individual Controls) matches After Effects 2026 in 32 bpc (channel controls before RGB, "Off for 32 bpc Color" unclipped; plain Levels' channel order not measured); Brightness & Contrast (non-legacy) close to After Effects 2026 ramps, not identical (up to 6.2/255 off; the test allows 7/255) (negative settings invert the positive curves); Curves interpolates a natural cubic spline through its points, as After Effects 2026 (exact on its grey steps); Tint has Swap Colors (#963); ACES 2.0 / OCIO 2.5 (26.5); Apply Color LUT (#514) and 32-bpc blending (#494) are closed |
|
||||
| Distort | 39 | 35 | Liquify has no viewer brush; #303 (Mesh Warp drag) and #534 (CC Power Pin Unstretch) are closed |
|
||||
| Expression Controls | 9 | 9 | — |
|
||||
| Generate | 26 | 25 | 4-Color Gradient's Blend matches After Effects 2026 (weights 1 / (d² + Blend²)); Fill's Opacity is the filled layer's opacity, as After Effects 2026 renders it (with a Fill Mask it still mixes with the original, not measured); Fractal on the CPU (needs f64); #526 (Reset) and #536 (Write-on marks) are closed |
|
||||
@@ -67,7 +67,7 @@ the GPU is performance, not parity.
|
||||
| 2026-10-19 | minor | Tritone uses Rec. 601 luminance, measured against After Effects 2026 renders of a red solid and a grey ramp (CPU and GPU) |
|
||||
| 2026-10-19 | minor | Black & White's Tint keeps the gray's luminosity (Color blend), measured against After Effects 2026 renders (CPU and GPU) |
|
||||
| 2026-10-19 | minor | Levels: "Off for 32 bpc Color" no longer clips in 32 bpc projects, unclipped values extrapolate below Output Black; Levels (Individual Controls) applies the channel controls before the RGB ones, measured against After Effects 2026 (CPU and GPU) |
|
||||
| 2026-10-19 | minor | Brightness & Contrast: negative Brightness / Contrast are the inverse curves of the positive ones (Brightness −40: 20/255 → 6/255 off After Effects 2026), CPU and GPU |
|
||||
| 2026-10-19 | minor | Brightness & Contrast: negative Brightness / Contrast are the inverse curves of the positive ones (Brightness −40: 20/255 → 6/255 off After Effects 2026; still up to 6.2/255 off, not the same pixels), CPU and GPU |
|
||||
| 2026-10-19 | minor | Luma Key: Key Out Brighter / Darker cut hard at Threshold, measured against After Effects 2026 renders of a ramp (CPU and GPU) |
|
||||
| 2026-10-19 | minor | Luma Key and Extract: Rec. 601 luminance (was Rec. 709), measured against After Effects 2026 (CPU and GPU) |
|
||||
| 2026-10-19 | minor | 4-Color Gradient: each point weighs 1 / (d² + Blend²), measured against After Effects 2026 at Blend 10 / 50 / 100 / 300 (CPU and GPU) |
|
||||
|
||||
Reference in New Issue
Block a user