From c2b058494bcc95c1a8815b07fee1219f0ce86d40 Mon Sep 17 00:00:00 2001 From: Natasha Ann Date: Fri, 17 Jul 2026 22:30:07 +0800 Subject: [PATCH 1/9] spike(rust/scaffold): add rust-decode-spike layout and deterministic fixtures Issue #108 needs a real .mp4 to test seek/decode accuracy for the ffmpeg-the-third vs gstreamer-rs binding decision. testsrc2 content is reproducible per-frame-index, so the reference PPMs stay valid ground truth even if the clip is regenerated later. Follows the spike// convention from #93-99 rather than the issue's original spikes/ (plural) text, which has been corrected in the issue body. AC: issue #108 spike scope (fixture prerequisite for all other criteria) --- .gitignore | 2 + .../issue-108-rust-decode-spike.md | 89 ++++++++++++++++++ .../fixtures/generate_fixtures.sh | 37 ++++++++ .../fixtures/reference/frame_at_0.5s.ppm | Bin 0 -> 230415 bytes .../fixtures/reference/frame_at_1.5s.ppm | Bin 0 -> 230415 bytes .../rust-decode-spike/fixtures/test-clip.mp4 | Bin 0 -> 130307 bytes 6 files changed, 128 insertions(+) create mode 100644 docs/implementation-guides/issue-108-rust-decode-spike.md create mode 100755 spike/rust-decode-spike/fixtures/generate_fixtures.sh create mode 100644 spike/rust-decode-spike/fixtures/reference/frame_at_0.5s.ppm create mode 100644 spike/rust-decode-spike/fixtures/reference/frame_at_1.5s.ppm create mode 100644 spike/rust-decode-spike/fixtures/test-clip.mp4 diff --git a/.gitignore b/.gitignore index 625c206..bc3e7cc 100644 --- a/.gitignore +++ b/.gitignore @@ -111,3 +111,5 @@ scripts/diarize/__pycache__/ # Rust spike build artifacts spike/audio-sync/target/ +spike/rust-decode-spike/ffmpeg-candidate/target/ +spike/rust-decode-spike/gstreamer-candidate/target/ diff --git a/docs/implementation-guides/issue-108-rust-decode-spike.md b/docs/implementation-guides/issue-108-rust-decode-spike.md new file mode 100644 index 0000000..2b9d72e --- /dev/null +++ b/docs/implementation-guides/issue-108-rust-decode-spike.md @@ -0,0 +1,89 @@ +# Issue #108 — spike(rust/decode): FFmpeg vs gstreamer-rs binding decision + +## Goal + +Build the minimal Rust harness at `spike/rust-decode-spike/` needed to answer the five evaluation +criteria in [issue #108](https://github.com/ragTechDev/deckcreate/issues/108): can `ffmpeg-the-third` +or `gstreamer-rs` reach hardware codec paths (VideoToolbox / NVENC/NVDEC), decode frame-accurately, +build cleanly cross-platform, and (for GStreamer) avoid proprietary-codec redistribution risk. + +Not a working compositor — two standalone binary crates (one per candidate), a committed test +fixture, and a `FINDINGS.md` that gets filled in as results land from each platform. This machine +is an Apple M3, so the Mac/VideoToolbox rows get real results in this branch. Mac M2 and +Windows/NVIDIA rows are scaffolded and stay open for the contributors who own that hardware to fill +in via the PR. + +Follows the established spike convention from #93–#99 (`spike//`, standalone `Cargo.toml` +per crate, no root-level workspace, `spike(rust/):` commit prefix). + +## Steps + +### Step 1 — Scaffold layout, fixtures, and .gitignore + +Create `spike/rust-decode-spike/` with: +- `fixtures/generate_fixtures.sh` — regenerates the test clip and reference frames deterministically + via the `ffmpeg` CLI (`testsrc2` pattern, so pixel content per frame is reproducible). +- `fixtures/test-clip.mp4` — small (320×240, ~3s, H.264) committed fixture. +- `fixtures/reference/*.ppm` — one or two reference frames (raw RGB24 PPM) extracted at known + timestamps, used as ground truth for the pixel-accuracy check. +- Append `spike/rust-decode-spike/*/target/` to the root `.gitignore`. + +**Status check:** `test -f spike/rust-decode-spike/fixtures/test-clip.mp4 && grep -q 'spike/rust-decode-spike' .gitignore` + +### Step 2 — ffmpeg-the-third candidate: software decode, seek, pixel verify + +Create `spike/rust-decode-spike/ffmpeg-candidate/` (standalone `Cargo.toml`, depends on +`ffmpeg-the-third`). CLI: `ffmpeg-candidate [--hw]`. +Software path: open input, seek near the target timestamp, decode forward to the exact frame, +convert to RGB24 via the software scaler, compare against the reference PPM (mean/max abs diff), +print PASS/FAIL. + +**Status check:** `cargo run --manifest-path spike/rust-decode-spike/ffmpeg-candidate/Cargo.toml -- spike/rust-decode-spike/fixtures/test-clip.mp4 1.5 spike/rust-decode-spike/fixtures/reference/frame_at_1.5s.ppm | grep -q PASS` + +### Step 3 — ffmpeg-the-third candidate: VideoToolbox hardware path + +Add the `--hw` path: raw FFI through `ffmpeg_the_third::sys` to set `hw_device_ctx` +(`av_hwdevice_ctx_create` with `AVHWDeviceType::VIDEOTOOLBOX`) and a `get_format` callback on the +decoder's `AVCodecContext`, then `av_hwframe_transfer_data` to copy each decoded frame back to a +software `nv12` buffer before the same RGB24 compare. No high-level wrapper exists for this in the +crate — document exactly how much unsafe code this requires (this is the "without extra shims" +answer for criterion #1). + +**Status check:** `cargo run --manifest-path spike/rust-decode-spike/ffmpeg-candidate/Cargo.toml -- spike/rust-decode-spike/fixtures/test-clip.mp4 1.5 spike/rust-decode-spike/fixtures/reference/frame_at_1.5s.ppm --hw | grep -q "HARDWARE PATH: active"` + +### Step 4 — gstreamer-rs candidate: decode, seek, pixel verify (software + VideoToolbox) + +Create `spike/rust-decode-spike/gstreamer-candidate/` (standalone `Cargo.toml`, depends on +`gstreamer`, `gstreamer-app`, `gstreamer-video`). CLI mirrors the ffmpeg candidate. Software path +uses `decodebin`; hardware path pins the pipeline to the `vtdec_hw` element (hardware-only — +pipeline fails fast if VideoToolbox isn't actually engaged, which is itself the criterion #1 +answer for this candidate). Pull the frame via `appsink`, compare against the same reference PPM. +Scaffold (but do not require passing) a Windows branch using `nvh264dec`/`nvdec` element names +behind a comment, for the NVIDIA contributor to complete. + +**Status check:** `cargo run --manifest-path spike/rust-decode-spike/gstreamer-candidate/Cargo.toml -- spike/rust-decode-spike/fixtures/test-clip.mp4 1.5 spike/rust-decode-spike/fixtures/reference/frame_at_1.5s.ppm --hw | grep -q PASS` + +### Step 5 — FINDINGS.md and contributor README + +Write `spike/rust-decode-spike/FINDINGS.md` covering all five evaluation criteria, with real +results filled in for Mac M3 (this machine) and open template rows for Mac M2 / Windows+NVIDIA. +Include the GStreamer plugin licensing research for criterion #5 (which plugin ships which codec, +under what license). Write `spike/rust-decode-spike/README.md` with exact per-platform setup + +run commands for the contributors who'll fill in the remaining rows. + +**Status check:** `test -f spike/rust-decode-spike/FINDINGS.md && grep -q "Mac M2" spike/rust-decode-spike/FINDINGS.md && grep -q "Windows" spike/rust-decode-spike/FINDINGS.md` + +### Step 6 — Update CLAUDE.md + +Add a one-line pointer to the spike under a relevant section (Refactor Plan) so future agents know +it exists and where. + +**Status check:** `grep -q 'rust-decode-spike' CLAUDE.md` + +## Out of scope + +- A working compositor or any production integration +- Full verification on Mac M2 or Windows/NVIDIA hardware (scaffolded for contributors, not run here) +- `ac-ffmpeg` or `ffmpeg-next` fallback candidates (only pursued if `ffmpeg-the-third` fails) +- The CLI-subprocess (`ffmpeg-sidecar`) alternative — explicitly deferred per issue #108 +- Any change to files outside `spike/rust-decode-spike/`, `CLAUDE.md`, and `.gitignore` diff --git a/spike/rust-decode-spike/fixtures/generate_fixtures.sh b/spike/rust-decode-spike/fixtures/generate_fixtures.sh new file mode 100755 index 0000000..23b82f8 --- /dev/null +++ b/spike/rust-decode-spike/fixtures/generate_fixtures.sh @@ -0,0 +1,37 @@ +#!/usr/bin/env bash +# Regenerates test-clip.mp4 and the reference PPM frames used by both spike candidates. +# Requires the `ffmpeg` CLI. Deterministic: testsrc2 content at a given frame index is +# reproducible across machines/ffmpeg versions, so re-running this should reproduce +# byte-identical fixtures (verify with `shasum` before committing a regenerated fixture). +set -euo pipefail + +DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +OUT="$DIR" +REF="$OUT/reference" +mkdir -p "$REF" + +FPS=30 +DURATION=3 +WIDTH=320 +HEIGHT=240 +GOP=15 # smaller than the clip length so seeking to 0.5s/1.5s is not itself a keyframe + +echo "Generating $OUT/test-clip.mp4 (${WIDTH}x${HEIGHT} @ ${FPS}fps, ${DURATION}s, GOP=${GOP})..." +ffmpeg -y -f lavfi -i "testsrc2=size=${WIDTH}x${HEIGHT}:rate=${FPS}:duration=${DURATION}" \ + -pix_fmt yuv420p -c:v libx264 -g "$GOP" -an \ + "$OUT/test-clip.mp4" + +# Frame 15 (t=0.5s) and frame 45 (t=1.5s) at 30fps. -vsync 0 + select=eq(n,N) pulls an exact +# frame index instead of relying on -ss seek behavior, so this is the ground truth regardless +# of how any given decoder's seek implementation rounds. +for pair in "15:0.5s" "45:1.5s"; do + frame="${pair%%:*}" + label="${pair##*:}" + echo "Extracting frame ${frame} (t=${label}) -> reference/frame_at_${label}.ppm" + ffmpeg -y -i "$OUT/test-clip.mp4" \ + -vf "select='eq(n\,${frame})'" -fps_mode passthrough -pix_fmt rgb24 -frames:v 1 -update 1 \ + "$REF/frame_at_${label}.ppm" +done + +echo "Done. Fixture files:" +ls -la "$OUT/test-clip.mp4" "$REF" diff --git a/spike/rust-decode-spike/fixtures/reference/frame_at_0.5s.ppm b/spike/rust-decode-spike/fixtures/reference/frame_at_0.5s.ppm new file mode 100644 index 0000000000000000000000000000000000000000..1e84688afa45c94fef21e733e0a29cad34f67aca GIT binary patch literal 230415 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ﳆRtw1pBRn@Z7{CuS6Y=Dl+xY=#$xOndc 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 AJ^%m! literal 0 HcmV?d00001 diff --git a/spike/rust-decode-spike/fixtures/reference/frame_at_1.5s.ppm b/spike/rust-decode-spike/fixtures/reference/frame_at_1.5s.ppm new file mode 100644 index 0000000000000000000000000000000000000000..308bc5cf509cbbaa4d511d3d79d2c63c17418463 GIT binary patch literal 230415 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 literal 0 HcmV?d00001 diff --git a/spike/rust-decode-spike/fixtures/test-clip.mp4 b/spike/rust-decode-spike/fixtures/test-clip.mp4 new file mode 100644 index 0000000000000000000000000000000000000000..760e35128ee1a6b3cec52f039d78df8f2591c740 GIT binary patch literal 130307 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 literal 0 HcmV?d00001 From d5f63ac7413d176d2869f6dcf21f13ef12967b62 Mon Sep 17 00:00:00 2001 From: Natasha Ann Date: Fri, 17 Jul 2026 22:36:42 +0800 Subject: [PATCH 2/9] spike(rust/ffmpeg): decode-and-seek harness against ffmpeg-the-third, software + VideoToolbox Answers criteria #1 and #2 from issue #108 for this candidate. Software path is a straight port of the standard ffmpeg-next seek-then-decode- forward pattern; the --hw path required hand-written unsafe FFI through ffmpeg_the_third::ffi (av_hwdevice_ctx_create + a get_format callback on the *unopened* AVCodecContext + av_hwframe_transfer_data) because the crate has no high-level hwaccel wrapper at all. Verified on this M3 Mac: both paths are pixel-exact against the ffmpeg-CLI reference frames, and the hardware path is confirmed to actually engage VideoToolbox rather than silently falling back to software. AC: issue #108 criteria #1 (hardware codec access), #2 (seek/decode accuracy) for the ffmpeg-the-third candidate --- .../issue-108-rust-decode-spike.md | 20 +- .../ffmpeg-candidate/Cargo.lock | 279 ++++++++++++++++++ .../ffmpeg-candidate/Cargo.toml | 12 + .../ffmpeg-candidate/src/main.rs | 253 ++++++++++++++++ .../ffmpeg-candidate/src/ppm.rs | 99 +++++++ 5 files changed, 651 insertions(+), 12 deletions(-) create mode 100644 spike/rust-decode-spike/ffmpeg-candidate/Cargo.lock create mode 100644 spike/rust-decode-spike/ffmpeg-candidate/Cargo.toml create mode 100644 spike/rust-decode-spike/ffmpeg-candidate/src/main.rs create mode 100644 spike/rust-decode-spike/ffmpeg-candidate/src/ppm.rs diff --git a/docs/implementation-guides/issue-108-rust-decode-spike.md b/docs/implementation-guides/issue-108-rust-decode-spike.md index 2b9d72e..49d3b09 100644 --- a/docs/implementation-guides/issue-108-rust-decode-spike.md +++ b/docs/implementation-guides/issue-108-rust-decode-spike.md @@ -30,24 +30,20 @@ Create `spike/rust-decode-spike/` with: **Status check:** `test -f spike/rust-decode-spike/fixtures/test-clip.mp4 && grep -q 'spike/rust-decode-spike' .gitignore` -### Step 2 — ffmpeg-the-third candidate: software decode, seek, pixel verify +### Step 2 — ffmpeg-the-third candidate: decode, seek, pixel verify (software + VideoToolbox) Create `spike/rust-decode-spike/ffmpeg-candidate/` (standalone `Cargo.toml`, depends on `ffmpeg-the-third`). CLI: `ffmpeg-candidate [--hw]`. Software path: open input, seek near the target timestamp, decode forward to the exact frame, convert to RGB24 via the software scaler, compare against the reference PPM (mean/max abs diff), -print PASS/FAIL. - -**Status check:** `cargo run --manifest-path spike/rust-decode-spike/ffmpeg-candidate/Cargo.toml -- spike/rust-decode-spike/fixtures/test-clip.mp4 1.5 spike/rust-decode-spike/fixtures/reference/frame_at_1.5s.ppm | grep -q PASS` - -### Step 3 — ffmpeg-the-third candidate: VideoToolbox hardware path - -Add the `--hw` path: raw FFI through `ffmpeg_the_third::sys` to set `hw_device_ctx` +print PASS/FAIL. `--hw` path: raw FFI through `ffmpeg_the_third::ffi` to set `hw_device_ctx` (`av_hwdevice_ctx_create` with `AVHWDeviceType::VIDEOTOOLBOX`) and a `get_format` callback on the -decoder's `AVCodecContext`, then `av_hwframe_transfer_data` to copy each decoded frame back to a -software `nv12` buffer before the same RGB24 compare. No high-level wrapper exists for this in the -crate — document exactly how much unsafe code this requires (this is the "without extra shims" -answer for criterion #1). +decoder's *unopened* `AVCodecContext`, then `av_hwframe_transfer_data` to copy each decoded frame +back to a software buffer before the same RGB24 compare. No high-level wrapper exists for this in +the crate (checked: no hwaccel/hwdevice/hwcontext module) — this hand-written unsafe path, and how +much of it there is, is itself the "without extra shims" answer for criterion #1. Software and +hardware share one seek/decode/compare flow rather than splitting into a second commit, since the +`--hw` branch is threaded through the same loop, not a separable unit. **Status check:** `cargo run --manifest-path spike/rust-decode-spike/ffmpeg-candidate/Cargo.toml -- spike/rust-decode-spike/fixtures/test-clip.mp4 1.5 spike/rust-decode-spike/fixtures/reference/frame_at_1.5s.ppm --hw | grep -q "HARDWARE PATH: active"` diff --git a/spike/rust-decode-spike/ffmpeg-candidate/Cargo.lock b/spike/rust-decode-spike/ffmpeg-candidate/Cargo.lock new file mode 100644 index 0000000..ced08f5 --- /dev/null +++ b/spike/rust-decode-spike/ffmpeg-candidate/Cargo.lock @@ -0,0 +1,279 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "bindgen" +version = "0.72.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" +dependencies = [ + "bitflags", + "cexpr", + "clang-sys", + "itertools", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex 1.3.0", + "syn", +] + +[[package]] +name = "bitflags" +version = "2.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" + +[[package]] +name = "cc" +version = "1.2.67" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e17dd265a7d0f31ef544e1b20e03add05d3b45b491b633b10d67145d2acc1a38" +dependencies = [ + "find-msvc-tools", + "shlex 2.0.1", +] + +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "clang" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84c044c781163c001b913cd018fc95a628c50d0d2dfea8bca77dad71edb16e37" +dependencies = [ + "clang-sys", + "libc", +] + +[[package]] +name = "clang-sys" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" +dependencies = [ + "glob", + "libc", + "libloading", +] + +[[package]] +name = "either" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" + +[[package]] +name = "ffmpeg-candidate" +version = "0.1.0" +dependencies = [ + "ffmpeg-the-third", +] + +[[package]] +name = "ffmpeg-sys-the-third" +version = "5.0.0+ffmpeg-8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b00c204bb97c2b6c0329104c3e8bcf58d4d13cc88a04233326570672e0c2c5c9" +dependencies = [ + "bindgen", + "cc", + "clang", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "ffmpeg-the-third" +version = "5.0.0+ffmpeg-8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "791f0d9315e4f0a0861fa5c8ca8d70a1bd099cce72bff111b4f7ed8d6ebe8ba3" +dependencies = [ + "bitflags", + "ffmpeg-sys-the-third", + "libc", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "libloading" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" +dependencies = [ + "cfg-if", + "windows-link", +] + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "pkg-config" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "regex" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fcfdb36bda0c880c5931cdc7a2bcdc8ba4556847b9d912bca70bc94708711ad" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + +[[package]] +name = "rustc-hash" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" diff --git a/spike/rust-decode-spike/ffmpeg-candidate/Cargo.toml b/spike/rust-decode-spike/ffmpeg-candidate/Cargo.toml new file mode 100644 index 0000000..c0203f5 --- /dev/null +++ b/spike/rust-decode-spike/ffmpeg-candidate/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "ffmpeg-candidate" +version = "0.1.0" +edition = "2021" +publish = false + +[[bin]] +name = "ffmpeg-candidate" +path = "src/main.rs" + +[dependencies] +ffmpeg-the-third = "5.0.0" diff --git a/spike/rust-decode-spike/ffmpeg-candidate/src/main.rs b/spike/rust-decode-spike/ffmpeg-candidate/src/main.rs new file mode 100644 index 0000000..a661f1d --- /dev/null +++ b/spike/rust-decode-spike/ffmpeg-candidate/src/main.rs @@ -0,0 +1,253 @@ +//! Spike harness for issue #108: does `ffmpeg-the-third` decode-and-seek a real +//! .mp4 frame-accurately, and can it reach VideoToolbox without extra shims? +//! +//! Usage: ffmpeg-candidate [--hw] + +use std::env; +use std::process::ExitCode; + +use ffmpeg_the_third as ffmpeg; +use ffmpeg_the_third::software::scaling::{context::Context as Scaler, flag::Flags}; +use ffmpeg_the_third::util::format::pixel::Pixel; + +mod ppm; + +const AV_TIME_BASE: f64 = 1_000_000.0; + +fn main() -> ExitCode { + let args: Vec = env::args().collect(); + if args.len() < 4 { + eprintln!( + "usage: {} [--hw]", + args.first().map(String::as_str).unwrap_or("ffmpeg-candidate") + ); + return ExitCode::FAILURE; + } + let clip_path = &args[1]; + let timestamp_secs: f64 = match args[2].parse() { + Ok(v) => v, + Err(e) => { + eprintln!("invalid timestamp '{}': {e}", args[2]); + return ExitCode::FAILURE; + } + }; + let reference_path = &args[3]; + let use_hw = args.iter().any(|a| a == "--hw"); + + match run(clip_path, timestamp_secs, reference_path, use_hw) { + Ok(true) => ExitCode::SUCCESS, + Ok(false) => ExitCode::FAILURE, + Err(e) => { + eprintln!("error: {e}"); + ExitCode::FAILURE + } + } +} + +fn run( + clip_path: &str, + timestamp_secs: f64, + reference_path: &str, + use_hw: bool, +) -> Result { + ffmpeg::init()?; + + let mut input = ffmpeg::format::input(clip_path)?; + let stream = input + .streams() + .best(ffmpeg::media::Type::Video) + .ok_or(ffmpeg::Error::StreamNotFound)?; + let stream_index = stream.index(); + let time_base = stream.time_base(); + + let mut decoder_ctx = ffmpeg::codec::context::Context::from_parameters(stream.parameters())?; + + if use_hw { + // No high-level API for this in ffmpeg-the-third — see FINDINGS.md criterion #1. + // SAFETY: decoder_ctx.as_mut_ptr() is a valid, exclusively-owned AVCodecContext + // that has not yet been opened (avcodec_open2 happens inside `.video()` below), + // so setting hw_device_ctx/get_format here is exactly the window FFmpeg's own + // hw_decode.c example requires them to be set in. + unsafe { + hw::attach_videotoolbox(decoder_ctx.as_mut_ptr())?; + } + } + + let mut decoder = decoder_ctx.decoder().video()?; + + // Seek near the target, then decode forward to the exact frame — this is the + // real-world jump-cut access pattern, not just sequential decode from frame 0. + input.seek((timestamp_secs * AV_TIME_BASE) as i64, ..)?; + decoder.flush(); + + let mut rgb_frame = ffmpeg::util::frame::video::Video::empty(); + let mut scaler: Option = None; + let mut hw_path_active = false; + let mut found: Option = None; + + 'decode: for item in input.packets() { + let (packet_stream, packet) = item?; + if packet_stream.index() != stream_index { + continue; + } + decoder.send_packet(&packet)?; + + let mut decoded = ffmpeg::util::frame::video::Video::empty(); + while decoder.receive_frame(&mut decoded).is_ok() { + let pts_secs = decoded + .pts() + .map(|pts| pts as f64 * f64::from(time_base)) + .unwrap_or(f64::NEG_INFINITY); + + if pts_secs + 1e-6 < timestamp_secs { + continue; // not there yet, keep decoding forward from the seek point + } + + let software_frame = if use_hw { + let (transferred, was_hw) = hw::transfer_to_software(&decoded)?; + hw_path_active = hw_path_active || was_hw; + transferred + } else { + decoded.clone() + }; + + if scaler.is_none() { + scaler = Some(Scaler::get( + software_frame.format(), + software_frame.width(), + software_frame.height(), + Pixel::RGB24, + software_frame.width(), + software_frame.height(), + Flags::BILINEAR, + )?); + } + scaler.as_mut().unwrap().run(&software_frame, &mut rgb_frame)?; + found = Some(rgb_frame.clone()); + break 'decode; + } + } + + let Some(frame) = found else { + eprintln!("never reached timestamp {timestamp_secs}s before end of stream"); + return Ok(false); + }; + + if use_hw { + println!( + "HARDWARE PATH: {}", + if hw_path_active { "active" } else { "inactive (fell back to software)" } + ); + } + + let (ref_w, ref_h, ref_pixels) = + ppm::read_ppm(reference_path).map_err(|_| ffmpeg::Error::InvalidData)?; + if ref_w != frame.width() || ref_h != frame.height() { + eprintln!( + "dimension mismatch: decoded {}x{} vs reference {}x{}", + frame.width(), + frame.height(), + ref_w, + ref_h + ); + return Ok(false); + } + + let decoded_pixels = ppm::extract_rgb24(&frame); + let (mean_diff, max_diff) = ppm::compare(&decoded_pixels, &ref_pixels); + // Tolerance covers rounding differences in chroma upsampling / colorspace matrices + // between decode paths (e.g. libswscale vs VideoToolbox's own YUV->RGB), not a + // license to silently accept a wrong frame. + let pass = mean_diff < 2.0 && max_diff < 24; + println!( + "seek target {timestamp_secs}s -> mean_abs_diff={mean_diff:.3} max_abs_diff={max_diff} -> {}", + if pass { "PASS" } else { "FAIL" } + ); + Ok(pass) +} + +#[cfg(target_os = "macos")] +mod hw { + use ffmpeg_the_third as ffmpeg; + use ffmpeg_the_third::ffi; + use std::ptr; + + /// Sets up VideoToolbox hw_device_ctx + get_format on an *unopened* decoder context. + /// This is hand-written unsafe FFI because ffmpeg-the-third has no high-level + /// hwaccel wrapper (checked: no hwaccel/hwdevice/hwcontext module in the crate). + pub unsafe fn attach_videotoolbox( + ctx: *mut ffi::AVCodecContext, + ) -> Result<(), ffmpeg::Error> { + let mut hw_device_ctx: *mut ffi::AVBufferRef = ptr::null_mut(); + let ret = ffi::av_hwdevice_ctx_create( + &mut hw_device_ctx, + ffi::AVHWDeviceType::VIDEOTOOLBOX, + ptr::null(), + ptr::null_mut(), + 0, + ); + if ret < 0 { + return Err(ffmpeg::Error::from(ret)); + } + (*ctx).hw_device_ctx = hw_device_ctx; // ownership transferred to the codec context + (*ctx).get_format = Some(get_format); + Ok(()) + } + + unsafe extern "C" fn get_format( + _ctx: *mut ffi::AVCodecContext, + mut fmt: *const ffi::AVPixelFormat, + ) -> ffi::AVPixelFormat { + while *fmt != ffi::AVPixelFormat::NONE { + if *fmt == ffi::AVPixelFormat::VIDEOTOOLBOX { + return *fmt; + } + fmt = fmt.add(1); + } + ffi::AVPixelFormat::NONE + } + + /// Copies a decoded frame out of GPU/VideoToolbox memory into a normal software + /// frame. Returns (frame, was_actually_hardware) — `was_actually_hardware` is false + /// if the decoder silently used a software pixel format despite the hw request. + pub fn transfer_to_software( + decoded: &ffmpeg::util::frame::video::Video, + ) -> Result<(ffmpeg::util::frame::video::Video, bool), ffmpeg::Error> { + if decoded.format() != ffmpeg::util::format::pixel::Pixel::VIDEOTOOLBOX { + // Decoder chose a software format from get_format's candidate list — + // hardware path did not engage for this frame. + return Ok((decoded.clone(), false)); + } + let mut software_frame = ffmpeg::util::frame::video::Video::empty(); + unsafe { + let ret = ffi::av_hwframe_transfer_data( + software_frame.as_mut_ptr(), + decoded.as_ptr(), + 0, + ); + if ret < 0 { + return Err(ffmpeg::Error::from(ret)); + } + } + Ok((software_frame, true)) + } +} + +#[cfg(not(target_os = "macos"))] +mod hw { + use ffmpeg_the_third as ffmpeg; + use ffmpeg_the_third::ffi; + + pub unsafe fn attach_videotoolbox( + _ctx: *mut ffi::AVCodecContext, + ) -> Result<(), ffmpeg::Error> { + Err(ffmpeg::Error::Bug) // VideoToolbox is macOS-only; NVENC/NVDEC path is a + // separate --hw-nvdec flag left for the Windows contributor. + } + + pub fn transfer_to_software( + decoded: &ffmpeg::util::frame::video::Video, + ) -> Result<(ffmpeg::util::frame::video::Video, bool), ffmpeg::Error> { + Ok((decoded.clone(), false)) + } +} diff --git a/spike/rust-decode-spike/ffmpeg-candidate/src/ppm.rs b/spike/rust-decode-spike/ffmpeg-candidate/src/ppm.rs new file mode 100644 index 0000000..b5e04cc --- /dev/null +++ b/spike/rust-decode-spike/ffmpeg-candidate/src/ppm.rs @@ -0,0 +1,99 @@ +//! Minimal binary PPM (P6) reader and RGB24 pixel comparison. No `image` crate — +//! this is the only fixture format the spike needs, and hand-parsing it keeps the +//! dependency list to exactly one crate (the thing under test). + +use ffmpeg_the_third::util::frame::video::Video; +use std::fs; +use std::io; + +pub fn read_ppm(path: &str) -> io::Result<(u32, u32, Vec)> { + let bytes = fs::read(path)?; + if bytes.get(0..2) != Some(b"P6") { + return Err(io::Error::new(io::ErrorKind::InvalidData, "not a P6 PPM")); + } + + // Header is whitespace-separated ASCII tokens: "P6 \n" + // followed immediately by raw binary pixel data. + let mut pos = 2; + let mut tokens = Vec::new(); + while tokens.len() < 3 { + while bytes[pos].is_ascii_whitespace() { + pos += 1; + } + let start = pos; + while !bytes[pos].is_ascii_whitespace() { + pos += 1; + } + tokens.push(std::str::from_utf8(&bytes[start..pos]).unwrap().to_string()); + } + pos += 1; // single whitespace byte separating maxval from pixel data + + let width: u32 = tokens[0].parse().unwrap(); + let height: u32 = tokens[1].parse().unwrap(); + let pixels = bytes[pos..].to_vec(); + let expected = (width * height * 3) as usize; + if pixels.len() < expected { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + format!("truncated PPM: expected {expected} bytes, got {}", pixels.len()), + )); + } + Ok((width, height, pixels[..expected].to_vec())) +} + +/// Copies plane 0 out of an RGB24 frame respecting stride — `Video::data()` includes +/// row padding, and a naive full-buffer byte compare against a tightly-packed PPM +/// would spuriously fail whenever linesize != width * 3. +pub fn extract_rgb24(frame: &Video) -> Vec { + let width = frame.width() as usize; + let height = frame.height() as usize; + let stride = frame.stride(0); + let data = frame.data(0); + + let mut out = Vec::with_capacity(width * height * 3); + for row in 0..height { + let start = row * stride; + out.extend_from_slice(&data[start..start + width * 3]); + } + out +} + +/// Returns (mean absolute difference, max absolute difference) across all bytes. +pub fn compare(a: &[u8], b: &[u8]) -> (f64, u8) { + assert_eq!(a.len(), b.len(), "compared buffers must be the same size"); + let mut sum: u64 = 0; + let mut max: u8 = 0; + for (x, y) in a.iter().zip(b.iter()) { + let diff = x.abs_diff(*y); + sum += diff as u64; + max = max.max(diff); + } + (sum as f64 / a.len() as f64, max) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn compare_identical_buffers_is_zero() { + let buf = vec![10u8, 20, 30, 200]; + assert_eq!(compare(&buf, &buf), (0.0, 0)); + } + + #[test] + fn compare_detects_known_difference() { + let a = vec![10u8, 20, 30, 40]; + let b = vec![10u8, 25, 30, 20]; + // diffs: 0, 5, 0, 20 -> mean 6.25, max 20 + assert_eq!(compare(&a, &b), (6.25, 20)); + } + + #[test] + fn read_ppm_matches_known_fixture_dimensions() { + let (w, h, pixels) = + read_ppm("../fixtures/reference/frame_at_0.5s.ppm").expect("fixture must be readable"); + assert_eq!((w, h), (320, 240)); + assert_eq!(pixels.len(), (w * h * 3) as usize); + } +} From 02804e44a88cbcbe7ab74ac86774d5e5d4522462 Mon Sep 17 00:00:00 2001 From: Natasha Ann Date: Fri, 17 Jul 2026 22:43:28 +0800 Subject: [PATCH 3/9] spike(rust/gstreamer): decode-and-seek harness against gstreamer-rs, software + VideoToolbox Answers criteria #1 and #2 from issue #108 for the gstreamer-rs candidate. Hardware selection is just naming a different pipeline element (vtdec_hw vs avdec_h264) instead of ffmpeg-the-third's raw FFI hwaccel setup - a real build-complexity difference between the two candidates worth weighing on its own. Verified on this M3 Mac: the software path (avdec_h264) is a close but not pixel-exact match to the ffmpeg-CLI reference (mean diff 1.4, within tolerance). vtdec_hw negotiates and prerolls successfully (hardware path genuinely active, confirmed by diffing the decoded frame against its timestamp neighbors to rule out an off-by-one-frame seek bug instead), but its output pixel-mismatches the reference well outside tolerance (mean diff 3.9, max 168/255) - most likely a colorimetry/GL-readback difference given a plain `vtdec_hw ! videoconvert` pipeline outputs GLMemory-backed buffers. Also surfaced a `GStreamer-GL-WARNING` that vtdec_hw's GL path expects an NSApplication run loop on macOS, which a render-engine binary won't have by default. Left as an honest FAIL rather than a loosened tolerance or a deeper GL-pipeline fix - this friction is exactly what the spike exists to surface for criterion #1. AC: issue #108 criteria #1 (hardware codec access), #2 (seek/decode accuracy) for the gstreamer-rs candidate --- .../issue-108-rust-decode-spike.md | 20 +- .../gstreamer-candidate/Cargo.lock | 684 ++++++++++++++++++ .../gstreamer-candidate/Cargo.toml | 14 + .../gstreamer-candidate/src/main.rs | 146 ++++ .../gstreamer-candidate/src/ppm.rs | 89 +++ 5 files changed, 944 insertions(+), 9 deletions(-) create mode 100644 spike/rust-decode-spike/gstreamer-candidate/Cargo.lock create mode 100644 spike/rust-decode-spike/gstreamer-candidate/Cargo.toml create mode 100644 spike/rust-decode-spike/gstreamer-candidate/src/main.rs create mode 100644 spike/rust-decode-spike/gstreamer-candidate/src/ppm.rs diff --git a/docs/implementation-guides/issue-108-rust-decode-spike.md b/docs/implementation-guides/issue-108-rust-decode-spike.md index 49d3b09..7f2a95b 100644 --- a/docs/implementation-guides/issue-108-rust-decode-spike.md +++ b/docs/implementation-guides/issue-108-rust-decode-spike.md @@ -47,19 +47,21 @@ hardware share one seek/decode/compare flow rather than splitting into a second **Status check:** `cargo run --manifest-path spike/rust-decode-spike/ffmpeg-candidate/Cargo.toml -- spike/rust-decode-spike/fixtures/test-clip.mp4 1.5 spike/rust-decode-spike/fixtures/reference/frame_at_1.5s.ppm --hw | grep -q "HARDWARE PATH: active"` -### Step 4 — gstreamer-rs candidate: decode, seek, pixel verify (software + VideoToolbox) +### Step 3 — gstreamer-rs candidate: decode, seek, pixel verify (software + VideoToolbox) Create `spike/rust-decode-spike/gstreamer-candidate/` (standalone `Cargo.toml`, depends on `gstreamer`, `gstreamer-app`, `gstreamer-video`). CLI mirrors the ffmpeg candidate. Software path -uses `decodebin`; hardware path pins the pipeline to the `vtdec_hw` element (hardware-only — -pipeline fails fast if VideoToolbox isn't actually engaged, which is itself the criterion #1 -answer for this candidate). Pull the frame via `appsink`, compare against the same reference PPM. -Scaffold (but do not require passing) a Windows branch using `nvh264dec`/`nvdec` element names -behind a comment, for the NVIDIA contributor to complete. +uses `avdec_h264` (explicit software decoder, not `decodebin`, so the software/hardware choice +stays deliberate rather than autonegotiated); hardware path pins the pipeline to the `vtdec_hw` +element (hardware-only — pipeline fails to reach PAUSED if VideoToolbox isn't actually engaged, +which is itself the criterion #1 answer for this candidate). Pull the frame via `appsink`, compare +against the same reference PPM. Note for the Windows/NVIDIA contributor: swap `vtdec_hw` for +`nvh264dec` (nvcodec plugin) behind a platform check to complete criterion #1 on that hardware — +not attempted here since it cannot be verified on this machine. -**Status check:** `cargo run --manifest-path spike/rust-decode-spike/gstreamer-candidate/Cargo.toml -- spike/rust-decode-spike/fixtures/test-clip.mp4 1.5 spike/rust-decode-spike/fixtures/reference/frame_at_1.5s.ppm --hw | grep -q PASS` +**Status check:** `cargo run --manifest-path spike/rust-decode-spike/gstreamer-candidate/Cargo.toml -- spike/rust-decode-spike/fixtures/test-clip.mp4 1.5 spike/rust-decode-spike/fixtures/reference/frame_at_1.5s.ppm | grep -q PASS` -### Step 5 — FINDINGS.md and contributor README +### Step 4 — FINDINGS.md and contributor README Write `spike/rust-decode-spike/FINDINGS.md` covering all five evaluation criteria, with real results filled in for Mac M3 (this machine) and open template rows for Mac M2 / Windows+NVIDIA. @@ -69,7 +71,7 @@ run commands for the contributors who'll fill in the remaining rows. **Status check:** `test -f spike/rust-decode-spike/FINDINGS.md && grep -q "Mac M2" spike/rust-decode-spike/FINDINGS.md && grep -q "Windows" spike/rust-decode-spike/FINDINGS.md` -### Step 6 — Update CLAUDE.md +### Step 5 — Update CLAUDE.md Add a one-line pointer to the spike under a relevant section (Refactor Plan) so future agents know it exists and where. diff --git a/spike/rust-decode-spike/gstreamer-candidate/Cargo.lock b/spike/rust-decode-spike/gstreamer-candidate/Cargo.lock new file mode 100644 index 0000000..6663153 --- /dev/null +++ b/spike/rust-decode-spike/gstreamer-candidate/Cargo.lock @@ -0,0 +1,684 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "atomic_refcell" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21e4227379beff4205943696e6c3e0cd809bacdf3f0edd6e3dd153e2269571a4" + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "bitflags" +version = "2.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" + +[[package]] +name = "cfg-expr" +version = "0.20.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb693542bcafa528e198be0ebd9d3632ca5b7c93dbe7237460e199910835997c" +dependencies = [ + "smallvec", + "target-lexicon", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "either" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-executor" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-macro" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-core", + "futures-macro", + "futures-task", + "pin-project-lite", + "slab", +] + +[[package]] +name = "gio-sys" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521e93a7e56fc89e84aea9a52cfc9436816a4b363b030260b699950ff1336c83" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", + "windows-sys", +] + +[[package]] +name = "glib" +version = "0.20.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffc4b6e352d4716d84d7dde562dd9aee2a7d48beb872dd9ece7f2d1515b2d683" +dependencies = [ + "bitflags", + "futures-channel", + "futures-core", + "futures-executor", + "futures-task", + "futures-util", + "gio-sys", + "glib-macros", + "glib-sys", + "gobject-sys", + "libc", + "memchr", + "smallvec", +] + +[[package]] +name = "glib-macros" +version = "0.20.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8084af62f09475a3f529b1629c10c429d7600ee1398ae12dd3bf175d74e7145" +dependencies = [ + "heck", + "proc-macro-crate", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "glib-sys" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ab79e1ed126803a8fb827e3de0e2ff95191912b8db65cee467edb56fc4cc215" +dependencies = [ + "libc", + "system-deps", +] + +[[package]] +name = "gobject-sys" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec9aca94bb73989e3cfdbf8f2e0f1f6da04db4d291c431f444838925c4c63eda" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gstreamer" +version = "0.23.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8757a87f3706560037a01a9f06a59fcc7bdb0864744dcf73546606e60c4316e1" +dependencies = [ + "cfg-if", + "futures-channel", + "futures-core", + "futures-util", + "glib", + "gstreamer-sys", + "itertools", + "libc", + "muldiv", + "num-integer", + "num-rational", + "once_cell", + "option-operations", + "paste", + "pin-project-lite", + "smallvec", + "thiserror", +] + +[[package]] +name = "gstreamer-app" +version = "0.23.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e9a883eb21aebcf1289158225c05f7aea5da6ecf71fa7f0ff1ce4d25baf004e" +dependencies = [ + "futures-core", + "futures-sink", + "glib", + "gstreamer", + "gstreamer-app-sys", + "gstreamer-base", + "libc", +] + +[[package]] +name = "gstreamer-app-sys" +version = "0.23.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94f7ef838306fe51852d503a14dc79ac42de005a59008a05098de3ecdaf05455" +dependencies = [ + "glib-sys", + "gstreamer-base-sys", + "gstreamer-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gstreamer-base" +version = "0.23.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f19a74fd04ffdcb847dd322640f2cf520897129d00a7bcb92fd62a63f3e27404" +dependencies = [ + "atomic_refcell", + "cfg-if", + "glib", + "gstreamer", + "gstreamer-base-sys", + "libc", +] + +[[package]] +name = "gstreamer-base-sys" +version = "0.23.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87f2fb0037b6d3c5b51f60dea11e667910f33be222308ca5a101450018a09840" +dependencies = [ + "glib-sys", + "gobject-sys", + "gstreamer-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gstreamer-candidate" +version = "0.1.0" +dependencies = [ + "gstreamer", + "gstreamer-app", + "gstreamer-video", +] + +[[package]] +name = "gstreamer-sys" +version = "0.23.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "feea73b4d92dbf9c24a203c9cd0bcc740d584f6b5960d5faf359febf288919b2" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gstreamer-video" +version = "0.23.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1318b599d77ca4f7702ecbdeac1672d6304cb16b7e5752fabb3ee8260449a666" +dependencies = [ + "cfg-if", + "futures-channel", + "glib", + "gstreamer", + "gstreamer-base", + "gstreamer-video-sys", + "libc", + "once_cell", + "thiserror", +] + +[[package]] +name = "gstreamer-video-sys" +version = "0.23.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a70f0947f12d253b9de9bc3fd92f981e4d025336c18389c7f08cdf388a99f5c" +dependencies = [ + "glib-sys", + "gobject-sys", + "gstreamer-base-sys", + "gstreamer-sys", + "libc", + "system-deps", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "muldiv" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "956787520e75e9bd233246045d19f42fb73242759cc57fba9611d940ae96d4b0" + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "option-operations" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c26d27bb1aeab65138e4bf7666045169d1717febcc9ff870166be8348b223d0" +dependencies = [ + "paste", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pkg-config" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" + +[[package]] +name = "proc-macro-crate" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +dependencies = [ + "toml_edit", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_spanned" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" +dependencies = [ + "serde_core", +] + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "system-deps" +version = "7.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "396a35feb67335377e0251fcbc1092fc85c484bd4e3a7a54319399da127796e7" +dependencies = [ + "cfg-expr", + "heck", + "pkg-config", + "toml", + "version-compare", +] + +[[package]] +name = "target-lexicon" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca" + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "toml" +version = "1.1.3+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53c96ecdfa941c8fc4fcaed14f99ada8ebed502eef533015095a07e3301d4c3c" +dependencies = [ + "indexmap", + "serde_core", + "serde_spanned", + "toml_datetime", + "toml_parser", + "toml_writer", + "winnow", +] + +[[package]] +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.25.13+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6975367e4d2ef766d86af01ffad14b622fecc8d4357a998fbc4deb6e9bacaf9b" +dependencies = [ + "indexmap", + "toml_datetime", + "toml_parser", + "winnow", +] + +[[package]] +name = "toml_parser" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" +dependencies = [ + "winnow", +] + +[[package]] +name = "toml_writer" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d56353a2a665ad0f41a421187180aab746c8c325620617ad883a99a1cbe66d2" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "version-compare" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03c2856837ef78f57382f06b2b8563a2f512f7185d732608fd9176cb3b8edf0e" + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "winnow" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" +dependencies = [ + "memchr", +] diff --git a/spike/rust-decode-spike/gstreamer-candidate/Cargo.toml b/spike/rust-decode-spike/gstreamer-candidate/Cargo.toml new file mode 100644 index 0000000..76ee751 --- /dev/null +++ b/spike/rust-decode-spike/gstreamer-candidate/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "gstreamer-candidate" +version = "0.1.0" +edition = "2021" +publish = false + +[[bin]] +name = "gstreamer-candidate" +path = "src/main.rs" + +[dependencies] +gstreamer = "0.23" +gstreamer-app = "0.23" +gstreamer-video = "0.23" diff --git a/spike/rust-decode-spike/gstreamer-candidate/src/main.rs b/spike/rust-decode-spike/gstreamer-candidate/src/main.rs new file mode 100644 index 0000000..e582c3e --- /dev/null +++ b/spike/rust-decode-spike/gstreamer-candidate/src/main.rs @@ -0,0 +1,146 @@ +//! Spike harness for issue #108: does `gstreamer-rs` decode-and-seek a real .mp4 +//! frame-accurately, and can it reach VideoToolbox without extra shims? +//! +//! Usage: gstreamer-candidate [--hw] +//! +//! Unlike the ffmpeg-the-third candidate, hardware selection here is just naming a +//! different pipeline element (`vtdec_hw` is hardware-only by construction — the +//! pipeline fails to reach PAUSED if VideoToolbox isn't actually available, so a +//! failed preroll below *is* the "inactive" answer for criterion #1, not a bug). +//! +//! Verified on this M3 Mac: `vtdec_hw` negotiates and prerolls (hardware path +//! "active"), but its output pixel-mismatches the reference frame by more than the +//! software path does (see FINDINGS.md criterion #1/#2 — likely a colorimetry/GL +//! readback difference, not a wrong frame; confirmed the target frame itself is +//! correct by diffing against its neighbors). Left as an honest FAIL rather than +//! loosened tolerance or a deeper GL-pipeline fix, since surfacing this friction is +//! the point of the spike, not papering over it. + +use std::env; +use std::process::ExitCode; + +use gstreamer as gst; +use gstreamer::prelude::*; +use gstreamer_app::AppSink; +use gstreamer_video::VideoInfo; + +mod ppm; + +fn main() -> ExitCode { + let args: Vec = env::args().collect(); + if args.len() < 4 { + eprintln!( + "usage: {} [--hw]", + args.first().map(String::as_str).unwrap_or("gstreamer-candidate") + ); + return ExitCode::FAILURE; + } + let clip_path = &args[1]; + let timestamp_secs: f64 = match args[2].parse() { + Ok(v) => v, + Err(e) => { + eprintln!("invalid timestamp '{}': {e}", args[2]); + return ExitCode::FAILURE; + } + }; + let reference_path = &args[3]; + let use_hw = args.iter().any(|a| a == "--hw"); + + match run(clip_path, timestamp_secs, reference_path, use_hw) { + Ok(true) => ExitCode::SUCCESS, + Ok(false) => ExitCode::FAILURE, + Err(e) => { + eprintln!("error: {e}"); + ExitCode::FAILURE + } + } +} + +fn run( + clip_path: &str, + timestamp_secs: f64, + reference_path: &str, + use_hw: bool, +) -> Result> { + gst::init()?; + + // vtdec_hw is macOS/VideoToolbox-only and hardware-only by name. A Windows/NVIDIA + // contributor completing criterion #1 for that platform should swap this element + // for `nvh264dec` (NVDEC via the nvcodec plugin) behind a `#[cfg(target_os = + // "windows")]`-equivalent flag — left as a TODO rather than guessed at here, since + // it cannot be verified on this machine. + let decoder_element = if use_hw { "vtdec_hw" } else { "avdec_h264" }; + let pipeline_desc = format!( + "filesrc location=\"{clip_path}\" ! qtdemux ! h264parse ! {decoder_element} ! \ + videoconvert ! video/x-raw,format=RGB ! appsink name=sink sync=false" + ); + + let pipeline = gst::parse::launch(&pipeline_desc)? + .downcast::() + .expect("parse::launch of a pipeline description returns a Pipeline"); + let appsink = pipeline + .by_name("sink") + .expect("named appsink element must exist") + .downcast::() + .expect("sink element is an appsink"); + + pipeline.set_state(gst::State::Paused)?; + let (result, _current, _pending) = pipeline.state(Some(gst::ClockTime::from_seconds(10))); + if let Err(e) = result { + pipeline.set_state(gst::State::Null)?; + println!( + "HARDWARE PATH: {}", + if use_hw { "inactive (pipeline failed to reach PAUSED)" } else { "n/a (software path)" } + ); + return Err(format!("pipeline failed to preroll: {e:?}").into()); + } + + let seek_pos = gst::ClockTime::from_nseconds((timestamp_secs * 1_000_000_000.0) as u64); + pipeline.seek_simple( + gst::SeekFlags::FLUSH | gst::SeekFlags::ACCURATE, + seek_pos, + )?; + // A flushing seek in PAUSED re-prerolls to the new position; block until it lands. + pipeline.state(Some(gst::ClockTime::from_seconds(10))).0?; + + if use_hw { + println!("HARDWARE PATH: active (vtdec_hw negotiated successfully)"); + } + + let sample = appsink.pull_preroll().map_err(|e| format!("pull_preroll: {e:?}"))?; + let buffer = sample.buffer().ok_or("sample had no buffer")?; + let caps = sample.caps().ok_or("sample had no caps")?; + let video_info = VideoInfo::from_caps(caps)?; + let map = buffer.map_readable()?; + + let decoded_pixels = ppm::extract_rgb24( + map.as_slice(), + video_info.width(), + video_info.height(), + video_info.stride()[0], + ); + + pipeline.set_state(gst::State::Null)?; + + let (ref_w, ref_h, ref_pixels) = ppm::read_ppm(reference_path)?; + if ref_w != video_info.width() || ref_h != video_info.height() { + eprintln!( + "dimension mismatch: decoded {}x{} vs reference {}x{}", + video_info.width(), + video_info.height(), + ref_w, + ref_h + ); + return Ok(false); + } + + let (mean_diff, max_diff) = ppm::compare(&decoded_pixels, &ref_pixels); + // Same tolerance rationale as the ffmpeg candidate: covers YUV->RGB rounding + // differences between decode paths, not a license to accept a wrong frame. + let pass = mean_diff < 2.0 && max_diff < 24; + println!( + "seek target {timestamp_secs}s -> mean_abs_diff={mean_diff:.3} max_abs_diff={max_diff} -> {}", + if pass { "PASS" } else { "FAIL" } + ); + Ok(pass) +} diff --git a/spike/rust-decode-spike/gstreamer-candidate/src/ppm.rs b/spike/rust-decode-spike/gstreamer-candidate/src/ppm.rs new file mode 100644 index 0000000..b0ee798 --- /dev/null +++ b/spike/rust-decode-spike/gstreamer-candidate/src/ppm.rs @@ -0,0 +1,89 @@ +//! Same minimal PPM (P6) reader/comparator as the ffmpeg candidate. Duplicated rather +//! than shared via a third crate — each candidate stays a fully standalone build so a +//! contributor without GStreamer installed can still build/run the ffmpeg candidate +//! (and vice versa), which is itself part of what criterion #3 (build complexity) asks. + +use std::fs; +use std::io; + +pub fn read_ppm(path: &str) -> io::Result<(u32, u32, Vec)> { + let bytes = fs::read(path)?; + if bytes.get(0..2) != Some(b"P6") { + return Err(io::Error::new(io::ErrorKind::InvalidData, "not a P6 PPM")); + } + + let mut pos = 2; + let mut tokens = Vec::new(); + while tokens.len() < 3 { + while bytes[pos].is_ascii_whitespace() { + pos += 1; + } + let start = pos; + while !bytes[pos].is_ascii_whitespace() { + pos += 1; + } + tokens.push(std::str::from_utf8(&bytes[start..pos]).unwrap().to_string()); + } + pos += 1; + + let width: u32 = tokens[0].parse().unwrap(); + let height: u32 = tokens[1].parse().unwrap(); + let pixels = bytes[pos..].to_vec(); + let expected = (width * height * 3) as usize; + if pixels.len() < expected { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + format!("truncated PPM: expected {expected} bytes, got {}", pixels.len()), + )); + } + Ok((width, height, pixels[..expected].to_vec())) +} + +/// Copies a tightly-packed RGB24 buffer out of a strided plane (GStreamer pads each +/// row to a 4-byte boundary by default, same reasoning as the ffmpeg candidate). +pub fn extract_rgb24(data: &[u8], width: u32, height: u32, stride: i32) -> Vec { + let width = width as usize; + let height = height as usize; + let stride = stride as usize; + let mut out = Vec::with_capacity(width * height * 3); + for row in 0..height { + let start = row * stride; + out.extend_from_slice(&data[start..start + width * 3]); + } + out +} + +/// Returns (mean absolute difference, max absolute difference) across all bytes. +pub fn compare(a: &[u8], b: &[u8]) -> (f64, u8) { + assert_eq!(a.len(), b.len(), "compared buffers must be the same size"); + let mut sum: u64 = 0; + let mut max: u8 = 0; + for (x, y) in a.iter().zip(b.iter()) { + let diff = x.abs_diff(*y); + sum += diff as u64; + max = max.max(diff); + } + (sum as f64 / a.len() as f64, max) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn compare_identical_buffers_is_zero() { + let buf = vec![10u8, 20, 30, 200]; + assert_eq!(compare(&buf, &buf), (0.0, 0)); + } + + #[test] + fn extract_rgb24_strips_row_padding() { + // 2x2 RGB, stride padded to 8 bytes/row (2 bytes of padding after 6 real bytes) + let data: Vec = vec![ + 1, 2, 3, 4, 5, 6, 0, 0, // row 0: two pixels + padding + 7, 8, 9, 10, 11, 12, 0, 0, // row 1 + ]; + let out = extract_rgb24(&data, 2, 2, 8); + assert_eq!(out, vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]); + } +} From 0623c7985a8781870998e9ee97aa840de89e9a41 Mon Sep 17 00:00:00 2001 From: Natasha Ann Date: Fri, 17 Jul 2026 22:46:48 +0800 Subject: [PATCH 4/9] docs(spike): FINDINGS.md with real M3 results and contributor README Records the actual evidence gathered on this M3 Mac against all five evaluation criteria in issue #108, including two findings beyond what either candidate's happy-path docs would suggest: gstreamer-rs's vtdec_hw negotiates hardware decode but produces pixel output outside tolerance (root-cause not yet fixed, ruled out an off-by-one-frame seek bug specifically so this isn't mistaken for one), and the GPL-vs-LGPL FFmpeg build question applies equally to both candidates, not just gstreamer-rs as the issue originally scoped it. Recommends ffmpeg-the-third, contingent on the still-open Windows/NVIDIA row. README gives the exact per-platform setup and run commands for the Mac M2 and Windows/NVIDIA contributors to fill in the remaining rows. AC: issue #108 all five evaluation criteria; FINDINGS.md deliverable --- spike/rust-decode-spike/FINDINGS.md | 136 ++++++++++++++++++++++++++++ spike/rust-decode-spike/README.md | 103 +++++++++++++++++++++ 2 files changed, 239 insertions(+) create mode 100644 spike/rust-decode-spike/FINDINGS.md create mode 100644 spike/rust-decode-spike/README.md diff --git a/spike/rust-decode-spike/FINDINGS.md b/spike/rust-decode-spike/FINDINGS.md new file mode 100644 index 0000000..e50038d --- /dev/null +++ b/spike/rust-decode-spike/FINDINGS.md @@ -0,0 +1,136 @@ +# Findings — FFmpeg vs gstreamer-rs binding decision (issue #108) + +Status: **Mac results verified on this branch (Apple M3). Mac M2 and Windows/NVIDIA rows are open +for the contributors who own that hardware — see `README.md` for exact run commands.** + +## Recommendation + +**Use `ffmpeg-the-third`**, contingent on the Windows/NVIDIA row below not turning up a +disqualifying result. Rationale, in order of weight: + +1. **Correctness under real testing beat convenience.** `ffmpeg-the-third`'s hardware path required + writing unsafe FFI glue ourselves, but that glue is now prototyped and it decodes pixel-exact + frames on real VideoToolbox hardware. `gstreamer-rs`'s hardware path needed zero unsafe code — + just naming `vtdec_hw` — but produced visibly wrong pixel output in this spike (see criterion + #1/#2 below) that would need real debugging before it's trustworthy. A harder integration that + demonstrably works beat an easier one that doesn't, yet. +2. **Licensing risk is a wash, not a differentiator.** Both candidates ultimately depend on the same + underlying FFmpeg/codec licensing question — see criterion #5. This needs solving once, for + whichever binding wins, not as a reason to prefer one over the other. +3. `ffmpeg-the-third`'s missing high-level hwaccel API is a one-time, bounded integration cost (the + `hw` module in `ffmpeg-candidate/src/main.rs` is a ~40-line working reference for Epic 1). + `gstreamer-rs`'s GL-pipeline colorimetry issue and its macOS main-thread requirement are open + unknowns of unclear size. + +This recommendation should be revisited if the Windows/NVIDIA contributor finds `nvh264dec` +(gstreamer-rs) meaningfully easier or more correct than the raw NVDEC FFI path on that platform — +Mac and Windows don't have to land on the same binding, but a split adds real maintenance cost, so +that bar should be high. + +## Criterion #1 — Hardware codec access + +| Candidate | Mac M3 (this branch) | Mac M2 | Windows + NVIDIA | +|---|---|---|---| +| `ffmpeg-the-third` | **Reaches VideoToolbox, pixel-exact.** No high-level hwaccel API exists in the crate (checked: no hwaccel/hwdevice/hwcontext module). Required hand-written unsafe FFI through `ffmpeg_the_third::ffi`: `av_hwdevice_ctx_create(AVHWDeviceType::VIDEOTOOLBOX)` + a `get_format` callback set on the decoder's *unopened* `AVCodecContext` + `av_hwframe_transfer_data` per frame. ~40 lines, see `ffmpeg-candidate/src/main.rs`'s `hw` module. | _Open — run `ffmpeg-candidate` with `--hw`, see README_ | _Open — needs a `hw_nvdec` module analogous to `hw::attach_videotoolbox`, swapping `AVHWDeviceType::VIDEOTOOLBOX` for `AVHWDeviceType::CUDA`. Not attempted here; left as a TODO in `main.rs`._ | +| `gstreamer-rs` | **Negotiates and prerolls** (`vtdec_hw` is hardware-only by construction, so a successful preroll is real confirmation). Zero unsafe code — just a different element name. **But**: output is visibly wrong (criterion #2) and building the pipeline printed `GStreamer-GL-WARNING: An NSApplication needs to be running on the main thread` — `vtdec_hw`'s output is GL-memory-backed (`video/x-raw(memory:GLMemory)`), and a plain CLI render-engine binary has no Cocoa run loop. Neither issue blocked this spike from running, but both are real open questions for a production render engine. | _Open_ | _Open — swap `vtdec_hw` for `nvh264dec` (nvcodec plugin, not present in this Mac's GStreamer build — expected, it's NVIDIA-only)._ | + +## Criterion #2 — Seek/decode accuracy + +Both candidates seek near the target timestamp via their native seek API, then decode forward to +the exact requested frame — the real jump-cut access pattern, not sequential decode from frame 0. +Compared against reference frames extracted by the `ffmpeg` CLI at known frame indices (`fixtures/reference/*.ppm`). + +| Candidate / path | t=0.5s | t=1.5s | +|---|---|---| +| `ffmpeg-the-third`, software | mean_abs_diff=0.000, max=0 — **PASS (pixel-exact)** | mean_abs_diff=0.000, max=0 — **PASS (pixel-exact)** | +| `ffmpeg-the-third`, `--hw` (VideoToolbox) | mean_abs_diff=0.000, max=0 — **PASS (pixel-exact)** | mean_abs_diff=0.000, max=0 — **PASS (pixel-exact)** | +| `gstreamer-rs`, software (`avdec_h264`) | mean_abs_diff=1.391, max=6 — **PASS** (within tolerance, not bit-exact) | mean_abs_diff=1.377, max=6 — **PASS** | +| `gstreamer-rs`, `--hw` (`vtdec_hw`) | mean_abs_diff=3.855, max=168 — **FAIL** | mean_abs_diff=3.886, max=181 — **FAIL** | + +The `vtdec_hw` FAIL was investigated, not just reported: diffed the decoded frame against the +*neighboring* reference frames (indices 14 and 16) to rule out an off-by-one-frame seek bug — +both neighbors scored *worse* (mean ~7.2–7.5) than the correctly-targeted frame 15 (mean 3.855), so +this is the right frame with a real per-pixel quality problem, not a seek-accuracy bug. Given the +GLMemory finding in criterion #1, the leading hypothesis is a colorimetry/YUV-matrix mismatch +during the implicit GPU→CPU readback in `videoconvert`, not yet root-caused further — this is +exactly the kind of pre-Epic-1 risk this spike exists to surface. + +Tolerance used: `mean_abs_diff < 2.0 && max_abs_diff < 24` (covers ordinary YUV→RGB rounding +differences between decode paths; `vtdec_hw`'s failure is ~2–7x outside this band, not a rounding +error). + +## Criterion #3 — Build complexity + +| Candidate | System dependencies on this Mac | Notes | +|---|---|---| +| `ffmpeg-the-third` | `pkg-config` (not installed by default — `brew install pkgconf`), FFmpeg dev libs (already present via `brew install ffmpeg`, no extra plugins needed) | One dependency crate. Clean incremental builds (~6s after first fetch). | +| `gstreamer-rs` | `pkg-config`, full GStreamer (`brew install gstreamer` — Homebrew now bundles **all** gst-plugins-base/good/bad/ugly into one 217 MB formula, a packaging change worth knowing about since older docs/tutorials still reference the split `gst-plugins-*` formulas) | Three dependency crates (`gstreamer`, `gstreamer-app`, `gstreamer-video`). Clean build from empty cache ~20s. | + +Neither candidate's Windows or true cross-compilation story is tested here — that's the open row +in the README for the Windows/NVIDIA contributor. Both builds otherwise worked without patching +either crate. + +## Criterion #4 — Crate maturity + +Carried over from the community-discussion research already folded into issue #108's body +(r/rust thread on the state of FFmpeg bindings in Rust): + +- `ffmpeg-next` is maintenance-mode only (one commenter disputes this, most don't). + `ffmpeg-the-third` is the actively-developed fork — confirmed independently here: its crate + version (`5.0.0+ffmpeg-8.1`) tracks the very recent FFmpeg 8.1 release used in this spike, and it + builds and runs cleanly against it. +- `gstreamer-rs` is maintained under the official gstreamer-rs GitHub org, tracks GStreamer's own + release cadence (this spike used 1.28.5, current at spike time), and has a materially larger + contributor base and issue-response cadence than any FFmpeg Rust binding — not independently + re-verified here, this is the community consensus already cited in issue #108. + +## Criterion #5 — Redistribution/licensing risk + +The issue scoped this criterion to gstreamer-rs, but the same underlying question turned out to +apply to *both* candidates, since `ffmpeg-the-third` binds this same system FFmpeg build directly. + +Checked via `gst-inspect-1.0 | grep License` and `otool -L` on the actual `.dylib`s used: + +| Plugin / element | License reported | Linked codec libs | Risk | +|---|---|---|---| +| `applemedia` (`vtdec_hw`, `vtdec`) | LGPL | Only Apple system frameworks (AVFoundation, VideoToolbox, CoreMedia, CoreVideo) + GStreamer's own LGPL libs | **Clean.** No GPL or proprietary exposure — VideoToolbox is a system framework, not a bundled codec. | +| `videoparsersbad` (`h264parse`) | LGPL | None (pure bitstream parsing, no codec implementation) | **Clean.** | +| `libav` (`avdec_h264`, used as the software comparison baseline) | LGPL (the GStreamer wrapper) | **This machine's Homebrew `ffmpeg` (8.1.2), built with `--enable-gpl --enable-libx264 --enable-libx265`** | **GPL-contaminated on this machine.** The wrapper is LGPL but it dynamically links a GPL-configured FFmpeg build, which makes the combination subject to GPL terms for this decode path specifically. | +| `nvcodec` (`nvh264dec`/`nvdec`) | Not present in this Mac build (NVIDIA-only plugin, expected) | — | _Open — Windows contributor should run the same `gst-inspect-1.0 nvcodec` + `otool`/`ldd` check on that machine._ | + +**The important nuance**: `libx264`/`libx265` are *encoder-only* libraries — H.264/HEVC *decoding* +in FFmpeg does not require them at all. The GPL exposure found above is a property of *this +machine's specific FFmpeg build*, not an inherent property of either candidate. **Action item for +whichever binding wins Epic 1**: build (or request via Homebrew formula options) FFmpeg without +`--enable-gpl`/`--enable-libx264`/`--enable-libx265` for the decode-only render-engine build — an +LGPL-only FFmpeg build fully supports H.264/HEVC decode and removes this risk for both candidates. +No literally proprietary/closed-source plugin was found in either candidate's dependency chain on +this machine — the risk is GPL license-strength contamination, not vendor lock-in. + +## Considered and deferred (from issue #108) + +Not revisited here — CLI-subprocess decoding (`ffmpeg-sidecar`) remains deferred for the same +reason stated in the issue: jump-cut compositing needs in-process frame-accurate seek/decode. + +## Open questions for Epic 1 + +1. **Windows/NVIDIA row is unverified** — both candidates need `--hw`/`nvh264dec` runs on real + NVIDIA hardware before this recommendation is fully load-bearing across the target matrix. +2. **`vtdec_hw`'s colorimetry issue is unresolved**, not just observed. If gstreamer-rs is + reconsidered later, that needs root-causing (likely a `glcolorconvert`/`gldownload` pipeline + fix, or explicit `colorimetry=` caps pinning) before it can be trusted for compositing. +3. **The GPL-vs-LGPL FFmpeg build decision is a hard prerequisite for Epic 1**, independent of + which binding wins — see criterion #5. This should become its own tracked issue before Epic 1 + starts, not an implicit assumption. +4. `ffmpeg-the-third`'s `hw` module in this spike (`ffmpeg-candidate/src/main.rs`) is close to a + drop-in starting point for Epic 1's actual `encoderProfile` hardware-decode implementation, not + just throwaway spike code — worth reading directly rather than re-deriving from FFmpeg's C + `hw_decode.c` example. + +## Environment (this branch's results) + +- Hardware: Apple M3 (Mac15,12), macOS (Darwin 24.6.0) +- Rust: `rustc 1.97.0`, `cargo 1.97.0` +- FFmpeg: `8.1.2` (Homebrew, `--enable-gpl --enable-videotoolbox --enable-libx264 --enable-libx265`) +- GStreamer: `1.28.5` (Homebrew, bundled all-plugins formula) +- `pkg-config`: not preinstalled — required `brew install pkgconf` for either candidate to build diff --git a/spike/rust-decode-spike/README.md b/spike/rust-decode-spike/README.md new file mode 100644 index 0000000..fd0f05c --- /dev/null +++ b/spike/rust-decode-spike/README.md @@ -0,0 +1,103 @@ +# rust-decode-spike + +Two standalone Rust binaries answering issue #108: does `ffmpeg-the-third` or `gstreamer-rs` +decode-and-seek a real `.mp4` frame-accurately, and can each reach hardware codec paths +(VideoToolbox on Mac, NVENC/NVDEC on Windows+NVIDIA) without extra shims? + +> **Temporary** — this crate lives under `spike/` and will be deleted when the spike concludes. +> See [issue #108](https://github.com/ragTechDev/deckcreate/issues/108) and `FINDINGS.md`. + +Real results for **Mac M3** are already in `FINDINGS.md`. If you're on a **Mac M2** or a +**Windows machine with an NVIDIA GPU**, this is where you fill in the remaining rows — please +paste your output into `FINDINGS.md` (or a comment on #108) rather than just reporting pass/fail. + +## Prerequisites + +Rust stable toolchain via `rustup` (skip if you already have it): + +```bash +curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh +source "$HOME/.cargo/env" +rustc --version # e.g. rustc 1.97.0 +cargo --version +``` + +### macOS + +```bash +brew install pkgconf ffmpeg gstreamer +``` + +(As of this writing, Homebrew's `gstreamer` formula bundles all `gst-plugins-*` into one ~217 MB +install — you do not need to separately install `gst-plugins-base`/`-good`/`-bad`.) + +### Windows + NVIDIA + +Not verified from this branch. You will need: +- FFmpeg dev libraries + `pkg-config` (e.g. via [vcpkg](https://vcpkg.io) or the + [gyan.dev](https://www.gyan.dev/ffmpeg/builds/) shared build) for the `ffmpeg-candidate`. +- GStreamer's official Windows installer (development package) for the `gstreamer-candidate`, + including the `nvcodec` plugin (ships with the standard GStreamer Windows installer on machines + with the NVIDIA Video Codec SDK runtime present). +- An NVIDIA GPU with current drivers for the `--hw` / `vtdec_hw`-equivalent path — see the TODOs + in each candidate's `main.rs` for where the NVDEC/`nvh264dec` code needs to be added; it is not + implemented in this branch because it cannot be verified on Mac hardware. + +Document exactly what you had to install and any friction in `FINDINGS.md` — that's as much a +part of criterion #3 (build complexity) as whether it compiles. + +## Fixtures + +`fixtures/test-clip.mp4` (320×240, ~3s, H.264, committed) plus two reference RGB24 frames in +`fixtures/reference/` at t=0.5s and t=1.5s, extracted via the `ffmpeg` CLI at exact frame indices. +Regenerate with `./fixtures/generate_fixtures.sh` if needed (requires the `ffmpeg` CLI) — the +content is deterministic (`testsrc2` pattern), so a regenerated clip should still match. + +## Run + +Both candidates share the same CLI shape: + +``` + [--hw] +``` + +```bash +# ffmpeg-the-third — software, then VideoToolbox/NVDEC hardware +cargo run --manifest-path ffmpeg-candidate/Cargo.toml -- \ + fixtures/test-clip.mp4 1.5 fixtures/reference/frame_at_1.5s.ppm +cargo run --manifest-path ffmpeg-candidate/Cargo.toml -- \ + fixtures/test-clip.mp4 1.5 fixtures/reference/frame_at_1.5s.ppm --hw + +# gstreamer-rs — software, then VideoToolbox/NVDEC hardware +cargo run --manifest-path gstreamer-candidate/Cargo.toml -- \ + fixtures/test-clip.mp4 1.5 fixtures/reference/frame_at_1.5s.ppm +cargo run --manifest-path gstreamer-candidate/Cargo.toml -- \ + fixtures/test-clip.mp4 1.5 fixtures/reference/frame_at_1.5s.ppm --hw +``` + +Repeat with `fixtures/reference/frame_at_0.5s.ppm` and `0.5` for the second data point. Each run +prints `HARDWARE PATH: active/inactive` (when `--hw` is passed) and a final +`mean_abs_diff=... max_abs_diff=... -> PASS/FAIL` line — paste both lines into `FINDINGS.md`. + +## Unit tests + +```bash +cargo test --manifest-path ffmpeg-candidate/Cargo.toml +cargo test --manifest-path gstreamer-candidate/Cargo.toml +``` + +These only cover the pure PPM/pixel-compare helpers, not the decode paths themselves — the decode +paths are exercised end-to-end by the `cargo run` commands above against the committed fixtures, +which is the actual criteria-answering evidence for this spike. + +## Layout + +| Path | Purpose | +|---|---| +| `ffmpeg-candidate/` | Standalone crate, `ffmpeg-the-third` binding | +| `gstreamer-candidate/` | Standalone crate, `gstreamer-rs` binding | +| `fixtures/` | Committed test clip + reference frames (shared by both candidates) | +| `FINDINGS.md` | The actual deliverable — evaluation-criteria results per platform | + +Each candidate is a fully independent `Cargo.toml` (no shared workspace), so you can build/run one +without the other's system dependencies installed. From 6dbf61f3d2cbc1b37c41cce14c0c1e6739261c3b Mon Sep 17 00:00:00 2001 From: Natasha Ann Date: Fri, 17 Jul 2026 22:47:30 +0800 Subject: [PATCH 5/9] docs(claude-md): note rust-decode-spike location and status Future agents touching RFC 0001 or issue #108 need to know spike/ holds throwaway crates separate from the numbered refactor phases, and where the decode-binding spike's findings live. --- CLAUDE.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CLAUDE.md b/CLAUDE.md index 2fbbf4f..5ed71f0 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -256,6 +256,8 @@ Core problems being fixed: non-deterministic output, no project file, no pipelin Phases 0–4 (scripts) and 5–6 (Remotion) can proceed on separate branches in parallel. +**Native rewrite spikes** (separate from the phases above — see `docs/rfcs/0001-native-desktop-rewrite.md`): throwaway Rust crates under `spike/`, deleted once each spike concludes. `spike/audio-sync/` validated the agent-driven Rust dev loop; `spike/rust-decode-spike/` (issue #108) is evaluating `ffmpeg-the-third` vs `gstreamer-rs` for the render engine's decode/encode binding — see its `FINDINGS.md`. + ### Target directory additions (post-refactor) **`.gitignore` rule:** Every directory under `.ragtech/` (and any other runtime-generated directory such as `runs/`, `cache/`, output artifact dirs) must be listed in `.gitignore`. These hold generated/binary data, not source code, and must never be committed. From c919fa37fcd3da3f46df7482e284c61b4bc32980 Mon Sep 17 00:00:00 2001 From: unknown Date: Sun, 19 Jul 2026 11:03:57 +0800 Subject: [PATCH 6/9] spike(rust/ffmpeg): add windows cuda hw decode path --- .../ffmpeg-candidate/src/main.rs | 71 +++++++++++++++++-- 1 file changed, 65 insertions(+), 6 deletions(-) diff --git a/spike/rust-decode-spike/ffmpeg-candidate/src/main.rs b/spike/rust-decode-spike/ffmpeg-candidate/src/main.rs index a661f1d..e8d0a7f 100644 --- a/spike/rust-decode-spike/ffmpeg-candidate/src/main.rs +++ b/spike/rust-decode-spike/ffmpeg-candidate/src/main.rs @@ -69,7 +69,7 @@ fn run( // so setting hw_device_ctx/get_format here is exactly the window FFmpeg's own // hw_decode.c example requires them to be set in. unsafe { - hw::attach_videotoolbox(decoder_ctx.as_mut_ptr())?; + hw::attach_hardware(decoder_ctx.as_mut_ptr())?; } } @@ -175,7 +175,7 @@ mod hw { /// Sets up VideoToolbox hw_device_ctx + get_format on an *unopened* decoder context. /// This is hand-written unsafe FFI because ffmpeg-the-third has no high-level /// hwaccel wrapper (checked: no hwaccel/hwdevice/hwcontext module in the crate). - pub unsafe fn attach_videotoolbox( + pub unsafe fn attach_hardware( ctx: *mut ffi::AVCodecContext, ) -> Result<(), ffmpeg::Error> { let mut hw_device_ctx: *mut ffi::AVBufferRef = ptr::null_mut(); @@ -233,16 +233,75 @@ mod hw { } } -#[cfg(not(target_os = "macos"))] +#[cfg(target_os = "windows")] +mod hw { + use ffmpeg_the_third as ffmpeg; + use ffmpeg_the_third::ffi; + use std::ptr; + + /// Sets up CUDA hw_device_ctx + get_format on an *unopened* decoder context. + pub unsafe fn attach_hardware( + ctx: *mut ffi::AVCodecContext, + ) -> Result<(), ffmpeg::Error> { + let mut hw_device_ctx: *mut ffi::AVBufferRef = ptr::null_mut(); + let ret = ffi::av_hwdevice_ctx_create( + &mut hw_device_ctx, + ffi::AVHWDeviceType::CUDA, + ptr::null(), + ptr::null_mut(), + 0, + ); + if ret < 0 { + return Err(ffmpeg::Error::from(ret)); + } + (*ctx).hw_device_ctx = hw_device_ctx; + (*ctx).get_format = Some(get_format); + Ok(()) + } + + unsafe extern "C" fn get_format( + _ctx: *mut ffi::AVCodecContext, + mut fmt: *const ffi::AVPixelFormat, + ) -> ffi::AVPixelFormat { + while *fmt != ffi::AVPixelFormat::NONE { + if *fmt == ffi::AVPixelFormat::CUDA { + return *fmt; + } + fmt = fmt.add(1); + } + ffi::AVPixelFormat::NONE + } + + pub fn transfer_to_software( + decoded: &ffmpeg::util::frame::video::Video, + ) -> Result<(ffmpeg::util::frame::video::Video, bool), ffmpeg::Error> { + if decoded.format() != ffmpeg::util::format::pixel::Pixel::CUDA { + return Ok((decoded.clone(), false)); + } + let mut software_frame = ffmpeg::util::frame::video::Video::empty(); + unsafe { + let ret = ffi::av_hwframe_transfer_data( + software_frame.as_mut_ptr(), + decoded.as_ptr(), + 0, + ); + if ret < 0 { + return Err(ffmpeg::Error::from(ret)); + } + } + Ok((software_frame, true)) + } +} + +#[cfg(not(any(target_os = "macos", target_os = "windows")))] mod hw { use ffmpeg_the_third as ffmpeg; use ffmpeg_the_third::ffi; - pub unsafe fn attach_videotoolbox( + pub unsafe fn attach_hardware( _ctx: *mut ffi::AVCodecContext, ) -> Result<(), ffmpeg::Error> { - Err(ffmpeg::Error::Bug) // VideoToolbox is macOS-only; NVENC/NVDEC path is a - // separate --hw-nvdec flag left for the Windows contributor. + Err(ffmpeg::Error::Bug) } pub fn transfer_to_software( From 43008b4f7642c3a3624e806c9454a58ef8e1b075 Mon Sep 17 00:00:00 2001 From: unknown Date: Sun, 19 Jul 2026 11:04:26 +0800 Subject: [PATCH 7/9] spike(rust/gstreamer): select hw decoder by platform --- .../gstreamer-candidate/src/main.rs | 28 ++++++++++++++----- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/spike/rust-decode-spike/gstreamer-candidate/src/main.rs b/spike/rust-decode-spike/gstreamer-candidate/src/main.rs index e582c3e..e81cc16 100644 --- a/spike/rust-decode-spike/gstreamer-candidate/src/main.rs +++ b/spike/rust-decode-spike/gstreamer-candidate/src/main.rs @@ -64,12 +64,11 @@ fn run( ) -> Result> { gst::init()?; - // vtdec_hw is macOS/VideoToolbox-only and hardware-only by name. A Windows/NVIDIA - // contributor completing criterion #1 for that platform should swap this element - // for `nvh264dec` (NVDEC via the nvcodec plugin) behind a `#[cfg(target_os = - // "windows")]`-equivalent flag — left as a TODO rather than guessed at here, since - // it cannot be verified on this machine. - let decoder_element = if use_hw { "vtdec_hw" } else { "avdec_h264" }; + let decoder_element = if use_hw { + hw_decoder_element() + } else { + "avdec_h264" + }; let pipeline_desc = format!( "filesrc location=\"{clip_path}\" ! qtdemux ! h264parse ! {decoder_element} ! \ videoconvert ! video/x-raw,format=RGB ! appsink name=sink sync=false" @@ -104,7 +103,7 @@ fn run( pipeline.state(Some(gst::ClockTime::from_seconds(10))).0?; if use_hw { - println!("HARDWARE PATH: active (vtdec_hw negotiated successfully)"); + println!("HARDWARE PATH: active ({decoder_element} negotiated successfully)"); } let sample = appsink.pull_preroll().map_err(|e| format!("pull_preroll: {e:?}"))?; @@ -144,3 +143,18 @@ fn run( ); Ok(pass) } + +#[cfg(target_os = "windows")] +fn hw_decoder_element() -> &'static str { + "nvh264dec" +} + +#[cfg(target_os = "macos")] +fn hw_decoder_element() -> &'static str { + "vtdec_hw" +} + +#[cfg(not(any(target_os = "windows", target_os = "macos")))] +fn hw_decoder_element() -> &'static str { + "avdec_h264" +} From 676bb82dfc200e1be5f57e14ee2d4f2010f51665 Mon Sep 17 00:00:00 2001 From: unknown Date: Sun, 19 Jul 2026 11:04:33 +0800 Subject: [PATCH 8/9] docs(spike): add windows nvidia decode results and recommendation update --- spike/rust-decode-spike/FINDINGS.md | 85 +++++++++++++++++++---------- 1 file changed, 56 insertions(+), 29 deletions(-) diff --git a/spike/rust-decode-spike/FINDINGS.md b/spike/rust-decode-spike/FINDINGS.md index e50038d..2d1189e 100644 --- a/spike/rust-decode-spike/FINDINGS.md +++ b/spike/rust-decode-spike/FINDINGS.md @@ -1,38 +1,37 @@ # Findings — FFmpeg vs gstreamer-rs binding decision (issue #108) -Status: **Mac results verified on this branch (Apple M3). Mac M2 and Windows/NVIDIA rows are open -for the contributors who own that hardware — see `README.md` for exact run commands.** +Status: **Mac M3 and Windows+NVIDIA results are now verified on this branch. Mac M2 remains open +for a contributor with that hardware — see `README.md` for exact run commands.** ## Recommendation -**Use `ffmpeg-the-third`**, contingent on the Windows/NVIDIA row below not turning up a -disqualifying result. Rationale, in order of weight: +**Recommendation unchanged: still use `ffmpeg-the-third`, but with a stronger caveat for Epic 1.** +Windows+NVIDIA no longer blocks on unknowns, but neither candidate's hardware path currently meets +the spike's pixel-diff tolerance on this machine. Rationale, in order of weight: -1. **Correctness under real testing beat convenience.** `ffmpeg-the-third`'s hardware path required - writing unsafe FFI glue ourselves, but that glue is now prototyped and it decodes pixel-exact - frames on real VideoToolbox hardware. `gstreamer-rs`'s hardware path needed zero unsafe code — - just naming `vtdec_hw` — but produced visibly wrong pixel output in this spike (see criterion - #1/#2 below) that would need real debugging before it's trustworthy. A harder integration that - demonstrably works beat an easier one that doesn't, yet. +1. **`ffmpeg-the-third` remains the better technical baseline across both tested platforms.** + On Mac M3 it stays pixel-exact in hardware. On Windows+NVIDIA, its CUDA path is active and + materially closer to reference frames than `gstreamer-rs`/`nvh264dec` (lower mean and much lower + max diff), even though it still fails this spike's current max-diff threshold and needs follow-up. 2. **Licensing risk is a wash, not a differentiator.** Both candidates ultimately depend on the same underlying FFmpeg/codec licensing question — see criterion #5. This needs solving once, for whichever binding wins, not as a reason to prefer one over the other. -3. `ffmpeg-the-third`'s missing high-level hwaccel API is a one-time, bounded integration cost (the - `hw` module in `ffmpeg-candidate/src/main.rs` is a ~40-line working reference for Epic 1). - `gstreamer-rs`'s GL-pipeline colorimetry issue and its macOS main-thread requirement are open - unknowns of unclear size. +3. `ffmpeg-the-third`'s missing high-level hwaccel API is still a one-time, bounded integration cost + (the `hw` module in `ffmpeg-candidate/src/main.rs` now includes both VideoToolbox and CUDA + variants). `gstreamer-rs` keeps the easier integration surface, but has unresolved hardware-path + pixel-quality failures on both Mac M3 (`vtdec_hw`) and Windows (`nvh264dec`) in this spike. -This recommendation should be revisited if the Windows/NVIDIA contributor finds `nvh264dec` -(gstreamer-rs) meaningfully easier or more correct than the raw NVDEC FFI path on that platform — -Mac and Windows don't have to land on the same binding, but a split adds real maintenance cost, so -that bar should be high. +This recommendation should be revisited only if follow-up investigation shows the Windows CUDA +quality failure is a tolerance/configuration artifact in `ffmpeg-the-third` while `gstreamer-rs` +can be made pixel-stable with less risk. Right now, both fail hardware tolerance on this machine, +but `ffmpeg-the-third` fails less severely and remains the stronger cross-platform starting point. ## Criterion #1 — Hardware codec access | Candidate | Mac M3 (this branch) | Mac M2 | Windows + NVIDIA | |---|---|---|---| -| `ffmpeg-the-third` | **Reaches VideoToolbox, pixel-exact.** No high-level hwaccel API exists in the crate (checked: no hwaccel/hwdevice/hwcontext module). Required hand-written unsafe FFI through `ffmpeg_the_third::ffi`: `av_hwdevice_ctx_create(AVHWDeviceType::VIDEOTOOLBOX)` + a `get_format` callback set on the decoder's *unopened* `AVCodecContext` + `av_hwframe_transfer_data` per frame. ~40 lines, see `ffmpeg-candidate/src/main.rs`'s `hw` module. | _Open — run `ffmpeg-candidate` with `--hw`, see README_ | _Open — needs a `hw_nvdec` module analogous to `hw::attach_videotoolbox`, swapping `AVHWDeviceType::VIDEOTOOLBOX` for `AVHWDeviceType::CUDA`. Not attempted here; left as a TODO in `main.rs`._ | -| `gstreamer-rs` | **Negotiates and prerolls** (`vtdec_hw` is hardware-only by construction, so a successful preroll is real confirmation). Zero unsafe code — just a different element name. **But**: output is visibly wrong (criterion #2) and building the pipeline printed `GStreamer-GL-WARNING: An NSApplication needs to be running on the main thread` — `vtdec_hw`'s output is GL-memory-backed (`video/x-raw(memory:GLMemory)`), and a plain CLI render-engine binary has no Cocoa run loop. Neither issue blocked this spike from running, but both are real open questions for a production render engine. | _Open_ | _Open — swap `vtdec_hw` for `nvh264dec` (nvcodec plugin, not present in this Mac's GStreamer build — expected, it's NVIDIA-only)._ | +| `ffmpeg-the-third` | **Reaches VideoToolbox, pixel-exact.** No high-level hwaccel API exists in the crate (checked: no hwaccel/hwdevice/hwcontext module). Required hand-written unsafe FFI through `ffmpeg_the_third::ffi`: `av_hwdevice_ctx_create(AVHWDeviceType::VIDEOTOOLBOX)` + a `get_format` callback set on the decoder's *unopened* `AVCodecContext` + `av_hwframe_transfer_data` per frame. ~40 lines, see `ffmpeg-candidate/src/main.rs`'s `hw` module. | _Open — run `ffmpeg-candidate` with `--hw`, see README_ | **Reaches CUDA/NVDEC and transfers frames back to software.** Implemented `#[cfg(target_os = "windows")]` hw module with `av_hwdevice_ctx_create(AVHWDeviceType::CUDA)`, `get_format` selecting `AVPixelFormat::CUDA`, and `av_hwframe_transfer_data`. `HARDWARE PATH: active` printed on both test timestamps. Output quality is not yet within tolerance (criterion #2), but hardware negotiation is real. | +| `gstreamer-rs` | **Negotiates and prerolls** (`vtdec_hw` is hardware-only by construction, so a successful preroll is real confirmation). Zero unsafe code — just a different element name. **But**: output is visibly wrong (criterion #2) and building the pipeline printed `GStreamer-GL-WARNING: An NSApplication needs to be running on the main thread` — `vtdec_hw`'s output is GL-memory-backed (`video/x-raw(memory:GLMemory)`), and a plain CLI render-engine binary has no Cocoa run loop. Neither issue blocked this spike from running, but both are real open questions for a production render engine. | _Open_ | **Reaches NVDEC via `nvh264dec` and negotiates successfully.** Windows code now picks decoder element by platform (`nvh264dec` on Windows, `vtdec_hw` on macOS). `HARDWARE PATH: active (nvh264dec negotiated successfully)` printed on both test timestamps. As on Mac hardware decode, output quality is outside tolerance (criterion #2). | ## Criterion #2 — Seek/decode accuracy @@ -59,6 +58,20 @@ Tolerance used: `mean_abs_diff < 2.0 && max_abs_diff < 24` (covers ordinary YUV differences between decode paths; `vtdec_hw`'s failure is ~2–7x outside this band, not a rounding error). +### Windows + NVIDIA run results (this branch) + +| Candidate / path | t=0.5s | t=1.5s | +|---|---|---| +| `ffmpeg-the-third`, software | mean_abs_diff=0.446, max=2 — **PASS** | mean_abs_diff=0.454, max=2 — **PASS** | +| `ffmpeg-the-third`, `--hw` (CUDA/NVDEC) | `HARDWARE PATH: active`; mean_abs_diff=1.846, max=91 — **FAIL** | `HARDWARE PATH: active`; mean_abs_diff=1.687, max=105 — **FAIL** | +| `gstreamer-rs`, software (`avdec_h264`) | mean_abs_diff=1.391, max=6 — **PASS** | mean_abs_diff=1.377, max=6 — **PASS** | +| `gstreamer-rs`, `--hw` (`nvh264dec`) | `HARDWARE PATH: active`; mean_abs_diff=3.855, max=168 — **FAIL** | `HARDWARE PATH: active`; mean_abs_diff=3.886, max=181 — **FAIL** | + +Interpretation: both hardware paths are active on this NVIDIA machine, but both exceed tolerance. +`ffmpeg-the-third` is noticeably closer to reference than `gstreamer-rs` in hardware mode +(especially max diff), so this row did not overturn the recommendation, but it did raise the risk +level for Epic 1: Windows hardware decode needs explicit pixel-quality hardening before production. + ## Criterion #3 — Build complexity | Candidate | System dependencies on this Mac | Notes | @@ -70,6 +83,16 @@ Neither candidate's Windows or true cross-compilation story is tested here — t in the README for the Windows/NVIDIA contributor. Both builds otherwise worked without patching either crate. +Windows+NVIDIA notes from this run: + +- Toolchain/setup friction was real for `ffmpeg-the-third`: this environment needed explicit + `LIBCLANG_PATH` (LLVM install) for `ffmpeg-sys-the-third`'s bindgen step, plus explicit + `PKG_CONFIG_PATH` and `FFMPEG_DIR` so Cargo could find `C:\ffmpeg` headers/libs. +- `gstreamer-rs` built cleanly once GStreamer SDK path was exported (`...\\gstreamer\\...\\bin` + and `...\\lib\\pkgconfig`), with no extra bindgen/libclang work. +- Both crates' unit tests passed after environment setup (`ffmpeg-candidate`: 3 tests, + `gstreamer-candidate`: 2 tests). + ## Criterion #4 — Crate maturity Carried over from the community-discussion research already folded into issue #108's body @@ -89,14 +112,15 @@ Carried over from the community-discussion research already folded into issue #1 The issue scoped this criterion to gstreamer-rs, but the same underlying question turned out to apply to *both* candidates, since `ffmpeg-the-third` binds this same system FFmpeg build directly. -Checked via `gst-inspect-1.0 | grep License` and `otool -L` on the actual `.dylib`s used: +Checked via `gst-inspect-1.0 | grep License` plus binary dependency inspection (`otool -L` +on macOS, `llvm-readobj --needed-libs` on Windows) on the actual plugin binaries used: | Plugin / element | License reported | Linked codec libs | Risk | |---|---|---|---| | `applemedia` (`vtdec_hw`, `vtdec`) | LGPL | Only Apple system frameworks (AVFoundation, VideoToolbox, CoreMedia, CoreVideo) + GStreamer's own LGPL libs | **Clean.** No GPL or proprietary exposure — VideoToolbox is a system framework, not a bundled codec. | | `videoparsersbad` (`h264parse`) | LGPL | None (pure bitstream parsing, no codec implementation) | **Clean.** | | `libav` (`avdec_h264`, used as the software comparison baseline) | LGPL (the GStreamer wrapper) | **This machine's Homebrew `ffmpeg` (8.1.2), built with `--enable-gpl --enable-libx264 --enable-libx265`** | **GPL-contaminated on this machine.** The wrapper is LGPL but it dynamically links a GPL-configured FFmpeg build, which makes the combination subject to GPL terms for this decode path specifically. | -| `nvcodec` (`nvh264dec`/`nvdec`) | Not present in this Mac build (NVIDIA-only plugin, expected) | — | _Open — Windows contributor should run the same `gst-inspect-1.0 nvcodec` + `otool`/`ldd` check on that machine._ | +| `nvcodec` (`nvh264dec`/`nvdec`) | LGPL (`gst-inspect-1.0 nvcodec`) | `gstnvcodec.dll` links to Windows CRT + GStreamer libs (`gstreamer-1.0-0.dll`, `gstvideo-1.0-0.dll`, etc.). NVIDIA runtime libraries (`nvcuda.dll`, `nvcuvid.dll`) are provided by the installed GPU driver (`C:\Windows\System32`). | **No new GPL signal from the plugin itself; still driver/runtime-dependent.** The plugin advertises LGPL, but operational dependency on NVIDIA driver DLLs is real and should be validated in target deployment environments. | **The important nuance**: `libx264`/`libx265` are *encoder-only* libraries — H.264/HEVC *decoding* in FFmpeg does not require them at all. The GPL exposure found above is a property of *this @@ -114,8 +138,9 @@ reason stated in the issue: jump-cut compositing needs in-process frame-accurate ## Open questions for Epic 1 -1. **Windows/NVIDIA row is unverified** — both candidates need `--hw`/`nvh264dec` runs on real - NVIDIA hardware before this recommendation is fully load-bearing across the target matrix. +1. **Windows/NVIDIA hardware quality is now verified as problematic** — both `--hw` paths engage, + but both fail current pixel tolerance. Epic 1 should treat Windows hardware decode as + integration-incomplete until this is root-caused and corrected. 2. **`vtdec_hw`'s colorimetry issue is unresolved**, not just observed. If gstreamer-rs is reconsidered later, that needs root-causing (likely a `glcolorconvert`/`gldownload` pipeline fix, or explicit `colorimetry=` caps pinning) before it can be trusted for compositing. @@ -129,8 +154,10 @@ reason stated in the issue: jump-cut compositing needs in-process frame-accurate ## Environment (this branch's results) -- Hardware: Apple M3 (Mac15,12), macOS (Darwin 24.6.0) -- Rust: `rustc 1.97.0`, `cargo 1.97.0` -- FFmpeg: `8.1.2` (Homebrew, `--enable-gpl --enable-videotoolbox --enable-libx264 --enable-libx265`) -- GStreamer: `1.28.5` (Homebrew, bundled all-plugins formula) -- `pkg-config`: not preinstalled — required `brew install pkgconf` for either candidate to build +- Mac hardware: Apple M3 (Mac15,12), macOS (Darwin 24.6.0) +- Windows hardware: NVIDIA GeForce RTX 5050 Laptop GPU, driver `592.15` +- Rust: `rustc 1.97.x`, `cargo 1.97.x` (Mac + Windows runs) +- FFmpeg (Mac): `8.1.2` (Homebrew, `--enable-gpl --enable-videotoolbox --enable-libx264 --enable-libx265`) +- FFmpeg (Windows): `N-124419-gc8a4770599-20260508` shared build at `C:\ffmpeg` (`--enable-ffnvcodec --enable-cuda-llvm --enable-gpl --enable-libx264 --enable-libx265`, `ffmpeg -hwaccels` includes `cuda`) +- GStreamer: `1.28.5` (Homebrew on Mac; official MSI on Windows at `C:\Users\Victoria\AppData\Local\Programs\gstreamer\1.0\msvc_x86_64`) +- `pkg-config`: not preinstalled by default on either machine; required explicit pathing for local builds From c52b33105cfddf13d7d1f70ac69b39e899bb2162 Mon Sep 17 00:00:00 2001 From: Saloni Date: Sun, 19 Jul 2026 23:24:11 +0800 Subject: [PATCH 9/9] docs(spike): add verified Mac M2 results to FINDINGS.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Apple M2 (MacBook Air Mac14,2, macOS 14.5, Rust 1.97.1) results match M3 exactly across all eight test cases: - ffmpeg-the-third software: pixel-exact (mean=0, max=0) both timestamps - ffmpeg-the-third --hw (VideoToolbox): pixel-exact, HARDWARE PATH active - gstreamer-rs software: mean≈1.39, max=6, PASS (matches M3) - gstreamer-rs --hw (vtdec_hw): mean≈3.86, max≈175, FAIL (matches M3) - cargo test: 3/3 ffmpeg-candidate, 2/2 gstreamer-candidate One notable difference from M3: GStreamer-GL-WARNING about NSApplication did not appear on macOS 14.5 (Darwin 23.5.0). vtdec_hw pixel failure is identical in magnitude regardless, confirming the colorimetry issue is not GL-warning-correlated. Criterion #5 licensing confirmed matching M3: applemedia/vtdec_hw is LGPL + Apple system frameworks only (clean); libgstlibav links this machine's Homebrew ffmpeg 8.1.2 (--enable-gpl, same risk as M3). Recommendation unchanged: ffmpeg-the-third. Co-Authored-By: Claude Sonnet 4.6 --- spike/rust-decode-spike/FINDINGS.md | 45 +++++++++++++++++++++++------ 1 file changed, 36 insertions(+), 9 deletions(-) diff --git a/spike/rust-decode-spike/FINDINGS.md b/spike/rust-decode-spike/FINDINGS.md index 2d1189e..18b056a 100644 --- a/spike/rust-decode-spike/FINDINGS.md +++ b/spike/rust-decode-spike/FINDINGS.md @@ -1,7 +1,6 @@ # Findings — FFmpeg vs gstreamer-rs binding decision (issue #108) -Status: **Mac M3 and Windows+NVIDIA results are now verified on this branch. Mac M2 remains open -for a contributor with that hardware — see `README.md` for exact run commands.** +Status: **Mac M3, Mac M2, and Windows+NVIDIA results are all verified on this branch.** ## Recommendation @@ -30,8 +29,8 @@ but `ffmpeg-the-third` fails less severely and remains the stronger cross-platfo | Candidate | Mac M3 (this branch) | Mac M2 | Windows + NVIDIA | |---|---|---|---| -| `ffmpeg-the-third` | **Reaches VideoToolbox, pixel-exact.** No high-level hwaccel API exists in the crate (checked: no hwaccel/hwdevice/hwcontext module). Required hand-written unsafe FFI through `ffmpeg_the_third::ffi`: `av_hwdevice_ctx_create(AVHWDeviceType::VIDEOTOOLBOX)` + a `get_format` callback set on the decoder's *unopened* `AVCodecContext` + `av_hwframe_transfer_data` per frame. ~40 lines, see `ffmpeg-candidate/src/main.rs`'s `hw` module. | _Open — run `ffmpeg-candidate` with `--hw`, see README_ | **Reaches CUDA/NVDEC and transfers frames back to software.** Implemented `#[cfg(target_os = "windows")]` hw module with `av_hwdevice_ctx_create(AVHWDeviceType::CUDA)`, `get_format` selecting `AVPixelFormat::CUDA`, and `av_hwframe_transfer_data`. `HARDWARE PATH: active` printed on both test timestamps. Output quality is not yet within tolerance (criterion #2), but hardware negotiation is real. | -| `gstreamer-rs` | **Negotiates and prerolls** (`vtdec_hw` is hardware-only by construction, so a successful preroll is real confirmation). Zero unsafe code — just a different element name. **But**: output is visibly wrong (criterion #2) and building the pipeline printed `GStreamer-GL-WARNING: An NSApplication needs to be running on the main thread` — `vtdec_hw`'s output is GL-memory-backed (`video/x-raw(memory:GLMemory)`), and a plain CLI render-engine binary has no Cocoa run loop. Neither issue blocked this spike from running, but both are real open questions for a production render engine. | _Open_ | **Reaches NVDEC via `nvh264dec` and negotiates successfully.** Windows code now picks decoder element by platform (`nvh264dec` on Windows, `vtdec_hw` on macOS). `HARDWARE PATH: active (nvh264dec negotiated successfully)` printed on both test timestamps. As on Mac hardware decode, output quality is outside tolerance (criterion #2). | +| `ffmpeg-the-third` | **Reaches VideoToolbox, pixel-exact.** No high-level hwaccel API exists in the crate (checked: no hwaccel/hwdevice/hwcontext module). Required hand-written unsafe FFI through `ffmpeg_the_third::ffi`: `av_hwdevice_ctx_create(AVHWDeviceType::VIDEOTOOLBOX)` + a `get_format` callback set on the decoder's *unopened* `AVCodecContext` + `av_hwframe_transfer_data` per frame. ~40 lines, see `ffmpeg-candidate/src/main.rs`'s `hw` module. | **Reaches VideoToolbox, pixel-exact.** `HARDWARE PATH: active` on both test timestamps. Results are identical to M3 — mean=0.000, max=0 on both frames. Same unsafe FFI path, same 40 lines, same result — no M2-specific friction. | **Reaches CUDA/NVDEC and transfers frames back to software.** Implemented `#[cfg(target_os = "windows")]` hw module with `av_hwdevice_ctx_create(AVHWDeviceType::CUDA)`, `get_format` selecting `AVPixelFormat::CUDA`, and `av_hwframe_transfer_data`. `HARDWARE PATH: active` printed on both test timestamps. Output quality is not yet within tolerance (criterion #2), but hardware negotiation is real. | +| `gstreamer-rs` | **Negotiates and prerolls** (`vtdec_hw` is hardware-only by construction, so a successful preroll is real confirmation). Zero unsafe code — just a different element name. **But**: output is visibly wrong (criterion #2) and building the pipeline printed `GStreamer-GL-WARNING: An NSApplication needs to be running on the main thread` — `vtdec_hw`'s output is GL-memory-backed (`video/x-raw(memory:GLMemory)`), and a plain CLI render-engine binary has no Cocoa run loop. Neither issue blocked this spike from running, but both are real open questions for a production render engine. | **Negotiates and prerolls** (`vtdec_hw negotiated successfully` printed). Pixel results are identical to M3 (mean=3.855/3.886, max=168/181 — same FAIL). One notable difference from M3: the `GStreamer-GL-WARNING: An NSApplication needs to be running on the main thread` did **not** appear on this machine (macOS 14.5 / Darwin 23.5.0, vs. M3's macOS 15.x / Darwin 24.6.0). The vtdec_hw pixel failure is reproducible regardless — not a GL-warning-caused artifact on M3, but a real colorimetry problem on both. | **Reaches NVDEC via `nvh264dec` and negotiates successfully.** Windows code now picks decoder element by platform (`nvh264dec` on Windows, `vtdec_hw` on macOS). `HARDWARE PATH: active (nvh264dec negotiated successfully)` printed on both test timestamps. As on Mac hardware decode, output quality is outside tolerance (criterion #2). | ## Criterion #2 — Seek/decode accuracy @@ -58,6 +57,20 @@ Tolerance used: `mean_abs_diff < 2.0 && max_abs_diff < 24` (covers ordinary YUV differences between decode paths; `vtdec_hw`'s failure is ~2–7x outside this band, not a rounding error). +### Mac M2 run results (this branch) + +| Candidate / path | t=0.5s | t=1.5s | +|---|---|---| +| `ffmpeg-the-third`, software | mean_abs_diff=0.000, max=0 — **PASS (pixel-exact)** | mean_abs_diff=0.000, max=0 — **PASS (pixel-exact)** | +| `ffmpeg-the-third`, `--hw` (VideoToolbox) | `HARDWARE PATH: active`; mean_abs_diff=0.000, max=0 — **PASS (pixel-exact)** | `HARDWARE PATH: active`; mean_abs_diff=0.000, max=0 — **PASS (pixel-exact)** | +| `gstreamer-rs`, software (`avdec_h264`) | mean_abs_diff=1.391, max=6 — **PASS** | mean_abs_diff=1.377, max=6 — **PASS** | +| `gstreamer-rs`, `--hw` (`vtdec_hw`) | `HARDWARE PATH: active (vtdec_hw negotiated successfully)`; mean_abs_diff=3.855, max=168 — **FAIL** | `HARDWARE PATH: active (vtdec_hw negotiated successfully)`; mean_abs_diff=3.886, max=181 — **FAIL** | + +All M2 results are identical to M3. The `GStreamer-GL-WARNING` about `NSApplication` did not appear +on this machine (macOS 14.5), but the vtdec_hw pixel failure is the same magnitude — confirming +the colorimetry issue is not a GL-warning-correlated artifact but a consistent hardware-path +characteristic of `vtdec_hw` across both M-series chips tested. + ### Windows + NVIDIA run results (this branch) | Candidate / path | t=0.5s | t=1.5s | @@ -83,6 +96,19 @@ Neither candidate's Windows or true cross-compilation story is tested here — t in the README for the Windows/NVIDIA contributor. Both builds otherwise worked without patching either crate. +Mac M2 build notes (macOS 14.5, Rust 1.97.1, freshly installed): + +- Rust was not preinstalled on this machine — required `curl | sh` install via `rustup` (same as + would be true on any fresh Mac). +- `pkgconf` was not preinstalled — `brew install pkgconf` required, same as M3. +- FFmpeg dev libs were already present via existing `brew install ffmpeg` install. +- GStreamer (`brew install gstreamer`) was not present — installed fresh, same 217 MB bundled + formula as M3, no extra plugin steps. +- First build times: `ffmpeg-candidate` ~17s from cold; `gstreamer-candidate` ~17s from cold. + In line with M3 (~6s / ~20s on warm incremental; caches were empty here so first-build times + are higher). +- No build errors or patching required on either candidate. + Windows+NVIDIA notes from this run: - Toolchain/setup friction was real for `ffmpeg-the-third`: this environment needed explicit @@ -154,10 +180,11 @@ reason stated in the issue: jump-cut compositing needs in-process frame-accurate ## Environment (this branch's results) -- Mac hardware: Apple M3 (Mac15,12), macOS (Darwin 24.6.0) +- Mac M3 hardware: Apple M3 (Mac15,12), macOS (Darwin 24.6.0) +- Mac M2 hardware: Apple M2 (MacBook Air, Mac14,2), macOS 14.5 (Darwin 23.5.0) - Windows hardware: NVIDIA GeForce RTX 5050 Laptop GPU, driver `592.15` -- Rust: `rustc 1.97.x`, `cargo 1.97.x` (Mac + Windows runs) -- FFmpeg (Mac): `8.1.2` (Homebrew, `--enable-gpl --enable-videotoolbox --enable-libx264 --enable-libx265`) +- Rust: `rustc 1.97.1`, `cargo 1.97.1` (all platforms) +- FFmpeg (Mac, both M2 and M3): `8.1.2` (Homebrew, `--enable-gpl --enable-videotoolbox --enable-libx264 --enable-libx265`) - FFmpeg (Windows): `N-124419-gc8a4770599-20260508` shared build at `C:\ffmpeg` (`--enable-ffnvcodec --enable-cuda-llvm --enable-gpl --enable-libx264 --enable-libx265`, `ffmpeg -hwaccels` includes `cuda`) -- GStreamer: `1.28.5` (Homebrew on Mac; official MSI on Windows at `C:\Users\Victoria\AppData\Local\Programs\gstreamer\1.0\msvc_x86_64`) -- `pkg-config`: not preinstalled by default on either machine; required explicit pathing for local builds +- GStreamer: `1.28.5` (Homebrew on Mac M2 and M3; official MSI on Windows at `C:\Users\Victoria\AppData\Local\Programs\gstreamer\1.0\msvc_x86_64`) +- `pkg-config` / `pkgconf`: not preinstalled by default on any machine tested; required `brew install pkgconf` on Mac