diff --git a/docs/TESTING.md b/docs/TESTING.md index a1e6e80..4c3b06a 100644 --- a/docs/TESTING.md +++ b/docs/TESTING.md @@ -14,6 +14,16 @@ python3 tools/riot-foundation-installer-offline-check.py python3 tools/riot-llvm-version-offline-check.py ``` +The Metal device-report check compiles on the host and needs no Darling: + +```sh +c++ -std=c++17 -I build/darling-src/src/external/metal/private-include \ + tools/test-metal-device-info.cpp -o build/test-metal-device-info +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. + 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. The patch check uses temporary base indexes and does not reset or modify the source checkout. The launcher check uses stub commands and never starts or contacts Riot or Darling. It checks installed-client and first-time-installer launches, strips inherited `DYLD_INSERT_LIBRARIES` even with the old VPX opt-in set, and preserves Darling runtime library/framework paths. The memory-installer check imports the installer and exercises its policy (default source is the prepared artifact; the artifact is never a destination of a different library) in temporary directories. The Foundation-installer check does the same for the overlay installer: it seeds a temporary framework from a stub SDK, replaces `Versions/C/Foundation`, keeps the standard symlinks, stages a backup, treats a re-run as current and refuses a non-Mach-O source. The installer handoff mock covers 28 scenarios, including HTTP 409, interrupted verification and preserving active downloads. diff --git a/docs/TROUBLESHOOTING.md b/docs/TROUBLESHOOTING.md index a44f5bd..973f429 100644 --- a/docs/TROUBLESHOOTING.md +++ b/docs/TROUBLESHOOTING.md @@ -234,6 +234,18 @@ A non-zero error or `FAILED` means the frames cannot reach the window. Before 20 An additional NVIDIA report on `68f816d` showed successful optimal-tiling import but repeated `Metal Device timed out giving us a Drawable to present` messages. Its [presentation trace](https://github.com/lucannez64/darling-lol/issues/3#issuecomment-6214732591) shows all three drawables queued and all three main-thread presentation callbacks running, followed by timeouts with three frames still queued. The compositor only prepared a Metal layer when it was the root layer, skipping nested Metal layers. It now prepares Metal layers throughout the tree. A standalone nested-layer reproduction exhausts the pool before this correction and passes ten presentations with pixel readback after it; the root-layer probe also passes. Confirmation on the reporter's NVIDIA system is still needed. +### What Metal reports about the GPU + +`MTLDevice.name` is the selected Vulkan device's name in macOS style: the trailing driver note in brackets and the `(R)`/`(TM)` marks are removed (`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`, `llvmpipe (LLVM …)` becomes `llvmpipe`). NVIDIA names are unchanged. The name is never replaced with another vendor's GPU. Before 2026-10-17 it was always `Darling Indium Device`. + +`recommendedMaxWorkingSetSize` is the size of the device-local Vulkan heaps (the VRAM) on a discrete GPU. On integrated and software GPUs it is the size of all heaps, because their device-local heap can be just the firmware carve-out while they also use system RAM; macOS likewise reports a large share of system RAM for unified-memory GPUs. It falls back to the old 1 GiB when Vulkan reports no usable heap. `hasUnifiedMemory` stays `YES` by default: with `NO`, apps switch to managed and private resource paths (`synchronizeResource:`, private buffers created with data) that are not implemented here. Set these in the game's environment to override: + +- `DARLING_METAL_GPU_NAME=`: report this name. +- `DARLING_METAL_VRAM_MB=`: report this working-set size in MiB. Other values are ignored. +- `DARLING_METAL_HOST_UNIFIED_MEMORY=1`: report `hasUnifiedMemory` from the GPU type (`YES` for integrated and software GPUs, `NO` for discrete ones). This is experimental; on a discrete GPU it can break rendering. + +Each process reads these once. + ### 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 d529851..adeb448 100644 --- a/patches/metal.patch +++ b/patches/metal.patch @@ -1,5 +1,5 @@ diff --git a/CMakeLists.txt b/CMakeLists.txt -index 431f29a..7f10875 100644 +index 431f29a..eb3eec8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -42,8 +42,10 @@ add_framework(Metal @@ -13,6 +13,17 @@ index 431f29a..7f10875 100644 src/Metal/MTLCommandQueue.mm src/Metal/MTLCompileOptions.mm src/Metal/MTLComputeCommandEncoder.mm +@@ -97,6 +99,10 @@ if (BUILD_METAL) + target_compile_definitions(Metal PUBLIC + DARLING_METAL_ENABLED=1 + ) ++ # MTLDevice.mm reads the Vulkan device properties from Indium's private headers. ++ target_link_libraries(Metal PRIVATE ++ indium_private ++ ) + endif() + + # diff --git a/include/Metal/MTLCommandBuffer.h b/include/Metal/MTLCommandBuffer.h index ef43df8..9045a84 100644 --- a/include/Metal/MTLCommandBuffer.h @@ -1123,10 +1134,10 @@ index 2a57796..0cbedf2 100644 - @end diff --git a/src/Metal/MTLDevice.mm b/src/Metal/MTLDevice.mm -index f58184a..6cfc697 100644 +index f58184a..a80a625 100644 --- a/src/Metal/MTLDevice.mm +++ b/src/Metal/MTLDevice.mm -@@ -2,13 +2,26 @@ +@@ -2,14 +2,32 @@ // SPDX-License-Identifier: MPL-2.0 #import @@ -1151,9 +1162,15 @@ index f58184a..6cfc697 100644 +#import +#include #import ++#import ++#if DARLING_METAL_ENABLED ++#include ++#include ++#endif MTL_EXTERN const MTLDeviceNotificationName MTLDeviceWasAddedNotification = @"MTLDeviceWasAdded"; -@@ -88,6 +101,37 @@ void MTLRemoveDeviceObserver(id observer) { + MTL_EXTERN const MTLDeviceNotificationName MTLDeviceRemovalRequestedNotification = @"MTLDeviceRemovalRequested"; +@@ -88,6 +106,37 @@ void MTLRemoveDeviceObserver(id observer) { [observer release]; }; @@ -1191,7 +1208,73 @@ index f58184a..6cfc697 100644 @implementation MTLDeviceInternal #if DARLING_METAL_ENABLED -@@ -153,12 +197,46 @@ void MTLRemoveDeviceObserver(id observer) { +@@ -96,6 +145,9 @@ void MTLRemoveDeviceObserver(id observer) { + NSThread* _pollingThread; + NSCondition* _threadExitCondition; + BOOL _threadIsRunning; ++ NSString* _name; ++ uint64_t _workingSetSize; ++ BOOL _sharesSystemMemory; + } + + @synthesize device = _device; +@@ -116,6 +168,39 @@ void MTLRemoveDeviceObserver(id observer) { + [_threadExitCondition unlock]; + } + ++// Real host GPU facts, computed once per device in -initWithDevice: (games may query them per frame). ++- (void)loadHostDeviceInfo ++{ ++ // 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")); ++ ++ std::string vulkanName = _device->name(); ++ _name = [[NSString alloc] initWithUTF8String: MTLDeviceInfo::resolveName(nameOverride, vulkanName.c_str()).c_str()]; ++ if (_name == nil) { ++ _name = [[NSString alloc] initWithUTF8String: MTLDeviceInfo::kFallbackName]; ++ } ++ ++ uint64_t workingSet = 0; ++ _sharesSystemMemory = NO; ++ if (auto priv = dynamic_cast(_device.get())) { ++ 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(); ++ MTLDeviceInfo::Heap heaps[VK_MAX_MEMORY_HEAPS]; ++ const size_t count = memory.memoryHeapCount < VK_MAX_MEMORY_HEAPS ? memory.memoryHeapCount : VK_MAX_MEMORY_HEAPS; ++ for (size_t i = 0; i < count; ++i) { ++ heaps[i] = { memory.memoryHeaps[i].size, (memory.memoryHeaps[i].flags & VK_MEMORY_HEAP_DEVICE_LOCAL_BIT) != 0 }; ++ } ++ workingSet = MTLDeviceInfo::workingSetBytes(heaps, count, _sharesSystemMemory); ++ } ++ if (vramOverride != 0) { ++ workingSet = vramOverride; ++ } ++ // 1 GiB was the previous fixed value; keep it when Vulkan reports nothing usable. ++ _workingSetSize = workingSet != 0 ? workingSet : (1ull << 30); ++} ++ + - (instancetype)initWithDevice: (std::shared_ptr)device + { + self = [super init]; +@@ -123,6 +208,7 @@ void MTLRemoveDeviceObserver(id observer) { + _device = device; + _threadExitCondition = [NSCondition new]; + _threadIsRunning = NO; ++ [self loadHostDeviceInfo]; + _pollingThread = [[NSThread alloc] initWithTarget: self selector: @selector(pollingLoop) object: nil]; + [_pollingThread start]; + } +@@ -133,6 +219,7 @@ void MTLRemoveDeviceObserver(id observer) { + { + [_pollingThread release]; + [_threadExitCondition release]; ++ [_name release]; + + [super dealloc]; + } +@@ -153,12 +240,60 @@ void MTLRemoveDeviceObserver(id observer) { [_threadExitCondition unlock]; } @@ -1207,13 +1290,27 @@ index f58184a..6cfc697 100644 + +/* darling-lol M57: feature queries ANGLE's Metal backend needs. */ +- (BOOL)isDepth24Stencil8PixelFormatSupported { return _device->supportsDepth24Stencil8PixelFormat(); } -+- (BOOL)hasUnifiedMemory { return YES; } ++- (BOOL)hasUnifiedMemory ++{ ++ // Stays YES by default: with NO, ANGLE and games take the Managed/Private paths ++ // (synchronizeResource:, Private uploads) that this Metal/Indium does not fully implement. ++ // DARLING_METAL_HOST_UNIFIED_MEMORY=1 reports the host GPU type instead ++ // (YES for integrated and CPU devices, NO for discrete ones). ++ static const bool reportHost = [] { ++ const char* value = getenv("DARLING_METAL_HOST_UNIFIED_MEMORY"); ++ return value && value[0] == '1' && value[1] == '\0'; ++ }(); ++ return reportHost ? _sharesSystemMemory : YES; ++} +- (NSUInteger)maxBufferLength { return 256u * 1024u * 1024u; } -+- (NSUInteger)recommendedMaxWorkingSetSize { return 1ull << 30; } ++- (NSUInteger)recommendedMaxWorkingSetSize ++{ ++ return _workingSetSize > NSUIntegerMax ? NSUIntegerMax : static_cast(_workingSetSize); ++} +- (BOOL)isLowPower { return NO; } +- (BOOL)isRemovable { return NO; } +- (BOOL)isHeadless { return NO; } -+- (NSString*)name { return @"Darling Indium Device"; } ++- (NSString*)name { return _name; } +- (uint64_t)registryID { return 0; } +- (BOOL)supportsFamily: (NSUInteger)family { return YES; } +- (BOOL)supportsFeatureSet: (NSUInteger)featureSet { return YES; } @@ -1239,7 +1336,7 @@ index f58184a..6cfc697 100644 if (!pso) { if (error) { // TODO: better error and/or match what the official Metal method does -@@ -166,6 +244,7 @@ void MTLRemoveDeviceObserver(id observer) { +@@ -166,6 +301,7 @@ void MTLRemoveDeviceObserver(id observer) { } return nil; } @@ -1247,7 +1344,7 @@ index f58184a..6cfc697 100644 return [[MTLComputePipelineStateInternal alloc] initWithState: pso device: self label: descriptor.label]; } -@@ -183,7 +262,13 @@ void MTLRemoveDeviceObserver(id observer) { +@@ -183,7 +319,13 @@ void MTLRemoveDeviceObserver(id observer) { reflection: (MTLAutoreleasedComputePipelineReflection*)reflection error: (NSError**)error { @@ -1262,7 +1359,7 @@ index f58184a..6cfc697 100644 if (!pso) { if (error) { // TODO: better error and/or match what the official Metal method does -@@ -191,13 +276,44 @@ void MTLRemoveDeviceObserver(id observer) { +@@ -191,13 +333,44 @@ void MTLRemoveDeviceObserver(id observer) { } return nil; } @@ -1308,7 +1405,7 @@ index f58184a..6cfc697 100644 if (!pso) { if (error) { // TODO: better error and/or match what the official Metal method does -@@ -205,7 +321,114 @@ void MTLRemoveDeviceObserver(id observer) { +@@ -205,7 +378,114 @@ void MTLRemoveDeviceObserver(id observer) { } return nil; } @@ -1424,7 +1521,7 @@ index f58184a..6cfc697 100644 } - (id)newCommandQueue -@@ -275,8 +498,21 @@ void MTLRemoveDeviceObserver(id observer) { +@@ -275,8 +555,21 @@ void MTLRemoveDeviceObserver(id observer) { - (id)newLibraryWithData: (dispatch_data_t)data error: (NSError**)error { @@ -3369,6 +3466,159 @@ new file mode 100644 +const std::shared_ptr& MTLDepthStencilGetIndium(id object); + +#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 @@ ++// SPDX-FileCopyrightText: 2026 Darling Developers ++// SPDX-License-Identifier: MPL-2.0 ++ ++#ifndef _METAL_MTLDEVICEINFO_H_ ++#define _METAL_MTLDEVICEINFO_H_ ++ ++// Host-testable helpers for the values MTLDevice reports about the GPU. ++// Plain C++ only (no Objective-C, Vulkan or Indium) so tools/test-metal-device-info.cpp ++// can compile this header on the host. ++ ++#include ++#include ++#include ++ ++namespace MTLDeviceInfo { ++ ++// Used when neither the cleaned nor the raw Vulkan name has any characters. ++static constexpr const char* kFallbackName = "Darling Indium Device"; ++ ++static inline bool isSpace(char c) { ++ return c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == '\f' || c == '\v'; ++} ++ ++static inline std::string collapseWhitespace(const std::string& text) { ++ std::string out; ++ bool pendingSpace = false; ++ for (char c: text) { ++ if (isSpace(c)) { ++ pendingSpace = !out.empty(); ++ continue; ++ } ++ if (pendingSpace) { ++ out.push_back(' '); ++ pendingSpace = false; ++ } ++ out.push_back(c); ++ } ++ return out; ++} ++ ++static inline char lower(char c) { ++ return (c >= 'A' && c <= 'Z') ? static_cast(c - 'A' + 'a') : c; ++} ++ ++// Removes "(R)" and "(TM)" in any letter case (Intel's Vulkan names use both). ++static inline std::string removeTrademarks(const std::string& text) { ++ std::string out; ++ for (size_t i = 0; i < text.size();) { ++ if (text[i] == '(') { ++ if (i + 2 < text.size() && lower(text[i + 1]) == 'r' && text[i + 2] == ')') { ++ i += 3; ++ continue; ++ } ++ if (i + 3 < text.size() && lower(text[i + 1]) == 't' && lower(text[i + 2]) == 'm' && text[i + 3] == ')') { ++ i += 4; ++ continue; ++ } ++ } ++ out.push_back(text[i++]); ++ } ++ return out; ++} ++ ++// Drops one trailing parenthetical group (Mesa/driver annotation such as ++// " (RADV NAVI21)", " (CML GT2)" or " (LLVM 22.1.0, 256 bits)"), but never when ++// the group is the whole name. Expects whitespace already collapsed and trimmed. ++static inline std::string removeTrailingGroup(const std::string& text) { ++ if (text.empty() || text.back() != ')') { ++ return text; ++ } ++ int depth = 0; ++ for (size_t i = text.size(); i-- > 0;) { ++ if (text[i] == ')') { ++ ++depth; ++ } else if (text[i] == '(' && --depth == 0) { ++ std::string prefix = collapseWhitespace(text.substr(0, i)); ++ return prefix.empty() ? text : prefix; ++ } ++ } ++ return text; // unbalanced: leave it alone ++} ++ ++// Converts a VkPhysicalDeviceProperties::deviceName into macOS Metal style, ++// e.g. "AMD Radeon RX 6800 XT (RADV NAVI21)" -> "AMD Radeon RX 6800 XT" and ++// "Intel(R) UHD Graphics 630 (CML GT2)" -> "Intel UHD Graphics 630". ++// This only cleans the real name; it never substitutes another GPU. ++static inline std::string metalNameFromVulkanName(const char* vulkanName) { ++ std::string raw = vulkanName ? vulkanName : ""; ++ std::string cleaned = removeTrailingGroup(collapseWhitespace(removeTrademarks(raw))); ++ if (!cleaned.empty()) { ++ return cleaned; ++ } ++ std::string rawTrimmed = collapseWhitespace(raw); ++ return rawTrimmed.empty() ? std::string(kFallbackName) : rawTrimmed; ++} ++ ++// 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) { ++ if (overrideName && overrideName[0] != '\0') { ++ return overrideName; ++ } ++ return metalNameFromVulkanName(vulkanName); ++} ++ ++// Parses DARLING_METAL_VRAM_MB: a positive decimal integer of mebibytes, digits only. ++// Returns 0 (meaning "ignore") for anything else, including overflow. ++static inline uint64_t parseMegabytes(const char* text) { ++ if (!text || text[0] == '\0') { ++ return 0; ++ } ++ uint64_t megabytes = 0; ++ for (const char* p = text; *p; ++p) { ++ if (*p < '0' || *p > '9') { ++ return 0; ++ } ++ uint64_t digit = static_cast(*p - '0'); ++ if (megabytes > (UINT64_MAX / (1024ull * 1024ull) - digit) / 10) { ++ return 0; ++ } ++ megabytes = megabytes * 10 + digit; ++ } ++ return megabytes * 1024ull * 1024ull; ++} ++ ++struct Heap { ++ uint64_t size; ++ bool deviceLocal; ++}; ++ ++// Working-set size from the Vulkan memory heaps; 0 when nothing usable is reported. ++// Discrete GPUs report their device-local (VRAM) heaps. GPUs sharing system RAM ++// (integrated/CPU device types) report every heap: their device-local heap may be ++// only the firmware carve-out (RADV APUs) while the GPU can also use the system heap, ++// which matches macOS reporting a large share of system RAM for unified-memory GPUs. ++static inline uint64_t workingSetBytes(const Heap* heaps, size_t count, bool sharesSystemMemory) { ++ uint64_t total = 0; ++ for (size_t i = 0; i < count; ++i) { ++ if (!sharesSystemMemory && !heaps[i].deviceLocal) { ++ continue; ++ } ++ total = heaps[i].size > UINT64_MAX - total ? UINT64_MAX : total + heaps[i].size; ++ } ++ return total; ++} ++ ++} // namespace MTLDeviceInfo ++ ++#endif // _METAL_MTLDEVICEINFO_H_ diff --git a/private-include/Metal/MTLSamplerStateInternal.h b/private-include/Metal/MTLSamplerStateInternal.h new file mode 100644 --- /dev/null diff --git a/tools/build-riot-metal-runtime.py b/tools/build-riot-metal-runtime.py index 415bf4a..ed27f58 100644 --- a/tools/build-riot-metal-runtime.py +++ b/tools/build-riot-metal-runtime.py @@ -13,6 +13,8 @@ for line in (folder / 'CMakeFiles/Metal.dir/flags.make').read_text().splitlines( flags[key] = shlex.split(value) resource_include = subprocess.check_output(['clang++', '-print-resource-dir'], text=True).strip() + '/include' flags['CXX_INCLUDES'] = [resource_include if value.startswith('/usr/lib/clang/') and value.endswith('/include') else value for value in flags['CXX_INCLUDES']] +# MTLDevice.mm reads Vulkan device properties through Indium's private headers. +flags['CXX_INCLUDES'].append('-I' + str(root / 'build/darling-src/src/external/metal/deps/indium/private-include')) link = shlex.split((folder / 'CMakeFiles/Metal.dir/link.txt').read_text()) # These objects passed live League validation. Keep the broader resource/queue # optimization experiment out of the default build after fresh-match stalls. @@ -28,7 +30,6 @@ for name in ('MTLBuffer', 'MTLTexture', 'MTLDevice', 'MTLSamplerDescriptor', 'MT link.append(str(obj)) link[link.index('-o') + 1] = str(root / 'build/riot-Metal') subprocess.run(link, cwd=folder, check=True) -flags['CXX_INCLUDES'].append('-I' + str(root / 'build/darling-src/src/external/metal/deps/indium/private-include')) obj = root / 'build/riot-metal-sampler-check.o' subprocess.run(['clang++', *flags['CXX_DEFINES'], *flags['CXX_INCLUDES'], *flags['CXX_FLAGS'], '-c', str(root / 'tools/riot-metal-sampler-check.mm'), diff --git a/tools/test-metal-device-info.cpp b/tools/test-metal-device-info.cpp new file mode 100644 index 0000000..926dd75 --- /dev/null +++ b/tools/test-metal-device-info.cpp @@ -0,0 +1,89 @@ +// Host check for the GPU name and working-set helpers behind -[MTLDevice name] and +// -[MTLDevice recommendedMaxWorkingSetSize] (private-include/Metal/MTLDeviceInfo.h). +// Build and run after preparing the sources (see docs/TESTING.md): +// c++ -std=c++17 -I build/darling-src/src/external/metal/private-include +// tools/test-metal-device-info.cpp -o build/test-metal-device-info +// build/test-metal-device-info +#include +#include +#include + +static int failures = 0; + +static void expectName(const char* vulkan, const char* expected) { + std::string actual = MTLDeviceInfo::metalNameFromVulkanName(vulkan); + bool ok = actual == expected; + failures += !ok; + std::printf("%s name \"%s\" -> \"%s\"%s%s%s\n", ok ? "PASS" : "FAIL", vulkan ? vulkan : "(null)", + actual.c_str(), ok ? "" : " (expected \"", ok ? "" : expected, ok ? "" : "\")"); +} + +static void expectBytes(const char* what, unsigned long long actual, unsigned long long expected) { + bool ok = actual == expected; + failures += !ok; + std::printf("%s %s -> %llu%s\n", ok ? "PASS" : "FAIL", what, actual, ok ? "" : " (mismatch)"); + if (!ok) std::printf(" expected %llu\n", expected); +} + +int main() { + // Mesa and proprietary Vulkan names -> macOS Metal style. + expectName("AMD Radeon RX 6800 XT (RADV NAVI21)", "AMD Radeon RX 6800 XT"); + expectName("AMD Radeon RX 6650 XT (RADV NAVI23)", "AMD Radeon RX 6650 XT"); + expectName("AMD Radeon Graphics (RADV RENOIR)", "AMD Radeon Graphics"); + expectName("AMD Radeon Pro 5500M (RADV NAVI14)", "AMD Radeon Pro 5500M"); + expectName("AMD RADV NAVI21 (ACO)", "AMD RADV NAVI21"); + expectName("Intel(R) UHD Graphics 630 (CML GT2)", "Intel UHD Graphics 630"); + expectName("Intel(R) Arc(TM) A770 Graphics (DG2)", "Intel Arc A770 Graphics"); + expectName("NVIDIA GeForce GTX 680", "NVIDIA GeForce GTX 680"); + expectName("NVIDIA GeForce RTX 2070 SUPER", "NVIDIA GeForce RTX 2070 SUPER"); + expectName("NVIDIA GeForce RTX 4090", "NVIDIA GeForce RTX 4090"); + expectName("llvmpipe (LLVM 22.1.0, 256 bits)", "llvmpipe"); + // Whitespace, nesting and edge cases. + expectName(" AMD Radeon\tRX 7900 XTX (RADV NAVI31) ", "AMD Radeon RX 7900 XTX"); + expectName("Foo GPU (driver (nested) x)", "Foo GPU"); + expectName("Foo (Bar) GPU", "Foo (Bar) GPU"); // not trailing: kept + expectName("(RADV NAVI21)", "(RADV NAVI21)"); // whole name: kept + expectName("Broken (unbalanced", "Broken (unbalanced"); + expectName("(R)(TM)", "(R)(TM)"); // cleaned empty -> raw name + expectName(" ", MTLDeviceInfo::kFallbackName); // raw empty -> neutral fallback + expectName("", MTLDeviceInfo::kFallbackName); + expectName(nullptr, MTLDeviceInfo::kFallbackName); + + // DARLING_METAL_GPU_NAME override. + { + bool ok = MTLDeviceInfo::resolveName("My GPU", "AMD Radeon RX 6800 XT (RADV NAVI21)") == "My GPU" + && MTLDeviceInfo::resolveName("", "AMD Radeon RX 6800 XT (RADV NAVI21)") == "AMD Radeon RX 6800 XT" + && MTLDeviceInfo::resolveName(nullptr, "NVIDIA GeForce RTX 4090") == "NVIDIA GeForce RTX 4090"; + failures += !ok; + std::printf("%s name override\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); + expectBytes("vram \"1\"", MTLDeviceInfo::parseMegabytes("1"), MiB); + expectBytes("vram \"0\"", MTLDeviceInfo::parseMegabytes("0"), 0); + expectBytes("vram \"\"", MTLDeviceInfo::parseMegabytes(""), 0); + expectBytes("vram null", MTLDeviceInfo::parseMegabytes(nullptr), 0); + expectBytes("vram \"-5\"", MTLDeviceInfo::parseMegabytes("-5"), 0); + expectBytes("vram \"8G\"", MTLDeviceInfo::parseMegabytes("8G"), 0); + expectBytes("vram \" 8\"", MTLDeviceInfo::parseMegabytes(" 8"), 0); + expectBytes("vram \"1.5\"", MTLDeviceInfo::parseMegabytes("1.5"), 0); + expectBytes("vram max", MTLDeviceInfo::parseMegabytes("17592186044415"), 17592186044415ull * MiB); + expectBytes("vram overflow", MTLDeviceInfo::parseMegabytes("17592186044416"), 0); + expectBytes("vram huge", MTLDeviceInfo::parseMegabytes("99999999999999999999999"), 0); + + // Working set from Vulkan heaps. + const MTLDeviceInfo::Heap discrete[] = {{8192 * MiB, true}, {16384 * MiB, false}, {256 * MiB, true}}; + expectBytes("discrete heaps", MTLDeviceInfo::workingSetBytes(discrete, 3, false), 8448 * MiB); + const MTLDeviceInfo::Heap apu[] = {{512 * MiB, true}, {15000 * MiB, false}}; + expectBytes("integrated heaps", MTLDeviceInfo::workingSetBytes(apu, 2, true), 15512 * MiB); + const MTLDeviceInfo::Heap hostOnly[] = {{4096 * MiB, false}}; + expectBytes("discrete without device-local heap", MTLDeviceInfo::workingSetBytes(hostOnly, 1, false), 0); + expectBytes("no heaps", MTLDeviceInfo::workingSetBytes(nullptr, 0, false), 0); + const MTLDeviceInfo::Heap huge[] = {{UINT64_MAX, true}, {UINT64_MAX, true}}; + expectBytes("saturating sum", MTLDeviceInfo::workingSetBytes(huge, 2, false), UINT64_MAX); + + std::printf("METAL_DEVICE_INFO failures=%d pass=%d\n", failures, failures == 0); + return failures == 0 ? 0 : 1; +} diff --git a/upstream/manifest.json b/upstream/manifest.json index bc13a46..1fff96e 100644 --- a/upstream/manifest.json +++ b/upstream/manifest.json @@ -244,6 +244,7 @@ "private-include/Metal/MTLArgumentInternal.h", "private-include/Metal/MTLBlitCommandEncoderInternal.h", "private-include/Metal/MTLDepthStencilInternal.h", + "private-include/Metal/MTLDeviceInfo.h", "private-include/Metal/MTLSamplerStateInternal.h", "private-include/Metal/MTLVertexDescriptorInternal.h", "src/Metal/MTLArgument.mm",