fix(preflight): require Vulkan 1.3 and timeline semaphores on the same device

vulkan_devices() flattened every apiVersion and check_vulkan() accepted any
timelineSemaphore = true, so a host with an old fast device and a new device
without timeline semaphores could pass without a usable device. Each GPUn
block is now one record, and one device must report both.

The full dump supplies the features and --summary the versions, matched by
GPU index. A dump without feature sections still gets the version check, and a
summary that lists no device is a required failure.

Adds three tests: a second device satisfies both, requirements are not
combined across devices, and no reported device.
This commit is contained in:
MintMachineHQ committed 2026-10-25 14:17:28 -06:00
1 parent c19c9ad47e
commit 6a00849048
3 files changed
+110 -29

No files matched your search

+1 -1
View File
@@ -31,7 +31,7 @@ Use the [setup instructions](SETUP.md) from the repository root as your ordinary
- **kernel** 5.14 or newer for `MADV_POPULATE_WRITE`.
- **llvm** 16 or newer; 22 or 23 is what the patches are tested with. Iridium calls `LLVMGetVersion`, which LLVM 15 does not export, so an LLVM 15 runtime aborts as soon as Riot creates a Metal device ([#139](https://github.com/lucannez64/darling-lol/issues/139)). LLVM 16-21 builds with a warning.
- **llvm-tools**: `llvm-objdump` and `llvm-nm`. They are now resolved as an unversioned name, as `llvm-<tool>-<N>`, or from `llvm-config --bindir`, so Ubuntu's `llvm-15-dev` no longer needs symlinks in `/usr/local/bin`.
- **vulkan** API 1.3 and timeline semaphores. Indium rejects older devices, so this stops the setup with the version `vulkaninfo` reports. An installed `vulkaninfo` that fails or times out is also required to be fixed, because the loader or the ICD files are then broken; without `vulkaninfo` at all it only warns, because the tool may be missing even when the driver is fine.
- **vulkan** API 1.3 and timeline semaphores. Indium rejects older devices, so this stops the setup unless one device reports both the version and the semaphores, as `vulkaninfo` shows per GPU. An installed `vulkaninfo` that fails or times out is also required to be fixed, because the loader or the ICD files are then broken; without `vulkaninfo` at all it only warns, because the tool may be missing even when the driver is fine.
- **x11** only warns: `DISPLAY` is probed with `xdpyinfo`, and the GL/EGL vendor is reported when a probe is installed. An inaccessible display is what made `iokitd` restart in a loop before [#140](https://github.com/lucannez64/darling-lol/issues/140); export `XAUTHORITY` from servers and SSH sessions. A build without a display is still valid.
Run the same checks by hand, from the checkout:
+60 -17
View File
@@ -149,15 +149,57 @@ def check_llvm_tools(report):
'The runtime install validates Darwin binaries with them.')
def gpu_blocks(text):
"""{index: [lines]} for each `GPUn:` block in vulkaninfo output."""
blocks = {}
current = None
for line in (text or '').splitlines():
header = re.match(r'GPU(\d+):\s*$', line)
if header:
current = blocks.setdefault(int(header.group(1)), [])
continue
if current is not None:
current.append(line)
return blocks
def first_match(pattern, lines):
for line in lines:
found = re.search(pattern, line)
if found:
return found.group(1).strip()
return None
def vulkan_devices():
"""(highest apiVersion, device names) from vulkaninfo, or None when it is not available."""
"""One record per Vulkan device, or None when vulkaninfo does not run.
A device is kept as a whole record so a host with an old fast GPU and a new
GPU without timeline semaphores cannot pass by combining the two. The full
dump carries the features; `--summary` carries the versions that every
version of the tool prints.
"""
summary = output(['vulkaninfo', '--summary'])
if summary is None:
return None
api = [version(found) for found in re.findall(r'apiVersion\s*=\s*(\d+\.\d+)', summary)]
api = [value for value in api if value]
names = re.findall(r'deviceName\s*=\s*(.+)', summary)
return (max(api) if api else None, [name.strip() for name in names])
features = gpu_blocks(output(['vulkaninfo'], timeout=60))
devices = []
for index, lines in sorted(gpu_blocks(summary).items()):
found = features.get(index, [])
values = [value == 'true'
for value in re.findall(r'timelineSemaphore\s*=\s*(true|false)',
'\n'.join(found))]
devices.append({'name': first_match(r'deviceName\s*=\s*(.+)', lines),
'api': version(first_match(r'apiVersion\s*=\s*(\d+\.\d+)', lines)),
'timeline': any(values) if values else None})
return devices
def describe_device(device):
text = f'{device["name"] or "unnamed device"} {dotted(device["api"])}'
if device['timeline'] is not None:
text += ', timelineSemaphore ' + ('true' if device['timeline'] else 'false')
return text
def check_vulkan(report):
@@ -174,19 +216,20 @@ def check_vulkan(report):
'timeline semaphores; without a suitable device the game cannot render.',
required=False)
return
api, names = devices
detail = f'apiVersion {dotted(api)}' + (f' ({", ".join(names[:2])})' if names else '')
report('vulkan', api is not None and api >= MIN_VULKAN, detail,
f'Vulkan {dotted(MIN_VULKAN)} or newer is required: Indium rejects older devices. '
'Update Mesa or the vendor driver, or check `vulkaninfo --summary`.')
feature = output(['vulkaninfo'], timeout=60)
if feature is None:
if not devices:
report('vulkan', False, 'no devices reported',
'`vulkaninfo --summary` listed no Vulkan device. Check the loader and the ICD '
'files, or reinstall the driver: the game cannot render without a device.')
return
values = re.findall(r'timelineSemaphore\s*=\s*(true|false)', feature)
if values and 'true' not in values:
report('vulkan-timeline', False, 'timelineSemaphore = false',
'No Vulkan device reports timeline semaphores, which the Metal wrapper needs; '
'update the driver or select another ICD.')
# A dump that carries no feature section still gets the version check, as before.
checked = any(device['timeline'] is not None for device in devices)
usable = [device for device in devices
if device['api'] is not None and device['api'] >= MIN_VULKAN
and (device['timeline'] or not checked)]
report('vulkan', bool(usable), '; '.join(describe_device(device) for device in devices),
f'Vulkan {dotted(MIN_VULKAN)} or newer and timeline semaphores on the same device are '
'required: Indium rejects older devices and uses the semaphores. Update Mesa or the '
'vendor driver, or check `vulkaninfo`.')
def check_display(report):
+49 -11
View File
@@ -40,12 +40,24 @@ esac'''.replace('LLVM_BINDIR', str(self.llvm_bindir)))
self.stub('llvm-nm', 'exit 0')
self.stub('cmake', 'echo "cmake version 3.31.6"')
self.stub('git', 'case "$*" in "lfs version") echo "git-lfs/3.7.0 (GitHub; linux amd64)";; esac')
self.stub('vulkaninfo', '''case "$*" in
--summary) printf 'apiVersion = 1.3.289\\ndeviceName = Test GPU\\n';;
*) printf 'timelineSemaphore = true\\n';;
esac''')
self.vulkan([('1.3.289', 'Test GPU', True)])
self.stub('xdpyinfo', 'exit 0')
def vulkan(self, devices):
"""Stub vulkaninfo with [(apiVersion, name, timeline)] per device."""
summary = ['Devices:', '========']
features = ['Device Properties and Extensions:', '=================================']
for index, (api, name, timeline) in enumerate(devices):
summary += [f'GPU{index}:', f'\tapiVersion = {api}', f'\tdeviceName = {name}']
features += [f'GPU{index}:', 'VkPhysicalDeviceProperties:',
f'\tapiVersion = {api}', f'\tdeviceName = {name}']
if timeline is not None:
features += ['VkPhysicalDeviceVulkan12Features:',
f'\ttimelineSemaphore = {"true" if timeline else "false"}']
self.stub('vulkaninfo', 'case "$*" in\n'
" --summary) printf '%s\\n' " + ' '.join(f"'{line}'" for line in summary) + ';;\n'
" *) printf '%s\\n' " + ' '.join(f"'{line}'" for line in features) + ';;\nesac')
def stub(self, name, body, folder=None):
path = (folder or self.bin) / name
path.write_text('#!/bin/sh\nset -eu\n' + body + '\n')
@@ -152,11 +164,12 @@ esac''')
def test_old_vulkan_stops_the_setup(self):
self.stub('vulkaninfo', 'echo "apiVersion = 1.2.170"')
self.vulkan([('1.2.170', 'Old GPU', True)])
result = self.preflight()
self.assertEqual(result.returncode, 1)
self.assertIn('FAIL vulkan', result.stdout)
self.assertIn('Vulkan 1.3 or newer is required', result.stdout)
self.assertIn('Vulkan 1.3 or newer', result.stdout)
self.assertIn('Old GPU 1.2', result.stdout)
def test_missing_vulkaninfo_only_warns(self):
@@ -178,13 +191,38 @@ esac''')
def test_missing_timeline_semaphores_is_reported(self):
self.stub('vulkaninfo', '''case "$*" in
--summary) echo 'apiVersion = 1.3.289';;
*) echo 'timelineSemaphore = false';;
esac''')
self.vulkan([('1.3.289', 'Test GPU', False)])
result = self.preflight()
self.assertEqual(result.returncode, 1)
self.assertIn('FAIL vulkan-timeline', result.stdout)
self.assertIn('FAIL vulkan', result.stdout)
self.assertIn('timelineSemaphore false', result.stdout)
self.assertIn('on the same device', result.stdout)
def test_a_second_device_can_satisfy_both_requirements(self):
self.vulkan([('1.4.309', 'New GPU without semaphores', False),
('1.4.309', 'Second GPU', True)])
result = self.preflight()
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
self.assertIn('ok vulkan', result.stdout)
def test_requirements_are_not_combined_across_devices(self):
self.vulkan([('1.2.170', 'Old GPU with semaphores', True),
('1.4.309', 'New GPU without semaphores', False)])
result = self.preflight()
self.assertEqual(result.returncode, 1)
self.assertIn('FAIL vulkan', result.stdout)
self.assertIn('Old GPU with semaphores 1.2', result.stdout)
self.assertIn('New GPU without semaphores 1.4', result.stdout)
def test_no_reported_device_is_required(self):
self.vulkan([])
result = self.preflight()
self.assertEqual(result.returncode, 1)
self.assertIn('FAIL vulkan', result.stdout)
self.assertIn('no devices reported', result.stdout)
def test_unreachable_display_only_warns(self):