Fix Metal device edge cases; keep reporting the host GPU

From #61, without the default "AMD Radeon Pro 5500M" / 4 GB identity: the
host GPU's name and Vulkan working set stay the default for every app, and
the existing DARLING_METAL_GPU_NAME and DARLING_METAL_VRAM_MB overrides
remain the opt-in. Reporting 4 GB on a 2 GB card invites allocation
failures, and under-reports larger cards.

Kept from #61:
- registryID returns MTLDeviceInfo::kDefaultRegistryID instead of 0; a
  real Mac never reports 0 and apps key devices by it.
- Vertex buffer layout and attribute arrays release the old item after
  retiring it; the retire step read the item after the release could free it.
- newDepthStencilState, newSamplerState and the device name return nil (or
  the fallback name) when Indium throws, instead of unwinding through
  Objective-C.
- MTLDepthStencilGetIndium is declared only with DARLING_METAL_ENABLED.
- The replaceRegion trace probes at most `width` bytes, which stays inside
  the first row for every pixel format.

metal.patch is regenerated in its existing file order, so the diff is
these changes only. tests/test_metal_device_info.py compiles the host
check against MTLDeviceInfo.h as shipped in the patch and runs in CI.

Split from #61.

Co-authored-by: DevWebeloper <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
committed 2026-10-18 17:14:47 +02:00
1 parent c943a19606
commit e66bd9a394
6 files changed
+107 -25

No files matched your search

+46
View File
@@ -0,0 +1,46 @@
"""Compile tools/test-metal-device-info.cpp against MTLDeviceInfo.h as shipped in patches/metal.patch."""
from pathlib import Path
import re
import shutil
import subprocess
import tempfile
import unittest
ROOT = Path(__file__).resolve().parents[1]
HEADER = 'private-include/Metal/MTLDeviceInfo.h'
def header_from_patch():
"""MTLDeviceInfo.h is a new file in the patch, so its text is the section's added lines."""
patch = (ROOT / 'patches/metal.patch').read_text()
section = re.search(r'(?ms)^diff --git a/%s b/%s\n.*?^@@ [^\n]*\n(.*?)(?=^diff --git |\Z)' % (HEADER, HEADER), patch)
if not section:
raise AssertionError(HEADER + ' is not a new file in patches/metal.patch')
lines = section.group(1).splitlines(keepends=True)
if not all(line.startswith('+') for line in lines):
raise AssertionError(HEADER + ' section has context or removed lines')
return ''.join(line[1:] for line in lines)
class MetalDeviceInfoTests(unittest.TestCase):
def test_host_check_passes(self):
compiler = shutil.which('c++')
if not compiler:
self.skipTest('no C++ compiler')
with tempfile.TemporaryDirectory() as folder:
include = Path(folder) / 'include/Metal'
include.mkdir(parents=True)
(include / 'MTLDeviceInfo.h').write_text(header_from_patch())
binary = Path(folder) / 'test-metal-device-info'
build = subprocess.run([compiler, '-std=c++17', '-Wall', '-Werror', '-I', str(include.parent),
str(ROOT / 'tools/test-metal-device-info.cpp'), '-o', str(binary)],
capture_output=True, text=True)
self.assertEqual(build.returncode, 0, build.stderr)
result = subprocess.run([str(binary)], capture_output=True, text=True, timeout=30)
self.assertEqual(result.returncode, 0, result.stdout)
self.assertIn('METAL_DEVICE_INFO failures=0 pass=1', result.stdout)
self.assertIn('PASS registry ID is non-zero', result.stdout)
if __name__ == '__main__':
unittest.main()