diff --git a/docs/TROUBLESHOOTING.md b/docs/TROUBLESHOOTING.md index 6cf198f..dcd3338 100644 --- a/docs/TROUBLESHOOTING.md +++ b/docs/TROUBLESHOOTING.md @@ -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--`, 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: diff --git a/scripts/preflight.py b/scripts/preflight.py index 3dc72f3..bd9bc4b 100644 --- a/scripts/preflight.py +++ b/scripts/preflight.py @@ -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): diff --git a/tests/test_preflight.py b/tests/test_preflight.py index d999061..b755e4e 100644 --- a/tests/test_preflight.py +++ b/tests/test_preflight.py @@ -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):