CIColorCube sometimes producing no or broken output in macOS 13

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?

  • This was also filed as FB11736373, together with a demo project.

Add a Comment

Accepted Reply

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

Replies

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