forked from unom/punktfunk
`DmabufInner::open` builds four owned objects — a DRM device, a VAAPI device derived from it, a DRM-PRIME frames context, and a filter graph — and every one of its eight failure branches unwound them by hand. The same four-line block (`avfilter_graph_free` + three `av_buffer_unref`s) appeared eight times, once *inside a macro*, plus a ninth copy in `Drop`. Adding a step to that function meant remembering to extend the unwind at exactly the right depth; getting it wrong leaks a device per failed session (the persistent listener accumulates them) or frees one twice. All eight are gone. `AvBuffer` already owned the buffer refs; `AvFilterGraph` now does the same for the graph, so each handle is owned the moment it exists and an early `bail!` releases whatever was built so far. `open` lost ~40 lines of cleanup and gained none. Field order in `DmabufInner` is load-bearing and says so: graph, frames, VAAPI device, DRM device, then `enc` LAST. Fields drop in declaration order, and that sequence reproduces the old hand-written `Drop` exactly — including that it ran ahead of every field, so all four were released before ffmpeg-next dropped the encoder. Everything here holds its own reference, so refcounting makes any order sound; the ordering is pinned so a future reorder cannot quietly change what ships. One subtlety preserved deliberately: the buffersrc parameters take `drm_frames` BORROWED, not ref'd (`av_buffersrc_parameters_set` takes its own ref). That is now `drm_frames.as_ptr()` — same borrow, same single owned ref, no new leak. Verified on .21 (CachyOS, RTX 5070 Ti, FFmpeg 62): `cargo check -p pf-encode --all-targets` clean at exit 0, `cargo test -p pf-encode` 33 passed / 0 failed, and `cuda_hw_alloc_drop_cycles` still passes against real CUDA. vaapi.rs now contains zero `av_buffer_unref` and zero `avfilter_graph_free` calls, down from 40 and 8. The dmabuf path itself still needs AMD/Intel silicon to exercise end to end.