feat(xmf): expose decoded VPX image planes and color metadata - #85
Merged
irvingouj@Devolutions (irvingoujAtDevolution) merged 13 commits intoSep 25, 2026
Merged
irvingouj@Devolutions (irvingoujAtDevolution) merged 13 commits into
irvingouj@Devolutions (irvingoujAtDevolution) merged 13 commits into
Conversation
irvingouj@Devolutions (irvingoujAtDevolution)
force-pushed
the
feat/vpx-image-copy-to-bgra
branch
from
September 24, 2026 17:35
677032f to
417c462
Compare
Export XmfVpxImage_GetFormat, GetPlane, GetStride, GetColorSpace and GetColorRange so consumers can read the pixels of decoded images instead of only passing them back to the encoder. Each accessor is a separate read-only symbol, and no existing signature changes. Load them through xmf-sys and expose VpxImage::format, color_space, color_range and i420_planes in the safe crate.
irvingouj@Devolutions (irvingoujAtDevolution)
force-pushed
the
feat/vpx-image-copy-to-bgra
branch
from
September 24, 2026 17:57
417c462 to
2a2cbb2
Compare
irvingouj@Devolutions (irvingoujAtDevolution)
marked this pull request as ready for review
September 24, 2026 18:11
irvingouj@Devolutions (irvingoujAtDevolution)
requested review from
Benoît Cortier (CBenoit)
and
a balanced review from Copilot
and removed request for
Copilot
September 24, 2026 18:11
Richard Markiewicz (thenextman)
approved these changes
Sep 24, 2026
Devolutions.Cadeau had no C# binding for XmfVpxDecoder or XmfVpxImage, so .NET consumers had to write their own P/Invoke to decode VP8/VP9. Add XmfVpxDecoder (Create, Decode, GetNextFrame, GetLastError, Destroy) and XmfVpxImage (width, height, format, planes, strides, color space and range) with SafeHandle ownership, following the XmfBipBuffer pattern.
XmfVpxImage exposed raw plane pointers into buffers owned by the decoder, so reading an image after a later Decode, or after the decoder was disposed or finalized, read stale or freed memory. GetNextFrame now copies the planes into managed arrays while holding a reference on the decoder handle, frees the native wrapper, and returns an immutable XmfVpxImage with Y, U and V planes, format and color metadata. The copy handles 8- and 16-bit I420, YV12, I422, I440 and I444 and rejects other formats.
GetNextFrame copied every frame, unlike XmfVpxDecoder_GetNextFrame and the Rust next_frame, which both return a view of the decoder's buffers. It now returns that view again, and XmfVpxImage.Copy() makes the copy explicit, returning an XmfVpxImageCopy with managed Y, U and V planes. The view holds its decoder so the decoder cannot be finalized under it, and records the decode generation. After a later Decode or after the decoder is disposed, GetPlane, GetStride and Copy throw instead of reading reused or freed memory.
Copilot started reviewing on behalf of
Benoît Cortier (CBenoit)
September 25, 2026 03:31
View session
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
Decoder iteration, handle ownership, and invalid-input error reporting currently violate the managed API contracts.
Get a fresh assessment by requesting another Copilot review.
Review effort: Balanced
Findings: 2
Open (3)
What changed in this PR
Exposes decoded VPX planes and color metadata across native, Rust, and .NET APIs.
Changes:
- Adds native plane, stride, format, and color accessors.
- Exposes safe Rust I420 plane slices.
- Adds zero-copy and copied-image .NET decoder APIs.
| File | Description |
|---|---|
rust/xmf-sys/src/lib.rs |
Adds native accessor bindings. |
rust/cadeau/src/xmf/vpx/mod.rs |
Adds metadata and I420 plane APIs. |
libxmf/XmfVpxImage.h |
Declares exported image accessors. |
libxmf/XmfVpxImage.c |
Implements image accessors. |
dotnet/Devolutions.Cadeau/XmfVpxImage.cs |
Adds managed image views and copies. |
dotnet/Devolutions.Cadeau/XmfVpxDecoder.cs |
Adds managed VPX decoding support. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Share one plane-index check between GetPlane and GetStride, return -1 from GetColorRange when the image is unavailable because studio range is a real value, document the getters in the @brief/@param/@return style of XmfVpxDecoder.h, and include <stdint.h> for uint8_t explicitly.
The raw libvpx constants live in xmf-sys. The cadeau crate returns VpxImageFormat, VpxColorSpace and VpxColorRange newtypes instead of i32, and bundles each plane's data and stride into a VpxPlane. The plane helper is an unsafe fn with a documented safety contract and no underflow on empty images.
…ccess An XmfVpxImage points into buffers that XmfVpxDecoder_Destroy frees. Each image handle now holds a reference on the decoder handle, so destruction waits until every image is released, even if the decoder is disposed, finalized or its Handle is disposed directly. Such images then report themselves unusable instead of reading freed memory. Decode, GetNextFrame, GetLastError and the image's GetPlane, GetStride and Copy share one lock per decoder, so concurrent decoding cannot reuse buffers mid-copy. Copy() now handles 8-bit I420, matching the Rust i420_planes; other formats are read through GetPlane and GetStride.
Expose only initialized pixel rows in Rust and keep .NET decoder memory alive through Copy. Add regressions for row padding, odd-sized VP8 frames, stale views and concurrent disposal. Publish xmf_sys::vpx and clarify the plane-index helper.
XmfBipBuffer.Handle became a SafeHandle, so the four recorder setups that passed it to SetBipBuffer(IntPtr) no longer compiled. Pass the XmfBipBuffer to the existing SetBipBuffer(XmfBipBuffer) overload instead.
VpxPlane only offered per-row slices, so a caller could not hand a plane to a C or SIMD converter that takes a base pointer and a stride without copying it. Add the unsafe VpxPlane::as_ptr, whose safety section spells out what the caller must uphold (read only, pixel bytes only, not after the image is dropped), plus width() and height() to walk the plane with it. Debug now prints the plane geometry instead of every byte, and xmf_sys::vpx stays private again since its items are already re-exported at the crate root.
Disposing XmfVpxDecoder.Handle directly bypassed the decoder's disposed flag, so Decode and GetNextFrame kept working until the GC finalized every image that still referenced the native decoder. They now throw ObjectDisposedException as soon as either the decoder or its handle is disposed.
Move the checks from the separate Devolutions.Cadeau.Vpx.Test console into the existing test program, run with the vpx argument, and read the decoder lock through InternalsVisibleTo instead of reflection. Add checks that images and the decoder throw after decoder.Dispose() and after decoder.Handle.Dispose().
Native XmfVpxDecoder_Decode returns -1 for a null or empty buffer without setting lastError, so Decode returned false while GetLastError reported no error or a stale one. Decode now throws ArgumentNullException or ArgumentException before reaching native code, and a rejected frame leaves the last decoded image usable. GetNextFrame now documents that libvpx yields at most one frame per Decode, so a second call returns null; a check pins that behaviour.
irvingouj@Devolutions (irvingoujAtDevolution)
merged commit Sep 25, 2026
7a25d83
into
master
9 checks passed
irvingouj@Devolutions (irvingoujAtDevolution)
pushed a commit
that referenced
this pull request
Sep 25, 2026
## 🤖 New release * `xmf-sys`: 0.4.1 -> 0.4.2 (✓ API compatible changes) * `cadeau`: 0.5.2 -> 0.5.3 (✓ API compatible changes) <details><summary><i><b>Changelog</b></i></summary><p> ## `xmf-sys` <blockquote> ## [0.4.2](xmf-sys-v0.4.1...xmf-sys-v0.4.2) - 2026-09-25 ### Added - *(xmf)* expose decoded VPX image planes and color metadata ([#85](#85)) </blockquote> ## `cadeau` <blockquote> ## [0.5.3](cadeau-v0.5.2...cadeau-v0.5.3) - 2026-09-25 ### Added - *(xmf)* expose decoded VPX image planes and color metadata ([#85](#85)) </blockquote> </p></details> --- This PR was generated with [release-plz](https://github.com/release-plz/release-plz/).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.


