feat(video): decode the AV1 movies with dav1d on Windows (#2316) #2515
No reviewers
Labels
No labels
accessibility
autorelease: pending
autorelease: tagged
bug
dependencies
documentation
duplicate
enhancement
github_actions
good first issue
help wanted
invalid
question
testing wanted
wontfix
No milestone
No assignees
1 participant
Due date
No due date set.
Dependencies
No dependencies set
Reference
MagicBeanDev/Hookshot!2515
Loading…
Reference in a new issue
No description provided.
Delete branch "feat/windows-dav1d-movies"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Movies stutter and fall behind their sound on Windows (#2316, now tracked in #2503; on an RTX 4060; a GTX 1050 in Discord). The Windows FFmpeg has no AV1 decoder, so the 1080p AV1 movies go to the AV1 Video Extension's software decoder on every GPU. This PR decodes them with dav1d instead, the same decoder Linux uses.
Why it was Media Foundation's software decoder everywhere
--disable-everythingplus h264, vp8, vp9 and opus. It has no AV1 decoder of any kind, and not even the AV1 parser.host/nn_video.ctherefore hands AV1 tohost/mf_av1.c. That file asks Media Foundation for its synchronous decoder first, which is the extension's software one; the hardware decoder is used only when no synchronous one exists. So an RTX 4060 decodes the movies in software too, and a PC without the extension gets the Store dialog (#380).What changed
host/dav1d_av1.cwraps dav1d behind the same open, decode, flush and close interface asmf_av1.c, handing back NV12, whichnn_video.ckeeps as it is. It fetches one picture per submission: a seconddav1d_get_picturewith no data sent in between makes dav1d drain, which waits for every frame in flight and leaves its frame threads decoding one picture at a time. At the end of the stream it drains everything.HOS_DAV1D_THREADS=<n>sets the thread count (default: dav1d's, one per logical core).HOS_NO_DAV1D=1goes back to Media Foundation.host/nn_video.cturns "our own AV1 decoder" into a small table of functions: dav1d first when it is built in, then Media Foundation. Nothing changes when FFmpeg has its own AV1 decoder (Linux).video: a movie of <n> pictures on <decoder>: <mean> ms to decode each (worst <ms>), one shown every <ms> ms; <n> skipped to keep up with its sound. A slower-than-real-time decoder is flagged "(slower than it plays)". The next #2316 log will show whether decoding is what falls behind.scripts/windows/dav1d.shbuilds dav1d with llvm-mingw from the same release and source-tree hash thatscripts/linux/deps.shpins (1.5.4; it reads them from there). It installs into the CPM layout<cpm>/dav1d/1.5.4/{include,lib/libdav1d.a,COPYING}. The output is reproducible: two builds give the samelibdav1d.a(SHA-256e6f1adec…7e98with llvm-mingw 20261104). That took clang's-mno-incremental-linker-compatibleand nasm's--reproducible; without them the COFF objects carry timestamps.nx2/build.pycompilesdav1d_av1.cand linkslibdav1d.awhen the CPM cache has dav1d, on the llvm-mingw route only (it is a MinGW library). Without it, or on the Visual Studio route, nothing changes and Media Foundation decodes the movies as before.windows-builder.ymlbuilds dav1d with the pinned Linux-hosted llvm-mingw and runs the wrapper's test on the Windows build under Wine. It uploads the CPM folder as thedav1d-windowsartifact. The Linux test job installslibdav1d-devfor the new test.How dav1d reaches players
Players build the game on their own PC, so dav1d has to reach them in the Builder's cpm part. The cpm part is normally reused from the latest release. This PR makes the release workflow add dav1d to it: the package job takes the
dav1d-windowsartifact and runsrelease_parts.py package --cpm-add. If the latest release's cpm part lacksdav1d/1.5.4, the job unpacks that part intotools/cpmwith dav1d added.make_setup_parts.pythen makes a new cpm part, and the release job uploads it tobuilder-partslike any other new part. Once a release carries it, later runs reuse it. No manual step is needed.I checked this against the real v2.1.0 setup list and the dav1d built here. The new part is
HookshotBuilder-cpm-73d3c9f5a31d.zip(19.6 MB, up from 18.6 MB). It holds the 451 files ofHookshotBuilder-cpm-c30c77e4cf00.zipbyte-for-byte, plus dav1d's 8 files. Two runs gave the same SHA-256, because the zip andlibdav1d.aare both reproducible.Testing
tests/test_dav1d_av1.py(new) uses a 3.8 KB synthetic clip intests/dav1d_av1/: 12 pictures of a formula pattern, encoded losslessly with libaom, so no game data. Every picture must come out in order, with its pts, as the exact NV12 bytes of the pattern. That includes the pictures dav1d holds back until the end, and a second pass after a flush. It runs at 1 and 4 threads, and checks thatHOS_NO_DAV1D=1leaves the decoder closed. It passes on Linux with clang, clang++, gcc and g++ against the system dav1d. It also passes for the Windows build (llvm-mingw, the script'slibdav1d.a) under Wine at 1 thread, 4 threads and the default. Breaking the end-of-stream drain makes it fail.tests/test_cpm_discovery.py: dav1d is used only on the llvm-mingw route, never on the Visual Studio route or Linux, and only with both its header and library present.tests/test_release_cpm_add.py:--cpm-addadds dav1d to a cpm part that lacks it and keeps every file the part had. It leaves a part that already has dav1d to be reused as it is, and adds a new version beside an old one.tests/test_vcore_video_acquire.pyandtests/test_run_all_declarations.pywere updated for the new type and tool.python -I -B tests/run_all.py: 420 passed, 0 failed on Linux, rebased on currentmain.host/nn_video.ccompiles for Windows with llvm-mingw and the cpm part's own FFmpeg headers, with and withoutHOOKSHOT_DAV1D, and for Linux.Not tested
No screenshots: nothing new shows on screen. The movies should just play in sync, and the extension's Store dialog no longer appears once dav1d is in the Builder.
Thanks, this is a careful piece of work and the diagnosis in the description matches the code: the CPM FFmpeg has no AV1 decoder, so
nn_video.cfalls through tomf_av1.cand its synchronous (software) MFT on every GPU. Linking dav1d directly is the right fix, and the per-movie log line will make the #2503 / #2316 reports much easier to read.What I checked and found fine:
dav1d_av1.c: everydav1d_get_pictureis paired withdav1d_picture_unref, theDav1dDatais unref'd on a refused send (and the 64-try cap can't spin forever), flush isdav1d_flushwith the NV12 scratch kept, close frees everything and takes NULL. The one-picture-per-submission rule is right about dav1d's drain-on-second-call behaviour, and the EAGAIN path's second call is exactly that intended blocking drain.deliver(): odd sizes are truncated to even before the chroma loops, so for 4:2:0, 4:2:2 and 4:4:4 the source row and column indexes stay inside the plane;stride[1]is shared by U and V as dav1d defines it; high bit depth readsuint16_trows with byte strides; 4:0:0 doesn't touchdata[1].nn_video.c: the decoder table is only reached when FFmpeg has no AV1 decoder,d->av1is only dereferenced behindd->mf(dec_flushis guarded byd->ctx || d->mf), and withoutHOOKSHOT_DAV1Dthe table is just Media Foundation. So Linux and a Windows build without dav1d in the CPM cache behave as before (dav1d_windows()returns None off llvm-mingw or with a partial install, and the newtest_cpm_discoverycase covers that).scripts/linux/deps.sh'stree_is, read from that file so they can't drift;--reproduciblenasm plus-mno-incremental-linker-compatibleis the right pair for stable COFF objects; the BSD-2COPYINGtravels with the library. Actions are SHA-pinned,permissions: contents: readat the top, llvm-mingw checked by SHA-256.dav1dplaceholder is infind_tools, the declarations test'sTOOLSis updated, it's skipped on Windows and skipped (not failed) on Linux withoutlibdav1d-dev.Open points:
main, andpull_requestworkflows don't start until it merges cleanly, so the newdav1djob (which you say runs for the first time here) and the Linux test job are both still unproven. The conflicts arenx2/build.py(main'slinker()rework of the link lines from the same area) anddocs/worklist.md(a row added next to yours); both look mechanical. A rebase gets CI going.Nit: #2316 was closed into #2503; the worklist row could link #2503 too.
Overlap: #2114 also edits
host/nn_video.c(pool RGBA conversion), with hunks next to yours indec_init_sizedand afterdec_finalize, so whichever lands second gets a small textual rebase; the changes themselves don't interact.Happy to approve once it's rebased and CI (including the new job) is green.
@ -0,0 +32,4 @@if (env_on("HOS_NO_DAV1D")) return NULL;Dav1dSettings s;dav1d_default_settings(&s);if (env_int("HOS_DAV1D_THREADS", 0) > 0) s.n_threads = env_int("HOS_DAV1D_THREADS", 0);Suggestion: default to 4 threads, like the FFmpeg path on Linux (
thread_count = 4for AV1 indec_init_sized), unlessHOS_DAV1D_THREADSsays otherwise. Withn_threads = 0dav1d takes one thread per logical core and, through its automaticmax_frame_delay, holds more pictures back as the count grows. Since #2322 the decode no longer holds a virtual core, so on a 16 to 32 thread CPU those threads compete with the game's own while the movie plays. Your own numbers (1.9 ms on 4 threads against a 33 ms budget) suggest 4 is plenty, and it keeps Windows and Linux decoding the same way, which makes logs easier to compare.wouldn't it be better to make use of hardware acceleration instead of relying on software decoding? just curious
github-sync referenced this pull request2026-11-12 06:22:54 +00:00