diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..d61aab4 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,21 @@ +name: Test Swift Package + +on: + pull_request: + branches: [main] + +jobs: + test: + runs-on: macos-latest + + steps: + - uses: actions/checkout@v3 + - uses: swift-actions/setup-swift@v2 + with: + swift-version: '5.9' + + - name: Build + run: swift build + + - name: Run Tests + run: swift test \ No newline at end of file diff --git a/.spi.yml b/.spi.yml new file mode 100644 index 0000000..9e5f5e1 --- /dev/null +++ b/.spi.yml @@ -0,0 +1,5 @@ +version: 1 +builder: + configs: + - documentation_targets: [CoreVideoTools] + scheme: CoreVideoTools \ No newline at end of file diff --git a/LICENSE b/LICENSE index 6631fc8..24d7c8a 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2022 Eugene Bokhan +Copyright (c) 2024 Eugene Bokhan Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/Package.swift b/Package.swift index 4b4482e..49e303d 100644 --- a/Package.swift +++ b/Package.swift @@ -1,11 +1,11 @@ -// swift-tools-version: 5.4 +// swift-tools-version: 5.9 import PackageDescription let package = Package( name: "core-video-tools", platforms: [ - .iOS(.v11), + .iOS(.v12), .macOS(.v10_13) ], products: [ @@ -15,6 +15,10 @@ let package = Package( ) ], targets: [ - .target(name: "CoreVideoTools") + .target(name: "CoreVideoTools"), + .testTarget( + name: "CoreVideoToolsTests", + dependencies: ["CoreVideoTools"] + ) ] ) diff --git a/README.md b/README.md index 5385ba8..d4f32c8 100644 --- a/README.md +++ b/README.md @@ -1,56 +1,89 @@ -# core-video-tools +# CoreVideoTools -A set of extensions and utilities to work with [CoreVideo](https://developer.apple.com/documentation/corevideo?language=objc) types. +[![Platform Compatibility](https://img.shields.io/badge/Platforms-iOS%20|%20macOS-brightgreen)](https://swift.org/platforms/) +[![Swift Version](https://img.shields.io/badge/Swift-5.9-orange)](https://swift.org) -## CVPixelFormat +

+ +

