TN3133: Packaging a Metal renderer
Distribute a Metal renderer in a Swift package.
Overview
Several individual pieces make up a Metal renderer: The CPU-side app code, the GPU-side shaders, and the structures that the app code and the shaders share. Bundling these pieces together in a single Swift package is an excellent way to modularize a renderer for use in multiple projects. Read this technote to discover a Swift package structure that shares C structs between Swift and Metal code, and to learn how to access the compiled Metal source as a MTLLibrary.
Configure the package manifest
The package structure that enables you to package Swift, Metal, and shared C sources in a single Swift package requires two Target declarations, as shown in the following example package manifest:
// swift-tools-version:5.5
import PackageDescription
let package = Package(
name: "MyRenderer",
products: [
.library(
name: "MyRenderer",
targets: ["MyRenderer"]),
],
targets: [
// MyRenderer contains .swift and .metal files.
.target(
name: "MyRenderer",
dependencies: ["MySharedTypes"]),
// MySharedTypes contains a .h file nested inside of a folder named "include", and an empty .m file, specifying that the target should be compiled as an Obj-C target.
.target(name: "MySharedTypes")
]
)The MyRenderer target contains the Swift source files, as well as the Metal source files.
The MySharedTypes target contains the shared C structs within a header file. Store this header in the directory specified as the publicHeadersPath for this target, so that the header is accessible from the MyRenderer target. It’s also important to have at least one Obj-C, C, or C++ implementation file in this target.
Add the MySharedTypes target as a dependency of the MyRenderer target to access the shared C structs in the MyRenderer target.
Here is a visual representation of the file structure described in the example above:
.
└── MyRenderer
├── Package.swift
├── README.md
└── Sources
├── MyRenderer
│ ├── Renderer.swift
│ └── Shaders.metal
└── MySharedTypes
├── SharedTypes.m
└── include
└── SharedTypes.hAccessing the shared C structs in Swift
Swift Package Manager creates a module that contains the C structs found in the publicHeadersPath, and because the Swift target is dependent on the C target, the C structs are directly accessible from Swift.
Continuing with the same naming from the example above, consider the following header located at MyRenderer/Sources/MySharedTypes/include/SharedTypes.h:
#ifndef SharedTypes_h
#define SharedTypes_h
#import <simd/simd.h>
typedef struct {
vector_float2 position;
vector_float4 color;
} AAPLVertex;
#endif /* SharedTypes_h */The shared types are accessible in Swift files after importing the MySharedTypes module, for example:
import MySharedTypes
let vertex = AAPLVertex(position: .init(250, -250), color: .init(1, 0, 0, 1))Accessing the shared C structs in Metal
Using the same package structure defined above, the shared types are accessible in Metal files after importing the appropriate header file:
// A relative path to SharedTypes.h.
#import "../MySharedTypes/include/SharedTypes.h"
// Use any C types found in the imported header in this Metal file.Retrieving the precompiled Metal library
Swift Package Manager compiles the Metal source to a .metallib and stores it in the resource bundle of the target. This resource bundle is accessible from Swift through the Bundle.module static property.
To create a MTLLibrary from this bundle in the Swift target:
do {
// device is a `MTLDevice`.
let library = try device.makeDefaultLibrary(bundle: Bundle.module)
} catch {
// Handle the error.
}For more information about the Bundle.module static property, see Bundling resources with a Swift package.
Introducing a custom Metal compilation step
You might want to invoke the metal command-line tool yourself, and provide it with arguments that fit your specific needs. For example, you could compile your Metal source with debug symbols to enable shader debugging in a client app.
To introduce a custom Metal compilation step to the build process, create a Swift Package Build Tool Plugin that invokes the metal command-line tool with custom arguments, precompiles a .metallib, and stores it in the target’s resources directory by specifying it as an output file of the build command. Then, apply the plugin to the target that contains your .metal files.
For more information about creating and applying a Swift Package Build Tool Plugin, see Create Swift Package plugins.
Revision History
2022-11-08 First published.
See Also
Latest
TN3205: Low-latency communication with RDMA over ThunderboltTN3206: Updating Apple Pay certificatesTN3179: Understanding local network privacyTN3190: USB audio device design considerationsTN3194: Handling account deletions and revoking tokens for Sign in with AppleTN3193: Managing the on-device foundation model’s context windowTN3115: Bluetooth State Restoration app relaunch rulesTN3192: Migrating your iPad app from the deprecated UIRequiresFullScreen keyTN3151: Choosing the right networking APITN3111: iOS Wi-Fi API overviewTN3191: IMAP extensions supported by Mail for iOS, iPadOS, and visionOSTN3134: Network Extension provider deploymentTN3189: Managing Mail background traffic loadTN3187: Migrating to the UIKit scene-based life cycleTN3188: Troubleshooting In-App Purchases availability in the App Store