From 3752d9e812e9b7cd9efac35925645220345744cf Mon Sep 17 00:00:00 2001 From: lucannez64 <58551020+lucannez64@users.noreply.github.com> Date: Sun, 18 Oct 2026 17:14:47 +0200 Subject: [PATCH 1/2] Fix Metal device edge cases; keep reporting the host GPU From #61, without the default "AMD Radeon Pro 5500M" / 4 GB identity: the host GPU's name and Vulkan working set stay the default for every app, and the existing DARLING_METAL_GPU_NAME and DARLING_METAL_VRAM_MB overrides remain the opt-in. Reporting 4 GB on a 2 GB card invites allocation failures, and under-reports larger cards. Kept from #61: - registryID returns MTLDeviceInfo::kDefaultRegistryID instead of 0; a real Mac never reports 0 and apps key devices by it. - Vertex buffer layout and attribute arrays release the old item after retiring it; the retire step read the item after the release could free it. - newDepthStencilState, newSamplerState and the device name return nil (or the fallback name) when Indium throws, instead of unwinding through Objective-C. - MTLDepthStencilGetIndium is declared only with DARLING_METAL_ENABLED. - The replaceRegion trace probes at most `width` bytes, which stays inside the first row for every pixel format. metal.patch is regenerated in its existing file order, so the diff is these changes only. tests/test_metal_device_info.py compiles the host check against MTLDeviceInfo.h as shipped in the patch and runs in CI. Split from #61. Co-authored-by: DevWebeloper Co-Authored-By: Claude Opus 5.5 --- .github/workflows/source-check.yml | 1 + docs/TESTING.md | 2 +- docs/TROUBLESHOOTING.md | 14 +++++++ patches/metal.patch | 62 ++++++++++++++++++------------ tests/test_metal_device_info.py | 46 ++++++++++++++++++++++ tools/test-metal-device-info.cpp | 7 ++++ 6 files changed, 107 insertions(+), 25 deletions(-) create mode 100644 tests/test_metal_device_info.py diff --git a/.github/workflows/source-check.yml b/.github/workflows/source-check.yml index 1d02ffc..893aafd 100644 --- a/.github/workflows/source-check.yml +++ b/.github/workflows/source-check.yml @@ -17,3 +17,4 @@ jobs: - run: python -m unittest discover -s tests -p 'test_hidden_riot_recovery.py' - run: python -m unittest discover -s tests -p 'test_desktop_run.py' - run: python -m unittest discover -s tests -p 'test_urlsession_runner.py' + - run: python -m unittest discover -s tests -p 'test_metal_device_info.py' diff --git a/docs/TESTING.md b/docs/TESTING.md index 8a544f9..78b7f54 100644 --- a/docs/TESTING.md +++ b/docs/TESTING.md @@ -23,7 +23,7 @@ c++ -std=c++17 -I build/darling-src/src/external/metal/private-include \ build/test-metal-device-info ``` -Expected: `METAL_DEVICE_INFO failures=0 pass=1`. It covers how `MTLDevice.name` turns Vulkan names into macOS style (`AMD Radeon RX 6800 XT (RADV NAVI21)` becomes `AMD Radeon RX 6800 XT`, `Intel(R) UHD Graphics 630 (CML GT2)` becomes `Intel UHD Graphics 630`, NVIDIA names stay as they are), the `DARLING_METAL_VRAM_MB` parser, and the working-set sum over Vulkan heaps. +Expected: `METAL_DEVICE_INFO failures=0 pass=1`. It covers how `MTLDevice.name` turns Vulkan names into macOS style (`AMD Radeon RX 6800 XT (RADV NAVI21)` becomes `AMD Radeon RX 6800 XT`, `Intel(R) UHD Graphics 630 (CML GT2)` becomes `Intel UHD Graphics 630`, NVIDIA names stay as they are), the `DARLING_METAL_VRAM_MB` parser, the working-set sum over Vulkan heaps, and a non-zero `registryID`. `python3 -m unittest tests.test_metal_device_info` runs the same check against the header in `patches/metal.patch`, so it also works without a prepared source tree; CI runs it. The LLVM version check compiles Iridium's actual dynamic loader against small stub libraries. It verifies that a matching major initializes, older/newer majors produce a version mismatch, and a missing library fails initialization. It does not start Darling. The native AIR translator (`python3 tools/build-riot-air-check.py --native`) selects both headers and the shared library from the same `llvm-config`; its build prints those paths so shader results can be attributed to the tested LLVM installation. diff --git a/docs/TROUBLESHOOTING.md b/docs/TROUBLESHOOTING.md index e9494f3..a498826 100644 --- a/docs/TROUBLESHOOTING.md +++ b/docs/TROUBLESHOOTING.md @@ -375,6 +375,20 @@ Tested on a GTX 1060 6GB with driver 580.178.04 on NixOS, Hyprland and Xwayland: the client and the tutorial match render on the NVIDIA GPU. Newer drivers were not retested with the option off. +### Metal device name and memory budget + +`MTLDevice` reports the host GPU: its Vulkan name in macOS style (for example +`AMD Radeon RX 6800 XT (RADV NAVI21)` becomes `AMD Radeon RX 6800 XT`) and a working set +(`recommendedMaxWorkingSetSize`) from the GPU's Vulkan memory heaps. Apps size their GPU +allocations from the working set, so only override it to match the card's real memory. +Both can be overridden per launch: + +```sh +DARLING_METAL_GPU_NAME='AMD Radeon Pro 5500M' DARLING_METAL_VRAM_MB=4096 ./scripts/run.sh +``` + +`registryID` is a fixed non-zero value, as on a real Mac. + ### Host-side checks These are host settings, not game settings. Run on AC power, and use a performance power profile (`powerprofilesctl set performance`, or `asusctl profile -P Performance` on ASUS laptops). On the tested desktop the game is limited mostly by its own main thread on the CPU, not by the GPU (see [status](STATUS.md#game-fps-2026-10-1314)), so laptop CPU power limits matter as much as the GPU choice. diff --git a/patches/metal.patch b/patches/metal.patch index adeb448..a5292d3 100644 --- a/patches/metal.patch +++ b/patches/metal.patch @@ -1134,7 +1134,7 @@ index 2a57796..0cbedf2 100644 - @end diff --git a/src/Metal/MTLDevice.mm b/src/Metal/MTLDevice.mm -index f58184a..a80a625 100644 +index f58184a..ed918ed 100644 --- a/src/Metal/MTLDevice.mm +++ b/src/Metal/MTLDevice.mm @@ -2,14 +2,32 @@ @@ -1218,7 +1218,7 @@ index f58184a..a80a625 100644 } @synthesize device = _device; -@@ -116,6 +168,39 @@ void MTLRemoveDeviceObserver(id observer) { +@@ -116,6 +168,44 @@ void MTLRemoveDeviceObserver(id observer) { [_threadExitCondition unlock]; } @@ -1229,7 +1229,12 @@ index f58184a..a80a625 100644 + static const char* const nameOverride = getenv("DARLING_METAL_GPU_NAME"); + static const uint64_t vramOverride = MTLDeviceInfo::parseMegabytes(getenv("DARLING_METAL_VRAM_MB")); + -+ std::string vulkanName = _device->name(); ++ std::string vulkanName; ++ try { ++ vulkanName = _device ? _device->name() : ""; ++ } catch (...) { ++ vulkanName = ""; ++ } + _name = [[NSString alloc] initWithUTF8String: MTLDeviceInfo::resolveName(nameOverride, vulkanName.c_str()).c_str()]; + if (_name == nil) { + _name = [[NSString alloc] initWithUTF8String: MTLDeviceInfo::kFallbackName]; @@ -1258,7 +1263,7 @@ index f58184a..a80a625 100644 - (instancetype)initWithDevice: (std::shared_ptr)device { self = [super init]; -@@ -123,6 +208,7 @@ void MTLRemoveDeviceObserver(id observer) { +@@ -123,6 +213,7 @@ void MTLRemoveDeviceObserver(id observer) { _device = device; _threadExitCondition = [NSCondition new]; _threadIsRunning = NO; @@ -1266,7 +1271,7 @@ index f58184a..a80a625 100644 _pollingThread = [[NSThread alloc] initWithTarget: self selector: @selector(pollingLoop) object: nil]; [_pollingThread start]; } -@@ -133,6 +219,7 @@ void MTLRemoveDeviceObserver(id observer) { +@@ -133,6 +224,7 @@ void MTLRemoveDeviceObserver(id observer) { { [_pollingThread release]; [_threadExitCondition release]; @@ -1274,7 +1279,7 @@ index f58184a..a80a625 100644 [super dealloc]; } -@@ -153,12 +240,60 @@ void MTLRemoveDeviceObserver(id observer) { +@@ -153,12 +245,60 @@ void MTLRemoveDeviceObserver(id observer) { [_threadExitCondition unlock]; } @@ -1311,7 +1316,7 @@ index f58184a..a80a625 100644 +- (BOOL)isRemovable { return NO; } +- (BOOL)isHeadless { return NO; } +- (NSString*)name { return _name; } -+- (uint64_t)registryID { return 0; } ++- (uint64_t)registryID { return MTLDeviceInfo::kDefaultRegistryID; } +- (BOOL)supportsFamily: (NSUInteger)family { return YES; } +- (BOOL)supportsFeatureSet: (NSUInteger)featureSet { return YES; } +- (BOOL)supportsTextureSampleCount: (NSUInteger)sampleCount { return sampleCount <= 4; } @@ -1336,7 +1341,7 @@ index f58184a..a80a625 100644 if (!pso) { if (error) { // TODO: better error and/or match what the official Metal method does -@@ -166,6 +301,7 @@ void MTLRemoveDeviceObserver(id observer) { +@@ -166,6 +306,7 @@ void MTLRemoveDeviceObserver(id observer) { } return nil; } @@ -1344,7 +1349,7 @@ index f58184a..a80a625 100644 return [[MTLComputePipelineStateInternal alloc] initWithState: pso device: self label: descriptor.label]; } -@@ -183,7 +319,13 @@ void MTLRemoveDeviceObserver(id observer) { +@@ -183,7 +324,13 @@ void MTLRemoveDeviceObserver(id observer) { reflection: (MTLAutoreleasedComputePipelineReflection*)reflection error: (NSError**)error { @@ -1359,7 +1364,7 @@ index f58184a..a80a625 100644 if (!pso) { if (error) { // TODO: better error and/or match what the official Metal method does -@@ -191,13 +333,44 @@ void MTLRemoveDeviceObserver(id observer) { +@@ -191,13 +338,44 @@ void MTLRemoveDeviceObserver(id observer) { } return nil; } @@ -1405,7 +1410,7 @@ index f58184a..a80a625 100644 if (!pso) { if (error) { // TODO: better error and/or match what the official Metal method does -@@ -205,7 +378,114 @@ void MTLRemoveDeviceObserver(id observer) { +@@ -205,7 +383,118 @@ void MTLRemoveDeviceObserver(id observer) { } return nil; } @@ -1453,7 +1458,9 @@ index f58184a..a80a625 100644 +- (id)newDepthStencilStateWithDescriptor:(MTLDepthStencilDescriptor*)descriptor +{ + if (!descriptor) return nil; -+ auto state = _device->newDepthStencilState([descriptor asIndiumDescriptor]); ++ std::shared_ptr state; ++ try { state = _device->newDepthStencilState([descriptor asIndiumDescriptor]); } ++ catch (...) { return nil; } + if (!state) return nil; + return [[MTLDepthStencilStateInternal alloc] initWithState:state device:self label:descriptor.label]; +} @@ -1475,7 +1482,9 @@ index f58184a..a80a625 100644 + desc.lodMaxClamp = descriptor.lodMaxClamp; + desc.supportArgumentBuffers = descriptor.supportArgumentBuffers; + desc.compareFunction = static_cast(descriptor.compareFunction); -+ auto sampler = _device->newSamplerState(desc); ++ std::shared_ptr sampler; ++ try { sampler = _device->newSamplerState(desc); } ++ catch (...) { return nil; } + if (!sampler) return nil; + return [[MTLSamplerStateInternal alloc] initWithSampler:sampler device:self label:descriptor.label]; +} @@ -1521,7 +1530,7 @@ index f58184a..a80a625 100644 } - (id)newCommandQueue -@@ -275,8 +555,21 @@ void MTLRemoveDeviceObserver(id observer) { +@@ -275,8 +564,21 @@ void MTLRemoveDeviceObserver(id observer) { - (id)newLibraryWithData: (dispatch_data_t)data error: (NSError**)error { @@ -2581,7 +2590,7 @@ index 66a9056..f28235c 100644 - @end diff --git a/src/Metal/MTLTexture.mm b/src/Metal/MTLTexture.mm -index 6c71fb9..822c112 100644 +index 6c71fb9..186b8c0 100644 --- a/src/Metal/MTLTexture.mm +++ b/src/Metal/MTLTexture.mm @@ -1,3 +1,5 @@ @@ -2590,7 +2599,7 @@ index 6c71fb9..822c112 100644 // SPDX-FileCopyrightText: 2026 Darling Developers // SPDX-License-Identifier: MPL-2.0 -@@ -7,11 +9,37 @@ +@@ -7,11 +9,38 @@ #import #import @@ -2606,7 +2615,8 @@ index 6c71fb9..822c112 100644 + if (FILE* log = fopen("/tmp/metal-trace.log", "a")) { + const unsigned char* b = (const unsigned char*) bytes; + unsigned long nonZero = 0; -+ size_t probe = std::min(4096, (size_t) region.size.width * 4); ++ // Every pixel format takes at least one byte per pixel, so the first `width` bytes lie inside the first row. ++ size_t probe = std::min(4096, (size_t) region.size.width); + for (size_t i = 0; i < probe; ++i) if (b[i]) ++nonZero; + fprintf(log, "replaceRegion tex=%p %lux%lu fmt=%lu level=%lu region=(%lu,%lu %lux%lu) firstBytesNonZero=%lu/%zu took=%.1fms\n", texture, + (unsigned long) texture.width, (unsigned long) texture.height, (unsigned long) texture.pixelFormat, (unsigned long) level, @@ -2628,7 +2638,7 @@ index 6c71fb9..822c112 100644 @synthesize device = _device; @synthesize resourceOptions = _resourceOptions; @synthesize label = _label; -@@ -164,7 +192,9 @@ +@@ -164,7 +193,9 @@ withBytes: (const void*)pixelBytes bytesPerRow: (NSUInteger)bytesPerRow { @@ -2638,7 +2648,7 @@ index 6c71fb9..822c112 100644 } - (void)replaceRegion: (MTLRegion)region -@@ -174,7 +204,9 @@ +@@ -174,7 +205,9 @@ bytesPerRow: (NSUInteger)bytesPerRow bytesPerImage: (NSUInteger)bytesPerImage { @@ -2736,7 +2746,7 @@ index 37cccb3..79b2350 100644 - @end diff --git a/src/Metal/MTLVertexDescriptor.mm b/src/Metal/MTLVertexDescriptor.mm -index cde968d..aa7fe05 100644 +index cde968d..0a016c6 100644 --- a/src/Metal/MTLVertexDescriptor.mm +++ b/src/Metal/MTLVertexDescriptor.mm @@ -1,24 +1,435 @@ @@ -3015,8 +3025,8 @@ index cde968d..aa7fe05 100644 + if (index >= 31) [NSException raise:NSRangeException format:@"Vertex descriptor index %lu exceeds 30", (unsigned long)index]; + MTLVertexBufferLayoutDescriptor* old = _items[index]; + _items[index] = [descriptor retain]; -+ [old release]; + MTLVertexBufferLayoutArrayRetireItem(self, old); ++ [old release]; + const uint32_t bit = 1u << index; + if (descriptor) _present |= bit; else _present &= ~bit; + if (descriptor && object_getClass(descriptor) == [MTLVertexBufferLayoutDescriptor class]) _concrete |= bit; else _concrete &= ~bit; @@ -3077,8 +3087,8 @@ index cde968d..aa7fe05 100644 + if (index >= 31) [NSException raise:NSRangeException format:@"Vertex descriptor index %lu exceeds 30", (unsigned long)index]; + MTLVertexAttributeDescriptor* old = _items[index]; + _items[index] = [descriptor retain]; -+ [old release]; + MTLVertexAttributeArrayRetireItem(self, old); ++ [old release]; + const uint32_t bit = 1u << index; + if (descriptor) _present |= bit; else _present &= ~bit; + if (descriptor && object_getClass(descriptor) == [MTLVertexAttributeDescriptor class]) _concrete |= bit; else _concrete &= ~bit; @@ -3440,7 +3450,7 @@ diff --git a/private-include/Metal/MTLDepthStencilInternal.h b/private-include/M new file mode 100644 --- /dev/null +++ b/private-include/Metal/MTLDepthStencilInternal.h -@@ -0,0 +1,25 @@ +@@ -0,0 +1,27 @@ +// SPDX-FileCopyrightText: 2026 Darling Developers +// SPDX-License-Identifier: MPL-2.0 + @@ -3462,15 +3472,17 @@ new file mode 100644 +- (instancetype)initWithState:(std::shared_ptr)state device:(id)device label:(NSString*)label; +#endif +@end ++#if DARLING_METAL_ENABLED +// Direct access for the encoders: the property getter is a message send that returns the shared_ptr by value. +const std::shared_ptr& MTLDepthStencilGetIndium(id object); ++#endif + +#endif diff --git a/private-include/Metal/MTLDeviceInfo.h b/private-include/Metal/MTLDeviceInfo.h new file mode 100644 --- /dev/null +++ b/private-include/Metal/MTLDeviceInfo.h -@@ -0,0 +1,148 @@ +@@ -0,0 +1,150 @@ +// SPDX-FileCopyrightText: 2026 Darling Developers +// SPDX-License-Identifier: MPL-2.0 + @@ -3489,6 +3501,8 @@ new file mode 100644 + +// Used when neither the cleaned nor the raw Vulkan name has any characters. +static constexpr const char* kFallbackName = "Darling Indium Device"; ++// A real Mac never reports registry ID 0, and apps use it as a key for the device. ++static constexpr uint64_t kDefaultRegistryID = 0x100000439ULL; + +static inline bool isSpace(char c) { + return c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == '\f' || c == '\v'; diff --git a/tests/test_metal_device_info.py b/tests/test_metal_device_info.py new file mode 100644 index 0000000..4eac0f9 --- /dev/null +++ b/tests/test_metal_device_info.py @@ -0,0 +1,46 @@ +"""Compile tools/test-metal-device-info.cpp against MTLDeviceInfo.h as shipped in patches/metal.patch.""" +from pathlib import Path +import re +import shutil +import subprocess +import tempfile +import unittest + +ROOT = Path(__file__).resolve().parents[1] +HEADER = 'private-include/Metal/MTLDeviceInfo.h' + + +def header_from_patch(): + """MTLDeviceInfo.h is a new file in the patch, so its text is the section's added lines.""" + patch = (ROOT / 'patches/metal.patch').read_text() + section = re.search(r'(?ms)^diff --git a/%s b/%s\n.*?^@@ [^\n]*\n(.*?)(?=^diff --git |\Z)' % (HEADER, HEADER), patch) + if not section: + raise AssertionError(HEADER + ' is not a new file in patches/metal.patch') + lines = section.group(1).splitlines(keepends=True) + if not all(line.startswith('+') for line in lines): + raise AssertionError(HEADER + ' section has context or removed lines') + return ''.join(line[1:] for line in lines) + + +class MetalDeviceInfoTests(unittest.TestCase): + def test_host_check_passes(self): + compiler = shutil.which('c++') + if not compiler: + self.skipTest('no C++ compiler') + with tempfile.TemporaryDirectory() as folder: + include = Path(folder) / 'include/Metal' + include.mkdir(parents=True) + (include / 'MTLDeviceInfo.h').write_text(header_from_patch()) + binary = Path(folder) / 'test-metal-device-info' + build = subprocess.run([compiler, '-std=c++17', '-Wall', '-Werror', '-I', str(include.parent), + str(ROOT / 'tools/test-metal-device-info.cpp'), '-o', str(binary)], + capture_output=True, text=True) + self.assertEqual(build.returncode, 0, build.stderr) + result = subprocess.run([str(binary)], capture_output=True, text=True, timeout=30) + self.assertEqual(result.returncode, 0, result.stdout) + self.assertIn('METAL_DEVICE_INFO failures=0 pass=1', result.stdout) + self.assertIn('PASS registry ID is non-zero', result.stdout) + + +if __name__ == '__main__': + unittest.main() diff --git a/tools/test-metal-device-info.cpp b/tools/test-metal-device-info.cpp index 926dd75..dd64710 100644 --- a/tools/test-metal-device-info.cpp +++ b/tools/test-metal-device-info.cpp @@ -49,6 +49,13 @@ int main() { expectName("", MTLDeviceInfo::kFallbackName); expectName(nullptr, MTLDeviceInfo::kFallbackName); + // -[MTLDevice registryID]: never 0, like a real Mac. + { + bool ok = MTLDeviceInfo::kDefaultRegistryID != 0; + failures += !ok; + std::printf("%s registry ID is non-zero\n", ok ? "PASS" : "FAIL"); + } + // DARLING_METAL_GPU_NAME override. { bool ok = MTLDeviceInfo::resolveName("My GPU", "AMD Radeon RX 6800 XT (RADV NAVI21)") == "My GPU" From 59327fa9f0efd661dcec19dafda14f243b1e8feb Mon Sep 17 00:00:00 2001 From: lucannez64 <58551020+lucannez64@users.noreply.github.com> Date: Sun, 18 Oct 2026 17:24:49 +0200 Subject: [PATCH 2/2] Report the MacBookPro16,1 GPU name for Riot on AMD and Intel hosts With DARLING_METAL_MAC_GPU_NAME=1, MTLDevice.name is the GPU the MacBookPro16,1 that IOKit reports shipped with, from the host's vendor: "AMD Radeon Pro 5500M" (vendor 0x1002) or "Intel UHD Graphics 630" (0x8086). NVIDIA keeps its own name so the game never takes another vendor's driver paths. recommendedMaxWorkingSetSize and every other value stay the host's, which was the risky part of #61's identity. The Riot launcher turns it on by default (0 or empty turns it off); other Darling apps keep the host name. DARLING_METAL_GPU_NAME still wins. Co-Authored-By: Claude Opus 5.5 --- docs/TROUBLESHOOTING.md | 7 +++++ patches/metal.patch | 50 +++++++++++++++++++++++--------- tests/test_metal_device_info.py | 1 + tools/run-riot-client.sh | 4 ++- tools/test-metal-device-info.cpp | 13 +++++++++ 5 files changed, 60 insertions(+), 15 deletions(-) diff --git a/docs/TROUBLESHOOTING.md b/docs/TROUBLESHOOTING.md index a498826..a6da2e1 100644 --- a/docs/TROUBLESHOOTING.md +++ b/docs/TROUBLESHOOTING.md @@ -387,6 +387,13 @@ Both can be overridden per launch: DARLING_METAL_GPU_NAME='AMD Radeon Pro 5500M' DARLING_METAL_VRAM_MB=4096 ./scripts/run.sh ``` +The Riot launcher sets `DARLING_METAL_MAC_GPU_NAME=1`: the name becomes the GPU that +the MacBookPro16,1 IOKit reports shipped with, from the host's vendor: `AMD Radeon Pro 5500M` +on AMD and `Intel UHD Graphics 630` on Intel. NVIDIA GPUs keep their own name, so the game +never takes AMD or Intel driver paths on an NVIDIA card. The working set is still the host's. +`DARLING_METAL_MAC_GPU_NAME=0 ./scripts/run.sh` reports the host name instead; other Darling +apps report it unless they set the variable. `DARLING_METAL_GPU_NAME` wins over both. + `registryID` is a fixed non-zero value, as on a real Mac. ### Host-side checks diff --git a/patches/metal.patch b/patches/metal.patch index a5292d3..597b074 100644 --- a/patches/metal.patch +++ b/patches/metal.patch @@ -1134,7 +1134,7 @@ index 2a57796..0cbedf2 100644 - @end diff --git a/src/Metal/MTLDevice.mm b/src/Metal/MTLDevice.mm -index f58184a..ed918ed 100644 +index f58184a..2c45608 100644 --- a/src/Metal/MTLDevice.mm +++ b/src/Metal/MTLDevice.mm @@ -2,14 +2,32 @@ @@ -1218,7 +1218,7 @@ index f58184a..ed918ed 100644 } @synthesize device = _device; -@@ -116,6 +168,44 @@ void MTLRemoveDeviceObserver(id observer) { +@@ -116,6 +168,50 @@ void MTLRemoveDeviceObserver(id observer) { [_threadExitCondition unlock]; } @@ -1228,21 +1228,27 @@ index f58184a..ed918ed 100644 + // Overrides are read once per process. + static const char* const nameOverride = getenv("DARLING_METAL_GPU_NAME"); + static const uint64_t vramOverride = MTLDeviceInfo::parseMegabytes(getenv("DARLING_METAL_VRAM_MB")); ++ static const bool macName = [] { ++ const char* value = getenv("DARLING_METAL_MAC_GPU_NAME"); ++ return value && value[0] == '1' && value[1] == '\0'; ++ }(); + ++ auto priv = dynamic_cast(_device.get()); + std::string vulkanName; + try { + vulkanName = _device ? _device->name() : ""; + } catch (...) { + vulkanName = ""; + } -+ _name = [[NSString alloc] initWithUTF8String: MTLDeviceInfo::resolveName(nameOverride, vulkanName.c_str()).c_str()]; ++ const char* mac = macName && priv ? MTLDeviceInfo::macGpuName(priv->properties().vendorID) : nullptr; ++ _name = [[NSString alloc] initWithUTF8String: MTLDeviceInfo::resolveName(nameOverride, vulkanName.c_str(), mac).c_str()]; + if (_name == nil) { + _name = [[NSString alloc] initWithUTF8String: MTLDeviceInfo::kFallbackName]; + } + + uint64_t workingSet = 0; + _sharesSystemMemory = NO; -+ if (auto priv = dynamic_cast(_device.get())) { ++ if (priv) { + const VkPhysicalDeviceType type = priv->properties().deviceType; + _sharesSystemMemory = type == VK_PHYSICAL_DEVICE_TYPE_INTEGRATED_GPU || type == VK_PHYSICAL_DEVICE_TYPE_CPU; + const VkPhysicalDeviceMemoryProperties memory = priv->memoryProperties(); @@ -1263,7 +1269,7 @@ index f58184a..ed918ed 100644 - (instancetype)initWithDevice: (std::shared_ptr)device { self = [super init]; -@@ -123,6 +213,7 @@ void MTLRemoveDeviceObserver(id observer) { +@@ -123,6 +219,7 @@ void MTLRemoveDeviceObserver(id observer) { _device = device; _threadExitCondition = [NSCondition new]; _threadIsRunning = NO; @@ -1271,7 +1277,7 @@ index f58184a..ed918ed 100644 _pollingThread = [[NSThread alloc] initWithTarget: self selector: @selector(pollingLoop) object: nil]; [_pollingThread start]; } -@@ -133,6 +224,7 @@ void MTLRemoveDeviceObserver(id observer) { +@@ -133,6 +230,7 @@ void MTLRemoveDeviceObserver(id observer) { { [_pollingThread release]; [_threadExitCondition release]; @@ -1279,7 +1285,7 @@ index f58184a..ed918ed 100644 [super dealloc]; } -@@ -153,12 +245,60 @@ void MTLRemoveDeviceObserver(id observer) { +@@ -153,12 +251,60 @@ void MTLRemoveDeviceObserver(id observer) { [_threadExitCondition unlock]; } @@ -1341,7 +1347,7 @@ index f58184a..ed918ed 100644 if (!pso) { if (error) { // TODO: better error and/or match what the official Metal method does -@@ -166,6 +306,7 @@ void MTLRemoveDeviceObserver(id observer) { +@@ -166,6 +312,7 @@ void MTLRemoveDeviceObserver(id observer) { } return nil; } @@ -1349,7 +1355,7 @@ index f58184a..ed918ed 100644 return [[MTLComputePipelineStateInternal alloc] initWithState: pso device: self label: descriptor.label]; } -@@ -183,7 +324,13 @@ void MTLRemoveDeviceObserver(id observer) { +@@ -183,7 +330,13 @@ void MTLRemoveDeviceObserver(id observer) { reflection: (MTLAutoreleasedComputePipelineReflection*)reflection error: (NSError**)error { @@ -1364,7 +1370,7 @@ index f58184a..ed918ed 100644 if (!pso) { if (error) { // TODO: better error and/or match what the official Metal method does -@@ -191,13 +338,44 @@ void MTLRemoveDeviceObserver(id observer) { +@@ -191,13 +344,44 @@ void MTLRemoveDeviceObserver(id observer) { } return nil; } @@ -1410,7 +1416,7 @@ index f58184a..ed918ed 100644 if (!pso) { if (error) { // TODO: better error and/or match what the official Metal method does -@@ -205,7 +383,118 @@ void MTLRemoveDeviceObserver(id observer) { +@@ -205,7 +389,118 @@ void MTLRemoveDeviceObserver(id observer) { } return nil; } @@ -1530,7 +1536,7 @@ index f58184a..ed918ed 100644 } - (id)newCommandQueue -@@ -275,8 +564,21 @@ void MTLRemoveDeviceObserver(id observer) { +@@ -275,8 +570,21 @@ void MTLRemoveDeviceObserver(id observer) { - (id)newLibraryWithData: (dispatch_data_t)data error: (NSError**)error { @@ -3482,7 +3488,7 @@ diff --git a/private-include/Metal/MTLDeviceInfo.h b/private-include/Metal/MTLDe new file mode 100644 --- /dev/null +++ b/private-include/Metal/MTLDeviceInfo.h -@@ -0,0 +1,150 @@ +@@ -0,0 +1,166 @@ +// SPDX-FileCopyrightText: 2026 Darling Developers +// SPDX-License-Identifier: MPL-2.0 + @@ -3582,10 +3588,26 @@ new file mode 100644 +} + +// DARLING_METAL_GPU_NAME (used verbatim when non-empty) wins over the Vulkan name. -+static inline std::string resolveName(const char* overrideName, const char* vulkanName) { ++// With DARLING_METAL_MAC_GPU_NAME=1: the GPU from the host's vendor that the MacBookPro16,1 ++// iokitd reports shipped with, by Vulkan vendor ID. nullptr for other vendors (NVIDIA), which ++// keep their own name so apps never take another vendor's driver paths. ++static inline const char* macGpuName(uint32_t vendorID) { ++ switch (vendorID) { ++ case 0x1002: return "AMD Radeon Pro 5500M"; ++ case 0x8086: return "Intel UHD Graphics 630"; ++ default: return nullptr; ++ } ++} ++ ++// DARLING_METAL_GPU_NAME wins, then the Mac GPU name (when enabled and known), then the ++// cleaned Vulkan name. ++static inline std::string resolveName(const char* overrideName, const char* vulkanName, const char* macName = nullptr) { + if (overrideName && overrideName[0] != '\0') { + return overrideName; + } ++ if (macName && macName[0] != '\0') { ++ return macName; ++ } + return metalNameFromVulkanName(vulkanName); +} + diff --git a/tests/test_metal_device_info.py b/tests/test_metal_device_info.py index 4eac0f9..99e9b52 100644 --- a/tests/test_metal_device_info.py +++ b/tests/test_metal_device_info.py @@ -40,6 +40,7 @@ class MetalDeviceInfoTests(unittest.TestCase): self.assertEqual(result.returncode, 0, result.stdout) self.assertIn('METAL_DEVICE_INFO failures=0 pass=1', result.stdout) self.assertIn('PASS registry ID is non-zero', result.stdout) + self.assertIn('PASS Mac GPU name', result.stdout) if __name__ == '__main__': diff --git a/tools/run-riot-client.sh b/tools/run-riot-client.sh index 6889e0b..f4fe4e8 100755 --- a/tools/run-riot-client.sh +++ b/tools/run-riot-client.sh @@ -29,7 +29,9 @@ unset RCLOL_APP_ARG3 RCLOL_APP_ARG3_SLOT RCLOL_UX_ARG1 RCLOL_UX_ARG1_SLOT unset RCLOL_UX_ARG2 RCLOL_UX_ARG2_SLOT # The paired runtime presents completed GPU textures without CPU readback. # Explicit 0 or an empty value selects the CPU fallback for diagnostics. -for option in RCLOL_GPU_DIRECT_PRESENT RCLOL_GPU_DIRECT_ASYNC RCLOL_GPU_DIRECT_VSYNC DARLING_CA_PRESENT_AT_COMMIT DARLING_CA_SKIP_LOCAL_IOSURFACE_RENDER RCLOL_FORWARD_MOUSE_MOVED RCLOL_MOUSE_CONFINEMENT; do +# DARLING_METAL_MAC_GPU_NAME reports the MacBookPro16,1 GPU of the host vendor (AMD or Intel) as +# MTLDevice.name, matching the model IOKit reports; memory figures stay the host's. +for option in RCLOL_GPU_DIRECT_PRESENT RCLOL_GPU_DIRECT_ASYNC RCLOL_GPU_DIRECT_VSYNC DARLING_CA_PRESENT_AT_COMMIT DARLING_CA_SKIP_LOCAL_IOSURFACE_RENDER RCLOL_FORWARD_MOUSE_MOVED RCLOL_MOUSE_CONFINEMENT DARLING_METAL_MAC_GPU_NAME; do eval 'value=${'"$option"'-1}' case "$value" in 0|'') unset "$option";; *) export "$option=$value";; esac done diff --git a/tools/test-metal-device-info.cpp b/tools/test-metal-device-info.cpp index dd64710..45cd492 100644 --- a/tools/test-metal-device-info.cpp +++ b/tools/test-metal-device-info.cpp @@ -65,6 +65,19 @@ int main() { std::printf("%s name override\n", ok ? "PASS" : "FAIL"); } + // DARLING_METAL_MAC_GPU_NAME: MacBookPro16,1 GPUs for AMD and Intel hosts; NVIDIA keeps its name. + { + using MTLDeviceInfo::macGpuName; + bool ok = std::string(macGpuName(0x1002)) == "AMD Radeon Pro 5500M" + && std::string(macGpuName(0x8086)) == "Intel UHD Graphics 630" + && macGpuName(0x10de) == nullptr + && MTLDeviceInfo::resolveName(nullptr, "AMD Radeon RX 6600 (RADV NAVI23)", macGpuName(0x1002)) == "AMD Radeon Pro 5500M" + && MTLDeviceInfo::resolveName(nullptr, "NVIDIA GeForce GTX 1060 6GB", macGpuName(0x10de)) == "NVIDIA GeForce GTX 1060 6GB" + && MTLDeviceInfo::resolveName("My GPU", "AMD Radeon RX 6600 (RADV NAVI23)", macGpuName(0x1002)) == "My GPU"; + failures += !ok; + std::printf("%s Mac GPU name\n", ok ? "PASS" : "FAIL"); + } + // DARLING_METAL_VRAM_MB parsing: positive decimal integer only. const unsigned long long MiB = 1024ull * 1024ull; expectBytes("vram \"8192\"", MTLDeviceInfo::parseMegabytes("8192"), 8192 * MiB);