Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 59 additions & 18 deletions crates/compositor/src/compositor_macos.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@

use crate::config::Cfg;
use crate::d3d::Gpu;
use crate::ffi::AVFrame;
use crate::ffi::{AVColorSpace, AVFrame};
// Le constant buffer est le MÊME struct des deux côtés — cf. `frame_geometry`.
// Constant buffer, params runtime et constantes de sortie : une seule définition pour
// les deux backends — cf. `frame_geometry`, qui documente les divergences que
Expand Down Expand Up @@ -205,25 +205,47 @@ impl CVMetalTextureCache {
}
}

/// Vue du plan CbCr dont le canal bleu vaut 1 : c'est ainsi que `sample_yuv` (`shaders.metal`)
/// sait qu'une frame est en pleine plage (`420f`, `xf20`). Un `RG` lu sans swizzle rend 0 en
/// bleu, donc la plage limitée reste le cas par défaut, sans rien lier de plus à chaque draw.
fn full_range_view(uv: &metal::TextureRef) -> metal::Texture {
// `MTLTextureSwizzle` ; absent des bindings `metal` 0.29.
const SWZ_ZERO: u8 = 0;
const SWZ_ONE: u8 = 1;
const SWZ_RED: u8 = 2;
const SWZ_GREEN: u8 = 3;
const SWZ_ALPHA: u8 = 5;

/// Vue de `tex` qui rend `swizzle` (rouge, vert, bleu, alpha). C'est ainsi que `sample_yuv`
/// (`shaders.metal`) lit le format d'une frame sans rien lier de plus à chaque draw :
/// - bleu à 1 sur le plan CbCr : pleine plage (`420f`, `xf20`) ;
/// - vert ou bleu à 1 sur le plan Y : matrice BT.601 ou BT.2020.
///
/// Sans vue, `R` et `RG` rendent 0 sur les canaux libres : plage limitée et BT.709 par défaut.
fn swizzled_view(tex: &metal::TextureRef, swizzle: [u8; 4]) -> metal::Texture {
use objc::{msg_send, sel, sel_impl};
// `MTLTextureSwizzleChannels` : rouge, vert, un, alpha ; absent des bindings `metal` 0.29.
#[repr(C)]
struct Swizzle([u8; 4]);
const RED: u8 = 2;
const GREEN: u8 = 3;
const ONE: u8 = 1;
const ALPHA: u8 = 5;
let one = metal::NSRange::new(0, 1);
unsafe {
msg_send![uv, newTextureViewWithPixelFormat:uv.pixel_format()
textureType:uv.texture_type()
levels:one
slices:one
swizzle:Swizzle([RED, GREEN, ONE, ALPHA])]
msg_send![tex, newTextureViewWithPixelFormat:tex.pixel_format()
textureType:tex.texture_type()
levels:one
slices:one
swizzle:Swizzle(swizzle)]
}
}

/// Le swizzle du plan Y qui porte la matrice de la frame, ou `None` pour BT.709 (aussi le
/// défaut d'un flux non tagué). La matrice vient de `AVFrame::colorspace`, que libavcodec lit
/// dans le VUI : VideoToolbox ne pose pas `kCVImageBufferYCbCrMatrixKey` sur ses buffers.
/// BT.2020 CL n'est pas une simple matrice : il garde le défaut plutôt qu'un rendu NCL faux.
///
/// Alpha à 0 : échantillons 10 bits (P010), que `yuv_matrix` convertit avec les bornes 10 bits.
fn matrix_swizzle(colorspace: AVColorSpace::Type, ten_bit: bool) -> Option<[u8; 4]> {
let depth = if ten_bit { SWZ_ZERO } else { SWZ_ONE };
match colorspace {
AVColorSpace::AVCOL_SPC_BT470BG | AVColorSpace::AVCOL_SPC_SMPTE170M => {
Some([SWZ_RED, SWZ_ONE, SWZ_ZERO, depth])
}
AVColorSpace::AVCOL_SPC_BT2020_NCL => Some([SWZ_RED, SWZ_ZERO, SWZ_ONE, depth]),
_ => None,
}
}

Expand Down Expand Up @@ -1208,10 +1230,14 @@ impl Compositor {
}
_ => (metal::MTLPixelFormat::R8Unorm, metal::MTLPixelFormat::RG8Unorm),
};
let y = cache.make_texture_from_pixel_buffer(pb, 0, y_fmt)?;
let uv = cache.make_texture_from_pixel_buffer(pb, 1, uv_fmt)?;
let mut y = cache.make_texture_from_pixel_buffer(pb, 0, y_fmt)?;
let mut uv = cache.make_texture_from_pixel_buffer(pb, 1, uv_fmt)?;
let ten_bit = matches!(pixel_format, CV_420_10_BIPLANAR_VIDEO | CV_420_10_BIPLANAR_FULL);
if let Some(swizzle) = matrix_swizzle((*frame).colorspace, ten_bit) {
y = swizzled_view(&y, swizzle);
}
if matches!(pixel_format, CV_420_8_BIPLANAR_FULL | CV_420_10_BIPLANAR_FULL) {
return Ok((y, full_range_view(&uv)));
uv = swizzled_view(&uv, [SWZ_RED, SWZ_GREEN, SWZ_ONE, SWZ_ALPHA]);
}
Ok((y, uv))
}
Expand Down Expand Up @@ -5428,4 +5454,19 @@ mod tests {
crate::shader_fx::compile_metal(&gpu.device, &code, &params).expect("compile");
eprintln!("recompilation d'une source déjà vue : {:.1} ms", t0.elapsed().as_secs_f64() * 1000.0);
}

