gfx: standalone fixes + the video pixel-format vocabulary - #2160
Open
jcelerier wants to merge 20 commits into
Open
gfx: standalone fixes + the video pixel-format vocabulary#2160jcelerier wants to merge 20 commits into
jcelerier wants to merge 20 commits into
Conversation
jcelerier
force-pushed
the
split/gfx-fixes-and-formats
branch
from
July 31, 2026 00:17
ecfeb84 to
04a2068
Compare
jcelerier
force-pushed
the
split/gfx-fixes-and-formats
branch
3 times, most recently
from
August 16, 2026 03:42
1675076 to
b69fa53
Compare
jcelerier
added a commit
that referenced
this pull request
Aug 16, 2026
staticVulkanInstance() could hand back an instance whose create() had failed; return null instead so callers fall back cleanly rather than crashing. Moved down from split/gfx-fixes-and-formats (35eb25e, itself folding 6b0b345 and 25d183b) so the Vulkan hardening sits once, at the bottom of the stack, where headless configurations reach it. #2160 keeps the identical patch; rebasing it onto this branch drops the duplicate by patch-id. Note this covers create() failing, not the probe above it: supportedApiVersion() is called eighteen lines earlier and still crashes under a platform with no Vulkan support. That is a separate defect, unfixed here. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
jcelerier
force-pushed
the
split/gfx-fixes-and-formats
branch
from
August 16, 2026 13:56
b69fa53 to
6aa4b39
Compare
jcelerier
added a commit
that referenced
this pull request
Aug 16, 2026
staticVulkanInstance() could hand back an instance whose create() had failed; return null instead so callers fall back cleanly rather than crashing. Moved down from split/gfx-fixes-and-formats (35eb25e, itself folding 6b0b345 and 25d183b) so the Vulkan hardening sits once, at the bottom of the stack, where headless configurations reach it. #2160 keeps the identical patch; rebasing it onto this branch drops the duplicate by patch-id. Note this covers create() failing, not the probe above it: supportedApiVersion() is called eighteen lines earlier and still crashes under a platform with no Vulkan support. That is a separate defect, unfixed here. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
jcelerier
force-pushed
the
split/gfx-fixes-and-formats
branch
from
August 16, 2026 16:33
6aa4b39 to
cc756f0
Compare
jcelerier
added a commit
that referenced
this pull request
Aug 16, 2026
staticVulkanInstance() could hand back an instance whose create() had failed; return null instead so callers fall back cleanly rather than crashing. Moved down from split/gfx-fixes-and-formats (35eb25e, itself folding 6b0b345 and 25d183b) so the Vulkan hardening sits once, at the bottom of the stack, where headless configurations reach it. #2160 keeps the identical patch; rebasing it onto this branch drops the duplicate by patch-id. Note this covers create() failing, not the probe above it: supportedApiVersion() is called eighteen lines earlier and still crashes under a platform with no Vulkan support. That is a separate defect, unfixed here. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
jcelerier
force-pushed
the
split/gfx-fixes-and-formats
branch
from
August 16, 2026 18:08
cc756f0 to
93801c4
Compare
jcelerier
added a commit
that referenced
this pull request
Aug 16, 2026
staticVulkanInstance() could hand back an instance whose create() had failed; return null instead so callers fall back cleanly rather than crashing. Moved down from split/gfx-fixes-and-formats (35eb25e, itself folding 6b0b345 and 25d183b) so the Vulkan hardening sits once, at the bottom of the stack, where headless configurations reach it. #2160 keeps the identical patch; rebasing it onto this branch drops the duplicate by patch-id. Note this covers create() failing, not the probe above it: supportedApiVersion() is called eighteen lines earlier and still crashes under a platform with no Vulkan support. That is a separate defect, unfixed here. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
jcelerier
force-pushed
the
split/gfx-fixes-and-formats
branch
3 times, most recently
from
August 17, 2026 02:51
2f72efa to
c6b09de
Compare
jcelerier
force-pushed
the
split/gfx-fixes-and-formats
branch
2 times, most recently
from
August 17, 2026 13:51
5552446 to
6c3686a
Compare
Request the xcb EGL integration on X11 so that dma-buf import can stay zero-copy, and pin a desktop-GL (not GLES) core-profile context with a stencil buffer on the desktop code path. (cherry picked from commit 38cd38d) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014rZgzE8JjWvHDtaVUhxpLE
…ine quantum (cherry picked from commit cb5ca64)
The plugin builds with -fvisibility=hidden, so an un-exported class has no externally linkable constructor. DecklinkRoundtrip uses ISFNode for its GPU test-pattern path and failed to link against it; every other node the harnesses construct already carries the export. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014rZgzE8JjWvHDtaVUhxpLE
A backend-neutral pixel-format description plus conversions to and from ffmpeg's AVPixelFormat, shared by the video encoders, decoders and the GPU interop strategies. (cherry picked from commit c835069) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014rZgzE8JjWvHDtaVUhxpLE
The enum, the descriptor table and the name lookup were three hand-maintained lists of the same thing, so they could disagree - and did: a format could be declared in the enum with no descriptor at all, leaving formatInfo() returning the "unknown" sentinel and bytesPerFrame() returning 0 for it. Generate all of them from a single X-macro table instead. Adding a row adds the enum value, the descriptor and the entry allFormats() exposes at once, so declaring a format without describing it is no longer expressible. Two modelling changes come with it: - Sizing is block-based (blockPixels/blockBytes) rather than an averaged bits-per-pixel. The average could not express v210 at all - 128 bytes per 48 pixels is 21.33 bits, which was truncated into a uint8_t and then special cased in the stride function - and it over-computed planar strides, which is why a separate bytesPerPrimarySample field had to exist alongside it. One block spec covers packed, semi-planar and planar uniformly, and v210 s SMPTE stride now falls out of the general expression. - bool isYuv becomes a ColorModel enum, because greyscale and Bayer are neither YUV nor RGB, and both are produced by the industrial-camera paths. hasAlpha and byteOrder become queryable instead of living in prose: "forced opaque" and the endianness of the 10/12-bit layouts were only recorded in comments. defaultStrideAlignment is renamed preferredStrideAlignment and documented as a fallback for callers with no constraint of their own: alignment is a property of a device or allocator, not of a pixel format. alignedRowBytes() is the form device paths should use, and rowBytes() gives the unpadded row. Verified size-identical: a dump of stride at 64/720/1920/1921/3840/7680 and frame size at 720p/1080p for all 40 formats is byte-for-byte unchanged. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014rZgzE8JjWvHDtaVUhxpLE
The GPU-interop branch added 29 enum values for the V4L2 camera layouts (the X-padded RGB variants, NV21/NV16/NV61/NV24/NV42, the packed 4:4:4 YUV orders, the sub-byte RGB/YUV group, YVU420P, YUV422P12, Mono16BE) but only one descriptor, so formatInfo() answered "unknown" for the other 28. rowBytes() and bytesPerFrame() therefore returned 0 for formats the V4L2 capture path actively produces -- 16 of the 28 are reachable from it today. Nothing crashed only because the single consumer, HostPinnedRing, prefers the stride the device reports and falls back to the computed one; a caller that omitted the stride would have allocated a zero-byte ring. Since a format can no longer be declared without being described, the values move up here where the vocabulary lives, each with its block geometry, colour model, alpha and byte order. Also map to AVPixelFormat the ones FFmpeg can express exactly, taking the number of twins from 26 to 42: the 0-prefixed/suffixed X-variants, NV21/NV16/NV24/NV42, VUYA/VUYX, the RGB565/555/444 pairs, GRAY16BE and yuv422p12le. The rest stay deliberately unmapped, each with its reason recorded, and two of those reasons matter: - YVU420P has no plane-swapped twin. FFmpeg spells YV12 as yuv420p with the U and V pointers exchanged, so returning yuv420p here would silently swap chroma, which is precisely the bug this bridge exists to prevent. - AV_PIX_FMT_RGB8/BGR8 are 2:3:3, not the 3:3:2 that V4L2 calls RGB332. The switch is now exhaustive over the enum, so -Wswitch makes a new format state its FFmpeg relationship rather than silently defaulting to none. Verified: 69 formats, no descriptor holes, no zero-size results, 42 AV twins with no asymmetric round-trip, and plane count, subsampling, planarity, alpha and RGB-ness all agree with the FFmpeg descriptor for every twin. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014rZgzE8JjWvHDtaVUhxpLE
The GStreamer format map had A444_10BE and A444_10LE both resolving to AV_PIX_FMT_YUVA444P10BE, so a little-endian 10-bit 4:4:4-with-alpha stream was decoded as big-endian: every 10-bit sample byte-swapped. This reaches the GStreamer, Sh4lt and Shmdata inputs, which all resolve caps through this map. An audit of the whole table for key/value endianness agreement found this as the only live mismatch; the GBR/GBRA rows that also look inconsistent are commented out. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014rZgzE8JjWvHDtaVUhxpLE
…he table
The suite lived on the GPU-interop branch, three PRs downstream of the header it
guards, so the PR that introduces a format never ran it. Move it here.
It also could not have caught the descriptor holes it was written to prevent: it
kept its own 41-entry allKnownFormats[] list and asserted that each entry had a
descriptor, which is a tautology with respect to the table. A format declared in
the enum but present in neither list -- the state 28 formats were in -- was
invisible to it.
Every sweep now iterates allFormats(), the same table the vocabulary is
generated from, so a format is covered the moment it is declared.
New invariants beyond the previous coverage:
- values as well as names are unique, and none reuses the 0 sentinel
- colour-model classification: exactly one of RGB / YUV / achromatic holds,
and neither RGB nor greyscale claims chroma subsampling
- alignedRowBytes pads without ever losing data and never wastes a whole
alignment unit, for 13 widths x 6 alignments x every format
- rowBytes is exactly the block count and is monotonic in width
- byte order is declared wherever a sample spans more than one byte
- the FFmpeg cross-check now also compares alpha, RGB-ness and endianness,
which the old bool isYuv/bitsPerPixel model could not express
Verified by injection: wrongly setting YUV420P to 2 planes, and dropping a
blockBytes to 0, each fail the suite; the correct table passes 19078 assertions
across 17 cases.
One assertion from the old suite was itself wrong and is corrected here: 10-bit
planar is exactly double its 8-bit twin only for tight rows. Alignment padding
is not linear in the sample size -- at 1920 the 8-bit luma row pads 1920 -> 2048
while the 10-bit row is already 256-aligned at 3840 -- so the padded sizes are
merely ordered, and the doubling is asserted on tight rows instead.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014rZgzE8JjWvHDtaVUhxpLE
Auditing the three camera backends against the vocabulary turned up formats none
of them could describe:
V4L2 YUV411P, YUV410P, YVU410
Win32 YV16 (YVU422P), Y210, Y216, P216, Y410 (XV30), Y416 (AYUV64),
Y41P (UYYVYY411)
AVF 422YpCbCr16 (YUV422P16), 4444YpCbCrA8 (YUVA444P)
Add all thirteen, plus P416 to complete the semi-planar 16-bit set. Ten have an
exact AVPixelFormat and are bridged; three do not and say why:
YVU410P, YVU422P plane-swapped, and FFmpeg expresses those by exchanging the
U and V pointers rather than with a distinct pixel format
Y216 no AV_PIX_FMT_Y216 exists in any libav we support
YUVA444P is the first four-plane format in the table, which exercises the alpha
plane arm of bytesPerFrame that until now had no format reaching it. YUV410P is
the first with vertical subsampling 4; the suite asserted 1 or 2, so it would
have rejected a correct 4:1:0 descriptor.
XV30 deliberately reports no alpha: its top two bits are padding, not an alpha
channel, and FFmpeg agrees - the cross-check against AV_PIX_FMT_FLAG_ALPHA
passes only because of that distinction.
82 formats, no descriptor holes, no zero-size results, 52 AV twins with no
asymmetric round-trip, and no disagreement with the FFmpeg descriptor for any
twin. 22670 assertions.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014rZgzE8JjWvHDtaVUhxpLE
BayerRG8/BayerRG12 named a generic mosaic, but the CFA order decides which sensor cell a demosaic reads as red: interpreting BGGR data as RGGB swaps red and blue. V4L2 exposes the four 8-bit orders as distinct fourccs and FFmpeg has a pixel format for each, so a single generic Bayer format cannot describe what the camera is actually sending. Add the four 8-bit orders plus the two 16-bit ones FFmpeg carries, all bridged. BayerRG8/BayerRG12 stay for the Ximea path, which reports its order out of band. The FFmpeg cross-check needed a real relationship rather than an equality here: FFmpeg sets AV_PIX_FMT_FLAG_RGB on Bayer, because a mosaic is made of R/G/B filters, while we keep Bayer as its own colour model because a demosaic has to run before the samples are RGB in any usable sense. The suite now asserts "RGB-like", which is what the flag actually means, instead of silently disagreeing. 88 formats, 58 AV twins, no descriptor holes, no asymmetric round-trip, no disagreement with the FFmpeg descriptor. 24366 assertions. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014rZgzE8JjWvHDtaVUhxpLE
The V4L2 fourcc mapping existed twice: a table vendored from libavdevice in the camera enumeration, and a second one in the DMA capture backend. They had already drifted -- the deprecated V4L2_PIX_FMT_RGB32 resolved to XRGB in one and RGBX in the other, which is a red/blue swap for whichever caller was wrong. Put the layouts in interop/V4L2PixelFormat, shared by both, and derive the AVPixelFormat through the existing bridge rather than maintaining a second opinion about it. The deprecated RGB32/BGR32 pair resolves the way libavdevice has long assumed, which is also what the kernel documents. The compressed fourccs stay in the camera enumeration as a five-entry table, because MJPEG, H.264, MPEG-4 and CPIA are codecs, not pixel formats. Mixing the two axes in one table is what made the old one describe a raw layout and a compressed stream with the same row shape. Also adds chromaSwappedTwin(). The V-before-U layouts (YV12, YV16, YVU9) have no AVPixelFormat of their own, so before this the enumeration named their U-first twin and forgot, which silently exchanges red and blue. Naming the twin *and* recording the relationship is strictly more information: the enumeration keeps offering those cameras exactly as before, and a consumer that wants correct chroma now has something to ask. Verified non-regressing by construction: all 28 raw entries of the vendored table produce the same AVPixelFormat through the new path, checked programmatically rather than by reading. New guardrails assert that every V4L2-mapped format round-trips, that compressed fourccs resolve to no layout, and that a swapped layout and its twin agree on every geometric field and on frame size. 24477 assertions. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014rZgzE8JjWvHDtaVUhxpLE
bytesPerFrame computed the chroma plane dimensions as width / hsub and height / vsub. A 4:2:0 frame of 1081 rows has 541 chroma rows, not 540, so the reported size was short by a whole chroma row: YUV420P 1920x1081 3319808 reported 3321856 needed -2048 B NV12 1920x1081 -2048 B P010 1920x1081 -3840 B YUV410P 1920x1081 270 chroma rows instead of 271 A caller sizing a buffer from this and then letting a capture card or decoder write the ceiling row count overflows the allocation. Stride padding hides the analogous odd-*width* truncation, but nothing hides a missing row. This predates the vocabulary rework -- the original carried the same expression, which is why comparing the rewrite against a captured baseline could not see it: both sides truncated identically. It took mutation testing the suite to surface it, and the reason the suite could not was that bytesPerFrame was only ever asserted at 1920x1080, where both dimensions divide evenly. The odd widths in the width sweep were used by rowBytes only. The new case sweeps every format at odd widths and heights, and computes the expectation with ceiling arithmetic rather than restating the implementation. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014rZgzE8JjWvHDtaVUhxpLE
An adversarial audit of every table row against the vendor specifications found two rows whose block geometry produced row sizes the hardware does not accept. r210 pads to a 64-pixel, 256-byte row, and that padding is part of the format rather than a preference -- DeckLink documents rowBytes as ((width + 63) / 64) * 256. It was described as 1 pixel per 4 bytes with a preferred 256-byte alignment, so the tight rowBytes() was 7684 at width 1921, which is not a legal r210 row. Putting the 64/256 group in the block, exactly as v210 already does with its 48/128 group, makes every row legal by construction and leaves aligned widths unchanged. R12B and R12L claimed 8 pixels per 36 bytes. 36 bits per pixel is exactly 2 pixels per 9 bytes, which is both the finer correct granularity and what the vendor formula (width * 36) / 8 implies; the coarser block over-reported by 27 bytes at width 1922. RGB12P, the same 36-bit packing, was already 2/9 in the same table -- so one layout had two different geometries in one file. The suite had frozen both answers, which is why it agreed with each. RGB12P also declared ByteOrder::NA, in violation of the rule stated a few lines above it: 12-bit components straddle byte boundaries, and R12B/R12L are identical rows differing only in that column, so a consumer handed RGB12P could not tell which packing it held. Corrected alongside: the header called 10-bit ARGB "A2-ARGB10", which describes a 4-byte A2R10G10B10 and would have justified "fixing" the correct 5-byte row. AJA packs it as four 10-bit components including a real 10-bit alpha. The plane-count and subsampling field comments were stale (a four-plane format and subsampling of 4 both exist now), and BayerRG8/BayerRG12 are now documented as the PFNC spellings of the RGGB order, deliberately left unbridged so that no two enumerators claim one AVPixelFormat. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014rZgzE8JjWvHDtaVUhxpLE
Mutation-testing the suite with 33 injected defects caught 12 and missed 21. The
headline miss: both bridges were verified for *symmetry* and never for
*correctness*. Swapping two formats of identical geometry in both directions --
BGRA8 with RGBA8, NV12 with NV21 -- satisfies the round-trip tests, because those
only compose the two functions, and satisfies the FFmpeg descriptor cross-check
too, since such pairs agree on plane count, subsampling, planarity, alpha and
endianness. Component-order confusion is the one bug class this vocabulary exists
to prevent, and it was the class the suite could not see: a mutant that makes
every decoded frame in score red/blue-swapped kept it green.
Pin every mapping instead:
- all 58 AVPixelFormat mappings to the FFmpeg format NAME, resolved through
av_get_pix_fmt() so the expectation comes from FFmpeg rather than from a
retyped enumerator
- all 54 V4L2 mappings to the kernel constant
- plus the four one-way aliases a score->fourcc->score sweep can never reach,
including the deprecated RGB32/BGR32 pair where the two old tables disagreed
Both cases also assert that no mapping exists which the pin list omits, so adding
one without pinning it fails rather than going unverified.
Verified against the two headline mutants: each is now caught.
Also closed:
- exact counts replace floors (>= 69, >= 40) which let batches of mappings be
deleted silently -- 15 could go and the suite stayed green
- positive coverage for chromaSwappedTwin, whose sweep skipped anything
returning Unknown, so deleting all six cases passed vacuously
- rowBytes cross-checked against av_image_get_linesize for every mapped format,
which catches a wrong blockBytes mechanically; the existing rowBytes sweep
derives its expectation from blockBytes and so agrees with any value there
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014rZgzE8JjWvHDtaVUhxpLE
… 88 values Closes the last three holes the mutation audit found. The 30 formats with no AVPixelFormat had nothing verifying them. The design goal of keeping no second list of formats is what makes the other 58 checkable against FFmpeg for free, and it is also exactly what left these 30 unverifiable: mutating any field changes the code and every expectation derived from it together. They now have a golden row each, freezing colour model, plane count, subsampling, block geometry, alpha, byte order and preferred alignment. The values were audited against the vendor specifications rather than copied from the table, and the case asserts that the golden set plus the 58 mapped ones is the whole vocabulary, so a new wire-only format has to be frozen before it passes. The byte-order rule was only asserted in one direction, so a multi-byte format could silently declare NA -- and FFmpeg's descriptor cannot see that, since its flag only marks big-endian. Two attempts at deriving the rule were wrong and are worth recording: component depth does not decide it (RGB565 has 5- and 6-bit components but is packed into a 16-bit word, so order matters) and neither does bytes-per-pixel (UYVY422 is 4 bytes per 2 pixels with four whole-byte components and correctly declares NA). FFmpeg's naming convention encodes precisely the distinction, spelling an le/be pair only where the order is part of the identity, so it is the authority for the mapped formats. All 88 serialized values are now frozen as literals, up from 14. The table header promises that a renumbering breaks the build; it was true for 16% of the vocabulary. Deliberately written out rather than generated from SCORE_VIDEO_PIXEL_FORMATS, which would be a tautology. Verified by renumbering one format and confirming the build fails. 26940 assertions in 26 cases. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014rZgzE8JjWvHDtaVUhxpLE
Every YUV MEDIASUBTYPE is {fourcc, 0x0000, 0x0010, {0x80,0,0,0xaa,0,0x38,0x9b,0x71}},
so the subtype reduces to the fourcc in Data1. Putting the table in those terms
makes it portable and unit-testable on any host, which matters because the
mapping it replaces lived in Windows-only code and had gone unexercised long
enough to accumulate:
YV12 -> AV_PIX_FMT_YUV420P V before U, so red and blue were exchanged
YVU9 -> AV_PIX_FMT_YUV410P same swap at 4:1:0
V216 -> AV_PIX_FMT_Y216LE a different component order, and marked "not sure"
All three now resolve through the shared table to YVU420P, YVU410P and V216, and
the enumeration falls back to chromaSwappedTwin for the V-first layouts so those
cameras stay offered exactly as before while the swap is recorded rather than
lost. The superseded branches are removed rather than left dead and wrong; IF09,
which the shared table does not cover, keeps its own branch.
Compression is now detected from the fourcc instead of from guidToPixelFormat
happening to answer AV_PIX_FMT_YUVJ420P. That sentinel worked, but it meant a
camera genuinely offering YUVJ420P would have been taken for MJPEG, and it put a
codec on the pixel-format axis. H.264 and the three historical Motion-JPEG
spellings are recognised alongside MJPG.
The RGB subtypes are genuine SDK GUIDs rather than fourccs and keep their GUID
comparisons. ARGB1555 still drops its single alpha bit, because FFmpeg has no
5:5:5 format carrying alpha; that is now stated rather than silent.
Testable here despite being Windows data: 30 fourccs pinned by literal
four-character code, and the GUID extraction verified against the real
MEDIASUBTYPE constants for YV12, V216, MJPG and an RGB subtype, confirming the
RGB GUIDs are not mistaken for fourccs. Compiling CameraDevice.win32.cpp itself
still needs the Windows box.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014rZgzE8JjWvHDtaVUhxpLE
DRM was the second format hub: dma-buf and PipeWire reached AVPixelFormat through their own table, and PipeWire got there in two hops via DRM, so nothing connected either to the layout vocabulary the capture cards use. Add the DRM fourcc mapping next to the V4L2 and DirectShow ones, computing the fourccs from their characters so it needs neither libdrm nor libspa and is testable on any host. The mapping states memory order in every comment, because a DRM fourcc reads in machine-word order and the memory bytes are its reverse: DRM_ARGB8888 is B,G,R,A in memory, so it is BGRA8, not ARGB8. That inversion is the most common source of red/blue swaps on the dma-buf path, and the suite asserts the direction explicitly rather than relying on a round-trip, which would accept either reading. Format modifiers are deliberately absent. Tiling and compression are not part of a pixel format -- the same fourcc arrives linear or tiled -- and folding one in would repeat the mistake of baking stride alignment into a descriptor. Callers pass modifiers alongside. Two rows were needed to make the DRM path lossless rather than dropping to Unknown: X2RGB10 and X2BGR10, the ARGB2101010/ABGR2101010 layouts, whose top two bits are padding rather than alpha. Adding those two immediately failed the exact-count and pin-completeness guardrails added earlier, which is what they are for: the new formats could not pass until they were pinned and frozen. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014rZgzE8JjWvHDtaVUhxpLE
Spout hands over a D3D11 texture, Syphon an IOSurface, dma-buf import an
EGLImage. None of those is a CPU buffer, so forcing them through the buffer
vocabulary is a category error -- the same mistake as baking stride alignment
into a format descriptor. What they share is a third axis: what a GPU texture
holds.
Map between the two, and be explicit that it is not a bijection:
- a planar layout needs one texture per plane, so the answer is per plane; a
semi-planar chroma plane is RG8/RG16, a fully planar one R8/R16
- a packed YUV layout samples as a wider RGBA8 texture and is unpacked in a
shader, because QRhi has no YUV format. Its texture format therefore says
nothing about its colour model, and UYVY422 is 960 texels wide for a
1920-pixel row
- the wire-only layouts (v210, r210, DPX, 12-bit packed) have no texture at
all: they must be decoded first, and answer UnknownFormat
- the reverse direction only covers the unambiguous RGB formats, since a
shader-unpacked YUV texture is RGBA8 like any other
Chroma plane dimensions round up, matching the buffer arithmetic, so an odd
raster does not lose its last chroma row or column here either.
Kept in its own header so the vocabulary stays free of Qt and RHI includes,
exactly as the libav bridge is kept separate.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014rZgzE8JjWvHDtaVUhxpLE
Sh4lt and Shmdata hand over shared-memory *buffers*, not streams, and resolve their caps through Video::gstreamerToLibav() to an AVPixelFormat -- after which a format the decode path does not take natively is CPU-converted to RGBA. Removing that conversion needs the layout and a stride, which is what a VideoPixelFormat is, so they need an edge into the vocabulary. The rest of the 54-entry gst map stays AV-first, as agreed: most of what arrives over GStreamer is a stream format and the decode pipeline is AV-based. This table covers the buffer layouts only. It is a direct table rather than the existing map composed with the libav bridge, and that is the point. Routing through AVPixelFormat loses the V-before-U layouts, because FFmpeg has no pixel format for them: "YV12" would arrive as YUV420P with red and blue exchanged, and "YVU9" likewise. The suite asserts that YV12 and I420 do not collapse onto each other and that the swap is recorded. This is the mapping, not the rewiring. Pointing Sh4lt and Shmdata at host-import instead of the rescaler is the follow-up, and it wants those transports actually running to validate, which needs a producer this machine does not have. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014rZgzE8JjWvHDtaVUhxpLE
The V4L2 round-trip case was inside #if defined(__linux__), but the pinned- constants case added alongside it was not, so every V4L2_PIX_FMT_* reference in it broke the build anywhere without linux/videodev2.h. Found by compiling on Windows, which is the only thing that could have found it: the Linux build is green either way, and nothing in review flags a missing guard on a block that sits next to a guarded one. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014rZgzE8JjWvHDtaVUhxpLE
jcelerier
force-pushed
the
split/gfx-fixes-and-formats
branch
from
August 17, 2026 19:04
6c3686a to
f5e872d
Compare
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.
Six self-contained changes lifted out of the
plane/interopstack (#2121) so theycan land without waiting for it. Each applies to current
masterwith no conflictsand no dependency on the rest of the stack — verified by cherry-picking them onto
masterindividually and as a sequence.Fixes
dma-buf import zero-copy. Sets
QT_XCB_GL_INTEGRATIONonly when the user has notalready chosen, so an explicit override sticks.
half-built instance on failure instead of nothing.
degrade when the backend was unavailable, it crashed. Turns a segfault under a
headless/offscreen QPA into a clean skip.
quantum — appsink classification was guessed rather than read from the pipeline.
Video pixel format vocabulary
A vendor-neutral
VideoPixelFormatenum plus a libav bridge(
VideoPixelFormatAV). All new files; adds itself toCMakeLists.txt. This is thecommon vocabulary the capture/encode paths in the stack are written against, and it
is useful on its own — it is what lets a wire format be named once rather than
re-derived per vendor.
Validation
master(both individually and as a sequence).master's headers, with thereal compile flags from
compile_commands.json(PCH stripped, include pathsretargeted).
master— that is what CI is for here.The encoders and decoders were deliberately not included: they depend on
pland's render-pipeline rework and onplane's texture-interop layer, so theycannot be split off without those. They stay in the stack's bottom-up order.
🤖 Generated with Claude Code
https://claude.ai/code/session_014rZgzE8JjWvHDtaVUhxpLE