diff --git a/README.md b/README.md index 8e8475d..c469bd1 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ Game files, Riot installers, account databases, tokens, local prefixes, captures ## Requirements -- x86-64 Linux **5.14 or newer**, an X11 session or Xwayland, and a working hardware Vulkan/OpenGL driver. AMD RADV has been exercised; other drivers are not validated. +- x86-64 Linux **5.14 or newer**, an X11 session or Xwayland, and a working hardware Vulkan/OpenGL driver. AMD RADV has been exercised; other drivers are not validated. Hybrid laptops usually render on the integrated GPU unless started with PRIME offload; NVIDIA is not validated. See [hybrid graphics](docs/TROUBLESHOOTING.md#hybrid-graphics-laptops-amdintel-igpu--nvidia). - Git + Git LFS, Bash, curl, unzip, CMake, Make, Clang/LLD, LLVM development files, a C/C++ compiler, Python 3.12+ and the Darling build dependencies. The patch includes LLVM 22/23 branch-opcode compatibility; the new PC used LLVM/Clang 23. Node.js is needed only for the offline JavaScript checks. - X11/XCB/SHM development files for the current runtime helpers. - Enough free disk for Darling sources/build, Git LFS assets and the game: budget at least 50 GB. Building is substantially larger than this source-only repository. diff --git a/docs/SETUP.md b/docs/SETUP.md index 660a29c..b7c07ed 100644 --- a/docs/SETUP.md +++ b/docs/SETUP.md @@ -99,6 +99,8 @@ Indium also contains experimental hand-off changes. They are **off by default**: | `/tmp/metal-deferred-blit` | blit encoders are recorded on the game thread and replayed on the submit thread (only while deferred render encoding is on) | | `/tmp/metal-worker-lease` | the Vulkan command buffer is set up by the first thread that records into it (the submit thread) instead of when `MTLCommandBuffer` is created | | `/tmp/metal-skip-cpu-devices` | software Vulkan devices (lavapipe) are skipped while a hardware device exists | +| `/tmp/metal-gpu` | diagnostic: selects the Vulkan device used for Metal by index or case-insensitive name substring instead of the loader's first device. The OpenGL context that presents does not follow it; see [hybrid laptops](TROUBLESHOOTING.md#hybrid-graphics-laptops-amdintel-igpu--nvidia). `METAL_INDIUM_GPU` is the environment equivalent; the file wins | +| `/tmp/metal-all-devices` | a `VkDevice` is created for every supported Vulkan device, as before 2026-10-16. Without it only the selected device gets one; on a machine with lavapipe installed this also drops lavapipe's idle rasterizer threads | | `/tmp/metal-debug-waits` | diagnostic: counts how often and how long threads block at the hand-off waits and in `nextDrawable`, every 5 s in `/tmp/metal-trace.log`; the first check writes a `waits enabled` line | Create these files from inside the guest, for example `darling shell touch /tmp/metal-debug-waits`, or while Darling is shut down. The guest `/tmp` is `~/.darling/private/tmp` seen through Darling's overlay mount. A file created on the host in that directory while Darling runs may stay invisible to guest processes that already looked for it. The same applies to removing a file. The older `/tmp/metal-no-present-dispatch-wait`, `/tmp/metal-no-deferred-blit` and `/tmp/metal-no-skip-cpu-devices` files are ignored. diff --git a/docs/TROUBLESHOOTING.md b/docs/TROUBLESHOOTING.md index f54b731..4a6b870 100644 --- a/docs/TROUBLESHOOTING.md +++ b/docs/TROUBLESHOOTING.md @@ -156,6 +156,39 @@ Compare matches on the same map: Bilgewater runs a few percent below Hexgate. On Hyprland, also check that direct scanout is off (`hyprctl getoption render:direct_scanout` prints `int: 0`). The first match with it off ran about 16 FPS faster than the previous one on the same build; see the next section. +## Hybrid graphics laptops (AMD/Intel iGPU + NVIDIA) + +Metal renders with Vulkan, and each finished frame is handed to an OpenGL (EGL) context that presents it. The hand-off uses opaque file descriptors, which only work when Vulkan and OpenGL run on the same GPU. Indium takes the first Vulkan device the loader lists, and on a hybrid laptop both that device and the EGL context are normally the integrated GPU, so the discrete GPU stays idle. A report in [issue #3](https://github.com/lucannez64/darling-lol/issues/3) (Ryzen 9 5900HS, RTX 3060 Laptop) measured about 70 FPS in ARAM; which GPU rendered that match is not known. **Running on an NVIDIA GPU is not validated.** + +### Run on the discrete GPU + +Start the launcher with PRIME render offload so that both Vulkan and OpenGL move to the NVIDIA GPU: + +```sh +prime-run sh scripts/run.sh +``` + +`prime-run` sets `__NV_PRIME_RENDER_OFFLOAD=1`, `__VK_LAYER_NV_optimus=NVIDIA_only` and `__GLX_VENDOR_LIBRARY_NAME=nvidia`. `darling` copies the caller's environment into the guest, and guest processes inherit it; whether the Riot client passes all of it to the game has not been checked; the log below shows the result. Darling's OpenGL uses EGL, so if the log below still shows the integrated GPU as the presenting renderer, also set `__EGL_VENDOR_LIBRARY_FILENAMES=/usr/share/glvnd/egl_vendor.d/10_nvidia.json`. On two Mesa GPUs, `DRI_PRIME=1` does the same. + +The guest file `/tmp/metal-gpu` (a device index or part of its name, for example `nvidia`) moves only the Vulkan side. Use it for diagnosis: on its own it splits the two sides across GPUs, and frames cannot be presented. + +### Check which GPU rendered + +Each process that uses Metal appends to guest `/tmp/metal-device.log` (host `~/.darling/private/tmp/metal-device.log`): every Vulkan device the loader reported, the selected one and why, and the renderer of the presenting OpenGL context with both device UUIDs: + +``` +[MetalDevice] pid=… physical name="NVIDIA GeForce RTX 3060 Laptop GPU" type=discrete vendorID=0x10de … supported=yes +[MetalDevice] pid=… selected name="NVIDIA GeForce RTX 3060 Laptop GPU" type=discrete … reason=loader-first +[MetalDevice] pid=… created VkDevice for 1 of 1 supported device(s) (selected only) +[MetalDevice] pid=… presenting GL renderer="…" vendor="NVIDIA Corporation" … match=yes +``` + +`MISMATCH` on the presenting line means Vulkan and OpenGL are on different GPUs. `match=unknown` means the OpenGL driver reports no device UUID. Include this file with performance reports. + +### 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. + ## Cursor stops changing shape in fullscreen On Hyprland the game's cursor can keep its first shape (for example, it never switches to the attack cursor) until you leave and re-enter fullscreen. Darling sends every change to the X server; what freezes the shape on screen is Hyprland's direct scanout of the fullscreen Xwayland window. Turn it off in `hyprland.conf`: diff --git a/patches/cocotron.patch b/patches/cocotron.patch index bfd0cf3..2f391a7 100644 --- a/patches/cocotron.patch +++ b/patches/cocotron.patch @@ -7105,10 +7105,122 @@ index 5ad375f8..c8e83814 100644 +- (BOOL) _treeHasAnimations; @end diff --git a/QuartzCore/CAMetalDrawable.mm b/QuartzCore/CAMetalDrawable.mm -index d6f86b31..81a4ac10 100644 +index d6f86b31..4d08ce26 100644 --- a/QuartzCore/CAMetalDrawable.mm +++ b/QuartzCore/CAMetalDrawable.mm -@@ -401,6 +401,10 @@ CAMetalDrawableTexture::CAMetalDrawableTexture(CGSize size, Indium::PixelFormat +@@ -31,6 +31,12 @@ + #include + #include + ++#include ++#include ++#include ++#include ++#include ++ + namespace DynamicVK = Indium::DynamicVK; + + static void reportGLErrors(void) { +@@ -50,8 +56,86 @@ static void reportGLErrors(void) { + namespace Indium::DynamicVK { + static DynamicFunction vkGetSemaphoreFdKHR("vkGetSemaphoreFdKHR"); + static DynamicFunction vkGetMemoryFdKHR("vkGetMemoryFdKHR"); ++ static DynamicFunction vkGetPhysicalDeviceProperties2Local("vkGetPhysicalDeviceProperties2"); + }; + ++// ++// GPU selection diagnostics ++// ++ ++// The drawable's memory and semaphores are exported as OPAQUE_FD and imported into ++// this layer's OpenGL context. That only works when Vulkan and OpenGL run on the ++// same physical device/driver, and until now nothing checked it: a mismatch would ++// silently break. Record which GL renderer actually presents and whether its ++// device UUID matches the Vulkan device's, once per process, in ++// /tmp/metal-device.log (alongside the device list Indium logs at startup). ++static std::string darlingUuidString(const GLubyte* uuid) { ++ static const char* digits = "0123456789abcdef"; ++ std::string out; ++ out.reserve(32); ++ for (int i = 0; i < GL_UUID_SIZE_EXT; ++i) { ++ out += digits[(uuid[i] >> 4) & 0xf]; ++ out += digits[uuid[i] & 0xf]; ++ } ++ return out; ++} ++ ++static void reportGpuDeviceMatch(std::shared_ptr device) { ++ if (!device) { ++ return; ++ } ++ static std::once_flag once; ++ std::call_once(once, [device]() { ++ const GLubyte* renderer = glGetString(GL_RENDERER); ++ const GLubyte* vendor = glGetString(GL_VENDOR); ++ ++ GLubyte glDeviceUUID[GL_UUID_SIZE_EXT] = {0}; ++ GLubyte glDriverUUID[GL_UUID_SIZE_EXT] = {0}; ++ GLint uuidCount = 0; ++ glGetIntegerv(GL_NUM_DEVICE_UUIDS_EXT, &uuidCount); ++ if (uuidCount > 0) { ++ glGetUnsignedBytei_vEXT(GL_DEVICE_UUID_EXT, 0, glDeviceUUID); ++ glGetUnsignedBytevEXT(GL_DRIVER_UUID_EXT, glDriverUUID); ++ } ++ ++ // Queried here rather than through a new libindium export, so this QuartzCore ++ // still loads against a libindium built before the device log. ++ VkPhysicalDeviceIDProperties idProperties {}; ++ idProperties.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_ID_PROPERTIES; ++ VkPhysicalDeviceProperties2 properties2 {}; ++ properties2.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2; ++ properties2.pNext = &idProperties; ++ bool haveVkUUID = false; ++ if (DynamicVK::vkGetPhysicalDeviceProperties2Local) { ++ DynamicVK::vkGetPhysicalDeviceProperties2Local(device->physicalDevice(), &properties2); ++ haveVkUUID = true; ++ } ++ const uint8_t* vkDeviceUUID = idProperties.deviceUUID; ++ const uint8_t* vkDriverUUID = idProperties.driverUUID; ++ ++ std::string line = "presenting GL renderer=\"" + std::string(renderer ? reinterpret_cast(renderer) : "(null)") + "\"" ++ + " vendor=\"" + std::string(vendor ? reinterpret_cast(vendor) : "(null)") + "\"" ++ + " glDeviceUUID=" + (uuidCount > 0 ? darlingUuidString(glDeviceUUID) : std::string("none")) ++ + " glDriverUUID=" + (uuidCount > 0 ? darlingUuidString(glDriverUUID) : std::string("none")) ++ + " vulkanDeviceUUID=" + (haveVkUUID ? darlingUuidString(vkDeviceUUID) : std::string("none")) ++ + " vulkanDriverUUID=" + (haveVkUUID ? darlingUuidString(vkDriverUUID) : std::string("none")); ++ ++ if (uuidCount > 0 && haveVkUUID && memcmp(glDeviceUUID, vkDeviceUUID, GL_UUID_SIZE_EXT) != 0) { ++ line += " MISMATCH: OpenGL and Vulkan are on different GPUs; the opaque-FD import cannot work." ++ " Run the launcher under prime-run so both use the same GPU."; ++ } else if (uuidCount > 0 && haveVkUUID) { ++ line += " match=yes"; ++ } else { ++ line += " match=unknown"; ++ } ++ ++ if (FILE* log = fopen("/tmp/metal-device.log", "a")) { ++ fprintf(log, "[MetalDevice] pid=%d %s\n", getpid(), line.c_str()); ++ fclose(log); ++ } ++ }); ++} ++ + // + // helper classes + // +@@ -161,6 +245,11 @@ CAMetalDrawableTexture::CAMetalDrawableTexture(CGSize size, Indium::PixelFormat + _pixelFormat(pixelFormat), + _framebufferOnly(framebufferOnly) + { ++ // The layer's GL context is current here (recreateDrawables / _setContext set ++ // it), so this is the first point where the presenting GL device can be ++ // compared with the Vulkan device. Logged once per process. ++ reportGpuDeviceMatch(privateDevice); ++ + // + // create the images + // +@@ -401,6 +490,10 @@ CAMetalDrawableTexture::CAMetalDrawableTexture(CGSize size, Indium::PixelFormat glTextureParameteri(_textureID, GL_TEXTURE_MIN_FILTER, GL_NEAREST); glTextureParameteri(_textureID, GL_TEXTURE_WRAP_S, GL_REPEAT); glTextureParameteri(_textureID, GL_TEXTURE_WRAP_T, GL_REPEAT); @@ -7119,7 +7231,7 @@ index d6f86b31..81a4ac10 100644 reportGLErrors(); glTextureStorageMem2DEXT(_textureID, 1, GL_RGBA8, _size.width, _size.height, _memoryObject, 0); -@@ -605,35 +609,16 @@ void CAMetalDrawableActual::present() { +@@ -605,35 +698,16 @@ void CAMetalDrawableActual::present() { return; } @@ -7161,7 +7273,7 @@ index d6f86b31..81a4ac10 100644 } } -@@ -702,9 +687,24 @@ void CAMetalDrawableActual::reset() { +@@ -702,9 +776,24 @@ void CAMetalDrawableActual::reset() { }; void CAMetalDrawableActual::synchronizeRender() { diff --git a/patches/metal--deps--indium.patch b/patches/metal--deps--indium.patch index faa1a83..45fd9b6 100644 --- a/patches/metal--deps--indium.patch +++ b/patches/metal--deps--indium.patch @@ -781,7 +781,7 @@ index 8546daa..7e4993c 100644 void updateBindings(); diff --git a/private-include/indium/device.private.hpp b/private-include/indium/device.private.hpp -index 58f1794..5b2b30d 100644 +index 58f1794..75cdeb3 100644 --- a/private-include/indium/device.private.hpp +++ b/private-include/indium/device.private.hpp @@ -13,6 +13,8 @@ @@ -802,15 +802,16 @@ index 58f1794..5b2b30d 100644 std::mutex _eventLoopMutex; std::mutex _pollingMutex; -@@ -59,6 +63,7 @@ namespace Indium { +@@ -59,6 +63,8 @@ namespace Indium { } virtual std::string name() const override; + virtual bool supportsDepth24Stencil8PixelFormat() override; ++ virtual std::shared_ptr newCommandQueue() override; virtual std::shared_ptr newRenderPipelineState(const RenderPipelineDescriptor& descriptor) override; virtual std::shared_ptr newComputePipelineState(const ComputePipelineDescriptor& descriptor, PipelineOption options, std::shared_ptr reflection) override; -@@ -73,6 +78,12 @@ namespace Indium { +@@ -73,6 +79,12 @@ namespace Indium { virtual void pollEvents(uint64_t timeoutNanoseconds) override; virtual void wakeupEventLoop() override; @@ -823,7 +824,7 @@ index 58f1794..5b2b30d 100644 void waitForSemaphore(VkSemaphore semaphore, uint64_t targetValue, std::function callback); /** -@@ -86,6 +97,11 @@ namespace Indium { +@@ -86,6 +98,11 @@ namespace Indium { * Similar to getTimelineSemaphore(), but wraps it in a shared pointer * that automatically releases the semaphore when all references to it die out. */ @@ -835,7 +836,7 @@ index 58f1794..5b2b30d 100644 std::shared_ptr getWrappedTimelineSemaphore(); BinarySemaphore getBinarySemaphore(bool exportable = false); -@@ -111,5 +127,31 @@ namespace Indium { +@@ -111,5 +128,31 @@ namespace Indium { INDIUM_PROPERTY(VkPhysicalDeviceMemoryProperties, m, M,emoryProperties); INDIUM_PROPERTY_READONLY(Feature, f, F,eatures); @@ -2802,7 +2803,7 @@ index 5f62ad0..5957bec 100644 DynamicVK::vkCmdBindDescriptorSets(buf->commandBuffer(), VK_PIPELINE_BIND_POINT_COMPUTE, _pso->layout(), 0, descriptorSets.size(), descriptorSets.data(), 0, nullptr); }; diff --git a/src/indium/device.cpp b/src/indium/device.cpp -index 322e186..70c4a15 100644 +index 322e186..2003684 100644 --- a/src/indium/device.cpp +++ b/src/indium/device.cpp @@ -1,4 +1,6 @@ @@ -2812,7 +2813,7 @@ index 322e186..70c4a15 100644 #include #include #include -@@ -12,10 +14,12 @@ +@@ -12,39 +14,167 @@ #include @@ -2823,45 +2824,274 @@ index 322e186..70c4a15 100644 #include +#include ++#include ++#include ++#include #include ++#include ++#include #include -@@ -39,6 +43,15 @@ void Indium::initGlobalDeviceList() { + + std::vector> Indium::globalDeviceList; + ++namespace { ++ // One line per event, appended to the guest's /tmp/metal-device.log (host: ++ // ~/.darling/private/tmp/metal-device.log). Low volume: a few lines while the ++ // device list is built, plus one when the presenting OpenGL context first ++ // imports a Vulkan drawable. It exists so a user can tell us which GPU ++ // actually rendered a match, which the game's own logs and FPS numbers do not ++ // show. ++ void appendDeviceLog(const std::string& line) { ++ if (FILE* log = fopen("/tmp/metal-device.log", "a")) { ++ fprintf(log, "[MetalDevice] pid=%d %s\n", getpid(), line.c_str()); ++ fclose(log); ++ } ++ } ++ ++ std::string hexValue(unsigned long long value, int width) { ++ char buffer[32]; ++ snprintf(buffer, sizeof(buffer), "%0*llx", width, value); ++ return buffer; ++ } ++ ++ std::string uuidToString(const uint8_t uuid[16]) { ++ static const char* digits = "0123456789abcdef"; ++ std::string out; ++ out.reserve(32); ++ for (int i = 0; i < 16; ++i) { ++ out += digits[(uuid[i] >> 4) & 0xf]; ++ out += digits[uuid[i] & 0xf]; ++ } ++ return out; ++ } ++ ++ const char* deviceTypeName(VkPhysicalDeviceType type) { ++ switch (type) { ++ case VK_PHYSICAL_DEVICE_TYPE_INTEGRATED_GPU: return "integrated"; ++ case VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU: return "discrete"; ++ case VK_PHYSICAL_DEVICE_TYPE_VIRTUAL_GPU: return "virtual"; ++ case VK_PHYSICAL_DEVICE_TYPE_CPU: return "cpu"; ++ default: return "other"; ++ } ++ } ++ ++ struct GpuSelector { ++ std::string value; ++ std::string source; // empty when the loader's default order should decide ++ }; ++ ++ // Explicit selection, in the style of the other opt-in guest files. The guest ++ // file /tmp/metal-gpu wins over the METAL_INDIUM_GPU environment variable: the ++ // file is scoped to the guest and does not depend on the environment reaching ++ // the game through the Riot/League client. The value is either the index of a ++ // supported device or a case-insensitive substring of its Vulkan name, so ++ // "nvidia", "amd" or "llvmpipe" all work. ++ GpuSelector readGpuSelector() { ++ if (FILE* file = fopen("/tmp/metal-gpu", "r")) { ++ char buffer[256] = {0}; ++ char* got = fgets(buffer, sizeof(buffer), file); ++ fclose(file); ++ if (got) { ++ std::string value(buffer); ++ while (!value.empty() && (value.back() == '\n' || value.back() == '\r' || value.back() == ' ' || value.back() == '\t')) { ++ value.pop_back(); ++ } ++ if (!value.empty()) return { value, "file:/tmp/metal-gpu" }; ++ } ++ } ++ if (const char* env = getenv("METAL_INDIUM_GPU")) { ++ if (env[0]) return { std::string(env), "env:METAL_INDIUM_GPU" }; ++ } ++ return {}; ++ } ++ ++ bool isAllDigits(const std::string& value) { ++ return !value.empty() && std::all_of(value.begin(), value.end(), [](unsigned char c) { return std::isdigit(c) != 0; }); ++ } ++ ++ bool nameContains(const std::string& name, const std::string& needle) { ++ return std::search(name.begin(), name.end(), needle.begin(), needle.end(), [](char a, char b) { ++ return std::tolower((unsigned char) a) == std::tolower((unsigned char) b); ++ }) != name.end(); ++ } ++ ++ struct DeviceCandidate { ++ VkPhysicalDevice device; ++ VkPhysicalDeviceProperties properties; ++ uint8_t deviceUUID[16]; ++ uint8_t driverUUID[16]; ++ }; ++} ++ + void Indium::initGlobalDeviceList() { + std::vector physicalDevices; + uint32_t count = 0; + + auto result = DynamicVK::vkEnumeratePhysicalDevices(globalInstance, &count, nullptr); + if (result != VK_SUCCESS && result != VK_INCOMPLETE) { +- // TODO: maybe warn? ++ appendDeviceLog("vkEnumeratePhysicalDevices failed when counting devices; Metal will have no device"); return; } -+ // A VkDevice is created (and its driver initialized) for every supported physical device below, and -+ // createSystemDefaultDevice() takes the first entry. Software devices (lavapipe) can be skipped -- a VkDevice on -+ // one of them starts its rasterizer pool (nproc llvmpipe threads, idle for a whole match) and the loader may order -+ // it first. Only skip them while some other supported device exists. OFF by default (round-3 behaviour: a device is -+ // created for every supported device, in the loader's order); guest flag file /tmp/metal-skip-cpu-devices turns the -+ // skip on. It is read here, at load time, because this list is built once. + physicalDevices.resize(count); + result = DynamicVK::vkEnumeratePhysicalDevices(globalInstance, &count, physicalDevices.data()); + if (result != VK_SUCCESS && result != VK_INCOMPLETE) { +- // TODO: maybe warn? ++ appendDeviceLog("vkEnumeratePhysicalDevices failed when listing devices; Metal will have no device"); + return; + } + ++ // The loader's order used to decide the default device (createSystemDefaultDevice ++ // returns the first entry). On a hybrid laptop that is usually the integrated ++ // GPU the display is wired to, not the discrete GPU. Record every device the ++ // loader lists, then choose explicitly. ++ GpuSelector selector = readGpuSelector(); + const bool skipCPUDevices = access("/tmp/metal-skip-cpu-devices", F_OK) == 0; -+ std::vector> candidates; ++ const bool allDevices = access("/tmp/metal-all-devices", F_OK) == 0; ++ appendDeviceLog("selector=" + (selector.source.empty() ? std::string("none") : selector.source + " value=\"" + selector.value + "\"") ++ + " skipCPUDevices=" + (skipCPUDevices ? "1" : "0") + " allDevices=" + (allDevices ? "1" : "0") ++ + " physicalDeviceCount=" + std::to_string(physicalDevices.size())); ++ ++ std::vector candidates; + for (auto&& device: physicalDevices) { - VkPhysicalDeviceProperties props; - DynamicVK::vkGetPhysicalDeviceProperties(device, &props); -@@ -71,7 +84,18 @@ void Indium::initGlobalDeviceList() { +- VkPhysicalDeviceProperties props; +- DynamicVK::vkGetPhysicalDeviceProperties(device, &props); ++ VkPhysicalDeviceProperties properties; ++ DynamicVK::vkGetPhysicalDeviceProperties(device, &properties); ++ ++ VkPhysicalDeviceIDProperties idProperties {}; ++ idProperties.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_ID_PROPERTIES; ++ VkPhysicalDeviceProperties2 properties2 {}; ++ properties2.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2; ++ properties2.pNext = &idProperties; ++ if (DynamicVK::vkGetPhysicalDeviceProperties2) { ++ DynamicVK::vkGetPhysicalDeviceProperties2(device, &properties2); ++ } + +- if (VK_API_VERSION_VARIANT(props.apiVersion) != 0 || props.apiVersion < VK_API_VERSION_1_3) { +- // unsupported device ++ std::string line = "physical name=\"" + std::string(properties.deviceName) + "\"" ++ + " type=" + deviceTypeName(properties.deviceType) ++ + " vendorID=0x" + hexValue(properties.vendorID, 4) ++ + " driverVersion=0x" + hexValue(properties.driverVersion, 8) ++ + " apiVersion=" + std::to_string(VK_API_VERSION_MAJOR(properties.apiVersion)) + "." ++ + std::to_string(VK_API_VERSION_MINOR(properties.apiVersion)) + "." ++ + std::to_string(VK_API_VERSION_PATCH(properties.apiVersion)) ++ + " deviceUUID=" + uuidToString(idProperties.deviceUUID) ++ + " driverUUID=" + uuidToString(idProperties.driverUUID); ++ ++ if (VK_API_VERSION_VARIANT(properties.apiVersion) != 0 || properties.apiVersion < VK_API_VERSION_1_3) { ++ appendDeviceLog(line + " supported=no reason=apiVersion"); + continue; + } + +@@ -67,12 +197,101 @@ void Indium::initGlobalDeviceList() { + DynamicVK::vkGetPhysicalDeviceFeatures2(device, &features); + + if (!features12.timelineSemaphore) { +- // unsupported device ++ appendDeviceLog(line + " supported=no reason=timelineSemaphore"); continue; } - globalDeviceList.push_back(std::make_shared(std::move(device))); -+ candidates.emplace_back(device, props.deviceType); ++ appendDeviceLog(line + " supported=yes"); ++ DeviceCandidate candidate {}; ++ candidate.device = device; ++ candidate.properties = properties; ++ memcpy(candidate.deviceUUID, idProperties.deviceUUID, 16); ++ memcpy(candidate.driverUUID, idProperties.driverUUID, 16); ++ candidates.push_back(candidate); + } ++ ++ // Resolve the explicit selection against the supported list. An explicit choice ++ // is honoured even when it names a software (CPU) device, because the user asked ++ // for it; the skip-CPU rule only affects the automatic default. ++ long selectedIndex = -1; ++ std::string selectedReason; ++ ++ if (!selector.source.empty()) { ++ if (isAllDigits(selector.value)) { ++ long index = strtol(selector.value.c_str(), nullptr, 10); ++ if (index >= 0 && index < (long) candidates.size()) { ++ selectedIndex = index; ++ selectedReason = selector.source + " index " + selector.value; ++ } ++ } ++ if (selectedIndex < 0) { ++ for (size_t i = 0; i < candidates.size(); ++i) { ++ if (nameContains(candidates[i].properties.deviceName, selector.value)) { ++ selectedIndex = (long) i; ++ selectedReason = selector.source + " name~=\"" + selector.value + "\""; ++ break; ++ } ++ } ++ } ++ if (selectedIndex < 0) { ++ appendDeviceLog("selector " + selector.source + " value=\"" + selector.value + "\" matched no supported device; falling back to the loader default"); ++ } + } + -+ const bool haveHardwareDevice = skipCPUDevices && std::any_of(candidates.begin(), candidates.end(), [](const auto& candidate) { -+ return candidate.second != VK_PHYSICAL_DEVICE_TYPE_CPU; ++ const bool haveHardwareDevice = std::any_of(candidates.begin(), candidates.end(), [](const DeviceCandidate& candidate) { ++ return candidate.properties.deviceType != VK_PHYSICAL_DEVICE_TYPE_CPU; + }); + -+ for (auto&& candidate: candidates) { -+ if (haveHardwareDevice && candidate.second == VK_PHYSICAL_DEVICE_TYPE_CPU) { -+ continue; ++ if (selectedIndex < 0) { ++ for (size_t i = 0; i < candidates.size(); ++i) { ++ if (skipCPUDevices && haveHardwareDevice && candidates[i].properties.deviceType == VK_PHYSICAL_DEVICE_TYPE_CPU) { ++ continue; ++ } ++ selectedIndex = (long) i; ++ selectedReason = skipCPUDevices ? "loader-first-hardware" : "loader-first"; ++ break; + } -+ globalDeviceList.push_back(std::make_shared(std::move(candidate.first))); - } ++ } ++ ++ if (selectedIndex < 0) { ++ appendDeviceLog("no usable supported Vulkan device found; Metal will have no device"); ++ return; ++ } ++ ++ DeviceCandidate& selected = candidates[selectedIndex]; ++ appendDeviceLog("selected name=\"" + std::string(selected.properties.deviceName) + "\"" ++ + " type=" + deviceTypeName(selected.properties.deviceType) ++ + " vendorID=0x" + hexValue(selected.properties.vendorID, 4) ++ + " deviceUUID=" + uuidToString(selected.deviceUUID) ++ + " reason=" + (selectedReason.empty() ? std::string("loader-first") : selectedReason)); ++ ++ // A VkDevice is created for the selected device only: its driver is initialized ++ // and, on a hybrid laptop, the discrete GPU is not woken for a device the ++ // rendering GPU cannot share memory with anyway. The old behaviour -- a VkDevice ++ // for every supported device, in the loader's order -- stays reachable with the ++ // opt-in guest file /tmp/metal-all-devices. MTLCopyAllDevices only ever exposes ++ // the default device, so nothing in the client depends on the full list. ++ std::vector creationOrder; ++ creationOrder.push_back((size_t) selectedIndex); ++ if (allDevices) { ++ for (size_t i = 0; i < candidates.size(); ++i) { ++ if (i == (size_t) selectedIndex) { ++ continue; ++ } ++ if (skipCPUDevices && haveHardwareDevice && candidates[i].properties.deviceType == VK_PHYSICAL_DEVICE_TYPE_CPU) { ++ continue; ++ } ++ creationOrder.push_back(i); ++ } ++ } ++ ++ for (size_t index: creationOrder) { ++ globalDeviceList.push_back(std::make_shared(candidates[index].device)); ++ } ++ ++ appendDeviceLog("created VkDevice for " + std::to_string(globalDeviceList.size()) + " of " ++ + std::to_string(candidates.size()) + " supported device(s)" ++ + (allDevices ? " (allDevices)" : " (selected only)")); }; -@@ -83,7 +107,8 @@ Indium::Device::~Device() {}; + void Indium::finitGlobalDeviceList() { +@@ -83,7 +302,8 @@ Indium::Device::~Device() {}; Indium::PrivateDevice::PrivateDevice(VkPhysicalDevice physicalDevice): _physicalDevice(physicalDevice) @@ -2871,7 +3101,7 @@ index 322e186..70c4a15 100644 DynamicVK::vkGetPhysicalDeviceProperties(_physicalDevice, &_properties); std::vector queueFamilies; -@@ -233,6 +258,36 @@ Indium::PrivateDevice::PrivateDevice(VkPhysicalDevice physicalDevice): +@@ -233,6 +453,36 @@ Indium::PrivateDevice::PrivateDevice(VkPhysicalDevice physicalDevice): } } @@ -2908,7 +3138,7 @@ index 322e186..70c4a15 100644 VkDeviceCreateInfo deviceCreateInfo {}; deviceCreateInfo.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO; deviceCreateInfo.pQueueCreateInfos = queueCreateInfos.data(); -@@ -246,6 +301,7 @@ Indium::PrivateDevice::PrivateDevice(VkPhysicalDevice physicalDevice): +@@ -246,6 +496,7 @@ Indium::PrivateDevice::PrivateDevice(VkPhysicalDevice physicalDevice): } _features = indiumFeatures; @@ -2916,7 +3146,7 @@ index 322e186..70c4a15 100644 for (const auto& index: queueFamilyIndices) { VkQueue queue; -@@ -297,6 +353,9 @@ Indium::PrivateDevice::PrivateDevice(VkPhysicalDevice physicalDevice): +@@ -297,6 +548,9 @@ Indium::PrivateDevice::PrivateDevice(VkPhysicalDevice physicalDevice): }; Indium::PrivateDevice::~PrivateDevice() { @@ -2926,7 +3156,7 @@ index 322e186..70c4a15 100644 if (_oneshotCommandPool) { DynamicVK::vkDestroyCommandPool(_device, _oneshotCommandPool, nullptr); } -@@ -308,6 +367,23 @@ std::string Indium::PrivateDevice::name() const { +@@ -308,6 +562,23 @@ std::string Indium::PrivateDevice::name() const { return _properties.deviceName; }; @@ -2950,7 +3180,7 @@ index 322e186..70c4a15 100644 std::shared_ptr Indium::PrivateDevice::newCommandQueue() { return std::make_shared(shared_from_this()); }; -@@ -317,9 +393,9 @@ std::shared_ptr Indium::PrivateDevice::newRenderPip +@@ -317,9 +588,9 @@ std::shared_ptr Indium::PrivateDevice::newRenderPip }; std::shared_ptr Indium::PrivateDevice::newComputePipelineState(const ComputePipelineDescriptor& descriptor, PipelineOption options, std::shared_ptr reflection) { @@ -2963,7 +3193,7 @@ index 322e186..70c4a15 100644 return std::make_shared(shared_from_this(), descriptor); }; -@@ -357,6 +433,7 @@ std::shared_ptr Indium::PrivateDevice::newLibrary(const void* d +@@ -357,6 +628,7 @@ std::shared_ptr Indium::PrivateDevice::newLibrary(const void* d break; } @@ -2971,7 +3201,7 @@ index 322e186..70c4a15 100644 funcInfo.bindings.insert(funcInfo.bindings.end(), info.bindings.begin(), info.bindings.end()); funcInfo.embeddedSamplers.insert(funcInfo.embeddedSamplers.end(), info.embeddedSamplers.begin(), info.embeddedSamplers.end()); } -@@ -480,9 +557,18 @@ void Indium::PrivateDevice::waitForSemaphore(VkSemaphore semaphore, uint64_t tar +@@ -480,9 +752,18 @@ void Indium::PrivateDevice::waitForSemaphore(VkSemaphore semaphore, uint64_t tar wakeupEventLoop(); }; @@ -2991,7 +3221,7 @@ index 322e186..70c4a15 100644 VkSemaphoreTypeCreateInfo typeInfo {}; typeInfo.sType = VK_STRUCTURE_TYPE_SEMAPHORE_TYPE_CREATE_INFO; typeInfo.semaphoreType = VK_SEMAPHORE_TYPE_TIMELINE; -@@ -505,7 +591,14 @@ Indium::TimelineSemaphore Indium::PrivateDevice::getTimelineSemaphore() { +@@ -505,7 +786,14 @@ Indium::TimelineSemaphore Indium::PrivateDevice::getTimelineSemaphore() { }; void Indium::PrivateDevice::putTimelineSemaphore(const TimelineSemaphore& semaphore) { @@ -3007,7 +3237,7 @@ index 322e186..70c4a15 100644 }; std::shared_ptr Indium::PrivateDevice::getWrappedTimelineSemaphore() { -@@ -551,3 +644,91 @@ std::shared_ptr Indium::PrivateDevice::getWrappedBinary +@@ -551,3 +839,91 @@ std::shared_ptr Indium::PrivateDevice::getWrappedBinary delete ptr; }); };