#[test]
fn matrix_swizzle_marks_matrix_and_depth() {
use AVColorSpace::*;
assert_eq!(
matrix_swizzle(AVCOL_SPC_SMPTE170M, false),
Some([SWZ_RED, SWZ_ONE, SWZ_ZERO, SWZ_ONE])
);
assert_eq!(
matrix_swizzle(AVCOL_SPC_BT2020_NCL, true),
Some([SWZ_RED, SWZ_ZERO, SWZ_ONE, SWZ_ZERO])
);
assert_eq!(matrix_swizzle(AVCOL_SPC_BT709, false), None);
assert_eq!(matrix_swizzle(AVCOL_SPC_BT2020_CL, true), None);
}
}
2 changes: 2 additions & 0 deletions crates/compositor/src/mac_frames.rs
Original file line number Diff line number Diff line change
Expand Up @@ -352,6 +352,8 @@ impl CpuFrames {
// reporter depuis la source, sinon toute la timeline se croit à t=0.
(*self.present).pts = (*src).pts;
(*self.present).best_effort_timestamp = (*src).best_effort_timestamp;
// sws garde la matrice YCbCr de la source : `nv12_srvs` la relit ici.
(*self.present).colorspace = (*src).colorspace;
Ok(())
}

Expand Down
38 changes: 35 additions & 3 deletions crates/compositor/src/shaders.metal
Original file line number Diff line number Diff line change
Expand Up @@ -153,17 +153,49 @@ inline float3 yuv709_full(float y, float2 cbcr)
return clamp(rgb, 0.0, 1.0);
}

