Log the Metal GPU and create a Vulkan device only for it

On a hybrid laptop (issue #3) Indium took whichever Vulkan device the
loader listed first, usually the integrated GPU, and still created a
VkDevice on every other supported GPU, waking a discrete GPU it never
used. Drawables reach the presenting OpenGL context as opaque FDs, which
only work on one physical device, and nothing checked that.

Indium now appends the device list, the selected device and the reason
to guest /tmp/metal-device.log, and creates a VkDevice for the selected
device only (/tmp/metal-all-devices restores the old behaviour).
/tmp/metal-gpu or METAL_INDIUM_GPU selects a device by index or name
for diagnosis. QuartzCore logs the presenting GL renderer and compares
its device UUID with the Vulkan device's (match=yes / MISMATCH); it
queries Vulkan itself, so it needs no new libindium export.

On the RX 6600 desktop the game selected RADV, created 1 VkDevice of 2
supported (lavapipe no longer gets one) and logged match=yes in a match.
Running on NVIDIA through prime-run is documented but not validated.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
lucannez64andClaude Opus 5.5 committed 2026-10-16 00:24:58 +02:00
1 parent 83e1db8758
commit 3cf599c62e
5 files changed
+418 -41

No files matched your search

+1 -1
View File
@@ -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.
+2
View File
@@ -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.
+33
View File
@@ -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`:
+116 -4
View File
@@ -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 <OpenGL/glext.h>
#include <indium/indium.private.hpp>
+#include <cstdio>
+#include <cstring>
+#include <mutex>
+#include <string>
+#include <unistd.h>
+
namespace DynamicVK = Indium::DynamicVK;
static void reportGLErrors(void) {
@@ -50,8 +56,86 @@ static void reportGLErrors(void) {
namespace Indium::DynamicVK {
static DynamicFunction<PFN_vkGetSemaphoreFdKHR> vkGetSemaphoreFdKHR("vkGetSemaphoreFdKHR");
static DynamicFunction<PFN_vkGetMemoryFdKHR> vkGetMemoryFdKHR("vkGetMemoryFdKHR");
+ static DynamicFunction<PFN_vkGetPhysicalDeviceProperties2> 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<Indium::PrivateDevice> 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<const char*>(renderer) : "(null)") + "\""
+ + " vendor=\"" + std::string(vendor ? reinterpret_cast<const char*>(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() {
+266 -36
View File
@@ -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<CommandQueue> newCommandQueue() override;
virtual std::shared_ptr<RenderPipelineState> newRenderPipelineState(const RenderPipelineDescriptor& descriptor) override;
virtual std::shared_ptr<ComputePipelineState> newComputePipelineState(const ComputePipelineDescriptor& descriptor, PipelineOption options, std::shared_ptr<ComputePipelineReflection> 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<void()> 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<TimelineSemaphore> 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 <indium/instance.private.hpp>
#include <indium/command-queue.private.hpp>
#include <indium/render-pipeline.private.hpp>
@@ -12,10 +14,12 @@
@@ -12,39 +14,167 @@
#include <iridium/iridium.hpp>
@@ -2823,45 +2824,274 @@ index 322e186..70c4a15 100644
#include <unordered_set>
+#include <utility>
+#include <cctype>
+#include <cstdio>
+#include <cstdlib>
#include <cstring>
+#include <string>
+#include <unistd.h>
#include <vector>
@@ -39,6 +43,15 @@ void Indium::initGlobalDeviceList() {
std::vector<std::shared_ptr<Indium::PrivateDevice>> 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<VkPhysicalDevice> 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<std::pair<VkPhysicalDevice, VkPhysicalDeviceType>> 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<DeviceCandidate> 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<PrivateDevice>(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<PrivateDevice>(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<size_t> 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<PrivateDevice>(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<VkQueueFamilyProperties> 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::CommandQueue> Indium::PrivateDevice::newCommandQueue() {
return std::make_shared<PrivateCommandQueue>(shared_from_this());
};
@@ -317,9 +393,9 @@ std::shared_ptr<Indium::RenderPipelineState> Indium::PrivateDevice::newRenderPip
@@ -317,9 +588,9 @@ std::shared_ptr<Indium::RenderPipelineState> Indium::PrivateDevice::newRenderPip
};
std::shared_ptr<Indium::ComputePipelineState> Indium::PrivateDevice::newComputePipelineState(const ComputePipelineDescriptor& descriptor, PipelineOption options, std::shared_ptr<ComputePipelineReflection> reflection) {
@@ -2963,7 +3193,7 @@ index 322e186..70c4a15 100644
return std::make_shared<PrivateComputePipelineState>(shared_from_this(), descriptor);
};
@@ -357,6 +433,7 @@ std::shared_ptr<Indium::Library> Indium::PrivateDevice::newLibrary(const void* d
@@ -357,6 +628,7 @@ std::shared_ptr<Indium::Library> 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::TimelineSemaphore> Indium::PrivateDevice::getWrappedTimelineSemaphore() {
@@ -551,3 +644,91 @@ std::shared_ptr<Indium::BinarySemaphore> Indium::PrivateDevice::getWrappedBinary
@@ -551,3 +839,91 @@ std::shared_ptr<Indium::BinarySemaphore> Indium::PrivateDevice::getWrappedBinary
delete ptr;
});
};