With macOS 13, the CIColorCube
and CIColorCubeWithColorSpace
filters gained the extrapolate
property for supporting EDR content.
When setting this property, we observe that the outputImage
of the filter sometimes (~1 in 3 tries) just returns nil
. And sometimes it “just” causes artifacts to appear when rendering EDR content (see screenshot below). The artifacts even appear sometimes when extrapolate
was not set.
input | correct output | broken output
This was reproduced on Intel-based and M1 Macs.
All of our LUT-based filters in our apps are broken in this way and we could not find a workaround for the issue so far. Does anyone experice the same?
It turns out the problem was caused by how we loaded the cube data. Previously, we did it like this:
let cubeImage: CGImage = ...
// render cube image into a 32-bit float context, since that's the data format needed by CIColorCube
let pixelData = UnsafeMutablePointer<simd_float4>.allocate(capacity: cubeImage.width * cubeImage.height)
let bitmapInfo = CGImageAlphaInfo.premultipliedLast.rawValue | CGBitmapInfo.floatComponents.rawValue | CGBitmapInfo.byteOrder32Little.rawValue
let colorSpace = cubeImage.colorSpace ?? CGColorSpace.sRGBColorSpace
guard let bitmapContext = CGContext(data: pixelData,
width: cubeImage.width,
height: cubeImage.height,
bitsPerComponent: MemoryLayout<simd_float4.Scalar>.size * 8,
bytesPerRow: MemoryLayout<simd_float4>.size * cubeImage.width,
space: colorSpace,
bitmapInfo: bitmapInfo)
else {
assertionFailure("Failed to create bitmap context for conversion")
}
bitmapContext.draw(cubeImage, in: CGRect(x: 0, y: 0, width: cubeImage.width, height: cubeImage.height))
let data = Data(bytesNoCopy: pixelData, count: bitmapContext.bytesPerRow * bitmapContext.height, deallocator: .free)
// pass data to filter
Note that we pre-allocated the pixelData
buffer and gave it to the CGContext
to render the cube image into it. It seems that data was corrupted or released too early in some cases, causing the erroneous behavior described above, even though we assumed that Data(bytesNoCopy:...)
would take ownership of the data.
To fix this, we let CGContext
create its own buffer and copy the cube data after the draw
:
let cubeImage: CGImage = ...
// render cube image into a 32-bit float context, since that's the data format needed by CIColorCube
let bitmapInfo = CGImageAlphaInfo.premultipliedLast.rawValue | CGBitmapInfo.floatComponents.rawValue | CGBitmapInfo.byteOrder32Little.rawValue
let colorSpace = cubeImage.colorSpace ?? CGColorSpace.sRGBColorSpace
guard let bitmapContext = CGContext(data: nil,
width: cubeImage.width,
height: cubeImage.height,
bitsPerComponent: MemoryLayout<simd_float4.Scalar>.size * 8,
bytesPerRow: MemoryLayout<simd_float4>.size * cubeImage.width,
space: colorSpace,
bitmapInfo: bitmapInfo)
else {
assertionFailure("Failed to create bitmap context for conversion")
}
bitmapContext.draw(cubeImage, in: CGRect(x: 0, y: 0, width: cubeImage.width, height: cubeImage.height))
guard let pixelData = bitmapContext.data else {
assertionFailure("Failed to get cube data")
}
let data = Data(bytes: pixelData, count: bitmapContext.bytesPerRow * bitmapContext.height)
// pass data to filter