-While debuging `Core Video` objects, you need to understand what pixel format is used in them. -To do this using vanilla API you are forced to find a mathing `OSType` value, because `OSType` if basically a number. -This project uses [`CVPixelFormat`](Sources/CoreVideoTools/CVPixelFormat.swift) enum istead of vanilla `OSType` public vars which is much more handy, and you can easyly get a `description` of a current pixel format. +## Overview -```swift -let cvPixelFormat: CVPixelFormat = cvPixelBuffer.cvPixelFormat -let description = cvPixelFormat.description -``` +CoreVideoTools offers a more idiomatic Swift interface to `CoreVideo` functionality, making it easier and safer to work with `CVPixelBuffers`, `IOSurfaces`, and related `CoreVideo` concepts in Swift code. + +Please see [the package's documentation](https://swiftpackageindex.com/computer-graphics-tools/core-video-tools/documentation/corevideotools) +for more detailed usage instructions. -## Swifty API +## CVPixelBuffer There are a lot Swift wrappers over vanilla CVPixelBuffer C-style API: -**Vanilla API:** +**Swifty API:** + ```swift -let width = CVPixelBufferGetWidth(cvPixelBuffer) -let height = CVPixelBufferGetHeight(cvPixelBuffer) +let width = pixelBuffer.width +let height = pixelBuffer.height +let format = pixelBuffer.cvPixelFormat +let bytesPerRow = pixelBuffer.bytesPerRow +``` -// ... +**Convenience Init:** -let bytesPerElement = IOSurfaceGetBytesPerRow(ioSurface) -let bytesPerRow = IOSurfaceGetBytesPerRow(ioSurface) +```swift +let pixelBuffer = try CVPixelBuffer.create( + width: 1920, + height: 1080, + cvPixelFormat: .type_32BGRA +) ``` -**Swifty API:** +Check out more examples in the [Working With CVPixelBuffer](Sources/CoreVideoTools/CoreVideoTools.docc/WorkingWithCVPixelBuffer.md). + +## IOSurface + +**Convenience Init:** + ```swift -let width = cvPixelBuffer.width -let height = cvPixelBuffer.height +let surface = try IOSurface.create( + width: 1920, + height: 1080, + cvPixelFormat: .type_32BGRA, + bytesPerElement: 4, + bytesPerRow: 1920 * 4 +) +``` + +For more detail, please checkout [Working With IOSurface](Sources/CoreVideoTools/CoreVideoTools.docc/WorkingWithIOSurface.md). -// ... +## CVPixelFormat + +While debuging `Core Video` objects, you need to understand what pixel format is used in them. +To do this using vanilla API you are forced to find a matching `OSType` value, because `OSType` if basically a number. +This project uses [`CVPixelFormat`](Sources/CoreVideoTools/CVPixelFormat.swift) enum istead of vanilla `OSType` public vars which is much more handy, and you can easily get a `description` of a current pixel format. -let bytesPerElement = ioSurface.bytesPerElement -let bytesPerRow = ioSurface.bytesPerRow +```swift +let cvPixelFormat: CVPixelFormat = cvPixelBuffer.cvPixelFormat +let description = cvPixelFormat.description ``` ## CVReturn Result & CVError -There are some functions in Core Video that return a code which helps if the operation succeeded. +There are some functions in Core Video that return a code which helps if the operation succeeded. This project aims to simplify this error checking. `CVReturn` [`Result`](Sources/CoreVideoTools/Extensions/CoreVideo/CVReturn/CVReturn+Result.swift) and [`CVError`](Sources/CoreVideoTools/CVError.swift) types are used to wrap vanilla API with thowable functions. **Vanilla API:** + ```swift let returnCode = CVPixelBufferLockBaseAddress(cvPixelBuffer, lockFlags) -guard returnCode == kCVReturnSuccess else { // handle the error ... +guard returnCode == kCVReturnSuccess else { /* handle the error ... */ } ``` **Swifty API:** + ```swift try cvPixelBuffer.lockBaseAddress(lockFlags: lockFlags) ``` + +## License + +MetalTools is licensed under [MIT license](LICENSE). diff --git a/Sources/CoreVideoTools/CVError.swift b/Sources/CoreVideoTools/CVError.swift index ffc13d8..7c6cf63 100644 --- a/Sources/CoreVideoTools/CVError.swift +++ b/Sources/CoreVideoTools/CVError.swift @@ -1,6 +1,4 @@ -import CoreVideo - -/// CoreVideo specific error codes +/// Represents CoreVideo specific error codes. public enum CVError: Error, Equatable { /// An otherwise undefined error occurred. case error @@ -8,6 +6,7 @@ public enum CVError: Error, Equatable { case invalidArgument /// The allocation for a buffer or buffer pool failed. Most likely because of lack of resources. case allocationFailed + /// The operation or feature is not supported. case unsupported // DisplayLink related errors @@ -23,15 +22,15 @@ public enum CVError: Error, Equatable { // Buffer related errors - /// The requested pixelformat is not supported for the CVBuffer type. + /// The requested pixel format is not supported for the CVBuffer type. case invalidPixelFormat /// The requested size (most likely too big) is not supported for the CVBuffer type. case invalidSize /// A CVBuffer cannot be created with the given attributes. case invalidPixelBufferAttributes - /// The Buffer cannot be used with OpenGL as either its size, pixelformat or attributes are not supported by OpenGL. + /// The Buffer cannot be used with OpenGL as either its size, pixel format or attributes are not supported by OpenGL. case pixelBufferNotOpenGLCompatible - /// The Buffer cannot be used with Metal as either its size, pixelformat or attributes are not supported by Metal. + /// The Buffer cannot be used with Metal as either its size, pixel format or attributes are not supported by Metal. case pixelBufferNotMetalCompatible // Buffer Pool related errors @@ -45,8 +44,9 @@ public enum CVError: Error, Equatable { /// A scan hasn't completely traversed the CVBufferPool due to a concurrent operation. The client can retry the scan. case retry - /// Initialize `CVError` value. - /// - Parameter rawValue: vanilla `CVReturn` value. + /// Initializes a `CVError` value from a raw `CVReturn` value. + /// - Parameter rawValue: The vanilla `CVReturn` value. + /// - Returns: The corresponding `CVError` instance. public init(rawValue: CVReturn) { switch rawValue { case kCVReturnInvalidArgument: self = .invalidArgument @@ -69,7 +69,7 @@ public enum CVError: Error, Equatable { } } - /// Vanilla `CVReturn` value. + /// The raw `CVReturn` value corresponding to this `CVError`. public var rawValue: CVReturn { switch self { case .error: return kCVReturnError diff --git a/Sources/CoreVideoTools/CVPixelFormat.swift b/Sources/CoreVideoTools/CVPixelFormat.swift index c393a49..c8b81da 100644 --- a/Sources/CoreVideoTools/CVPixelFormat.swift +++ b/Sources/CoreVideoTools/CVPixelFormat.swift @@ -1,7 +1,6 @@ -@_exported import CoreVideo -@_exported import IOSurface import Foundation +/// Represents various pixel format types used in Core Video. public enum CVPixelFormat: CustomStringConvertible, CaseIterable, Codable { /// 1 bit indexed case type_1Monochrome @@ -215,9 +214,10 @@ public enum CVPixelFormat: CustomStringConvertible, CaseIterable, Codable { /// Format is compressed-packed with no padding bits between pixels. case type_Lossy_422YpCbCr10PackedBiPlanarVideoRange case unknown - - /// Initialize `CVPixelFormat`. - /// - Parameter rawValue: vanilla `OSType` value. + + /// Initializes a `CVPixelFormat` instance from a raw `OSType` value. + /// - Parameter rawValue: The raw `OSType` value representing a pixel format. + /// - Returns: The corresponding `CVPixelFormat` instance, or `.unknown` if the value is not recognized. public init(rawValue: OSType) { switch rawValue { case kCVPixelFormatType_1Monochrome: self = .type_1Monochrome @@ -315,7 +315,7 @@ public enum CVPixelFormat: CustomStringConvertible, CaseIterable, Codable { } } - /// Vanilla `OSType` value. + /// The raw `OSType` value corresponding to this pixel format. public var rawValue: OSType { switch self { case .type_1Monochrome: return kCVPixelFormatType_1Monochrome @@ -413,7 +413,7 @@ public enum CVPixelFormat: CustomStringConvertible, CaseIterable, Codable { } } - /// The description of the pixel format. + /// A human-readable description of the pixel format. public var description: String { switch self { case .type_1Monochrome: return "1Monochrome" diff --git a/Sources/CoreVideoTools/CoreVideoTools.docc/CoreVideoTools.md b/Sources/CoreVideoTools/CoreVideoTools.docc/CoreVideoTools.md new file mode 100644 index 0000000..208590a --- /dev/null +++ b/Sources/CoreVideoTools/CoreVideoTools.docc/CoreVideoTools.md @@ -0,0 +1,39 @@ +# ``CoreVideoTools`` + +![CoreVideoTools](core-video-tools.png) + +CoreVideo Tools offers a more idiomatic Swift interface to CoreVideo functionality, making it easier and safer to work with pixel buffers, IOSurfaces, and related CoreVideo concepts in Swift code. + +## Topics + +### Articles + +- +- + +### CVPixelBuffer Extensions + +- ``CoreVideo/CVBuffer/width`` +- ``CoreVideo/CVBuffer/height`` +- ``CoreVideo/CVBuffer/cvPixelFormat`` +- ``CoreVideo/CVBuffer/baseAddress`` +- ``CoreVideo/CVBuffer/bytesPerRow`` +- ``CoreVideo/CVBuffer/lockBaseAddress(lockFlags:)`` +- ``CoreVideo/CVBuffer/unlockBaseAddress(unlockFlags:)`` +- ``CoreVideo/CVBuffer/create(width:height:cvPixelFormat:attachments:allocator:)`` +- ``CoreVideo/CVBuffer/blankCopy()`` +- ``CoreVideo/CVBuffer/deepCopy()`` + +### IOSurface Extensions + +- ``IOSurface/IOSurface/CacheMode`` +- ``IOSurface/IOSurface/create(width:height:cvPixelFormat:bytesPerElement:bytesPerRow:cacheMode:additionalProperties:)`` + +### Error Handling + +- ``CVError`` +- ``Swift/Int32/Result`` + +### Pixel Formats + +- ``CVPixelFormat`` diff --git a/Sources/CoreVideoTools/CoreVideoTools.docc/Resources/table-of-contents-art/core-video-tools@2x.png b/Sources/CoreVideoTools/CoreVideoTools.docc/Resources/table-of-contents-art/core-video-tools@2x.png new file mode 100644 index 0000000..1c490bd Binary files /dev/null and b/Sources/CoreVideoTools/CoreVideoTools.docc/Resources/table-of-contents-art/core-video-tools@2x.png differ diff --git a/Sources/CoreVideoTools/CoreVideoTools.docc/WorkingWithCVPixelBuffer.md b/Sources/CoreVideoTools/CoreVideoTools.docc/WorkingWithCVPixelBuffer.md new file mode 100644 index 0000000..4fbaa1b --- /dev/null +++ b/Sources/CoreVideoTools/CoreVideoTools.docc/WorkingWithCVPixelBuffer.md @@ -0,0 +1,122 @@ +# Working with CVPixelBuffer + +Learn how to create, manipulate, and access pixel buffer data using the CoreVideoTools package. + +## Overview + +`CVPixelBuffer` is a fundamental type in CoreVideo that represents an image buffer. CoreVideoTools provides extensions to `CVPixelBuffer` that make it easier to work with in Swift. This guide will walk you through common operations and best practices when working with `CVPixelBuffer` in your Swift projects. + +## Creating a CVPixelBuffer + +You can create a new `CVPixelBuffer` using the `create(width:height:cvPixelFormat:attachments:allocator:)` method: + +```swift +do { + let pixelBuffer = try CVPixelBuffer.create( + width: 1920, + height: 1080, + cvPixelFormat: .type_32BGRA + ) + print("Created pixel buffer with dimensions: \(pixelBuffer.width) x \(pixelBuffer.height)") +} catch { + print("Failed to create pixel buffer: \(error)") +} +``` + +## Accessing Pixel Buffer Properties + +CoreVideoTools provides convenient properties to access pixel buffer information: + +```swift +let width = pixelBuffer.width +let height = pixelBuffer.height +let format = pixelBuffer.cvPixelFormat +let bytesPerRow = pixelBuffer.bytesPerRow + +print("Pixel Buffer: \(width)x\(height), Format: \(format), Bytes per row: \(bytesPerRow)") +``` + +## Locking and Unlocking + +Before accessing the pixel buffer's data, you need to lock it. Always remember to unlock when you're done: + +```swift +do { + try pixelBuffer.lockBaseAddress(lockFlags: .readOnly) + defer { + try? pixelBuffer.unlockBaseAddress(unlockFlags: .readOnly) + } + + // Access pixel data here + if let baseAddress = pixelBuffer.baseAddress { + // Work with the pixel data + } +} catch { + print("Failed to lock pixel buffer: \(error)") +} +``` + +## Copying Pixel Buffers + +CoreVideoTools provides methods to create copies of pixel buffers: + +```swift +do { + // Create an empty copy with the same parameters + let blankCopy = try pixelBuffer.blankCopy() + + // Create a deep copy with the same content + let deepCopy = try pixelBuffer.deepCopy() +} catch { + print("Failed to copy pixel buffer: \(error)") +} +``` + +## Accessing Pixel Data + +You can access the raw pixel data as a `Data` object: + +```swift +do { + try pixelBuffer.lockBaseAddress(lockFlags: .readOnly) + defer { + try? pixelBuffer.unlockBaseAddress(unlockFlags: .readOnly) + } + + if let pixelData = pixelBuffer.data() { + // Work with pixelData + print("Pixel buffer data size: \(pixelData.count) bytes") + } +} catch { + print("Failed to access pixel data: \(error)") +} +``` + +## Working with Planar Pixel Buffers + +For planar pixel formats, you can access individual planes: + +```swift +let planeCount = pixelBuffer.planeCount + +for plane in 0.. IOSurface { + var props: [IOSurfacePropertyKey: Any] = [ + .width: width, + .height: height, + .pixelFormat: cvPixelFormat.rawValue, + .bytesPerElement: bytesPerElement, + .bytesPerRow: bytesPerRow, + .cacheMode: cacheMode.rawValue + ] + + if let additionalProperties { + props.merge(additionalProperties) { (current, _) in current } + } + + guard let ioSurface = IOSurface(properties: props) + else { throw CVError.error } + + return ioSurface + } + +} diff --git a/Sources/CoreVideoTools/Extensions/CoreVideo/IOSurfaceRef/IOSurfaceRef+Swift.swift b/Sources/CoreVideoTools/Extensions/CoreVideo/IOSurfaceRef/IOSurfaceRef+Swift.swift deleted file mode 100644 index 2d80a61..0000000 --- a/Sources/CoreVideoTools/Extensions/CoreVideo/IOSurfaceRef/IOSurfaceRef+Swift.swift +++ /dev/null @@ -1,473 +0,0 @@ -import Foundation - -public extension IOSurfaceRef { - - enum PropertyKey { - /// `CFNumber` of the total allocation size of the buffer including all planes. - /// - /// Defaults to BufferHeight. BytesPerRow if not specified. Must be specified for dimensionless buffers. - @available(iOS 11.0, macOS 10.6, macCatalyst 13.1, *) - case allocSize - - /// `CFNumber` for the width of the `IOSurface` buffer in pixels. - /// - /// Required for planar `IOSurfaces`. - @available(iOS 11.0, macOS 10.6, macCatalyst 13.1, *) - case width - - /// `CFNumber` for the height of the `IOSurface` buffer in pixels. - /// - /// Required for planar `IOSurfaces`. - @available(iOS 11.0, macOS 10.6, macCatalyst 13.1, *) - case height - - /// `CFNumber` for the bytes per row of the buffer. - /// - /// If not specified, `IOSurface` will first calculate the number full elements required on each row (by rounding up), - /// multiplied by the bytes per element for this buffer. - /// That value will then be appropriately aligned. - @available(iOS 11.0, macOS 10.6, macCatalyst 13.1, *) - case bytesPerRow - - /// `CFNumber` for the total number of bytes in an element. - /// - /// Default to 1. - @available(iOS 11.0, macOS 10.6, macCatalyst 13.1, *) - case bytesPerElement - - /// `CFNumber` for how many pixels wide each element is. - /// - /// Defaults to 1. - @available(iOS 11.0, macOS 10.6, macCatalyst 13.1, *) - case elementWidth - - /// `CFNumber` for how many pixels high each element is. - /// - /// Defaults to 1. - @available(iOS 11.0, macOS 10.6, macCatalyst 13.1, *) - case elementHeight - - /// `CFNumber` for the starting offset into the buffer. - /// - /// Defaults to 0. - @available(iOS 11.0, macOS 10.6, macCatalyst 13.1, *) - case offset - - /// `CFArray` describing each image plane in the buffer as a `CFDictionary`. - /// - /// The `CFArray` must have at least one entry. - @available(iOS 11.0, macOS 10.6, macCatalyst 13.1, *) - case planeInfo - - /// `CFNumber` for the width of this plane in pixels. - /// - /// Required for image planes. - @available(iOS 11.0, macOS 10.6, macCatalyst 13.1, *) - case planeWidth - - /// `CFNumber` for the height of this plane in pixels. - /// - /// Required for image planes. - @available(iOS 11.0, macOS 10.6, macCatalyst 13.1, *) - case planeHeight - - /// `CFNumber` for the bytes per row of this plane. - /// - /// If not specified, `IOSurface` will first calculate the number full elements required on each row (by rounding up), - /// multiplied by the bytes per element for this plane. - /// That value will then be appropriately aligned. - @available(iOS 11.0, macOS 10.6, macCatalyst 13.1, *) - case planeBytesPerRow - - /// `CFNumber` for the offset into the buffer for this plane. - /// - /// If not specified then `IOSurface` will lay out each plane sequentially based on the previous plane's allocation size. - @available(iOS 11.0, macOS 10.6, macCatalyst 13.1, *) - case planeOffset - - /// `CFNumber` for the total data size of this plane. - /// - /// Defaults to plane height * plane bytes per row if not specified. - @available(iOS 11.0, macOS 10.6, macCatalyst 13.1, *) - case planeSize - - /// `CFNumber` for the base offset into the buffer for this plane. - /// - /// Optional, defaults to the plane offset. - @available(iOS 11.0, macOS 10.6, macCatalyst 13.1, *) - case planeBase - - /// `CFNumber` for the bits per element of this plane. - /// - /// Optional, default is 1. - /// For use in cases where `.planeBytesPerElement` doesn't allow sufficient precision. - @available(iOS 11.0, macOS 10.6, macCatalyst 13.1, *) - case planeBitsPerElement - - /// `CFNumber` for the bytes per element of this plane. - /// - /// Optional, default is 1. - @available(iOS 11.0, macOS 10.6, macCatalyst 13.1, *) - case planeBytesPerElement - - /// `CFNumber` for the element width of this plane. - /// - /// Optional, default is 1. - @available(iOS 11.0, macOS 10.6, macCatalyst 13.1, *) - case planeElementWidth - - /// `CFNumber` for the element height of this plane. - /// - /// Optional, default is 1. - @available(iOS 11.0, macOS 10.6, macCatalyst 13.1, *) - case planeElementHeight - - /// `CFNumber` for the CPU cache mode to be used for the allocation. - /// - /// Default is kIOMapDefaultCache. - @available(iOS 11.0, macOS 10.6, macCatalyst 13.1, *) - case cacheMode - - /// A 32-bit unsigned integer that stores the traditional Mac OS X buffer format. - @available(iOS 11.0, macOS 10.6, macCatalyst 13.1, *) - case pixelFormat - - /// If false the creator promises that there will be no pixel size casting when used on the GPU. Default is true. - @available(iOS 11.0, macOS 10.12, macCatalyst 13.1, *) - case pixelSizeCastingAllowed - - /// `CFArray[CFNumber]` for bit depth of each component in this plane. - @available(iOS 11.0, macOS 10.13, macCatalyst 13.1, *) - case planeComponentBitDepths - - /// `CFArray[CFNumber]` for bit offset of each component in this plane, (low bit zero, high bit 7). For example 'BGRA' would be {0, 8, 16, 24} - @available(iOS 11.0, macOS 10.13, macCatalyst 13.1, *) - case planeComponentBitOffsets - case unknown - - public var rawValue: CFString { - switch self { - case .allocSize: return kIOSurfaceAllocSize - case .width: return kIOSurfaceWidth - case .height: return kIOSurfaceHeight - case .bytesPerRow: return kIOSurfaceBytesPerRow - case .bytesPerElement: return kIOSurfaceBytesPerElement - case .elementWidth: return kIOSurfaceElementWidth - case .elementHeight: return kIOSurfaceElementHeight - case .offset: return kIOSurfaceOffset - case .planeInfo: return kIOSurfacePlaneInfo - case .planeWidth: return kIOSurfacePlaneWidth - case .planeHeight: return kIOSurfacePlaneHeight - case .planeBytesPerRow: return kIOSurfacePlaneBytesPerRow - case .planeOffset: return kIOSurfacePlaneOffset - case .planeSize: return kIOSurfacePlaneSize - case .planeBase: return kIOSurfacePlaneBase - case .planeBitsPerElement: return kIOSurfacePlaneBitsPerElement - case .planeBytesPerElement: return kIOSurfacePlaneBytesPerElement - case .planeElementWidth: return kIOSurfacePlaneElementWidth - case .planeElementHeight: return kIOSurfacePlaneElementHeight - case .cacheMode: return kIOSurfaceCacheMode - case .pixelFormat: return kIOSurfacePixelFormat - case .pixelSizeCastingAllowed: return kIOSurfacePixelSizeCastingAllowed - case .planeComponentBitDepths: return kIOSurfacePlaneComponentBitDepths - case .planeComponentBitOffsets: return kIOSurfacePlaneComponentBitOffsets - case .unknown: return "" as CFString - } - } - - init(rawValue: CFString) { - switch rawValue { - case kIOSurfaceAllocSize: self = .allocSize - case kIOSurfaceWidth: self = .width - case kIOSurfaceHeight: self = .height - case kIOSurfaceBytesPerRow: self = .bytesPerRow - case kIOSurfaceBytesPerElement: self = .bytesPerElement - case kIOSurfaceElementWidth: self = .elementWidth - case kIOSurfaceElementHeight: self = .elementHeight - case kIOSurfaceOffset: self = .offset - case kIOSurfacePlaneInfo: self = .planeInfo - case kIOSurfacePlaneWidth: self = .planeWidth - case kIOSurfacePlaneHeight: self = .planeHeight - case kIOSurfacePlaneBytesPerRow: self = .planeBytesPerRow - case kIOSurfacePlaneOffset: self = .planeOffset - case kIOSurfacePlaneSize: self = .planeSize - case kIOSurfacePlaneBase: self = .planeBase - case kIOSurfacePlaneBitsPerElement: self = .planeBitsPerElement - case kIOSurfacePlaneBytesPerElement: self = .planeBytesPerElement - case kIOSurfacePlaneElementWidth: self = .planeElementWidth - case kIOSurfacePlaneElementHeight: self = .planeElementHeight - case kIOSurfaceCacheMode: self = .cacheMode - case kIOSurfacePixelFormat: self = .pixelFormat - case kIOSurfacePixelSizeCastingAllowed: self = .pixelSizeCastingAllowed - case kIOSurfacePlaneComponentBitDepths: self = .planeComponentBitDepths - case kIOSurfacePlaneComponentBitOffsets: self = .planeComponentBitOffsets - default: self = .unknown - } - } - } - - /// Returns the total allocation size of the buffer including all planes. - var allocationSize: Int { IOSurfaceGetAllocSize(self) } - - /// Returns the width of the IOSurface buffer in pixels. - var width: Int { IOSurfaceGetWidth(self) } - - /// Returns the height of the IOSurface buffer in pixels. - var height: Int { IOSurfaceGetHeight(self) } - - /// Returns the length (in bytes) of each row in a particular buffer. - var bytesPerElement: Int { IOSurfaceGetBytesPerRow(self) } - - /// Returns the length (in bytes) of each row in a particular buffer. - var bytesPerRow: Int { IOSurfaceGetBytesPerRow(self) } - - /// Returns the address of the first byte of data in a particular buffer. - /// - Returns: Returns `nil` if buffer is invalid. - var baseAddress: UnsafeMutableRawPointer { IOSurfaceGetBaseAddress(self) } - - /// Returns the width (in pixels) of each element in a particular buffer. - var elementWidth: Int { IOSurfaceGetElementWidth(self) } - - /// Returns the height (in pixels) of each element in a particular buffer. - var elementHeight: Int { IOSurfaceGetElementHeight(self) } - - /// Returns an unsigned integer that contains the traditional macOS buffer format. - var cvPixelFormat: CVPixelFormat { IOSurfaceGetPixelFormat(self).cvPixelFormat } - - /// This will return the current seed value of the buffer and is a cheap call to make to see if the contents of the buffer have changed since the last lock/unlock. - var seed: UInt32 { IOSurfaceGetSeed(self) } - - /// Return the number of planes in this buffer. May be 0. Returns 0 for an invalid or `nil` buffer pointer. - var planeCount: Int { IOSurfaceGetPlaneCount(self) } - - var subsampling: IOSurfaceSubsampling { IOSurfaceGetSubsampling(self) } - - /// Returns the per-process usage count for an IOSurface. - var useCount: Int32 { IOSurfaceGetUseCount(self) } - - /// Returns true of an IOSurface is in use by any process in the system, otherwise false. - var isInUse: Bool { IOSurfaceIsInUse(self) } - - var allowsPixelSizeCasting: Bool { IOSurfaceAllowsPixelSizeCasting(self) } - - /// “Lock” an IOSurface for reading or writing. - /// - /// The term “lock” is used loosely in this context, and is used along with the “unlock” information to put a bound on CPU access to the raw IOSurface data. - /// If the seed parameter is non-NULL, IOSurfaceLock(_:_:_:) will store the buffer’s internal modification seed value at the time you made the lock call. - /// You can compare this value to a value returned previously to determine of the contents of the buffer has been changed since the last lock. - /// In the case of IOSurfaceUnlock(_:_:_:), the seed value returned will be the internal seed value at the time of the unlock. - /// If you locked the buffer for writing, this value will be incremented as the unlock is performed and the new value will be returned. - /// See IOSurface lock flags for more information. - /// - /// - Note: Locking and unlocking an IOSurface is not a particularly cheap operation, so care should be taken to avoid the calls whenever possible. - /// The seed values are particularly useful for keeping a cache of the buffer contents. - @discardableResult - func lock( - options: IOSurfaceLockOptions = [], - seed: UnsafeMutablePointer? = nil - ) -> kern_return_t { - return IOSurfaceLock(self, options, seed) - } - - /// “Unlock” an IOSurface for reading or writing. - /// - /// The term “lock” is used loosely in this context, and is used along with the “unlock” information to put a bound on CPU access to the raw IOSurface data. - /// If the seed parameter is non-NULL, IOSurfaceLock(_:_:_:) will store the buffer’s internal modification seed value at the time you made the lock call. You can compare this value to a value returned previously to determine of the contents of the buffer has been changed since the last lock. - /// In the case of IOSurfaceUnlock(_:_:_:), the seed value returned will be the internal seed value at the time of the unlock. If you locked the buffer for writing, this value will be incremented as the unlock is performed and the new value will be returned. - /// See the kIOSurfaceLock enums for more information. - /// - /// - Note: Locking and unlocking an IOSurface is not a particularly cheap operation, so care should be taken to avoid the calls whenever possible. - /// The seed values are particularly useful for keeping a cache of the buffer contents. - @discardableResult - func unlock( - options: IOSurfaceLockOptions = [], - seed: UnsafeMutablePointer? = nil - ) -> kern_return_t { - return IOSurfaceUnlock( - self, - options, - seed - ) - } - - /// Returns the width of the specified plane (in pixels). - /// - /// If the planeIndex is greater than or equal to the plane count of the IOSurface, zero is returned.... with one exception. - /// If this IOSurface has zero planes and a planeIndex of zero is passed in, the routines function just like the non-planar APIs. - /// This is to allow higher level code to treat planar and non-planar buffers is a more uniform fashion. - func widthOfPlane(at planeIndex: Int) -> Int { - return IOSurfaceGetWidthOfPlane(self, planeIndex) - } - - /// Returns the height of the specified plane (in pixels). - /// - /// If the planeIndex is greater than or equal to the plane count of the IOSurface, zero is returned.... with one exception. - /// If this IOSurface has zero planes and a planeIndex of zero is passed in, the routines function just like the non-planar APIs. - /// This is to allow higher level code to treat planar and non-planar buffers is a more uniform fashion. - func heightOfPlane(at planeIndex: Int) -> Int { - return IOSurfaceGetHeightOfPlane(self, planeIndex) - } - - /// Returns the size of each element (in bytes) in the specified plane. - /// - /// If the planeIndex is greater than or equal to the plane count of the IOSurface, zero is returned.... with one exception. - /// If this IOSurface has zero planes and a planeIndex of zero is passed in, the routines function just like the non-planar APIs. - /// This is to allow higher level code to treat planar and non-planar buffers is a more uniform fashion. - func bytesPerElementOfPlane(at planeIndex: Int) -> Int { - return IOSurfaceGetBytesPerElementOfPlane(self, planeIndex) - } - - /// Returns the size of each row (in bytes) in the specified plane. - /// - /// If the planeIndex is greater than or equal to the plane count of the IOSurface, zero is returned.... with one exception. - /// If this IOSurface has zero planes and a planeIndex of zero is passed in, the routines function just like the non-planar APIs. - /// This is to allow higher level code to treat planar and non-planar buffers is a more uniform fashion. - func bytesPerRowOfPlane(at planeIndex: Int) -> Int { - return IOSurfaceGetBytesPerRowOfPlane(self, planeIndex) - } - - /// Returns the address of the first byte of data in the specified plane. - /// - /// If the planeIndex is greater than or equal to the plane count of the IOSurface, zero is returned.... with one exception. - /// If this IOSurface has zero planes and a planeIndex of zero is passed in, the routines function just like the non-planar APIs. - /// This is to allow higher level code to treat planar and non-planar buffers is a more uniform fashion. - func baseAddressOfPlane(at planeIndex: Int) -> UnsafeMutableRawPointer { - return IOSurfaceGetBaseAddressOfPlane(self, planeIndex) - } - - /// Returns the width (in pixels) of each element in the specified plane. - /// - /// If the planeIndex is greater than or equal to the plane count of the IOSurface, zero is returned.... with one exception. - /// If this IOSurface has zero planes and a planeIndex of zero is passed in, the routines function just like the non-planar APIs. - /// This is to allow higher level code to treat planar and non-planar buffers is a more uniform fashion. - func elementWidthOfPlane(at planeIndex: Int) -> Int { - return IOSurfaceGetElementWidthOfPlane(self, planeIndex) - } - - /// Returns the height of the specified plane (in pixels). - /// - /// If the planeIndex is greater than or equal to the plane count of the IOSurface, zero is returned.... with one exception. - /// If this IOSurface has zero planes and a planeIndex of zero is passed in, the routines function just like the non-planar APIs. - /// This is to allow higher level code to treat planar and non-planar buffers is a more uniform fashion. - func elementHeightOfPlane(at planeIndex: Int) -> Int { - return IOSurfaceGetHeightOfPlane(self, planeIndex) - } - - func numberOfComponentsOfPlane(at planeIndex: Int) -> Int { - return IOSurfaceGetNumberOfComponentsOfPlane(self, planeIndex) - } - func nameOfComponentsOfPlane(planeIndex: Int, componentIndex: Int) -> IOSurfaceComponentName { - return IOSurfaceGetNameOfComponentOfPlane(self, planeIndex, componentIndex) - } - func typeOfComponentsOfPlane(planeIndex: Int, componentIndex: Int) -> IOSurfaceComponentType { - return IOSurfaceGetTypeOfComponentOfPlane(self, planeIndex, componentIndex) - } - func rangeOfComponentsOfPlane(planeIndex: Int, componentIndex: Int) -> IOSurfaceComponentRange { - return IOSurfaceGetRangeOfComponentOfPlane(self, planeIndex, componentIndex) - } - func bitDepthOfComponentsOfPlane(planeIndex: Int, componentIndex: Int) -> Int { - return IOSurfaceGetBitDepthOfComponentOfPlane(self, planeIndex, componentIndex) - } - func bitOffsetOfComponentsOfPlane(planeIndex: Int, componentIndex: Int) -> Int { - return IOSurfaceGetBitOffsetOfComponentOfPlane(self, planeIndex, componentIndex) - } - - /// Sets a value in the dictionary associated with the buffer. - func set(value: AnyObject, for key: PropertyKey) { - IOSurfaceSetValue(self, key.rawValue, value) - } - - /// Retrieves a value from the dictionary associated with the buffer. - func copyValue(for key: PropertyKey) -> AnyObject? { - return IOSurfaceCopyValue(self, key.rawValue) - } - - /// Deletes a value in the dictionary associated with the buffer. - func removeValue(for key: PropertyKey) { - IOSurfaceRemoveValue(self, key.rawValue) - } - - func set(values: [PropertyKey: Any]) { - let cfValues = values.reduce(into: [:]) { $0[$1.key.rawValue] = $1.value } as CFDictionary - IOSurfaceSetValues(self, cfValues) - } - func copyAllValues() -> [PropertyKey: Any]? { - guard let cfValues = IOSurfaceCopyAllValues(self) as? [CFString: Any] - else { return nil } - return cfValues.reduce(into: [:]) { $0[PropertyKey(rawValue: $1.key)] = $1.value } - } - func removeAllValues() { - IOSurfaceRemoveAllValues(self) - } - - /// Returns a `mach_port_t` that holds a reference to the `IOSurface`. - /// - /// This is useful if you need to atomically or securely pass an `IOSurface` to another task without making the surface global to the entire system. - /// The returned port must be deallocated with `mach_port_deallocate` or the equivalent. - /// - /// - Note: Any live mach ports created from an IOSurfaceRef implicitly increase the IOSurface's global use count by one until the port is deleted. - func createMachPort() -> mach_port_t { - return IOSurfaceCreateMachPort(self) - } - - /// Increments the per-process usage count for an IOSurface. - func incrementUseCount() { - IOSurfaceIncrementUseCount(self) - } - - /// Decrements the per-process usage count for an IOSurface. - func decrementUseCount() { - IOSurfaceDecrementUseCount(self) - } - - func setPurgeable(_ newState: UInt32, oldState: UnsafeMutablePointer?) -> kern_return_t { - return IOSurfaceSetPurgeable(self, newState, oldState) - } - - /// Recreates an IOSurfaceRef from a mach port. - /// - /// This call takes a `mach_port_t` created via `createMachPort()` and recreates an `IOSurfaceRef` from it. - /// - /// - Note: Any live mach ports created from an `IOSurfaceRef` implicitly increase the IOSurface's global use count by one until the port is deleted. - static func lookupFromMachPort(_ port: mach_port_t) -> IOSurfaceRef? { - return IOSurfaceLookupFromMachPort(port) - } - - /// Returns the maximum value for a given property that is guaranteed to be compatible with all of the current devices (GPUs, etc.) in the system. - /// - /// The most important values to obtain are: - /// - `.bytesPerRow` - /// - `.width` - /// - `.height` - /// - `.planeBytesPerRow` - /// - `.planeWidth` - /// - `.planeHeight` - /// - /// For the width and height properties, the maximum values are the largest that are guaranteed to work for both reading and writing. - /// In OpenGL terms this translates into the largest size that will work for both textures and render targets. - /// This function returns 0 for properties that have no predefined limit or where the concept of a limit would be considered invalid (such as kIOSurfacePixelFormat). - static func maximum(of property: PropertyKey) -> Int { - return IOSurfaceGetPropertyMaximum(property.rawValue) - } - - /// Returns the alignment requirements for a property (if any). - /// - /// If the property has no alignment requirement then this function returns 1. - /// The following properties should always be aligned if you choose to calculate them yourself: - /// - `.bytesPerRow` - /// - `.width` - /// - `.height` - /// - `.planeBytesPerRow` - /// - `.planeWidth` - /// - `.planeHeight` - static func alignment(of property: PropertyKey) -> Int { - return IOSurfaceGetPropertyAlignment(property.rawValue) - } - - /// Returns the smallest aligned value greater than or equal to the specified value. - /// - /// For properties with no alignment requirements, the original value is returned. - static func align(property: PropertyKey, value: Int) -> Int { - return IOSurfaceAlignProperty(property.rawValue, value) - } -} diff --git a/Sources/CoreVideoTools/Extensions/CoreVideo/OSType/OSType+CVPixelFormat.swift b/Sources/CoreVideoTools/Extensions/CoreVideo/OSType/OSType+CVPixelFormat.swift index b97843b..093a135 100644 --- a/Sources/CoreVideoTools/Extensions/CoreVideo/OSType/OSType+CVPixelFormat.swift +++ b/Sources/CoreVideoTools/Extensions/CoreVideo/OSType/OSType+CVPixelFormat.swift @@ -1,5 +1,3 @@ -import CoreVideo - public extension OSType { - var cvPixelFormat: CVPixelFormat { CVPixelFormat(rawValue: self)} + var cvPixelFormat: CVPixelFormat { CVPixelFormat(rawValue: self) } } diff --git a/Tests/CoreVideoToolsTests/CVPixelBufferTests.swift b/Tests/CoreVideoToolsTests/CVPixelBufferTests.swift new file mode 100644 index 0000000..8e1d2f5 --- /dev/null +++ b/Tests/CoreVideoToolsTests/CVPixelBufferTests.swift @@ -0,0 +1,107 @@ +import XCTest +@testable import CoreVideoTools + +final class CVPixelBufferTests: XCTestCase { + + override func tearDown() { + super.tearDown() + // Force a garbage collection to clean up any lingering CVPixelBuffers + autoreleasepool { } + } + + func testCreatePixelBuffer() throws { + let pixelBuffer = try CVPixelBuffer.create(width: 640, height: 480, cvPixelFormat: .type_32BGRA) + XCTAssertEqual(pixelBuffer.width, 640) + XCTAssertEqual(pixelBuffer.height, 480) + XCTAssertEqual(pixelBuffer.cvPixelFormat, .type_32BGRA) + } + + func testPixelBufferProperties() throws { + let pixelBuffer = try CVPixelBuffer.create(width: 1920, height: 1080, cvPixelFormat: .type_32BGRA) + XCTAssertEqual(pixelBuffer.width, 1920) + XCTAssertEqual(pixelBuffer.height, 1080) + XCTAssertEqual(pixelBuffer.cvPixelFormat, .type_32BGRA) + XCTAssertGreaterThan(pixelBuffer.bytesPerRow, 0) + XCTAssertFalse(pixelBuffer.isPlanar) + XCTAssertEqual(pixelBuffer.planeCount, 0) + } + + func testLockUnlock() throws { + let pixelBuffer = try CVPixelBuffer.create(width: 100, height: 100, cvPixelFormat: .type_32BGRA) + try pixelBuffer.lockBaseAddress(lockFlags: .readOnly) + XCTAssertNotNil(pixelBuffer.baseAddress) + try pixelBuffer.unlockBaseAddress(unlockFlags: .readOnly) + } + + func testBlankCopy() throws { + let original = try CVPixelBuffer.create(width: 200, height: 150, cvPixelFormat: .type_32BGRA) + let copy = try original.blankCopy() + XCTAssertEqual(original.width, copy.width) + XCTAssertEqual(original.height, copy.height) + XCTAssertEqual(original.cvPixelFormat, copy.cvPixelFormat) + } + + func testDeepCopy() throws { + let original = try CVPixelBuffer.create(width: 300, height: 200, cvPixelFormat: .type_32BGRA) + try original.lockBaseAddress(lockFlags: []) + defer { try? original.unlockBaseAddress(unlockFlags: []) } + + // Fill original with some data + if let baseAddress = original.baseAddress { + memset(baseAddress, 255, original.dataSize) + } + + let copy = try original.deepCopy() + XCTAssertEqual(original.width, copy.width) + XCTAssertEqual(original.height, copy.height) + XCTAssertEqual(original.cvPixelFormat, copy.cvPixelFormat) + + // Check if data was copied + try copy.lockBaseAddress(lockFlags: .readOnly) + defer { try? copy.unlockBaseAddress(unlockFlags: .readOnly) } + + if let copyData = copy.data() { + XCTAssertEqual(copyData.count, original.dataSize) + XCTAssertEqual(copyData.first, 255) + XCTAssertEqual(copyData.last, 255) + } else { + XCTFail("Failed to get data from copy") + } + } + + func testPixelBufferData() throws { + let pixelBuffer = try CVPixelBuffer.create(width: 100, height: 100, cvPixelFormat: .type_32BGRA) + try pixelBuffer.lockBaseAddress(lockFlags: []) + defer { try? pixelBuffer.unlockBaseAddress(unlockFlags: []) } + + if let data = pixelBuffer.data() { + XCTAssertEqual(data.count, pixelBuffer.dataSize) + } else { + XCTFail("Failed to get pixel buffer data") + } + } + + func testPlanarPixelBuffer() throws { + let pixelBuffer = try CVPixelBuffer.create( + width: 100, + height: 100, + cvPixelFormat: .type_420YpCbCr8BiPlanarVideoRange + ) + XCTAssertTrue(pixelBuffer.isPlanar) + XCTAssertEqual(pixelBuffer.planeCount, 2) + + XCTAssertEqual(pixelBuffer.width(of: 0), 100) + XCTAssertEqual(pixelBuffer.height(of: 0), 100) + XCTAssertEqual(pixelBuffer.width(of: 1), 50) + XCTAssertEqual(pixelBuffer.height(of: 1), 50) + + try pixelBuffer.lockBaseAddress(lockFlags: .readOnly) + defer { try? pixelBuffer.unlockBaseAddress(unlockFlags: .readOnly) } + + XCTAssertNotNil(pixelBuffer.baseAddress(of: 0)) + XCTAssertNotNil(pixelBuffer.baseAddress(of: 1)) + + XCTAssertGreaterThan(pixelBuffer.bytesPerRow(of: 0), 0) + XCTAssertGreaterThan(pixelBuffer.bytesPerRow(of: 1), 0) + } +} diff --git a/Tests/CoreVideoToolsTests/IOSurfaceTests.swift b/Tests/CoreVideoToolsTests/IOSurfaceTests.swift new file mode 100644 index 0000000..06eda4b --- /dev/null +++ b/Tests/CoreVideoToolsTests/IOSurfaceTests.swift @@ -0,0 +1,142 @@ +import XCTest +@testable import CoreVideoTools + +final class IOSurfaceTests: XCTestCase { + + func testCreateIOSurfaceSuccess() throws { + let surface = try IOSurface.create( + width: 640, + height: 480, + cvPixelFormat: .type_32BGRA, + bytesPerElement: 4, + bytesPerRow: 640 * 4 + ) + XCTAssertEqual(surface.width, 640) + XCTAssertEqual(surface.height, 480) + XCTAssertEqual(surface.bytesPerElement, 4) + XCTAssertEqual(surface.bytesPerRow, 640 * 4) + XCTAssertEqual(surface.pixelFormat, kCVPixelFormatType_32BGRA) + } + + func testCreateIOSurfaceWithCustomCacheMode() throws { + let surface = try IOSurface.create( + width: 320, + height: 240, + cvPixelFormat: .type_32ARGB, + bytesPerElement: 4, + bytesPerRow: 320 * 4, + cacheMode: .writeCombine + ) + XCTAssertEqual(surface.width, 320) + XCTAssertEqual(surface.height, 240) + XCTAssertEqual(surface.pixelFormat, kCVPixelFormatType_32ARGB) + } + + func testCreateIOSurfaceWithAdditionalProperties() throws { + let surface = try IOSurface.create( + width: 320, + height: 240, + cvPixelFormat: .type_32ARGB, + bytesPerElement: 4, + bytesPerRow: 320 * 4, + additionalProperties: [ + .planeInfo: [ + [ + kIOSurfacePlaneWidth: 320, + kIOSurfacePlaneHeight: 240, + kIOSurfacePlaneBytesPerRow: 320 * 4, + kIOSurfacePlaneOffset: 0, + kIOSurfacePlaneSize: 320 * 240 * 4 + ] + ] + ] + ) + XCTAssertEqual(surface.width, 320) + XCTAssertEqual(surface.height, 240) + XCTAssertEqual(surface.pixelFormat, kCVPixelFormatType_32ARGB) + XCTAssertEqual(surface.planeCount, 1) + } + + func testCreateIOSurfaceWithInvalidDimensions() throws { + XCTAssertThrowsError(try IOSurface.create( + width: 0, + height: 480, + cvPixelFormat: .type_32BGRA, + bytesPerElement: 4, + bytesPerRow: 0 + )) { error in + XCTAssertEqual(error as? CVError, .error) + } + + XCTAssertThrowsError(try IOSurface.create( + width: 640, + height: 0, + cvPixelFormat: .type_32BGRA, + bytesPerElement: 4, + bytesPerRow: 640 * 4 + )) { error in + XCTAssertEqual(error as? CVError, .error) + } + } + + func testCreateIOSurfaceWithInvalidBytesPerElement() throws { + XCTAssertThrowsError(try IOSurface.create( + width: 640, + height: 480, + cvPixelFormat: .type_32BGRA, + bytesPerElement: 0, + bytesPerRow: 640 * 4 + )) { error in + XCTAssertEqual(error as? CVError, .error) + } + } + + func testCreateIOSurfaceWithInvalidBytesPerRow() throws { + XCTAssertThrowsError(try IOSurface.create( + width: 640, + height: 480, + cvPixelFormat: .type_32BGRA, + bytesPerElement: 4, + bytesPerRow: 100 // Too small for the width + )) { error in + XCTAssertEqual(error as? CVError, .error) + } + } + + func testCreateIOSurfaceWithDifferentPixelFormats() throws { + let formats: [CVPixelFormat] = [.type_32BGRA, .type_32RGBA, .type_16Gray, .type_8IndexedGray_WhiteIsZero] + + for format in formats { + let bytesPerElement = format == .type_16Gray ? 2 : (format == .type_8IndexedGray_WhiteIsZero ? 1 : 4) + let surface = try IOSurface.create( + width: 100, + height: 100, + cvPixelFormat: format, + bytesPerElement: bytesPerElement, + bytesPerRow: 100 * bytesPerElement + ) + XCTAssertEqual(surface.pixelFormat, format.rawValue) + } + } + + func testCreateIOSurfaceWithAllCacheModes() throws { + let cacheModes: [IOSurface.CacheMode] = [ + .default, .inhibit, .writeThrough, .copyback, + .writeCombine, .copybackInner, .postedWrite, + .realTime, .postedReordered, .postedCombinedReordered + ] + + for cacheMode in cacheModes { + let surface = try IOSurface.create( + width: 64, + height: 64, + cvPixelFormat: .type_32BGRA, + bytesPerElement: 4, + bytesPerRow: 64 * 4, + cacheMode: cacheMode + ) + XCTAssertNotNil(surface) + // We can't directly test the cache mode, but we can check that creation succeeded + } + } +}