Summary
Expose the planes and color metadata of decoded VPX images, so XMF consumers can read the pixels that
XmfVpxDecoderproduces.Today a decoded
XmfVpxImageexposes only its width and height (#84);XmfVpxImage_GetDatais internal. That works for Gateway, which hands decoded images straight back to the encoder. A player also needs the Y/U/V planes to render. The first consumer is the experimental Cadeau backend forAvalonia.Controls.MediaPlayerin RDM, which converts I420 to BGRA itself. Consumer PR: Devolutions/RDM#17997.Changes
Native
GetWidth/GetHeight:XmfVpxImage_GetFormatreturns the libvpxvpx_img_fmt_t, orVPX_IMG_FMT_NONEwhen the image is unavailable.XmfVpxImage_GetPlane(image, plane)andXmfVpxImage_GetStride(image, plane)return the plane pointer and row stride, or NULL / 0 for an invalid index.XmfVpxImage_GetColorSpaceandXmfVpxImage_GetColorRangereturn the libvpxvpx_color_space_t/vpx_color_range_treported by the decoder: VP8 reports unknown / studio. When the image is unavailable they returnVPX_CS_UNKNOWNand -1.No existing signature changes and no conversion code is added. Each accessor is its own symbol.
Rust
xmf-sysloads the new exports and re-exports the libvpx format, plane, color-space and range constants at the crate root.VpxImage::format(),color_space()andcolor_range()return theVpxImageFormat,VpxColorSpaceandVpxColorRangenewtypes.VpxImage::i420_planes()returnsVpxI420Planes { y, u, v }ofVpxPlane, borrowed until the next decoder call. libvpx may leave the padding between rows uninitialized, so a plane is never exposed as one&[u8]:rows()yields each row'swidth()pixel bytes as safe slices.width(),height()andstride()describe the plane.unsafe fn as_ptr()returns the first pixel for code that takes a base pointer and a stride, such as a C or SIMD converter. Its# Safetysection lists what the caller must uphold: read only, read just the pixel bytes of each row, and stop before the image is dropped. It includes a worked example.Debugprints the plane geometry, not its bytes..NET (
Devolutions.Cadeau, which had no VPX decoder bindings)XmfVpxDecoder:Decode,GetNextFrame,GetLastError.DecodethrowsArgumentNullException/ArgumentExceptionfor a null or empty frame. NativeXmfVpxDecoder_Decoderejects those without settinglastError, so returningfalsewould leaveGetLastErrorstale.GetNextFrame()is zero-copy, likeXmfVpxDecoder_GetNextFrameand Rust'snext_frame(). It returns anXmfVpxImageview withWidth,Height,Format,ColorSpace,ColorRange,GetPlaneandGetStrideover the decoder's buffers. libvpx yields at most one frame perDecode, so a second call returns null.XmfVpxImage.Copy()is the explicit copy. It returns anXmfVpxImageCopywith tightly packed managedY/U/Vplanes that outlive the decoder. Like the Rusti420_planes(), it handles 8-bit I420; other formats are read throughGetPlane/GetStride.XmfVpxDecoder_Destroywaits until every image is released, even if the decoder is disposed, finalized, or itsHandleis disposed directly.Decode, or once the decoder or its handle is disposed, the image'sGetPlane,GetStrideandCopythrow instead of reading reused buffers.ObjectDisposedExceptionas soon as either the decoder or its handle is disposed.Decode,GetNextFrame,GetLastErrorand the image methods share one lock per decoder.GetPlaneis valid until the nextDecodewhile the image is alive; the XML docs say so.XmfBipBuffer. Builds for netstandard2.0/2.1 and net10.0.Tests
rust/cadeau/src/xmf/vpx/tests.rs:rows(),as_ptr(), padding andDebug; they pass under Miri with strict provenance.rows()andas_ptr().Devolutions.Cadeau.Test vpxruns the .NET binding checks:decoder.Dispose()and afterdecoder.Handle.Dispose()Disposewaiting for an in-progress readDecode, then nullDevolutions.Cadeau.Testdid not build onmasterbecauseXmfBipBuffer.Handlebecame aSafeHandle. It now passes the buffer to the existingSetBipBuffer(XmfBipBuffer)overload.Validation
cargo clippyis clean forcadeau(default anddlopen) andxmf-sys, except the existingencoder.rswarning.cargo test -p cadeaupasses: 6 unit tests and 3 doctests. It needsXMF_SEARCH_PATHpointing at the XMF build, and the decode test is skipped withdlopen.Devolutions.Cadeau.Test vpxpasses 9/9 against this DLL, three runs in a row. The disposed-handle check fails without theObjectDisposedExceptionfix.Copy(), is byte-identical to ffmpeg's raw decodeCopy()throwsNotSupportedExceptionwhileGetPlane/GetStrideworkGetNextFrame/GetPlane/Copyfor 4 seconds produced only the expected stale-image exceptionscargo testor the .NET test program yet.threads > 1) differs from single-threaded and ffmpeg output by ±1 on 40 of 3.1M bytes in the 1080p fixture. Single-threaded is bit-exact.Notes
dlopen,xmf-sysresolves every listed symbol at load time. It requires an XMF build that includes these exports.