tl;dr how can I get raw YUV in a Metal fragment shader from a VideoToolbox 10-bit/BT.2020 HEVC stream without any extra/secret format conversions?
With VideoToolbox and 10-bit HEVC, I've found that it defaults to CVPixelBuffers w/ formats kCVPixelFormatType_Lossless_420YpCbCr10PackedBiPlanarFullRange or kCVPixelFormatType_Lossy_420YpCbCr10PackedBiPlanarFullRange. To mitigate this, I have the following snippet of code to my application:
// We need our pixels unpacked for 10-bit so that the Metal textures actually work
var pixelFormat:OSType? = nil
let bpc = getBpcForVideoFormat(videoFormat!)
let isFullRange = getIsFullRangeForVideoFormat(videoFormat!)
// TODO: figure out how to check for 422/444, CVImageBufferChromaLocationBottomField?
if bpc == 10 {
pixelFormat = isFullRange ? kCVPixelFormatType_420YpCbCr10BiPlanarFullRange : kCVPixelFormatType_420YpCbCr10BiPlanarVideoRange
}
let videoDecoderSpecification:[NSString: AnyObject] = [kVTVideoDecoderSpecification_EnableHardwareAcceleratedVideoDecoder:kCFBooleanTrue]
var destinationImageBufferAttributes:[NSString: AnyObject] = [kCVPixelBufferMetalCompatibilityKey: true as NSNumber, kCVPixelBufferPoolMinimumBufferCountKey: 3 as NSNumber]
if pixelFormat != nil {
destinationImageBufferAttributes[kCVPixelBufferPixelFormatTypeKey] = pixelFormat! as NSNumber
}
var decompressionSession:VTDecompressionSession? = nil
err = VTDecompressionSessionCreate(allocator: nil, formatDescription: videoFormat!, decoderSpecification: videoDecoderSpecification as CFDictionary, imageBufferAttributes: destinationImageBufferAttributes as CFDictionary, outputCallback: nil, decompressionSessionOut: &decompressionSession)
In short, I need kCVPixelFormatType_420YpCbCr10BiPlanar so that I have a straightforward MTLPixelFormat.r16Unorm/MTLPixelFormat.rg16Unorm texture binding for Y/CbCr. Metal, seemingly, has no direct pixel format for 420YpCbCr10PackedBiPlanar. I'd also rather not use any color conversion in VideoToolbox, in order to save on processing (and to ensure that the color transforms/transfer characteristics match between streamer/client, since I also have a custom transfer characteristic to mitigate blocking in dark scenes).
However, I noticed that in visionOS 2, the CVPixelBuffer I receive is no longer a compressed render target (likely a bug), which caused GPU texture read bandwidth to skyrocket from 2GiB/s to 30GiB/s. More importantly, this implies that VideoToolbox may in fact be doing an extra color conversion step, wasting memory bandwidth.
Does Metal actually have no way to handle 420YpCbCr10PackedBiPlanar? Are there any examples for reading 10-bit HDR HEVC buffers directly with Metal?
There is no public Metal pixel format for the packed 10-bit biplanar layout, which is the conclusion you reached. The reason is documented in CVPixelBuffer.h rather than on the reference pages.
The unpacked and packed formats are described differently there. kCVPixelFormatType_420YpCbCr10BiPlanarVideoRange (x420) reads "2 plane YCbCr10 4:2:0, each 10 bits in the MSBs of 16bits, video-range (luma=[64,940] chroma=[64,960])". The packed variants read "Format is compressed-packed with no padding bits between pixels", and that sentence appears on &xv0, &xv2, &xf0, -xv0 and -xv2.
So x420 gives every sample its own 16-bit container, which is what .r16Unorm and .rg16Unorm read. A Metal format that reads one 16-bit unit per sample does not match a layout documented as having no padding between pixels.
There is also no public single-channel or two-channel 10-bit MTLPixelFormat to bind against. The complete set of R and RG formats is 8, 16 and 32 bits per component. The enum declares no YUV formats beyond GBGR422 and BGRG422, both of which are 8-bit 4:2:2 interleaved.
CVMetalTextureCacheCreateTextureFromImage (https://developer.apple.com/documentation/corevideo/cvmetaltexturecachecreatetexturefromimage(::::::::_:)) is explicit that it offers no format contract: "Core Video doesn't explicitly declare any pixel format types as Metal compatible", and "You're responsible for ensuring the pixel format is appropriate to the buffer".
Each compressed constant's headerdoc also names the uncompressed format it corresponds to. &xf0 is documented as the "Lossless-compressed form of kCVPixelFormatType_420YpCbCr10BiPlanarFullRange", and that constant is xf20, described as "2 plane YCbCr10 4:2:0, each 10 bits in the MSBs of 16bits, full-range (Y range 0-1023)". So for a full-range packed source the range-matched unpacked counterpart is xf20, not x420.
If you need direct sampling of these formats, Feedback Assistant is where to request it: https://developer.apple.com/feedback-assistant/