// BT.601 / BT.2020 -> RGB. `m` : coefficients Cr->R, Cb->G, Cr->G, Cb->B. BT.709 garde ses
// deux fonctions à constantes littérales, pour rendre ses frames à l'octet près comme avant.
// `ten_bit` : un échantillon P010 lu en `R16Unorm` vaut code10 * 64 / 65535, pas code8 / 255 ;
// on revient au code 10 bits pour appliquer ses bornes (64..940, chroma 512, pleine 0..1023).
inline float3 yuv_matrix(float y, float2 cbcr, float4 m, bool full, bool ten_bit)
{
float Yf, Cb, Cr;
if (ten_bit) {
float code = 65535.0 / 64.0;
Yf = full ? y * code / 1023.0 : (y * code - 64.0) / 876.0;
Cb = full ? (cbcr.x * code - 512.0) / 1023.0 : (cbcr.x * code - 512.0) / 896.0;
Cr = full ? (cbcr.y * code - 512.0) / 1023.0 : (cbcr.y * code - 512.0) / 896.0;
} else {
Yf = full ? y : (y * 255.0 - 16.0) / 219.0;
Cb = full ? cbcr.x - 128.0 / 255.0 : (cbcr.x * 255.0 - 128.0) / 224.0;
Cr = full ? cbcr.y - 128.0 / 255.0 : (cbcr.y * 255.0 - 128.0) / 224.0;
}
float3 rgb;
rgb.r = Yf + m.x * Cr;
rgb.g = Yf - m.y * Cb - m.z * Cr;
rgb.b = Yf + m.w * Cb;
return clamp(rgb, 0.0, 1.0);
}

// `texture2d<float>::sample` rend TOUJOURS un `float4` en MSL, là où le HLSL
// `Texture2D<float>` rend un scalaire : d'où les `.r` / `.rg` que le port d'origine
// n'avait pas (et qui ne compilaient pas).
inline float3 sample_yuv(float2 uv,
texture2d<float, access::sample> texY,
texture2d<float, access::sample> texUV)
{
float y = texY.sample(samp, uv).r;
float4 y = texY.sample(samp, uv);
float4 c = texUV.sample(samp, uv);
// Bleu à 1 : vue pleine plage posée par `full_range_view` (420f, xf20).
return c.b > 0.5 ? yuv709_full(y, c.rg) : yuv709_limited(y, c.rg);
// Bleu à 1 sur la vue CbCr : pleine plage (420f, xf20).
bool full = c.b > 0.5;
// Vert ou bleu à 1 sur la vue Y : matrice BT.601 ou BT.2020 posée par `nv12_srvs` ;
// alpha à 0 sur cette vue : échantillons 10 bits.
bool ten_bit = y.a < 0.5;
if (y.g > 0.5)
return yuv_matrix(y.r, c.rg, float4(1.402, 0.344136, 0.714136, 1.772), full, ten_bit);
if (y.b > 0.5)
return yuv_matrix(y.r, c.rg, float4(1.4746, 0.164553, 0.571353, 1.8814), full, ten_bit);
return full ? yuv709_full(y.r, c.rg) : yuv709_limited(y.r, c.rg);
}

// SDF segment à bouts ronds — la primitive des flèches d'annotation.
Expand Down
19 changes: 19 additions & 0 deletions crates/compositor/tests/hevc10_decode.rs
Original file line number Diff line number Diff line change
Expand Up @@ -107,3 +107,22 @@ fn a_full_range_frame_keeps_its_levels() {
let frame = render_frame_10(&source.to_string_lossy());
assert_halves(&frame, [0x20; 3], [0xe0; 3], 6);
}

/// Une frame BT.601 n'est pas relue en BT.709 : la matrice vient de `AVFrame::colorspace`
/// (le VUI du flux). Sans le correctif, le vert pur tombe à ~216.
///
/// ```sh
/// ffmpeg -f lavfi -i "color=c=0x00ff00:size=640x360:rate=30:duration=1,drawbox=x=320:y=0:w=320:h=360:color=0x0000ff:t=fill" \
/// -vf scale=out_color_matrix=bt601,format=nv12 -colorspace smpte170m -color_primaries smpte170m -color_trc smpte170m \
/// -c:v h264_videotoolbox split601.mov
/// OPENSCREEN_BT601_SOURCE=$PWD/split601.mov cargo test -p openscreen-compositor --test hevc10_decode
/// ```
#[test]
fn a_bt601_frame_keeps_its_hues() {
let Some(source) = std::env::var_os("OPENSCREEN_BT601_SOURCE") else {
eprintln!("OPENSCREEN_BT601_SOURCE absent — test sauté");
return;
};
let frame = render_frame_10(&source.to_string_lossy());
assert_halves(&frame, [0, 255, 0], [0, 0, 255], 12);
}
Loading