mirror of
https://github.com/cirruslabs/macos-image-templates.git
synced 2026-09-29 19:31:12 +02:00
Include Metal Capabilities Helper (#372)
* Add Tart Metal capabilities to base images * Simplify Metal capability usage documentation * Make Tart Metal capabilities opt-in * Fix Metal shim architecture checks on Xcode 27 * Support protected TCC databases on macOS 27
This commit is contained in:
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 Cua AI, Inc.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,31 @@
|
||||
# Tart Metal capabilities
|
||||
|
||||
This is a vendored adaptation of the minimal [Cua/Lume Metal capability shim](https://github.com/trycua/cua/tree/3c1acf27748c3e0f8ff71cd0c9ab072b1e160997/libs/lume/metal-capability-shim)
|
||||
at revision `3c1acf27748c3e0f8ff71cd0c9ab072b1e160997`. The full shim source and
|
||||
Metal capability probe are copied here. The source's `Lume` identifiers,
|
||||
diagnostics, and `LUME_METAL_*` settings are renamed to `Tart` and `TART_METAL_*`.
|
||||
The original Cua copyright and [MIT license](LICENSE) are retained. The upstream
|
||||
source SHA-256 before renaming is
|
||||
`e1371b1e579bca895e6b3a2b581b9f328203d9786bf809912a4a5d1672010cce`.
|
||||
|
||||
The shim changes only Apple-family capability answers and selected memory
|
||||
limits. It does not advertise additional Common, Mac, or Metal families. It
|
||||
uses private, version-sensitive behavior; reported capabilities are not a
|
||||
guarantee that every corresponding GPU operation is supported.
|
||||
|
||||
Configuration is read once, when each process loads the library:
|
||||
|
||||
| Variable | Behavior |
|
||||
| --- | --- |
|
||||
| `TART_METAL_APPLE_FAMILY_MAX` | Required Apple-family ceiling, from 1001 through 1999. Missing, zero, or invalid values disable the shim. |
|
||||
| `TART_METAL_MAX_THREADGROUP_MEMORY` | Memory floor in bytes; defaults to 65536. |
|
||||
| `TART_METAL_RECOMMENDED_WORKING_SET_SIZE` | Optional working-set floor in bytes; unchanged when unset. |
|
||||
|
||||
The old `LUME_METAL_*` names are not recognized. Base images install the library
|
||||
without enabling it for the guest agent or other processes. See the repository's
|
||||
[Metal capabilities instructions](../../README.md#metal-capabilities) for
|
||||
explicit per-command activation and host setup.
|
||||
|
||||
From the repository root, run `bash scripts/test-tart-metal-capabilities.sh` to
|
||||
build and test without installing anything on the host. The installer accepts
|
||||
`DESTDIR` for staging and `TART_METAL_SOURCE_DIR` for Packer's uploaded sources.
|
||||
@@ -0,0 +1,207 @@
|
||||
// Copyright 2026 Cua AI, Inc.
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Adapted from the Cua/Lume team's process-scoped Metal capability shim.
|
||||
// Credit to the Lume folks for the idea and original implementation:
|
||||
// https://github.com/trycua/cua/blob/3c1acf27748c3e0f8ff71cd0c9ab072b1e160997/blog/gpu-passthrough-macos-vms.md
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <Metal/Metal.h>
|
||||
#import <objc/runtime.h>
|
||||
|
||||
#include <errno.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
typedef unsigned long long TartU64;
|
||||
|
||||
typedef struct {
|
||||
BOOL enabled;
|
||||
NSUInteger appleFamilyMax;
|
||||
NSUInteger maxThreadgroupMemory;
|
||||
BOOL hasRecommendedWorkingSetSize;
|
||||
NSUInteger recommendedWorkingSetSize;
|
||||
} TartMetalConfiguration;
|
||||
|
||||
static TartMetalConfiguration gConfiguration = {0};
|
||||
static IMP gOriginalInitGPUFamilySupport = NULL;
|
||||
static IMP gOriginalMaxThreadgroupMemoryLength = NULL;
|
||||
static IMP gOriginalRecommendedMaxWorkingSetSize = NULL;
|
||||
static IMP gOriginalSupportsFamily = NULL;
|
||||
static BOOL gDeviceHooksInstalled = NO;
|
||||
|
||||
static BOOL parseUnsignedEnvironmentValue(const char *name, TartU64 *value) {
|
||||
const char *rawValue = getenv(name);
|
||||
if (!rawValue || !*rawValue) return NO;
|
||||
|
||||
errno = 0;
|
||||
char *end = NULL;
|
||||
unsigned long long parsed = strtoull(rawValue, &end, 0);
|
||||
if (errno != 0 || end == rawValue || !end || *end != '\0') return NO;
|
||||
|
||||
*value = parsed;
|
||||
return YES;
|
||||
}
|
||||
|
||||
static BOOL loadConfiguration(void) {
|
||||
TartU64 appleFamilyMax = 0;
|
||||
if (!parseUnsignedEnvironmentValue(
|
||||
"TART_METAL_APPLE_FAMILY_MAX",
|
||||
&appleFamilyMax
|
||||
) || appleFamilyMax < 1001 || appleFamilyMax >= 2000) {
|
||||
return NO;
|
||||
}
|
||||
|
||||
TartU64 maxThreadgroupMemory = 65536;
|
||||
const char *rawThreadgroupMemory = getenv("TART_METAL_MAX_THREADGROUP_MEMORY");
|
||||
if (rawThreadgroupMemory && *rawThreadgroupMemory &&
|
||||
!parseUnsignedEnvironmentValue("TART_METAL_MAX_THREADGROUP_MEMORY", &maxThreadgroupMemory)) {
|
||||
return NO;
|
||||
}
|
||||
|
||||
TartU64 recommendedWorkingSetSize = 0;
|
||||
const char *rawWorkingSetSize = getenv("TART_METAL_RECOMMENDED_WORKING_SET_SIZE");
|
||||
BOOL hasRecommendedWorkingSetSize = rawWorkingSetSize && *rawWorkingSetSize;
|
||||
if (hasRecommendedWorkingSetSize &&
|
||||
!parseUnsignedEnvironmentValue(
|
||||
"TART_METAL_RECOMMENDED_WORKING_SET_SIZE",
|
||||
&recommendedWorkingSetSize
|
||||
)) {
|
||||
return NO;
|
||||
}
|
||||
|
||||
gConfiguration.enabled = YES;
|
||||
gConfiguration.appleFamilyMax = (NSUInteger)appleFamilyMax;
|
||||
gConfiguration.maxThreadgroupMemory = (NSUInteger)maxThreadgroupMemory;
|
||||
gConfiguration.hasRecommendedWorkingSetSize = hasRecommendedWorkingSetSize;
|
||||
gConfiguration.recommendedWorkingSetSize = (NSUInteger)recommendedWorkingSetSize;
|
||||
return YES;
|
||||
}
|
||||
|
||||
static NSUInteger hookMaxThreadgroupMemoryLength(id self, SEL selector) {
|
||||
NSUInteger original = gOriginalMaxThreadgroupMemoryLength
|
||||
? ((NSUInteger(*)(id, SEL))(void *)gOriginalMaxThreadgroupMemoryLength)(self, selector)
|
||||
: 0;
|
||||
return original < gConfiguration.maxThreadgroupMemory
|
||||
? gConfiguration.maxThreadgroupMemory
|
||||
: original;
|
||||
}
|
||||
|
||||
static NSUInteger hookRecommendedMaxWorkingSetSize(id self, SEL selector) {
|
||||
NSUInteger original = gOriginalRecommendedMaxWorkingSetSize
|
||||
? ((NSUInteger(*)(id, SEL))(void *)gOriginalRecommendedMaxWorkingSetSize)(self, selector)
|
||||
: 0;
|
||||
return original < gConfiguration.recommendedWorkingSetSize
|
||||
? gConfiguration.recommendedWorkingSetSize
|
||||
: original;
|
||||
}
|
||||
|
||||
static BOOL hookSupportsFamily(id self, SEL selector, NSUInteger family) {
|
||||
BOOL original = gOriginalSupportsFamily
|
||||
? ((BOOL(*)(id, SEL, NSUInteger))(void *)gOriginalSupportsFamily)(self, selector, family)
|
||||
: NO;
|
||||
BOOL isConfiguredAppleFamily = family >= 1001 &&
|
||||
family <= gConfiguration.appleFamilyMax;
|
||||
return original || isConfiguredAppleFamily;
|
||||
}
|
||||
|
||||
static BOOL replaceMethod(
|
||||
Class deviceClass,
|
||||
NSString *selectorName,
|
||||
IMP replacement,
|
||||
IMP *original
|
||||
) {
|
||||
SEL selector = NSSelectorFromString(selectorName);
|
||||
Method method = class_getInstanceMethod(deviceClass, selector);
|
||||
if (!method) return NO;
|
||||
|
||||
*original = method_setImplementation(method, replacement);
|
||||
return *original != NULL;
|
||||
}
|
||||
|
||||
static void installDeviceHooks(id device) {
|
||||
@synchronized([device class]) {
|
||||
if (gDeviceHooksInstalled) return;
|
||||
|
||||
Class deviceClass = [device class];
|
||||
Method maxThreadgroupMemory = class_getInstanceMethod(
|
||||
deviceClass,
|
||||
NSSelectorFromString(@"maxThreadgroupMemoryLength")
|
||||
);
|
||||
Method supportsFamily = class_getInstanceMethod(
|
||||
deviceClass,
|
||||
NSSelectorFromString(@"supportsFamily:")
|
||||
);
|
||||
Method recommendedWorkingSetSize = gConfiguration.hasRecommendedWorkingSetSize
|
||||
? class_getInstanceMethod(
|
||||
deviceClass,
|
||||
NSSelectorFromString(@"recommendedMaxWorkingSetSize")
|
||||
)
|
||||
: NULL;
|
||||
|
||||
if (!maxThreadgroupMemory || !supportsFamily ||
|
||||
(gConfiguration.hasRecommendedWorkingSetSize && !recommendedWorkingSetSize)) {
|
||||
NSLog(@"[TartMetalCapabilities] Required device methods are unavailable; leaving stock capabilities unchanged");
|
||||
return;
|
||||
}
|
||||
|
||||
BOOL installed = replaceMethod(
|
||||
deviceClass,
|
||||
@"maxThreadgroupMemoryLength",
|
||||
(IMP)hookMaxThreadgroupMemoryLength,
|
||||
&gOriginalMaxThreadgroupMemoryLength
|
||||
);
|
||||
installed = installed && replaceMethod(
|
||||
deviceClass,
|
||||
@"supportsFamily:",
|
||||
(IMP)hookSupportsFamily,
|
||||
&gOriginalSupportsFamily
|
||||
);
|
||||
|
||||
if (installed && gConfiguration.hasRecommendedWorkingSetSize) {
|
||||
installed = replaceMethod(
|
||||
deviceClass,
|
||||
@"recommendedMaxWorkingSetSize",
|
||||
(IMP)hookRecommendedMaxWorkingSetSize,
|
||||
&gOriginalRecommendedMaxWorkingSetSize
|
||||
);
|
||||
}
|
||||
|
||||
if (!installed) {
|
||||
NSLog(@"[TartMetalCapabilities] Capability hook installation was incomplete");
|
||||
return;
|
||||
}
|
||||
|
||||
gDeviceHooksInstalled = YES;
|
||||
NSLog(
|
||||
@"[TartMetalCapabilities] Enabled for %@ (appleFamilyMax=%llu maxThreadgroupMemory=%llu)",
|
||||
[NSProcessInfo processInfo].processName,
|
||||
(TartU64)gConfiguration.appleFamilyMax,
|
||||
(TartU64)gConfiguration.maxThreadgroupMemory
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
static void hookInitGPUFamilySupport(id self, SEL selector) {
|
||||
installDeviceHooks(self);
|
||||
((void(*)(id, SEL))(void *)gOriginalInitGPUFamilySupport)(self, selector);
|
||||
}
|
||||
|
||||
__attribute__((constructor))
|
||||
static void initializeTartMetalCapabilities(void) {
|
||||
@autoreleasepool {
|
||||
if (!loadConfiguration()) return;
|
||||
|
||||
Class deviceClass = NSClassFromString(@"_MTLDevice");
|
||||
if (!deviceClass) return;
|
||||
|
||||
Method method = class_getInstanceMethod(
|
||||
deviceClass,
|
||||
NSSelectorFromString(@"initGPUFamilySupport")
|
||||
);
|
||||
if (!method) return;
|
||||
|
||||
gOriginalInitGPUFamilySupport = method_setImplementation(
|
||||
method,
|
||||
(IMP)hookInitGPUFamilySupport
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
#import "../Sources/TartMetalCapabilities.m"
|
||||
|
||||
#include <assert.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
static void resetConfiguration(void) {
|
||||
unsetenv("TART_METAL_APPLE_FAMILY_MAX");
|
||||
unsetenv("TART_METAL_MAX_THREADGROUP_MEMORY");
|
||||
unsetenv("TART_METAL_RECOMMENDED_WORKING_SET_SIZE");
|
||||
unsetenv("LUME_METAL_APPLE_FAMILY_MAX");
|
||||
gConfiguration = (TartMetalConfiguration){0};
|
||||
}
|
||||
|
||||
int main(int argc, const char *argv[]) {
|
||||
// In this mode, check the configuration loaded by the real constructor
|
||||
// before main(), as it would be for a newly executed workload.
|
||||
if (argc == 2) {
|
||||
NSUInteger expected = (NSUInteger)strtoull(argv[1], NULL, 10);
|
||||
assert(gConfiguration.enabled == (expected != 0));
|
||||
assert(gConfiguration.appleFamilyMax == expected);
|
||||
return 0;
|
||||
}
|
||||
|
||||
resetConfiguration();
|
||||
assert(!loadConfiguration());
|
||||
setenv("LUME_METAL_APPLE_FAMILY_MAX", "1009", 1);
|
||||
assert(!loadConfiguration());
|
||||
|
||||
const char *invalidFamilies[] = {"", "0", "1000", "2000", "-1", "1009x"};
|
||||
for (size_t i = 0; i < sizeof(invalidFamilies) / sizeof(invalidFamilies[0]); i++) {
|
||||
resetConfiguration();
|
||||
setenv("TART_METAL_APPLE_FAMILY_MAX", invalidFamilies[i], 1);
|
||||
assert(!loadConfiguration());
|
||||
assert(!gConfiguration.enabled);
|
||||
}
|
||||
|
||||
resetConfiguration();
|
||||
setenv("TART_METAL_APPLE_FAMILY_MAX", "1009", 1);
|
||||
assert(loadConfiguration());
|
||||
assert(gConfiguration.appleFamilyMax == 1009);
|
||||
assert(gConfiguration.maxThreadgroupMemory == 65536);
|
||||
assert(!gConfiguration.hasRecommendedWorkingSetSize);
|
||||
|
||||
resetConfiguration();
|
||||
setenv("TART_METAL_APPLE_FAMILY_MAX", "1999", 1);
|
||||
setenv("TART_METAL_MAX_THREADGROUP_MEMORY", "32768", 1);
|
||||
setenv("TART_METAL_RECOMMENDED_WORKING_SET_SIZE", "1073741824", 1);
|
||||
assert(loadConfiguration());
|
||||
assert(gConfiguration.appleFamilyMax == 1999);
|
||||
assert(gConfiguration.maxThreadgroupMemory == 32768);
|
||||
assert(gConfiguration.hasRecommendedWorkingSetSize);
|
||||
assert(gConfiguration.recommendedWorkingSetSize == 1073741824);
|
||||
|
||||
resetConfiguration();
|
||||
setenv("TART_METAL_APPLE_FAMILY_MAX", "1009", 1);
|
||||
setenv("TART_METAL_MAX_THREADGROUP_MEMORY", "invalid", 1);
|
||||
assert(!loadConfiguration());
|
||||
unsetenv("TART_METAL_MAX_THREADGROUP_MEMORY");
|
||||
setenv("TART_METAL_RECOMMENDED_WORKING_SET_SIZE", "invalid", 1);
|
||||
assert(!loadConfiguration());
|
||||
|
||||
resetConfiguration();
|
||||
puts("configuration: OK");
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
#include <assert.h>
|
||||
#include <mach-o/dyld.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/wait.h>
|
||||
#include <unistd.h>
|
||||
|
||||
static pid_t startChild(const char *executable, const char *library, const char *family) {
|
||||
pid_t child = fork();
|
||||
assert(child >= 0);
|
||||
if (child == 0) {
|
||||
char *libraryAssignment = NULL;
|
||||
char *familyAssignment = NULL;
|
||||
assert(asprintf(&libraryAssignment, "DYLD_INSERT_LIBRARIES=%s", library) >= 0);
|
||||
assert(asprintf(&familyAssignment, "TART_METAL_APPLE_FAMILY_MAX=%s", family) >= 0);
|
||||
execl("/usr/bin/env", "env", libraryAssignment, familyAssignment,
|
||||
executable, family, "child", NULL);
|
||||
_exit(1);
|
||||
}
|
||||
return child;
|
||||
}
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
assert(argc >= 2);
|
||||
const char *library = getenv("DYLD_INSERT_LIBRARIES");
|
||||
const char *family = getenv("TART_METAL_APPLE_FAMILY_MAX");
|
||||
const char *memory = getenv("TART_METAL_MAX_THREADGROUP_MEMORY");
|
||||
int loaded = 0;
|
||||
for (uint32_t i = 0; i < _dyld_image_count(); i++) {
|
||||
if (strstr(_dyld_get_image_name(i), "/TartMetalCapabilities.dylib")) loaded = 1;
|
||||
}
|
||||
if (strcmp(argv[1], "stock") == 0) {
|
||||
assert(!library && !family && !memory && !loaded);
|
||||
puts("stock process without injection: OK");
|
||||
return 0;
|
||||
}
|
||||
assert(library && family && memory);
|
||||
assert(strcmp(family, argv[1]) == 0);
|
||||
assert(strcmp(memory, "65536") == 0);
|
||||
assert(loaded);
|
||||
if (argc == 3) return 0;
|
||||
|
||||
// Concurrent workloads must get independent overrides while their parent
|
||||
// retains its explicit opt-in. Exercise the documented /usr/bin/env path.
|
||||
pid_t children[] = {
|
||||
startChild(argv[0], library, "1008"),
|
||||
startChild(argv[0], library, "0"),
|
||||
};
|
||||
for (size_t i = 0; i < sizeof(children) / sizeof(children[0]); i++) {
|
||||
int status = 0;
|
||||
assert(waitpid(children[i], &status, 0) == children[i]);
|
||||
assert(WIFEXITED(status) && WEXITSTATUS(status) == 0);
|
||||
}
|
||||
assert(strcmp(getenv("TART_METAL_APPLE_FAMILY_MAX"), argv[1]) == 0);
|
||||
puts("per-process injection and overrides: OK");
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <Metal/Metal.h>
|
||||
|
||||
#include <errno.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
static BOOL parseFamily(const char *raw, NSUInteger *family) {
|
||||
errno = 0;
|
||||
char *end = NULL;
|
||||
unsigned long long parsed = strtoull(raw, &end, 0);
|
||||
if (errno != 0 || end == raw || !end || *end != '\0') return NO;
|
||||
*family = (NSUInteger)parsed;
|
||||
return YES;
|
||||
}
|
||||
|
||||
int main(int argc, const char *argv[]) {
|
||||
@autoreleasepool {
|
||||
NSUInteger family = 1009;
|
||||
if (argc > 1 && !parseFamily(argv[1], &family)) {
|
||||
fprintf(stderr, "invalid family: %s\n", argv[1]);
|
||||
return 2;
|
||||
}
|
||||
|
||||
id<MTLDevice> device = MTLCreateSystemDefaultDevice();
|
||||
if (!device) {
|
||||
fprintf(stderr, "Metal device unavailable\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
printf("device=%s\n", device.name.UTF8String);
|
||||
printf("family=%llu\n", (unsigned long long)family);
|
||||
printf(
|
||||
"supports_family=%s\n",
|
||||
[device supportsFamily:(MTLGPUFamily)family] ? "true" : "false"
|
||||
);
|
||||
printf(
|
||||
"max_threadgroup_memory=%llu\n",
|
||||
(unsigned long long)device.maxThreadgroupMemoryLength
|
||||
);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
Reference in New Issue
Block a user