Files
filmcraft/crates/platform/examples/mfprobe.rs
Davi AndreiandClaude Sonnet 5.5 79cb1d0f8d Windows: VP9 + AV1 hardware decode through Media Foundation / DXVA (#30)
Extends the Windows backend (#141) to VP9 (profiles 0 and 2) and AV1 (main, 8/10-bit), 4:2:0.

- codecs::hw: FrameStreamInfo (vpcC / av1C): profile, bit depth, chroma, picture size and colour as
  the software decoders report them (vp9_color / av1_color are now shared with them), random-access
  samples, in-band format changes; StreamInfo lets HybridDecoder handle NAL and frame-coded streams.
- platform: media_foundation::stream (what each codec needs: DXVA profile, MFT request, input
  format), MfDecoder generalised; VP9 frames / AV1 temporal units go in as they are; the MFT's
  declared picture size (display aperture) is checked against the bitstream's.
- Declined up front: VP9 profiles 1/3, AV1 profiles 1/2, 4:2:2/4:4:4, mono, 12-bit, no codec
  extension, no DXVA profile. A key frame of another size or a different sequence header falls back
  to the software decoder.
- Tests: bit-exact parity (360p / 1080p / 2160p, seeks, resets, flush, hidden frames), forced
  failure, damaged samples and av1C, format change fallback, WebM / Matroska through the media stack.
- Bench: AV1 fixtures with libaom when ffmpeg lacks SVT-AV1.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MMorBHwct72udS2LiC3ZHp
2026-10-07 12:39:45 -03:00

65 lines
2.6 KiB
Rust

//! Diagnostics for the Windows backend: why does Media Foundation take or decline a stream, and
//! where does a decode spend its time? `mfprobe [--time] <file.mp4>...` (`--time` decodes every
//! sample and prints the GPU decode, GPU to CPU readback and plane conversion costs per picture).
#[cfg(target_os = "windows")]
fn main() {
use filmcraft_codecs::VideoDecoder;
println!("register: {:?}", filmcraft_platform::register());
let time = std::env::args().any(|a| a == "--time");
for path in std::env::args().skip(1).filter(|a| !a.starts_with("--")) {
let Ok(bytes) = std::fs::read(&path) else {
println!("{path}: unreadable");
continue;
};
let Ok(file) = filmcraft_isobmff::open(bytes.clone()) else {
println!("{path}: not an MP4");
continue;
};
let Some(t) = file.track_of_kind(filmcraft_isobmff::TrackKind::Video) else {
println!("{path}: no video");
continue;
};
let Some(entry) = file.tracks[t].entries.first() else { continue };
let Some(info) = filmcraft_platform::media_foundation::stream_info(entry) else {
println!("{path}: not an avcC / hvcC / vpcC / av1C stream this backend reads");
continue;
};
let mut d = match filmcraft_platform::media_foundation::MfDecoder::new(info) {
Ok(d) => d,
Err(e) => {
println!("{path}: declined: {e}");
continue;
}
};
println!("{path}: hardware ok ({} via {})", d.name(), d.mft_name());
if !time {
continue;
}
let (mut n, t0) = (0usize, std::time::Instant::now());
for i in 0..file.tracks[t].samples.len() {
let Ok(s) = file.read_sample(&bytes, t, i) else { break };
match d.decode(&s, file.tracks[t].samples[i].pts) {
Ok(f) => n += f.len(),
Err(e) => {
println!(" error at sample {i}: {e}");
break;
}
}
}
n += d.flush().len();
let wall = t0.elapsed();
let t = d.timings();
let per = |x: std::time::Duration| x.as_secs_f64() * 1000.0 / t.frames.max(1) as f64;
println!(
" {n} pictures in {:.0} ms ({:.1} fps): DXVA decode {:.2} ms/picture, GPU to CPU readback {:.2} ms, plane conversion {:.2} ms",
wall.as_secs_f64() * 1000.0,
n as f64 / wall.as_secs_f64(),
per(t.decode),
per(t.readback),
per(t.convert)
);
}
}
#[cfg(not(target_os = "windows"))]
fn main() {}