mirror of
https://github.com/lucannez64/darling-lol.git
synced 2026-10-26 19:47:42 +00:00
Merge branch 'main' into fix/x11-drag-and-drop
This commit is contained in:
commit
1e38e01cb1
22 files changed
+893
-64
No files matched your search
@@ -12,8 +12,12 @@ jobs:
|
||||
python-version: '3.12'
|
||||
- run: python scripts/validate.py
|
||||
- run: python tools/riot-input-offline-check.py
|
||||
- run: python tools/riot-launcher-offline-check.py
|
||||
- run: python tools/riot-runtime-cleanup-offline-check.py
|
||||
- run: python tools/riot-cdn-dns-offline-check.py
|
||||
- run: python -m unittest discover -s tests -p 'test_hidden_riot_recovery.py'
|
||||
- run: python -m unittest discover -s tests -p 'test_desktop_run.py'
|
||||
- run: python -m unittest discover -s tests -p 'test_urlsession_runner.py'
|
||||
- run: python -m unittest discover -s tests -p 'test_system_profiler.py'
|
||||
- run: python -m unittest discover -s tests -p 'test_metal_device_info.py'
|
||||
- run: python -m unittest discover -s tests -p 'test_exec_guest.py'
|
||||
+27
-4
@@ -24,6 +24,9 @@ if (workspace / '.launcher.lock').is_symlink():
|
||||
raise SystemExit('Refusing symlink workspace lock')
|
||||
PY
|
||||
mkdir -p "$WORKSPACE"
|
||||
if [ "${1:-}" = --stop ]; then
|
||||
exec python3 "$SHARE_DIR/scripts/nix-stop.py" "$WORKSPACE" "$DPREFIX"
|
||||
fi
|
||||
# One mutable base runtime per workspace; do not refresh scripts mid-session.
|
||||
exec 9<>"$WORKSPACE/.launcher.lock"
|
||||
flock -n 9 || { echo 'Another darling-lol workspace session is active; use its terminal.' >&2; exit 1; }
|
||||
@@ -35,7 +38,7 @@ if [ -f "$DPREFIX/.init.pid" ]; then
|
||||
state=$(awk '/^State:/ {print $2}' "/proc/$server_pid/status")
|
||||
if [ "$state" != Z ]; then
|
||||
echo 'Prefix has a live server marker; namespace reconnect is unsupported.' >&2
|
||||
echo 'Use the original darling-lol-shell terminal to run darling shutdown, then retry. No processes were stopped.' >&2
|
||||
echo 'Run darling-lol --stop to stop it, then retry. No processes were stopped.' >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
@@ -45,16 +48,36 @@ mkdir -p "$WORKSPACE/usr-local"
|
||||
cd "$WORKSPACE"
|
||||
# Map subordinate IDs before bubblewrap sets no_new_privs. UID 0 is not host root.
|
||||
fhs_command=("$UNSHARE_BIN" --user --map-auto --map-current-user --setuid=0 --setgid=0 "$FHS_BIN")
|
||||
stop_request="$WORKSPACE/.stop-request"
|
||||
rm -f "$stop_request"
|
||||
# Ctrl+C, a closed terminal or SIGTERM must reach nix-lifecycle.py, which shuts
|
||||
# Darling down in its namespace. They would kill bubblewrap first, and
|
||||
# bubblewrap's --die-with-parent would kill the lifecycle with it. So bubblewrap
|
||||
# starts with them ignored and env restores them inside. This supervisor turns
|
||||
# them into a stop request, the same one darling-lol --stop makes.
|
||||
session() {
|
||||
(trap '' INT QUIT HUP TERM
|
||||
exec "${fhs_command[@]}" env --default-signal=INT,QUIT,HUP,TERM bash ./scripts/nix-session.sh "$@") 0<&0 &
|
||||
local pid=$! status
|
||||
trap 'touch "$stop_request"' INT HUP TERM
|
||||
while :; do
|
||||
wait "$pid" && status=0 || status=$?
|
||||
kill -0 "$pid" 2>/dev/null || break
|
||||
done
|
||||
trap - INT HUP TERM
|
||||
rm -f "$stop_request"
|
||||
return "$status"
|
||||
}
|
||||
# Keep this supervisor alive to hold the lock: bubblewrap may close extra FDs.
|
||||
# It never owns or signals game processes; foreground execution carries SIGINT.
|
||||
# It never owns or signals game processes.
|
||||
case "${1:-}" in
|
||||
--shell) shift; "${fhs_command[@]}" bash "$@";;
|
||||
--update) shift; "${fhs_command[@]}" ./scripts/update.sh "$@";;
|
||||
--installer) shift; "${fhs_command[@]}" bash ./scripts/nix-session.sh --installer "$@";;
|
||||
--installer) shift; session --installer "$@";;
|
||||
*)
|
||||
if [ $# -gt 0 ]; then
|
||||
"${fhs_command[@]}" "$@"
|
||||
else
|
||||
"${fhs_command[@]}" bash ./scripts/nix-session.sh
|
||||
session
|
||||
fi;;
|
||||
esac
|
||||
+10
-3
@@ -57,8 +57,15 @@ for prompts and session output.
|
||||
|
||||
Keep the terminal open while clients are running and until cleanup completes.
|
||||
Default and installer sessions wait for clients to exit, then shut Darling down
|
||||
in the same namespace. A failed shutdown is retried without discarding that
|
||||
namespace.
|
||||
in the same namespace. Crash handlers do not count as clients, because they do
|
||||
not exit on their own under Darling. A failed shutdown is retried without
|
||||
discarding that namespace.
|
||||
|
||||
To end a session while clients are still running, press Ctrl+C in its terminal,
|
||||
close the terminal, or run `darling-lol --stop` from anywhere. Each of these
|
||||
makes the session shut Darling down and exit. If the session does not exit
|
||||
within 30 seconds, `darling-lol --stop` kills the prefix's `darlingserver`
|
||||
itself. It also stops a server that outlived its session.
|
||||
|
||||
## Install permanently
|
||||
|
||||
@@ -198,7 +205,7 @@ nix run github:lucannez64/darling-lol#shell
|
||||
Use the launcher-provided shell rather than entering the raw FHS environment.
|
||||
If you start Darling manually in this shell, run `darling shutdown` there
|
||||
before exiting. A separately launched shell cannot reconnect to an existing
|
||||
Darling namespace.
|
||||
Darling namespace; `darling-lol --stop` can still stop it.
|
||||
|
||||
## Files and operational limits
|
||||
|
||||
|
||||
@@ -134,3 +134,15 @@ a per-prefix lock. Repeated writes preserve identifiers and unchanged files.
|
||||
Previewing a fresh identity generates a sample; only `--write` makes it stable.
|
||||
To restore host-derived defaults after provisioning, remove both identity files
|
||||
while stopped, so a later provisioning run cannot reimport the legacy copy.
|
||||
|
||||
## system_profiler
|
||||
|
||||
The prefix's `/usr/sbin/system_profiler` (installed by
|
||||
`tools/install-darling-system-profiler.py`) answers `SPHardwareDataType` and
|
||||
`SPSoftwareDataType` from the running guest: model, serial number, platform UUID
|
||||
and provisioning UDID from IOKit's `IOPlatformExpertDevice` (via `ioreg`), CPU,
|
||||
cache and memory from `sysctl`, and the OS version from `sw_vers` and `uname`. It
|
||||
keeps no identity of its own, so it reports the same values as IOKit, including any
|
||||
override saved by this tool. A field the guest does not provide is left out.
|
||||
`python3 -m unittest tests.test_system_profiler` checks the output with stub
|
||||
commands.
|
||||
@@ -73,6 +73,17 @@ opts in to Hyprland-only recovery of a default-prefix Riot session with no
|
||||
visible window; it is off by default. See [troubleshooting](TROUBLESHOOTING.md#riot-exits-and-league-reports-that-it-lost-its-connection)
|
||||
for the observed failures, recovery guards and NSURLSession limitations.
|
||||
|
||||
`RCLOL_FILTER_HOST_ENV=1` opts in to removing host desktop-session variables before
|
||||
the guest starts: `WAYLAND_DISPLAY`, `XDG_SESSION_*`, `XDG_CURRENT_DESKTOP`,
|
||||
GNOME/KDE/GDM, systemd, SSH agent, terminal, toolkit (`QT_*`, `GTK_*`, `GDK_*`),
|
||||
Flatpak/Snap/AppImage and exported shell functions, and `SHELL` becomes `/bin/zsh`.
|
||||
It is off by default. Variables that Darling or the host graphics and audio libraries read
|
||||
are always kept: `DISPLAY`, `XAUTHORITY`, `XDG_RUNTIME_DIR` and the XDG base directories,
|
||||
D-Bus, PulseAudio/PipeWire/ALSA, `DRI_PRIME`, `MESA_*`, `__GL_*`, `__NV_*`, `VK_*`,
|
||||
`LD_PRELOAD` and `LD_LIBRARY_PATH`. List names to keep anyway in `RCLOL_KEEP_HOST_ENV`,
|
||||
for example `RCLOL_KEEP_HOST_ENV=WAYLAND_DISPLAY`. The launcher always closes inherited
|
||||
file descriptors above stderr before the guest starts.
|
||||
|
||||
## Updating an existing runtime
|
||||
|
||||
`./scripts/update.sh` updates everything after `git pull`: `build.sh`, then the base install, then `install-runtime.sh`. When a pull changes a patch, `prepare.py` finds the earlier version applied in that component (its copy in `build/applied-patches`, or a version from this repository's history), reverts it, and applies the new one. It never resets a repository; if neither version matches, it stops and names the repository with other edits. The steps below update single components instead.
|
||||
|
||||
+3
-3
@@ -23,11 +23,11 @@ c++ -std=c++17 -I build/darling-src/src/external/metal/private-include \
|
||||
build/test-metal-device-info
|
||||
```
|
||||
|
||||
Expected: `METAL_DEVICE_INFO failures=0 pass=1`. It covers how `MTLDevice.name` turns Vulkan names into macOS style (`AMD Radeon RX 6800 XT (RADV NAVI21)` becomes `AMD Radeon RX 6800 XT`, `Intel(R) UHD Graphics 630 (CML GT2)` becomes `Intel UHD Graphics 630`, NVIDIA names stay as they are), the `DARLING_METAL_VRAM_MB` parser, and the working-set sum over Vulkan heaps.
|
||||
Expected: `METAL_DEVICE_INFO failures=0 pass=1`. It covers how `MTLDevice.name` turns Vulkan names into macOS style (`AMD Radeon RX 6800 XT (RADV NAVI21)` becomes `AMD Radeon RX 6800 XT`, `Intel(R) UHD Graphics 630 (CML GT2)` becomes `Intel UHD Graphics 630`, NVIDIA names stay as they are), the `DARLING_METAL_VRAM_MB` parser, the working-set sum over Vulkan heaps, and a non-zero `registryID`. `python3 -m unittest tests.test_metal_device_info` runs the same check against the header in `patches/metal.patch`, so it also works without a prepared source tree; CI runs it.
|
||||
|
||||
The LLVM version check compiles Iridium's actual dynamic loader against small stub libraries. It verifies that a matching major initializes, older/newer majors produce a version mismatch, and a missing library fails initialization. It does not start Darling. The native AIR translator (`python3 tools/build-riot-air-check.py --native`) selects both headers and the shared library from the same `llvm-config`; its build prints those paths so shader results can be attributed to the tested LLVM installation.
|
||||
|
||||
The patch check uses temporary base indexes and does not reset or modify the source checkout. The launcher check uses stub commands and never starts or contacts Riot or Darling. It checks installed-client and first-time-installer launches, strips inherited `DYLD_INSERT_LIBRARIES` even with the old VPX opt-in set, and preserves Darling runtime library/framework paths. The memory-installer check imports the installer and exercises its policy (default source is the prepared artifact; the artifact is never a destination of a different library) in temporary directories. The Foundation-installer check does the same for the overlay installer: it seeds a temporary framework from a stub SDK, replaces `Versions/C/Foundation`, keeps the standard symlinks, stages a backup, treats a re-run as current and refuses a non-Mach-O source. The installer handoff mock covers 28 scenarios, including HTTP 409, interrupted verification and preserving active downloads.
|
||||
The patch check uses temporary base indexes and does not reset or modify the source checkout. The launcher check uses stub commands and never starts or contacts Riot or Darling. It checks installed-client and first-time-installer launches, strips inherited `DYLD_INSERT_LIBRARIES` even with the old VPX opt-in set, preserves Darling runtime library/framework paths, and checks that host file descriptors the launcher inherited (including multi-digit ones) are closed before the guest starts. The memory-installer check imports the installer and exercises its policy (default source is the prepared artifact; the artifact is never a destination of a different library) in temporary directories. The Foundation-installer check does the same for the overlay installer: it seeds a temporary framework from a stub SDK, replaces `Versions/C/Foundation`, keeps the standard symlinks, stages a backup, treats a re-run as current and refuses a non-Mach-O source. The installer handoff mock covers 28 scenarios, including HTTP 409, interrupted verification and preserving active downloads.
|
||||
|
||||
## NSURLSession lifecycle regression
|
||||
|
||||
@@ -181,7 +181,7 @@ build/riot-host-receive-check
|
||||
python3 tools/riot-launcher-offline-check.py
|
||||
```
|
||||
|
||||
Expected: `warm_empty_allocations=0 single_packet_allocations=2`, correct credentials, descriptor ownership/CLOEXEC, sender addresses, multiple batches, truncation flags, callback reentrancy and exception cleanup. The old receive implementation makes 32 allocations for a warmed empty receive and 64 for a single-packet receive in the same fixture. These are allocation counts, not an FPS benchmark. The launcher check uses stub commands and never starts or contacts Riot or Darling. It checks installed-client and first-time-installer launches, strips inherited `DYLD_INSERT_LIBRARIES` even with the old VPX opt-in set, and preserves Darling runtime library/framework paths.
|
||||
Expected: `warm_empty_allocations=0 single_packet_allocations=2`, correct credentials, descriptor ownership/CLOEXEC, sender addresses, multiple batches, truncation flags, callback reentrancy and exception cleanup. The old receive implementation makes 32 allocations for a warmed empty receive and 64 for a single-packet receive in the same fixture. These are allocation counts, not an FPS benchmark. The launcher check uses stub commands and never starts or contacts Riot or Darling. It checks installed-client and first-time-installer launches, strips inherited `DYLD_INSERT_LIBRARIES` even with the old VPX opt-in set, preserves Darling runtime library/framework paths, and checks that host file descriptors the launcher inherited (including multi-digit ones) are closed before the guest starts.
|
||||
|
||||
The same source passed AddressSanitizer/UndefinedBehaviorSanitizer and leak detection. Allocation counts depend on this fixture/compiler/library; preserve the no-wasted-slot property rather than interpreting them as game performance. See [setup](SETUP.md#updating-an-existing-runtime) for server build/install/activation.
|
||||
|
||||
|
||||
@@ -375,6 +375,27 @@ Tested on a GTX 1060 6GB with driver 580.178.04 on NixOS, Hyprland and Xwayland:
|
||||
the client and the tutorial match render on the NVIDIA GPU. Newer drivers were not
|
||||
retested with the option off.
|
||||
|
||||
### Metal device name and memory budget
|
||||
|
||||
`MTLDevice` reports the host GPU: its Vulkan name in macOS style (for example
|
||||
`AMD Radeon RX 6800 XT (RADV NAVI21)` becomes `AMD Radeon RX 6800 XT`) and a working set
|
||||
(`recommendedMaxWorkingSetSize`) from the GPU's Vulkan memory heaps. Apps size their GPU
|
||||
allocations from the working set, so only override it to match the card's real memory.
|
||||
Both can be overridden per launch:
|
||||
|
||||
```sh
|
||||
DARLING_METAL_GPU_NAME='AMD Radeon Pro 5500M' DARLING_METAL_VRAM_MB=4096 ./scripts/run.sh
|
||||
```
|
||||
|
||||
The Riot launcher sets `DARLING_METAL_MAC_GPU_NAME=1`: the name becomes the GPU that
|
||||
the MacBookPro16,1 IOKit reports shipped with, from the host's vendor: `AMD Radeon Pro 5500M`
|
||||
on AMD and `Intel UHD Graphics 630` on Intel. NVIDIA GPUs keep their own name, so the game
|
||||
never takes AMD or Intel driver paths on an NVIDIA card. The working set is still the host's.
|
||||
`DARLING_METAL_MAC_GPU_NAME=0 ./scripts/run.sh` reports the host name instead; other Darling
|
||||
apps report it unless they set the variable. `DARLING_METAL_GPU_NAME` wins over both.
|
||||
|
||||
`registryID` is a fixed non-zero value, as on a real Mac.
|
||||
|
||||
### 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.
|
||||
|
||||
@@ -160,6 +160,18 @@
|
||||
export PKG_CONFIG_PATH="/usr/local/lib/pkgconfig:/usr/local/share/pkgconfig:/usr/lib/pkgconfig:/usr/lib64/pkgconfig:/usr/share/pkgconfig:''${PKG_CONFIG_PATH:-}"
|
||||
export CMAKE_PREFIX_PATH="/usr/local:/usr:/usr/lib/cmake/llvm:''${CMAKE_PREFIX_PATH:-}"
|
||||
unset NIX_LDFLAGS NIX_CFLAGS_COMPILE NIX_HARDENING_ENABLE CPATH LIBRARY_PATH
|
||||
# NixOS keeps the GPU driver's EGL vendor and external-platform files under
|
||||
# /run/opengl-driver, which glvnd and NVIDIA's driver in this FHS environment
|
||||
# do not search. Without them glvnd only finds Mesa, and NVIDIA cannot open
|
||||
# X11 displays (its X11 support is the egl-x11 external platform): the
|
||||
# client renders with Mesa's software rasterizer, which garbles text and
|
||||
# cannot present the game's NVIDIA Vulkan frames. FHS paths stay as fallback.
|
||||
if [ -d /run/opengl-driver/share/glvnd/egl_vendor.d ]; then
|
||||
export __EGL_VENDOR_LIBRARY_DIRS="/run/opengl-driver/share/glvnd/egl_vendor.d:/etc/glvnd/egl_vendor.d:/usr/share/glvnd/egl_vendor.d''${__EGL_VENDOR_LIBRARY_DIRS:+:$__EGL_VENDOR_LIBRARY_DIRS}"
|
||||
fi
|
||||
if [ -d /run/opengl-driver/share/egl/egl_external_platform.d ]; then
|
||||
export __EGL_EXTERNAL_PLATFORM_CONFIG_DIRS="/run/opengl-driver/share/egl/egl_external_platform.d:/etc/egl/egl_external_platform.d:/usr/share/egl/egl_external_platform.d''${__EGL_EXTERNAL_PLATFORM_CONFIG_DIRS:+:$__EGL_EXTERNAL_PLATFORM_CONFIG_DIRS}"
|
||||
fi
|
||||
'';
|
||||
|
||||
runScript = pkgs.writeShellScript "darling-lol-run" ''
|
||||
|
||||
+63
-27
@@ -1134,7 +1134,7 @@ index 2a57796..0cbedf2 100644
|
||||
-
|
||||
@end
|
||||
diff --git a/src/Metal/MTLDevice.mm b/src/Metal/MTLDevice.mm
|
||||
index f58184a..a80a625 100644
|
||||
index f58184a..2c45608 100644
|
||||
--- a/src/Metal/MTLDevice.mm
|
||||
+++ b/src/Metal/MTLDevice.mm
|
||||
@@ -2,14 +2,32 @@
|
||||
@@ -1218,7 +1218,7 @@ index f58184a..a80a625 100644
|
||||
}
|
||||
|
||||
@synthesize device = _device;
|
||||
@@ -116,6 +168,39 @@ void MTLRemoveDeviceObserver(id<NSObject> observer) {
|
||||
@@ -116,6 +168,50 @@ void MTLRemoveDeviceObserver(id<NSObject> observer) {
|
||||
[_threadExitCondition unlock];
|
||||
}
|
||||
|
||||
@@ -1228,16 +1228,27 @@ index f58184a..a80a625 100644
|
||||
+ // Overrides are read once per process.
|
||||
+ static const char* const nameOverride = getenv("DARLING_METAL_GPU_NAME");
|
||||
+ static const uint64_t vramOverride = MTLDeviceInfo::parseMegabytes(getenv("DARLING_METAL_VRAM_MB"));
|
||||
+ static const bool macName = [] {
|
||||
+ const char* value = getenv("DARLING_METAL_MAC_GPU_NAME");
|
||||
+ return value && value[0] == '1' && value[1] == '\0';
|
||||
+ }();
|
||||
+
|
||||
+ std::string vulkanName = _device->name();
|
||||
+ _name = [[NSString alloc] initWithUTF8String: MTLDeviceInfo::resolveName(nameOverride, vulkanName.c_str()).c_str()];
|
||||
+ auto priv = dynamic_cast<Indium::PrivateDevice*>(_device.get());
|
||||
+ std::string vulkanName;
|
||||
+ try {
|
||||
+ vulkanName = _device ? _device->name() : "";
|
||||
+ } catch (...) {
|
||||
+ vulkanName = "";
|
||||
+ }
|
||||
+ const char* mac = macName && priv ? MTLDeviceInfo::macGpuName(priv->properties().vendorID) : nullptr;
|
||||
+ _name = [[NSString alloc] initWithUTF8String: MTLDeviceInfo::resolveName(nameOverride, vulkanName.c_str(), mac).c_str()];
|
||||
+ if (_name == nil) {
|
||||
+ _name = [[NSString alloc] initWithUTF8String: MTLDeviceInfo::kFallbackName];
|
||||
+ }
|
||||
+
|
||||
+ uint64_t workingSet = 0;
|
||||
+ _sharesSystemMemory = NO;
|
||||
+ if (auto priv = dynamic_cast<Indium::PrivateDevice*>(_device.get())) {
|
||||
+ if (priv) {
|
||||
+ const VkPhysicalDeviceType type = priv->properties().deviceType;
|
||||
+ _sharesSystemMemory = type == VK_PHYSICAL_DEVICE_TYPE_INTEGRATED_GPU || type == VK_PHYSICAL_DEVICE_TYPE_CPU;
|
||||
+ const VkPhysicalDeviceMemoryProperties memory = priv->memoryProperties();
|
||||
@@ -1258,7 +1269,7 @@ index f58184a..a80a625 100644
|
||||
- (instancetype)initWithDevice: (std::shared_ptr<Indium::Device>)device
|
||||
{
|
||||
self = [super init];
|
||||
@@ -123,6 +208,7 @@ void MTLRemoveDeviceObserver(id<NSObject> observer) {
|
||||
@@ -123,6 +219,7 @@ void MTLRemoveDeviceObserver(id<NSObject> observer) {
|
||||
_device = device;
|
||||
_threadExitCondition = [NSCondition new];
|
||||
_threadIsRunning = NO;
|
||||
@@ -1266,7 +1277,7 @@ index f58184a..a80a625 100644
|
||||
_pollingThread = [[NSThread alloc] initWithTarget: self selector: @selector(pollingLoop) object: nil];
|
||||
[_pollingThread start];
|
||||
}
|
||||
@@ -133,6 +219,7 @@ void MTLRemoveDeviceObserver(id<NSObject> observer) {
|
||||
@@ -133,6 +230,7 @@ void MTLRemoveDeviceObserver(id<NSObject> observer) {
|
||||
{
|
||||
[_pollingThread release];
|
||||
[_threadExitCondition release];
|
||||
@@ -1274,7 +1285,7 @@ index f58184a..a80a625 100644
|
||||
|
||||
[super dealloc];
|
||||
}
|
||||
@@ -153,12 +240,60 @@ void MTLRemoveDeviceObserver(id<NSObject> observer) {
|
||||
@@ -153,12 +251,60 @@ void MTLRemoveDeviceObserver(id<NSObject> observer) {
|
||||
[_threadExitCondition unlock];
|
||||
}
|
||||
|
||||
@@ -1311,7 +1322,7 @@ index f58184a..a80a625 100644
|
||||
+- (BOOL)isRemovable { return NO; }
|
||||
+- (BOOL)isHeadless { return NO; }
|
||||
+- (NSString*)name { return _name; }
|
||||
+- (uint64_t)registryID { return 0; }
|
||||
+- (uint64_t)registryID { return MTLDeviceInfo::kDefaultRegistryID; }
|
||||
+- (BOOL)supportsFamily: (NSUInteger)family { return YES; }
|
||||
+- (BOOL)supportsFeatureSet: (NSUInteger)featureSet { return YES; }
|
||||
+- (BOOL)supportsTextureSampleCount: (NSUInteger)sampleCount { return sampleCount <= 4; }
|
||||
@@ -1336,7 +1347,7 @@ index f58184a..a80a625 100644
|
||||
if (!pso) {
|
||||
if (error) {
|
||||
// TODO: better error and/or match what the official Metal method does
|
||||
@@ -166,6 +301,7 @@ void MTLRemoveDeviceObserver(id<NSObject> observer) {
|
||||
@@ -166,6 +312,7 @@ void MTLRemoveDeviceObserver(id<NSObject> observer) {
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
@@ -1344,7 +1355,7 @@ index f58184a..a80a625 100644
|
||||
return [[MTLComputePipelineStateInternal alloc] initWithState: pso device: self label: descriptor.label];
|
||||
}
|
||||
|
||||
@@ -183,7 +319,13 @@ void MTLRemoveDeviceObserver(id<NSObject> observer) {
|
||||
@@ -183,7 +330,13 @@ void MTLRemoveDeviceObserver(id<NSObject> observer) {
|
||||
reflection: (MTLAutoreleasedComputePipelineReflection*)reflection
|
||||
error: (NSError**)error
|
||||
{
|
||||
@@ -1359,7 +1370,7 @@ index f58184a..a80a625 100644
|
||||
if (!pso) {
|
||||
if (error) {
|
||||
// TODO: better error and/or match what the official Metal method does
|
||||
@@ -191,13 +333,44 @@ void MTLRemoveDeviceObserver(id<NSObject> observer) {
|
||||
@@ -191,13 +344,44 @@ void MTLRemoveDeviceObserver(id<NSObject> observer) {
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
@@ -1405,7 +1416,7 @@ index f58184a..a80a625 100644
|
||||
if (!pso) {
|
||||
if (error) {
|
||||
// TODO: better error and/or match what the official Metal method does
|
||||
@@ -205,7 +378,114 @@ void MTLRemoveDeviceObserver(id<NSObject> observer) {
|
||||
@@ -205,7 +389,118 @@ void MTLRemoveDeviceObserver(id<NSObject> observer) {
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
@@ -1453,7 +1464,9 @@ index f58184a..a80a625 100644
|
||||
+- (id<MTLDepthStencilState>)newDepthStencilStateWithDescriptor:(MTLDepthStencilDescriptor*)descriptor
|
||||
+{
|
||||
+ if (!descriptor) return nil;
|
||||
+ auto state = _device->newDepthStencilState([descriptor asIndiumDescriptor]);
|
||||
+ std::shared_ptr<Indium::DepthStencilState> state;
|
||||
+ try { state = _device->newDepthStencilState([descriptor asIndiumDescriptor]); }
|
||||
+ catch (...) { return nil; }
|
||||
+ if (!state) return nil;
|
||||
+ return [[MTLDepthStencilStateInternal alloc] initWithState:state device:self label:descriptor.label];
|
||||
+}
|
||||
@@ -1475,7 +1488,9 @@ index f58184a..a80a625 100644
|
||||
+ desc.lodMaxClamp = descriptor.lodMaxClamp;
|
||||
+ desc.supportArgumentBuffers = descriptor.supportArgumentBuffers;
|
||||
+ desc.compareFunction = static_cast<Indium::CompareFunction>(descriptor.compareFunction);
|
||||
+ auto sampler = _device->newSamplerState(desc);
|
||||
+ std::shared_ptr<Indium::SamplerState> sampler;
|
||||
+ try { sampler = _device->newSamplerState(desc); }
|
||||
+ catch (...) { return nil; }
|
||||
+ if (!sampler) return nil;
|
||||
+ return [[MTLSamplerStateInternal alloc] initWithSampler:sampler device:self label:descriptor.label];
|
||||
+}
|
||||
@@ -1521,7 +1536,7 @@ index f58184a..a80a625 100644
|
||||
}
|
||||
|
||||
- (id<MTLCommandQueue>)newCommandQueue
|
||||
@@ -275,8 +555,21 @@ void MTLRemoveDeviceObserver(id<NSObject> observer) {
|
||||
@@ -275,8 +570,21 @@ void MTLRemoveDeviceObserver(id<NSObject> observer) {
|
||||
- (id<MTLLibrary>)newLibraryWithData: (dispatch_data_t)data
|
||||
error: (NSError**)error
|
||||
{
|
||||
@@ -2581,7 +2596,7 @@ index 66a9056..f28235c 100644
|
||||
-
|
||||
@end
|
||||
diff --git a/src/Metal/MTLTexture.mm b/src/Metal/MTLTexture.mm
|
||||
index 6c71fb9..822c112 100644
|
||||
index 6c71fb9..186b8c0 100644
|
||||
--- a/src/Metal/MTLTexture.mm
|
||||
+++ b/src/Metal/MTLTexture.mm
|
||||
@@ -1,3 +1,5 @@
|
||||
@@ -2590,7 +2605,7 @@ index 6c71fb9..822c112 100644
|
||||
// SPDX-FileCopyrightText: 2026 Darling Developers
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
@@ -7,11 +9,37 @@
|
||||
@@ -7,11 +9,38 @@
|
||||
#import <Metal/MTLPixelFormatInternal.h>
|
||||
#import <Metal/MTLTypesInternal.h>
|
||||
|
||||
@@ -2606,7 +2621,8 @@ index 6c71fb9..822c112 100644
|
||||
+ if (FILE* log = fopen("/tmp/metal-trace.log", "a")) {
|
||||
+ const unsigned char* b = (const unsigned char*) bytes;
|
||||
+ unsigned long nonZero = 0;
|
||||
+ size_t probe = std::min<size_t>(4096, (size_t) region.size.width * 4);
|
||||
+ // Every pixel format takes at least one byte per pixel, so the first `width` bytes lie inside the first row.
|
||||
+ size_t probe = std::min<size_t>(4096, (size_t) region.size.width);
|
||||
+ for (size_t i = 0; i < probe; ++i) if (b[i]) ++nonZero;
|
||||
+ fprintf(log, "replaceRegion tex=%p %lux%lu fmt=%lu level=%lu region=(%lu,%lu %lux%lu) firstBytesNonZero=%lu/%zu took=%.1fms\n", texture,
|
||||
+ (unsigned long) texture.width, (unsigned long) texture.height, (unsigned long) texture.pixelFormat, (unsigned long) level,
|
||||
@@ -2628,7 +2644,7 @@ index 6c71fb9..822c112 100644
|
||||
@synthesize device = _device;
|
||||
@synthesize resourceOptions = _resourceOptions;
|
||||
@synthesize label = _label;
|
||||
@@ -164,7 +192,9 @@
|
||||
@@ -164,7 +193,9 @@
|
||||
withBytes: (const void*)pixelBytes
|
||||
bytesPerRow: (NSUInteger)bytesPerRow
|
||||
{
|
||||
@@ -2638,7 +2654,7 @@ index 6c71fb9..822c112 100644
|
||||
}
|
||||
|
||||
- (void)replaceRegion: (MTLRegion)region
|
||||
@@ -174,7 +204,9 @@
|
||||
@@ -174,7 +205,9 @@
|
||||
bytesPerRow: (NSUInteger)bytesPerRow
|
||||
bytesPerImage: (NSUInteger)bytesPerImage
|
||||
{
|
||||
@@ -2736,7 +2752,7 @@ index 37cccb3..79b2350 100644
|
||||
-
|
||||
@end
|
||||
diff --git a/src/Metal/MTLVertexDescriptor.mm b/src/Metal/MTLVertexDescriptor.mm
|
||||
index cde968d..aa7fe05 100644
|
||||
index cde968d..0a016c6 100644
|
||||
--- a/src/Metal/MTLVertexDescriptor.mm
|
||||
+++ b/src/Metal/MTLVertexDescriptor.mm
|
||||
@@ -1,24 +1,435 @@
|
||||
@@ -3015,8 +3031,8 @@ index cde968d..aa7fe05 100644
|
||||
+ if (index >= 31) [NSException raise:NSRangeException format:@"Vertex descriptor index %lu exceeds 30", (unsigned long)index];
|
||||
+ MTLVertexBufferLayoutDescriptor* old = _items[index];
|
||||
+ _items[index] = [descriptor retain];
|
||||
+ [old release];
|
||||
+ MTLVertexBufferLayoutArrayRetireItem(self, old);
|
||||
+ [old release];
|
||||
+ const uint32_t bit = 1u << index;
|
||||
+ if (descriptor) _present |= bit; else _present &= ~bit;
|
||||
+ if (descriptor && object_getClass(descriptor) == [MTLVertexBufferLayoutDescriptor class]) _concrete |= bit; else _concrete &= ~bit;
|
||||
@@ -3077,8 +3093,8 @@ index cde968d..aa7fe05 100644
|
||||
+ if (index >= 31) [NSException raise:NSRangeException format:@"Vertex descriptor index %lu exceeds 30", (unsigned long)index];
|
||||
+ MTLVertexAttributeDescriptor* old = _items[index];
|
||||
+ _items[index] = [descriptor retain];
|
||||
+ [old release];
|
||||
+ MTLVertexAttributeArrayRetireItem(self, old);
|
||||
+ [old release];
|
||||
+ const uint32_t bit = 1u << index;
|
||||
+ if (descriptor) _present |= bit; else _present &= ~bit;
|
||||
+ if (descriptor && object_getClass(descriptor) == [MTLVertexAttributeDescriptor class]) _concrete |= bit; else _concrete &= ~bit;
|
||||
@@ -3440,7 +3456,7 @@ diff --git a/private-include/Metal/MTLDepthStencilInternal.h b/private-include/M
|
||||
new file mode 100644
|
||||
--- /dev/null
|
||||
+++ b/private-include/Metal/MTLDepthStencilInternal.h
|
||||
@@ -0,0 +1,25 @@
|
||||
@@ -0,0 +1,27 @@
|
||||
+// SPDX-FileCopyrightText: 2026 Darling Developers
|
||||
+// SPDX-License-Identifier: MPL-2.0
|
||||
+
|
||||
@@ -3462,15 +3478,17 @@ new file mode 100644
|
||||
+- (instancetype)initWithState:(std::shared_ptr<Indium::DepthStencilState>)state device:(id<MTLDevice>)device label:(NSString*)label;
|
||||
+#endif
|
||||
+@end
|
||||
+#if DARLING_METAL_ENABLED
|
||||
+// Direct access for the encoders: the property getter is a message send that returns the shared_ptr by value.
|
||||
+const std::shared_ptr<Indium::DepthStencilState>& MTLDepthStencilGetIndium(id object);
|
||||
+#endif
|
||||
+
|
||||
+#endif
|
||||
diff --git a/private-include/Metal/MTLDeviceInfo.h b/private-include/Metal/MTLDeviceInfo.h
|
||||
new file mode 100644
|
||||
--- /dev/null
|
||||
+++ b/private-include/Metal/MTLDeviceInfo.h
|
||||
@@ -0,0 +1,148 @@
|
||||
@@ -0,0 +1,166 @@
|
||||
+// SPDX-FileCopyrightText: 2026 Darling Developers
|
||||
+// SPDX-License-Identifier: MPL-2.0
|
||||
+
|
||||
@@ -3489,6 +3507,8 @@ new file mode 100644
|
||||
+
|
||||
+// Used when neither the cleaned nor the raw Vulkan name has any characters.
|
||||
+static constexpr const char* kFallbackName = "Darling Indium Device";
|
||||
+// A real Mac never reports registry ID 0, and apps use it as a key for the device.
|
||||
+static constexpr uint64_t kDefaultRegistryID = 0x100000439ULL;
|
||||
+
|
||||
+static inline bool isSpace(char c) {
|
||||
+ return c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == '\f' || c == '\v';
|
||||
@@ -3568,10 +3588,26 @@ new file mode 100644
|
||||
+}
|
||||
+
|
||||
+// DARLING_METAL_GPU_NAME (used verbatim when non-empty) wins over the Vulkan name.
|
||||
+static inline std::string resolveName(const char* overrideName, const char* vulkanName) {
|
||||
+// With DARLING_METAL_MAC_GPU_NAME=1: the GPU from the host's vendor that the MacBookPro16,1
|
||||
+// iokitd reports shipped with, by Vulkan vendor ID. nullptr for other vendors (NVIDIA), which
|
||||
+// keep their own name so apps never take another vendor's driver paths.
|
||||
+static inline const char* macGpuName(uint32_t vendorID) {
|
||||
+ switch (vendorID) {
|
||||
+ case 0x1002: return "AMD Radeon Pro 5500M";
|
||||
+ case 0x8086: return "Intel UHD Graphics 630";
|
||||
+ default: return nullptr;
|
||||
+ }
|
||||
+}
|
||||
+
|
||||
+// DARLING_METAL_GPU_NAME wins, then the Mac GPU name (when enabled and known), then the
|
||||
+// cleaned Vulkan name.
|
||||
+static inline std::string resolveName(const char* overrideName, const char* vulkanName, const char* macName = nullptr) {
|
||||
+ if (overrideName && overrideName[0] != '\0') {
|
||||
+ return overrideName;
|
||||
+ }
|
||||
+ if (macName && macName[0] != '\0') {
|
||||
+ return macName;
|
||||
+ }
|
||||
+ return metalNameFromVulkanName(vulkanName);
|
||||
+}
|
||||
+
|
||||
|
||||
+31
-14
@@ -3,6 +3,30 @@
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
# Crash handlers do not exit after their clients under Darling, so a mapping by
|
||||
# one of them alone does not mean that a client still uses the prefix.
|
||||
CRASH_HANDLERS = {'RiotClientCrashHandler', 'LeagueCrashHandler', 'chrome_crashpad_handler'}
|
||||
|
||||
|
||||
def crash_handler(process):
|
||||
executable = (process / 'cmdline').read_bytes().split(b'\0', 1)[0]
|
||||
return os.path.basename(executable.decode(errors='replace')) in CRASH_HANDLERS
|
||||
|
||||
|
||||
def server_running(prefix):
|
||||
"""Whether the prefix marker names a live (not zombie) process."""
|
||||
try:
|
||||
pid = int((prefix / '.init.pid').read_text().strip())
|
||||
except FileNotFoundError:
|
||||
return False
|
||||
if pid <= 0:
|
||||
return False
|
||||
try:
|
||||
status = (Path('/proc') / str(pid) / 'status').read_text()
|
||||
except (FileNotFoundError, ProcessLookupError):
|
||||
return False
|
||||
return not any(line.startswith('State:') and line.split()[1] == 'Z' for line in status.splitlines())
|
||||
|
||||
|
||||
def client_active(prefix, *, processes=None):
|
||||
"""Check exact-prefix mappings; only vanished processes may be skipped."""
|
||||
@@ -22,7 +46,7 @@ def client_active(prefix, *, processes=None):
|
||||
if processes is None and (process / 'uid_map').read_text() != namespace_map:
|
||||
continue
|
||||
mappings = (process / 'maps').read_text()
|
||||
if any(path in mappings for path in paths):
|
||||
if any(path in mappings for path in paths) and not crash_handler(process):
|
||||
return True
|
||||
except (FileNotFoundError, ProcessLookupError):
|
||||
# Process exited between enumeration and inspection.
|
||||
@@ -35,20 +59,13 @@ def client_active(prefix, *, processes=None):
|
||||
def main():
|
||||
prefix = Path(os.environ.get('DPREFIX', str(Path.home() / '.darling'))).resolve()
|
||||
try:
|
||||
pid = int((prefix / '.init.pid').read_text().strip())
|
||||
except FileNotFoundError:
|
||||
pid = 0
|
||||
except (OSError, ValueError):
|
||||
running = server_running(prefix)
|
||||
except ValueError:
|
||||
raise SystemExit('Cannot validate prefix server marker; inspect it before maintenance.')
|
||||
if pid > 0:
|
||||
try:
|
||||
status = (Path('/proc') / str(pid) / 'status').read_text()
|
||||
if not any(line.startswith('State:') and line.split()[1] == 'Z' for line in status.splitlines()):
|
||||
raise SystemExit('Prefix has a live server marker; exit Riot/League and run darling shutdown in the ORIGINAL namespace before updating. No process was stopped.')
|
||||
except (FileNotFoundError, ProcessLookupError):
|
||||
pass
|
||||
except OSError as error:
|
||||
raise SystemExit(f'Cannot validate prefix server marker: {error}') from error
|
||||
except OSError as error:
|
||||
raise SystemExit(f'Cannot validate prefix server marker: {error}') from error
|
||||
if running:
|
||||
raise SystemExit('Prefix has a live server marker; exit Riot/League and run darling-lol --stop before updating. No process was stopped.')
|
||||
try:
|
||||
active = client_active(prefix)
|
||||
except (OSError, RuntimeError) as error:
|
||||
|
||||
@@ -28,9 +28,14 @@ def main():
|
||||
idle = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(idle)
|
||||
prefix = Path(os.environ['DPREFIX']).resolve()
|
||||
# darling-lol --stop creates this file; the launcher removes it.
|
||||
stop_request = Path(os.environ['DARLING_WORKSPACE']) / '.stop-request'
|
||||
child = None
|
||||
interrupted = 0
|
||||
|
||||
def stopping():
|
||||
return interrupted or stop_request.exists()
|
||||
|
||||
def interrupted_by(signum, _frame):
|
||||
nonlocal interrupted
|
||||
interrupted = 128 + signum
|
||||
@@ -46,16 +51,18 @@ def main():
|
||||
result = 1
|
||||
try:
|
||||
child = subprocess.Popen(sys.argv[1:])
|
||||
result = child.wait()
|
||||
if result < 0:
|
||||
result = 128 - result
|
||||
# Poll rather than wait: guest processes can outlive the command, and a
|
||||
# signal or stop request must still reach the shutdown below.
|
||||
while child.poll() is None and not stopping():
|
||||
time.sleep(0.5)
|
||||
except OSError as error:
|
||||
warn(f'Cannot start client: {error}')
|
||||
finally:
|
||||
# A launcher can return while guest clients remain. Do not tear down
|
||||
# their server, or discard the only namespace capable of shutting it down.
|
||||
# their server, or discard the only namespace capable of shutting it down,
|
||||
# unless the user asked to stop.
|
||||
last_error = None
|
||||
while True:
|
||||
while not stopping():
|
||||
try:
|
||||
if not idle.client_active(prefix):
|
||||
break
|
||||
@@ -66,6 +73,8 @@ def main():
|
||||
'Exit clients to allow safe shutdown.')
|
||||
last_error = str(error)
|
||||
time.sleep(1)
|
||||
if stopping():
|
||||
warn('Stopping Darling.')
|
||||
while True:
|
||||
try:
|
||||
shutdown = subprocess.run(['bash', str(scripts / 'darling-rootless.sh'), 'shutdown']).returncode
|
||||
@@ -74,10 +83,25 @@ def main():
|
||||
shutdown = 1
|
||||
if not shutdown:
|
||||
break
|
||||
try:
|
||||
# Nothing is left to shut down, for example after the server crashed.
|
||||
if not idle.server_running(prefix):
|
||||
break
|
||||
except (OSError, ValueError):
|
||||
pass
|
||||
warn(f'Darling namespace-local shutdown failed (status {shutdown}); '
|
||||
'retaining the original namespace and retrying in 5 seconds. '
|
||||
'Keep this terminal open.')
|
||||
time.sleep(5)
|
||||
if child is not None:
|
||||
# Without its server the command exits; it is our own child to kill.
|
||||
try:
|
||||
result = child.wait(timeout=10)
|
||||
except subprocess.TimeoutExpired:
|
||||
child.kill()
|
||||
result = child.wait()
|
||||
if result < 0:
|
||||
result = 128 - result
|
||||
result = interrupted or result
|
||||
return result
|
||||
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
#!/usr/bin/env python3
|
||||
"""darling-lol --stop: end the workspace's session and its Darling server.
|
||||
|
||||
The session shuts Darling down in its own namespace when it finds the stop
|
||||
request. Only if it does not finish in time, or if no session holds the lock
|
||||
but the prefix server is still alive, is that server killed from here. The
|
||||
marker PID alone is not trusted: the process must be a darlingserver of this
|
||||
user for this prefix.
|
||||
"""
|
||||
import fcntl
|
||||
import os
|
||||
from pathlib import Path
|
||||
import signal
|
||||
import sys
|
||||
import time
|
||||
|
||||
WAIT_SECONDS = 30
|
||||
|
||||
|
||||
def session_running(lock):
|
||||
try:
|
||||
fcntl.flock(lock, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
||||
except BlockingIOError:
|
||||
return True
|
||||
fcntl.flock(lock, fcntl.LOCK_UN)
|
||||
return False
|
||||
|
||||
|
||||
def wait_for_session(lock):
|
||||
deadline = time.monotonic() + WAIT_SECONDS
|
||||
while session_running(lock):
|
||||
if time.monotonic() > deadline:
|
||||
return False
|
||||
time.sleep(0.5)
|
||||
return True
|
||||
|
||||
|
||||
def prefix_server(prefix):
|
||||
"""PID of this prefix's darlingserver, or 0 if none is running."""
|
||||
try:
|
||||
pid = int((prefix / '.init.pid').read_text().strip())
|
||||
except (FileNotFoundError, ValueError):
|
||||
return 0
|
||||
process = Path('/proc') / str(pid)
|
||||
try:
|
||||
argv = (process / 'cmdline').read_bytes().split(b'\0')
|
||||
owner = process.stat().st_uid
|
||||
except (FileNotFoundError, ProcessLookupError):
|
||||
return 0
|
||||
if (pid <= 0 or owner != os.getuid() or len(argv) < 2
|
||||
or os.path.basename(argv[0]) != b'darlingserver'
|
||||
or Path(os.fsdecode(argv[1])).resolve() != prefix):
|
||||
return 0
|
||||
return pid
|
||||
|
||||
|
||||
def kill_server(pid):
|
||||
"""Same as darling shutdown: kill launchd, which ends its PID namespace, then the server."""
|
||||
children = []
|
||||
for task in (Path('/proc') / str(pid) / 'task').glob('*/children'):
|
||||
try:
|
||||
children += [int(child) for child in task.read_text().split()]
|
||||
except (FileNotFoundError, ProcessLookupError):
|
||||
pass
|
||||
for target in [*children, pid]:
|
||||
try:
|
||||
os.kill(target, signal.SIGKILL)
|
||||
except ProcessLookupError:
|
||||
pass
|
||||
|
||||
|
||||
def main():
|
||||
workspace, prefix = Path(sys.argv[1]), Path(sys.argv[2]).resolve()
|
||||
request = workspace / '.stop-request'
|
||||
workspace.mkdir(parents=True, exist_ok=True)
|
||||
with open(workspace / '.launcher.lock', 'a+') as lock:
|
||||
if session_running(lock):
|
||||
print('Asking the darling-lol session to shut Darling down...', flush=True)
|
||||
request.touch()
|
||||
if wait_for_session(lock):
|
||||
request.unlink(missing_ok=True)
|
||||
print('Stopped.')
|
||||
return 0
|
||||
print(f'The session did not stop within {WAIT_SECONDS} seconds.', flush=True)
|
||||
server = prefix_server(prefix)
|
||||
if server:
|
||||
print(f'Killing the Darling server of {prefix} (PID {server}).', flush=True)
|
||||
kill_server(server)
|
||||
elif not session_running(lock):
|
||||
print('No darling-lol session is running.')
|
||||
return 0
|
||||
if wait_for_session(lock):
|
||||
request.unlink(missing_ok=True)
|
||||
print('Stopped.')
|
||||
return 0
|
||||
# Do not leave a request that would stop the next session at once.
|
||||
request.unlink(missing_ok=True)
|
||||
print('Darling is stopped, but the session that started it is still running. '
|
||||
'Close its terminal.', file=sys.stderr)
|
||||
return 1
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,90 @@
|
||||
"""exec-guest.py: inherited descriptors and the opt-in host environment filter."""
|
||||
import importlib.util
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
HELPER = ROOT / 'tools/exec-guest.py'
|
||||
spec = importlib.util.spec_from_file_location('exec_guest', HELPER)
|
||||
exec_guest = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(exec_guest)
|
||||
|
||||
# What the guest and the host libraries Darling loads still need.
|
||||
REQUIRED = {
|
||||
'DISPLAY': ':0', 'XAUTHORITY': '/run/user/1000/.mutter-Xwaylandauth.A', 'XCURSOR_SIZE': '24',
|
||||
'XDG_RUNTIME_DIR': '/run/user/1000', 'XDG_DATA_DIRS': '/usr/share', 'XDG_CONFIG_HOME': '/home/u/.config',
|
||||
'DBUS_SESSION_BUS_ADDRESS': 'unix:path=/run/user/1000/bus', 'PULSE_SERVER': 'unix:/run/pulse',
|
||||
'PIPEWIRE_LATENCY': '128/48000', 'ALSA_CONFIG_PATH': '/usr/share/alsa/alsa.conf',
|
||||
'DRI_PRIME': '1', 'MESA_NO_ERROR': '1', 'mesa_glthread': 'false', '__GL_THREADED_OPTIMIZATIONS': '0',
|
||||
'__NV_PRIME_RENDER_OFFLOAD': '1', '__EGL_VENDOR_LIBRARY_FILENAMES': '/usr/share/glvnd/egl_vendor.d/10_nvidia.json',
|
||||
'VK_ICD_FILENAMES': '/usr/share/vulkan/icd.d/nvidia_icd.json', 'LIBGL_ALWAYS_SOFTWARE': '0',
|
||||
'LD_PRELOAD': 'libgamemodeauto.so.0', 'LD_LIBRARY_PATH': '/run/opengl-driver/lib',
|
||||
'HOME': '/home/u', 'USER': 'u', 'PATH': '/usr/bin:/bin', 'LANG': 'en_US.UTF-8', 'TERM': 'xterm',
|
||||
'DPREFIX': '/home/u/.darling', 'RCLOL_REFRESH_HZ': '144', 'DYLD_LIBRARY_PATH': '/Users/u/RuntimeLibraries',
|
||||
}
|
||||
SESSION = {
|
||||
'WAYLAND_DISPLAY': 'wayland-0', 'XDG_CURRENT_DESKTOP': 'GNOME', 'XDG_SESSION_TYPE': 'wayland',
|
||||
'XDG_SESSION_ID': '2', 'XDG_SEAT': 'seat0', 'DESKTOP_SESSION': 'gnome', 'GDMSESSION': 'gnome',
|
||||
'GDM_LANG': 'en_US.UTF-8', 'SESSION_MANAGER': 'local/host:@/tmp/.ICE-unix/1', 'GNOME_SETUP_DISPLAY': ':1',
|
||||
'KDE_FULL_SESSION': 'true', 'SSH_AUTH_SOCK': '/run/user/1000/ssh', 'SYSTEMD_EXEC_PID': '1',
|
||||
'INVOCATION_ID': 'abc', 'JOURNAL_STREAM': '8:1', 'MANAGERPID': '1', 'NOTIFY_SOCKET': '/run/notify',
|
||||
'LISTEN_FDS': '1', 'MEMORY_PRESSURE_WATCH': '/sys/fs/cgroup', 'QT_QPA_PLATFORM': 'wayland',
|
||||
'GTK_MODULES': 'canberra', 'GDK_BACKEND': 'wayland', 'GIO_LAUNCHED_DESKTOP_FILE': '/x.desktop',
|
||||
'FLATPAK_ID': 'x', 'SNAP': '/snap/x', 'APPIMAGE': '/x.AppImage', 'VIRTUAL_ENV': '/home/u/.venv',
|
||||
'COLORTERM': 'truecolor', 'VTE_VERSION': '7600', 'WINDOWID': '1', 'TERM_PROGRAM': 'vscode',
|
||||
'LS_COLORS': 'di=01;34', 'SHLVL': '2', 'BASH_FUNC_f%%': '() { :; }',
|
||||
}
|
||||
|
||||
|
||||
class FilterTests(unittest.TestCase):
|
||||
def test_strips_session_metadata_and_keeps_runtime_variables(self):
|
||||
environ = {**REQUIRED, **SESSION, 'SHELL': '/bin/fish'}
|
||||
removed = exec_guest.filter_host_environment(environ)
|
||||
self.assertEqual(set(removed), set(SESSION))
|
||||
self.assertEqual(environ, {**REQUIRED, 'SHELL': '/bin/zsh'})
|
||||
|
||||
def test_keep_list(self):
|
||||
environ = {'WAYLAND_DISPLAY': 'wayland-0', 'SSH_AUTH_SOCK': '/s', 'SHELL': '/bin/fish',
|
||||
'RCLOL_KEEP_HOST_ENV': 'WAYLAND_DISPLAY, SHELL'}
|
||||
self.assertEqual(exec_guest.filter_host_environment(environ), ['SSH_AUTH_SOCK'])
|
||||
self.assertEqual(environ['WAYLAND_DISPLAY'], 'wayland-0')
|
||||
self.assertEqual(environ['SHELL'], '/bin/fish')
|
||||
|
||||
|
||||
class ExecTests(unittest.TestCase):
|
||||
DUMP = ('import json, os; print(json.dumps({"env": dict(os.environ), "fds": sorted('
|
||||
'fd for fd in map(int, os.listdir("/proc/self/fd")) if fd < 3 or os.path.exists("/proc/self/fd/%d" % fd))}))')
|
||||
|
||||
def run_helper(self, environ):
|
||||
leaked = list(os.pipe())
|
||||
leaked.append(os.dup2(leaked[0], 41, inheritable=False))
|
||||
try:
|
||||
result = subprocess.run([sys.executable, str(HELPER), sys.executable, '-c', self.DUMP],
|
||||
env=environ, pass_fds=leaked, capture_output=True, text=True, check=True)
|
||||
finally:
|
||||
for fd in leaked:
|
||||
os.close(fd)
|
||||
return json.loads(result.stdout)
|
||||
|
||||
def test_closes_descriptors_and_keeps_environment_by_default(self):
|
||||
environ = {**REQUIRED, **SESSION, 'SHELL': '/bin/fish'}
|
||||
child = self.run_helper(environ)
|
||||
self.assertEqual(child['fds'], [0, 1, 2])
|
||||
for name, value in environ.items():
|
||||
self.assertEqual(child['env'].get(name), value, name)
|
||||
|
||||
def test_opt_in_filter(self):
|
||||
child = self.run_helper({**REQUIRED, **SESSION, 'SHELL': '/bin/fish', 'RCLOL_FILTER_HOST_ENV': '1'})
|
||||
self.assertEqual(child['fds'], [0, 1, 2])
|
||||
self.assertFalse(set(SESSION) & set(child['env']))
|
||||
for name, value in REQUIRED.items():
|
||||
self.assertEqual(child['env'].get(name), value, name)
|
||||
self.assertEqual(child['env']['SHELL'], '/bin/zsh')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
@@ -0,0 +1,47 @@
|
||||
"""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)
|
||||
self.assertIn('PASS Mac GPU name', result.stdout)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
@@ -1,8 +1,11 @@
|
||||
"""Regression tests for the Nix launcher; no root or game account required."""
|
||||
import fcntl
|
||||
import os
|
||||
from pathlib import Path
|
||||
import signal
|
||||
import subprocess
|
||||
import tempfile
|
||||
import time
|
||||
import unittest
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
@@ -20,6 +23,7 @@ class LauncherTests(unittest.TestCase):
|
||||
(self.share / 'scripts/run.sh').write_text('new packaged script\n')
|
||||
import shutil
|
||||
shutil.copy(ROOT / 'scripts/nix-copy.py', self.share / 'scripts/nix-copy.py')
|
||||
shutil.copy(ROOT / 'scripts/nix-stop.py', self.share / 'scripts/nix-stop.py')
|
||||
self.workspace = self.home / '.local/share/darling-lol'
|
||||
self.log = self.folder / 'calls'
|
||||
self.bin = self.folder / 'bin'
|
||||
@@ -62,6 +66,59 @@ class LauncherTests(unittest.TestCase):
|
||||
self.assertIn('--setgid=0', calls)
|
||||
self.assertEqual(calls[-3:], [str(self.bin / 'fhs'), 'echo', 'hello world'])
|
||||
|
||||
def test_session_restores_signals_inside_the_namespace(self):
|
||||
result = self.launch()
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
self.assertEqual(self.log.read_text().splitlines()[-5:],
|
||||
[str(self.bin / 'fhs'), 'env', '--default-signal=INT,QUIT,HUP,TERM',
|
||||
'bash', './scripts/nix-session.sh'])
|
||||
|
||||
def test_interrupt_becomes_a_stop_request_and_namespace_survives_it(self):
|
||||
# Stands in for bubblewrap: it must outlive Ctrl+C and see the request.
|
||||
(self.bin / 'unshare').write_text(
|
||||
'#!/bin/sh\necho started > "$TEST_LOG"\n'
|
||||
'while [ ! -e .stop-request ]; do sleep 0.1; done\n'
|
||||
'echo stopped >> "$TEST_LOG"\n')
|
||||
launcher = subprocess.Popen(['bash', str(self.launcher)], env=self.env, text=True,
|
||||
stdout=subprocess.PIPE, stderr=subprocess.PIPE,
|
||||
start_new_session=True)
|
||||
for _ in range(100):
|
||||
if self.log.exists():
|
||||
break
|
||||
time.sleep(0.1)
|
||||
os.killpg(launcher.pid, signal.SIGINT)
|
||||
_, stderr = launcher.communicate(timeout=10)
|
||||
self.assertEqual(launcher.returncode, 0, stderr)
|
||||
self.assertEqual(self.log.read_text().splitlines(), ['started', 'stopped'])
|
||||
self.assertFalse((self.workspace / '.stop-request').exists())
|
||||
|
||||
def test_stop_without_session_does_not_enter_the_namespace(self):
|
||||
result = self.launch('--stop')
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
self.assertIn('No darling-lol session is running', result.stdout)
|
||||
self.assertFalse(self.log.exists())
|
||||
|
||||
def test_stop_asks_the_session_holding_the_lock_to_exit(self):
|
||||
self.workspace.mkdir(parents=True)
|
||||
lock = self.workspace / '.launcher.lock'
|
||||
session = subprocess.Popen(['flock', str(lock), 'sh', '-c',
|
||||
'while [ ! -e "$0/.stop-request" ]; do sleep 0.1; done',
|
||||
str(self.workspace)])
|
||||
self.addCleanup(session.kill)
|
||||
with open(lock, 'a+') as handle:
|
||||
for _ in range(100):
|
||||
try:
|
||||
fcntl.flock(handle, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
||||
fcntl.flock(handle, fcntl.LOCK_UN)
|
||||
time.sleep(0.1)
|
||||
except BlockingIOError:
|
||||
break
|
||||
result = self.launch('--stop')
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
self.assertIn('Stopped.', result.stdout)
|
||||
self.assertEqual(session.wait(timeout=5), 0)
|
||||
self.assertFalse(self.log.exists())
|
||||
|
||||
def test_setup_never_makes_a_user_writable_runtime_setuid(self):
|
||||
result = self.launch('--installer')
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
|
||||
@@ -3,8 +3,10 @@ import importlib.util
|
||||
import os
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import signal
|
||||
import subprocess
|
||||
import tempfile
|
||||
import time
|
||||
import unittest
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
@@ -30,6 +32,26 @@ class IdleTests(unittest.TestCase):
|
||||
(process / 'maps').read_text.side_effect = FileNotFoundError()
|
||||
self.assertFalse(idle.client_active(Path('/fixture/prefix'), processes=[process]))
|
||||
|
||||
def test_crash_handlers_alone_do_not_keep_the_prefix_active(self):
|
||||
spec = importlib.util.spec_from_file_location('nix_idle_handlers', ROOT / 'scripts/nix-idle.py')
|
||||
assert spec is not None and spec.loader is not None
|
||||
idle = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(idle)
|
||||
with tempfile.TemporaryDirectory() as folder:
|
||||
prefix = Path(folder) / 'prefix'
|
||||
app = prefix / 'Applications/League of Legends.app'
|
||||
|
||||
def process(pid, executable):
|
||||
entry = Path(folder) / 'proc' / str(pid)
|
||||
entry.mkdir(parents=True)
|
||||
(entry / 'maps').write_text(f'7f00-7f01 r-xp 0 0:0 1 {app}/Contents/MacOS/{executable}\n')
|
||||
(entry / 'cmdline').write_bytes(f'/Applications/League of Legends.app/Contents/MacOS/{executable}\0--flag\0'.encode())
|
||||
return entry
|
||||
|
||||
handlers = [process(1, 'LeagueCrashHandler'), process(2, 'chrome_crashpad_handler')]
|
||||
self.assertFalse(idle.client_active(prefix, processes=handlers))
|
||||
self.assertTrue(idle.client_active(prefix, processes=[*handlers, process(3, 'LeagueClientUx')]))
|
||||
|
||||
|
||||
class LifecycleTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
@@ -54,9 +76,16 @@ def client_active(prefix, **kwargs):
|
||||
with open(os.environ['TEST_LOG'], 'a') as f:
|
||||
f.write('scan\\n')
|
||||
return calls < 2
|
||||
def server_running(prefix):
|
||||
return os.environ.get('SERVER_RUNNING', '1') == '1'
|
||||
''')
|
||||
(scripts / 'darling-rootless.sh').write_text('''#!/bin/bash
|
||||
echo "shutdown:$*" >> "$TEST_LOG"
|
||||
# Like darling shutdown, end the client command that the server ran.
|
||||
if [ -n "${CLIENT_SLEEP:-}" ]; then
|
||||
for _ in 1 2 3 4 5 6 7 8 9 10; do [ -e "$TEST_LOG.pid" ] && break; sleep 0.2; done
|
||||
fi
|
||||
[ -e "$TEST_LOG.pid" ] && kill "$(cat "$TEST_LOG.pid")"
|
||||
if [ "${SHUTDOWN_RC:-0}" != 0 ] && [ ! -e "$TEST_LOG.retried" ]; then
|
||||
touch "$TEST_LOG.retried"
|
||||
exit "$SHUTDOWN_RC"
|
||||
@@ -65,6 +94,10 @@ exit 0
|
||||
''')
|
||||
client = scripts / 'run.sh'
|
||||
client.write_text('''#!/usr/bin/env bash
|
||||
if [ -n "${CLIENT_SLEEP:-}" ]; then
|
||||
echo "$$" > "$TEST_LOG.pid"
|
||||
exec sleep "$CLIENT_SLEEP"
|
||||
fi
|
||||
echo client-finished >> "$TEST_LOG"
|
||||
exit "${CLIENT_RC:-0}"
|
||||
'''.replace('#!/usr/bin/env bash', '#!' + str(shutil.which('bash'))))
|
||||
@@ -89,11 +122,38 @@ exit "${CLIENT_RC:-0}"
|
||||
self.assertEqual(self.log.read_text().splitlines().count('shutdown:shutdown'), 2)
|
||||
self.assertIn('shutdown:shutdown', self.log.read_text())
|
||||
|
||||
def test_stop_request_shuts_down_without_waiting_for_command_or_clients(self):
|
||||
(self.root / '.stop-request').touch()
|
||||
result = self.run_session(CLIENT_SLEEP='60')
|
||||
self.assertEqual(result.returncode, 128 + 15, result.stderr)
|
||||
self.assertIn('Stopping Darling', result.stderr)
|
||||
self.assertEqual(self.log.read_text().splitlines(), ['shutdown:shutdown'])
|
||||
|
||||
def test_interrupt_shuts_down_without_waiting_for_command_or_clients(self):
|
||||
session = subprocess.Popen(['bash', str(self.root / 'scripts/nix-session.sh')],
|
||||
env=dict(self.env, CLIENT_SLEEP='60'), text=True,
|
||||
stderr=subprocess.PIPE)
|
||||
pid_file = Path(str(self.log) + '.pid')
|
||||
for _ in range(50):
|
||||
if pid_file.exists():
|
||||
break
|
||||
time.sleep(0.1)
|
||||
session.send_signal(signal.SIGINT)
|
||||
_, stderr = session.communicate(timeout=10)
|
||||
self.assertEqual(session.returncode, 128 + signal.SIGINT, stderr)
|
||||
self.assertEqual(self.log.read_text().splitlines(), ['shutdown:shutdown'])
|
||||
|
||||
def test_failed_shutdown_is_not_retried_once_the_server_is_gone(self):
|
||||
result = self.run_session(SHUTDOWN_RC='23', SERVER_RUNNING='0')
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
self.assertEqual(self.log.read_text().splitlines().count('shutdown:shutdown'), 1)
|
||||
|
||||
def test_shutdown_retries_when_terminal_diagnostics_fail(self):
|
||||
spec = importlib.util.spec_from_file_location('lifecycle_fixture', self.root / 'scripts/nix-lifecycle.py')
|
||||
lifecycle = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(lifecycle)
|
||||
child = MagicMock()
|
||||
child.poll.return_value = 17
|
||||
child.wait.return_value = 17
|
||||
stderr = MagicMock()
|
||||
stderr.write.side_effect = OSError('terminal disconnected')
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
"""system_profiler hardware/software overviews, with stub guest commands; never starts Darling."""
|
||||
import os
|
||||
from pathlib import Path
|
||||
import plistlib
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
SCRIPT = ROOT / 'tools/darling-system_profiler.sh'
|
||||
|
||||
IOREG = '''+-o MacBookPro16,1 <class IOPlatformExpertDevice, id 0x100000110, registered, matched, active, busy 0 (0 ms), retain 30>
|
||||
{
|
||||
"IOPlatformSerialNumber" = "USER-SUPPLIED"
|
||||
"model" = <"MacBookPro16,1">
|
||||
"manufacturer" = <"Apple Inc.">
|
||||
"IOPlatformUUID" = "8E1F2A3B-4C5D-4E6F-8A7B-9C0D1E2F3A4B"
|
||||
"provisioning_UDID" = "8E1F2A3B-4C5D-4E6F-8A7B-9C0D1E2F3A4B"
|
||||
"ROM" = <0a1b2c3d4e5f>
|
||||
}
|
||||
'''
|
||||
SYSCTL = {
|
||||
'hw.physicalcpu': '8', 'hw.packages': '1', 'hw.memsize': str(32 << 30),
|
||||
'machdep.cpu.brand_string': 'AMD Ryzen 7 5800X 8-Core Processor & more',
|
||||
'hw.l2cachesize': str(512 << 10), 'hw.l3cachesize': str(32 << 20), 'hw.cpufrequency': '3800000000',
|
||||
}
|
||||
|
||||
|
||||
class SystemProfilerTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.folder = tempfile.TemporaryDirectory()
|
||||
self.bin = Path(self.folder.name)
|
||||
self.stub('sysctl', 'import sys\nvalues = %r\nname = sys.argv[-1]\n'
|
||||
'if name not in values: sys.exit(1)\nprint(values[name])' % SYSCTL)
|
||||
self.stub('sw_vers', 'import sys\nprint({"-productName": "macOS", "-productVersion": "11.7.4", '
|
||||
'"-buildVersion": "20G1120"}[sys.argv[1]])')
|
||||
self.stub('uname', 'print("Darwin 20.6.0")')
|
||||
self.stub('hostname', 'print("darling")')
|
||||
self.stub('ioreg', 'import sys\nsys.stdout.write(%r)' % IOREG)
|
||||
|
||||
def tearDown(self):
|
||||
self.folder.cleanup()
|
||||
|
||||
def stub(self, name, body):
|
||||
path = self.bin / name
|
||||
path.write_text('#!%s\n%s\n' % (sys.executable, body))
|
||||
path.chmod(0o755)
|
||||
|
||||
def run_profiler(self, *args):
|
||||
env = {'PATH': '%s:%s' % (self.bin, os.environ['PATH']), 'USER': 'player'}
|
||||
return subprocess.run(['sh', str(SCRIPT), *args], env=env, check=True, capture_output=True, text=True).stdout
|
||||
|
||||
def xml_items(self, data_type):
|
||||
report = plistlib.loads(self.run_profiler('-xml', data_type).encode())
|
||||
self.assertEqual(report[0]['_dataType'], data_type)
|
||||
return report[0]['_items'][0]
|
||||
|
||||
def test_hardware_reports_iokit_identity_and_sysctl_values(self):
|
||||
item = self.xml_items('SPHardwareDataType')
|
||||
self.assertEqual(item, {
|
||||
'_name': 'hardware_overview', 'machine_name': 'MacBook Pro', 'machine_model': 'MacBookPro16,1',
|
||||
'cpu_type': 'AMD Ryzen 7 5800X 8-Core Processor & more', 'current_processor_speed': '3.8 GHz',
|
||||
'packages': 1, 'number_processors': 8, 'l2_cache_core': '512 KB', 'l3_cache': '32 MB',
|
||||
'physical_memory': '32 GB', 'serial_number': 'USER-SUPPLIED',
|
||||
'platform_UUID': '8E1F2A3B-4C5D-4E6F-8A7B-9C0D1E2F3A4B',
|
||||
'provisioning_UDID': '8E1F2A3B-4C5D-4E6F-8A7B-9C0D1E2F3A4B',
|
||||
})
|
||||
text = self.run_profiler('SPHardwareDataType')
|
||||
self.assertIn(' Serial Number (system): USER-SUPPLIED\n', text)
|
||||
self.assertIn(' Model Name: MacBook Pro\n', text)
|
||||
|
||||
def test_missing_iokit_and_sysctl_values_are_omitted(self):
|
||||
self.stub('ioreg', 'import sys\nsys.exit(1)')
|
||||
self.stub('sysctl', 'import sys\nsys.exit(1)')
|
||||
self.assertEqual(self.xml_items('SPHardwareDataType'), {'_name': 'hardware_overview'})
|
||||
self.assertNotIn('Serial Number', self.run_profiler('SPHardwareDataType'))
|
||||
|
||||
def test_software_overview(self):
|
||||
item = self.xml_items('SPSoftwareDataType')
|
||||
self.assertEqual(item, {
|
||||
'_name': 'os_overview', 'os_version': 'macOS 11.7.4 (20G1120)', 'kernel_version': 'Darwin 20.6.0',
|
||||
'boot_volume': 'Macintosh HD', 'boot_mode': 'normal_boot', 'local_host_name': 'darling',
|
||||
'user_name': 'player',
|
||||
})
|
||||
self.assertIn(' Boot Mode: Normal\n', self.run_profiler('SPSoftwareDataType'))
|
||||
|
||||
def test_listed_and_default_types(self):
|
||||
listed = self.run_profiler('-listDataTypes')
|
||||
self.assertIn('SPHardwareDataType', listed)
|
||||
self.assertIn('SPSoftwareDataType', listed)
|
||||
types = [entry['_dataType'] for entry in plistlib.loads(self.run_profiler('-xml').encode())]
|
||||
self.assertEqual(types[:2], ['SPHardwareDataType', 'SPSoftwareDataType'])
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
@@ -1,6 +1,8 @@
|
||||
#!/bin/sh
|
||||
# system_profiler for the Darling prefix: reports the host's real display and
|
||||
# network hardware for the data types applications query at startup.
|
||||
# network hardware for the data types applications query at startup. The hardware
|
||||
# and software overviews come from the guest's own IOKit, sysctl and sw_vers, so they
|
||||
# match what other APIs report (including a configure-darling-platform.py override).
|
||||
# Installed as /usr/sbin/system_profiler by tools/install-darling-system-profiler.py.
|
||||
host=/Volumes/SystemRoot
|
||||
xml=0
|
||||
@@ -9,12 +11,12 @@ while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
-xml) xml=1;;
|
||||
-detailLevel|-timeout) shift;;
|
||||
-listDataTypes) printf 'Available Datatypes:\nSPAirPortDataType\nSPDisplaysDataType\nSPEthernetDataType\n'; exit 0;;
|
||||
-listDataTypes) printf 'Available Datatypes:\nSPAirPortDataType\nSPDisplaysDataType\nSPEthernetDataType\nSPHardwareDataType\nSPSoftwareDataType\n'; exit 0;;
|
||||
SP*DataType) types="$types $1";;
|
||||
esac
|
||||
shift
|
||||
done
|
||||
[ -n "$types" ] || types=" SPDisplaysDataType SPEthernetDataType SPAirPortDataType"
|
||||
[ -n "$types" ] || types=" SPHardwareDataType SPSoftwareDataType SPDisplaysDataType SPEthernetDataType SPAirPortDataType"
|
||||
|
||||
escape() { sed -e 's/&/\&/g' -e 's/</\</g' -e 's/>/\>/g'; }
|
||||
|
||||
@@ -46,6 +48,80 @@ displays() { # one line per GPU: vendor|device|vram MB or empty|name
|
||||
done
|
||||
}
|
||||
|
||||
load_platform() { # IOPlatformExpertDevice string properties, "key|value" per line
|
||||
# String values print as "value", NUL-terminated data as <"value">; other data is skipped.
|
||||
platform_properties=$(ioreg -rd1 -c IOPlatformExpertDevice 2>/dev/null |
|
||||
sed -n 's/^[ |]*"\([^"]*\)" = <\{0,1\}"\(.*\)">\{0,1\}$/\1|\2/p')
|
||||
}
|
||||
platform() {
|
||||
printf '%s\n' "$platform_properties" | awk -v key="$1" 'index($0, key "|") == 1 { print substr($0, length(key) + 2); exit }'
|
||||
}
|
||||
|
||||
machine_name() { # model identifier -> marketing family, as in About This Mac
|
||||
case "$1" in
|
||||
MacBookPro*) echo 'MacBook Pro';; MacBookAir*) echo 'MacBook Air';; MacBook*) echo 'MacBook';;
|
||||
iMacPro*) echo 'iMac Pro';; iMac*) echo 'iMac';; Macmini*) echo 'Mac mini';; MacPro*) echo 'Mac Pro';;
|
||||
*) echo 'Mac';;
|
||||
esac
|
||||
}
|
||||
|
||||
hardware() { # label|xml key|xml type|value for each known field
|
||||
load_platform
|
||||
model=$(platform model)
|
||||
cores=$(sysctl -n hw.physicalcpu 2>/dev/null)
|
||||
memory=$(sysctl -n hw.memsize 2>/dev/null | awk '$1 > 0 { printf "%d GB", $1 / 1073741824 + 0.5 }')
|
||||
speed=$(sysctl -n hw.cpufrequency 2>/dev/null | awk '$1 > 0 { printf "%.1f GHz", $1 / 1e9 }')
|
||||
l2=$(sysctl -n hw.l2cachesize 2>/dev/null | awk '$1 > 0 { printf "%d KB", $1 / 1024 }')
|
||||
l3=$(sysctl -n hw.l3cachesize 2>/dev/null | awk '$1 > 0 { printf "%d MB", $1 / 1048576 }')
|
||||
echo "Model Name|machine_name|string|${model:+$(machine_name "$model")}"
|
||||
echo "Model Identifier|machine_model|string|$model"
|
||||
echo "Processor Name|cpu_type|string|$(sysctl -n machdep.cpu.brand_string 2>/dev/null)"
|
||||
echo "Processor Speed|current_processor_speed|string|$speed"
|
||||
echo "Number of Processors|packages|integer|$(sysctl -n hw.packages 2>/dev/null)"
|
||||
echo "Total Number of Cores|number_processors|integer|$cores"
|
||||
echo "L2 Cache (per Core)|l2_cache_core|string|$l2"
|
||||
echo "L3 Cache|l3_cache|string|$l3"
|
||||
echo "Memory|physical_memory|string|$memory"
|
||||
echo "Serial Number (system)|serial_number|string|$(platform IOPlatformSerialNumber)"
|
||||
echo "Hardware UUID|platform_UUID|string|$(platform IOPlatformUUID)"
|
||||
echo "Provisioning UDID|provisioning_UDID|string|$(platform provisioning_UDID)"
|
||||
}
|
||||
|
||||
software() {
|
||||
name=$(sw_vers -productName 2>/dev/null); version=$(sw_vers -productVersion 2>/dev/null)
|
||||
build=$(sw_vers -buildVersion 2>/dev/null)
|
||||
echo "System Version|os_version|string|${version:+${name:-macOS} $version${build:+ ($build)}}"
|
||||
echo "Kernel Version|kernel_version|string|$(uname -sr 2>/dev/null)"
|
||||
echo "Boot Volume|boot_volume|string|Macintosh HD"
|
||||
echo "Boot Mode|boot_mode|string|normal_boot"
|
||||
echo "Computer Name|local_host_name|string|$(hostname -s 2>/dev/null)"
|
||||
echo "User Name|user_name|string|${USER:-$(id -un 2>/dev/null)}"
|
||||
}
|
||||
|
||||
overview_xml() { # $1 = _name; fields on stdin; empty values are omitted
|
||||
echo ' <dict>'
|
||||
echo " <key>_name</key><string>$1</string>"
|
||||
while IFS='|' read -r label key kind value; do
|
||||
[ -n "$value" ] || continue
|
||||
if [ "$kind" = integer ]; then
|
||||
case "$value" in *[!0-9]*) continue;; esac
|
||||
echo " <key>$key</key><integer>$value</integer>"
|
||||
else
|
||||
echo " <key>$key</key><string>$(printf '%s' "$value" | escape)</string>"
|
||||
fi
|
||||
done
|
||||
echo ' </dict>'
|
||||
}
|
||||
|
||||
overview_text() { # $1 = section title, $2 = overview title; fields on stdin
|
||||
printf '%s:\n\n %s:\n\n' "$1" "$2"
|
||||
while IFS='|' read -r label key kind value; do
|
||||
[ "$key" != boot_mode ] || value=Normal
|
||||
[ -z "$value" ] || printf ' %s: %s\n' "$label" "$value"
|
||||
done
|
||||
echo
|
||||
}
|
||||
|
||||
interfaces() { # $1 = wired|wireless; physical host interfaces only
|
||||
for path in "$host"/sys/class/net/*; do
|
||||
[ -e "$path/device" ] || continue
|
||||
@@ -65,6 +141,8 @@ if [ "$xml" = 1 ]; then
|
||||
echo ' <key>_items</key>'
|
||||
echo ' <array>'
|
||||
case "$type" in
|
||||
SPHardwareDataType) hardware | overview_xml hardware_overview;;
|
||||
SPSoftwareDataType) software | overview_xml os_overview;;
|
||||
SPDisplaysDataType)
|
||||
displays | while IFS='|' read -r vendor device vram name; do
|
||||
name=$(printf '%s' "$name" | escape)
|
||||
@@ -108,6 +186,8 @@ fi
|
||||
|
||||
for type in $types; do
|
||||
case "$type" in
|
||||
SPHardwareDataType) hardware | overview_text Hardware 'Hardware Overview';;
|
||||
SPSoftwareDataType) software | overview_text Software 'System Software Overview';;
|
||||
SPDisplaysDataType)
|
||||
printf 'Graphics/Displays:\n\n'
|
||||
displays | while IFS='|' read -r vendor device vram name; do
|
||||
|
||||
Executable
+71
@@ -0,0 +1,71 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Run a command without the host file descriptors this launcher inherited (anything above stderr).
|
||||
|
||||
With RCLOL_FILTER_HOST_ENV=1 it also drops host desktop-session variables first (see
|
||||
host_session_variable). RCLOL_KEEP_HOST_ENV lists names to keep anyway, separated by spaces or commas.
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
|
||||
# Desktop-session, service-manager, terminal and packaging metadata that nothing in the guest reads.
|
||||
# Everything Darling and the host libraries it loads need is kept: DISPLAY, XAUTHORITY, XCURSOR_*,
|
||||
# XDG_RUNTIME_DIR, XDG_DATA_DIRS and the other XDG base directories (the Vulkan loader searches
|
||||
# them), DBUS_SESSION_BUS_ADDRESS, PULSE_*, PIPEWIRE_*, ALSA_*, DRI_PRIME, MESA_*, mesa_glthread,
|
||||
# __GL_*, __NV_*, __EGL_*, VK_*, LIBGL_*, LD_PRELOAD (gamemoderun) and LD_LIBRARY_PATH.
|
||||
SESSION_PREFIXES = (
|
||||
'XDG_SESSION_', 'XDG_SEAT', 'XDG_VTNR', 'XDG_CURRENT_DESKTOP', 'XDG_MENU_PREFIX',
|
||||
'GNOME_', 'KDE_', 'GDM', 'SSH_', 'SYSTEMD_', 'MEMORY_PRESSURE_', 'QT_', 'GTK_', 'GDK_',
|
||||
'GIO_', 'GSETTINGS_', 'GJS_', 'FLATPAK_', 'SNAP_', 'KONSOLE_', 'TERM_PROGRAM', 'BASH_FUNC_',
|
||||
)
|
||||
SESSION_NAMES = {
|
||||
'WAYLAND_DISPLAY', 'DESKTOP_SESSION', 'DESKTOP_STARTUP_ID', 'SESSION_MANAGER',
|
||||
'INVOCATION_ID', 'JOURNAL_STREAM', 'MANAGERPID', 'MANAGERPIDFDID', 'NOTIFY_SOCKET',
|
||||
'LISTEN_FDS', 'LISTEN_PID', 'LISTEN_FDNAMES', 'COLORTERM', 'VTE_VERSION', 'WINDOWID',
|
||||
'WINDOWPATH', 'CHROME_DESKTOP', 'NO_AT_BRIDGE', 'MOTD_SHOWN', 'SNAP', 'APPIMAGE', 'APPDIR',
|
||||
'OWD', 'VIRTUAL_ENV', 'DEBUGINFOD_URLS', 'LS_COLORS', 'COLORFGBG', 'OLDPWD', 'SHLVL',
|
||||
}
|
||||
GUEST_SHELL = '/bin/zsh'
|
||||
|
||||
|
||||
def host_session_variable(name):
|
||||
return name in SESSION_NAMES or name.startswith(SESSION_PREFIXES)
|
||||
|
||||
|
||||
def filter_host_environment(environ):
|
||||
"""Return the names removed from environ (a mutable mapping), honoring RCLOL_KEEP_HOST_ENV."""
|
||||
keep = set(environ.get('RCLOL_KEEP_HOST_ENV', '').replace(',', ' ').split())
|
||||
removed = sorted(name for name in environ if host_session_variable(name) and name not in keep)
|
||||
for name in removed:
|
||||
del environ[name]
|
||||
# The host login shell (for example /bin/fish) does not exist in the guest.
|
||||
if 'SHELL' not in keep:
|
||||
environ['SHELL'] = GUEST_SHELL
|
||||
return removed
|
||||
|
||||
|
||||
def close_inherited_fds():
|
||||
"""A terminal, IDE or service manager can leak descriptors; the guest must not inherit them."""
|
||||
try:
|
||||
names = os.listdir('/proc/self/fd')
|
||||
except OSError:
|
||||
os.closerange(3, 65536)
|
||||
return
|
||||
for name in names:
|
||||
if name.isdigit() and int(name) > 2:
|
||||
try:
|
||||
os.close(int(name))
|
||||
except OSError:
|
||||
pass # the descriptor listdir used is already closed
|
||||
|
||||
|
||||
def main(argv):
|
||||
if len(argv) < 2:
|
||||
sys.exit('usage: exec-guest.py COMMAND [ARGUMENT]...')
|
||||
if os.environ.get('RCLOL_FILTER_HOST_ENV') == '1':
|
||||
filter_host_environment(os.environ)
|
||||
close_inherited_fds()
|
||||
os.execvp(argv[1], argv[1:])
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main(sys.argv)
|
||||
@@ -16,15 +16,28 @@ def check():
|
||||
folder = Path(directory)
|
||||
launcher = folder / "launch.sh"
|
||||
launcher.write_text((ROOT / "tools/run-riot-client.sh").read_text().replace("@GUEST_HOME@", "/Users/offline-check"))
|
||||
(folder / "exec-guest.py").write_text((ROOT / "tools/exec-guest.py").read_text())
|
||||
binary_folder = folder / "bin"
|
||||
binary_folder.mkdir()
|
||||
log = folder / "calls.jsonl"
|
||||
stub = f'''#!{sys.executable}
|
||||
import json, os, sys
|
||||
record = {{"command": os.path.basename(sys.argv[0]), "args": sys.argv[1:]}}
|
||||
# The launcher starts the guest through exec-guest.py; run it for real.
|
||||
if record["command"] == "python3" and sys.argv[1:] and os.path.basename(sys.argv[1]) == "exec-guest.py":
|
||||
os.execv(sys.executable, [sys.executable] + sys.argv[1:])
|
||||
if record["command"] == "darling":
|
||||
# Descriptors open now were inherited; the one listdir used is closed again before this check.
|
||||
def is_open(fd):
|
||||
try:
|
||||
os.fstat(fd)
|
||||
return True
|
||||
except OSError:
|
||||
return False
|
||||
record["fds"] = sorted(fd for fd in map(int, os.listdir("/proc/self/fd")) if is_open(fd))
|
||||
record["flags"] = {{key: os.environ.get(key) for key in
|
||||
("RCLOL_APP_ARG3", "RCLOL_APP_ARG3_SLOT", "RCLOL_UX_ARG2", "RCLOL_UX_ARG2_SLOT", "RCLOL_UX_ARG1", "RCLOL_APP_ARGS", "RCLOL_APP_ARG2", "DYLD_INSERT_LIBRARIES", "DYLD_LIBRARY_PATH", "DYLD_FRAMEWORK_PATH", "DARLING_XSHM_LIBRARY")}}
|
||||
("RCLOL_APP_ARG3", "RCLOL_APP_ARG3_SLOT", "RCLOL_UX_ARG2", "RCLOL_UX_ARG2_SLOT", "RCLOL_UX_ARG1", "RCLOL_APP_ARGS", "RCLOL_APP_ARG2", "DYLD_INSERT_LIBRARIES", "DYLD_LIBRARY_PATH", "DYLD_FRAMEWORK_PATH", "DARLING_XSHM_LIBRARY",
|
||||
"WAYLAND_DISPLAY", "XAUTHORITY", "DRI_PRIME", "MESA_NO_ERROR", "__GL_THREADED_OPTIMIZATIONS", "SHELL")}}
|
||||
with open({str(log)!r}, "a") as output:
|
||||
output.write(json.dumps(record) + "\\n")
|
||||
'''
|
||||
@@ -60,7 +73,15 @@ with open({str(log)!r}, "a") as output:
|
||||
"RCLOL_OPEN_URL_BRIDGE": "1",
|
||||
"RCLOL_DISABLE_GPU_VSYNC": "1",
|
||||
})
|
||||
subprocess.run(["sh", str(launcher)], env=env, check=True, capture_output=True, text=True)
|
||||
# Leaked host descriptors, including multi-digit ones, must not reach the guest.
|
||||
leaked = list(os.pipe())
|
||||
for target in (12, 200):
|
||||
leaked.append(os.dup2(leaked[0], target, inheritable=False))
|
||||
try:
|
||||
subprocess.run(["sh", str(launcher)], env=env, check=True, capture_output=True, text=True, pass_fds=leaked)
|
||||
finally:
|
||||
for fd in leaked:
|
||||
os.close(fd)
|
||||
records = [json.loads(line) for line in log.read_text().splitlines()]
|
||||
maintenance = [r for r in records if r["command"] == "python3"]
|
||||
assert [Path(r["args"][0]).name for r in maintenance] == [
|
||||
@@ -70,6 +91,7 @@ with open({str(log)!r}, "a") as output:
|
||||
launches = [r for r in records if r["command"] == "darling"]
|
||||
assert len(launches) == 1, "unexpected launch count"
|
||||
launch = launches[0]
|
||||
assert launch["fds"] == [0, 1, 2], f"inherited descriptors reached the guest: {launch['fds']}"
|
||||
assert all(launch["flags"][key] is None for key in (
|
||||
"RCLOL_APP_ARG3", "RCLOL_APP_ARG3_SLOT", "RCLOL_UX_ARG2", "RCLOL_UX_ARG2_SLOT"
|
||||
)), "inherited inspector/unlimited-frame argument survived"
|
||||
@@ -95,7 +117,17 @@ with open({str(log)!r}, "a") as output:
|
||||
subprocess.run(["sh", str(launcher)], env=env, check=True, capture_output=True, text=True)
|
||||
clean = json.loads(log.read_text().splitlines()[-1])
|
||||
assert clean["flags"]["DYLD_INSERT_LIBRARIES"] is None
|
||||
print("LAUNCHER_OFFLINE pass=1 installed=PASS installer=PASS inherited_injection=absent runtime=preserved")
|
||||
# The opt-in host environment filter drops session metadata but never the launcher's GPU tuning.
|
||||
env.update({"RCLOL_FILTER_HOST_ENV": "1", "WAYLAND_DISPLAY": "wayland-0", "XAUTHORITY": "/tmp/xauth",
|
||||
"DRI_PRIME": "1", "SHELL": "/bin/fish"})
|
||||
env.pop("MESA_NO_ERROR", None)
|
||||
env.pop("__GL_THREADED_OPTIMIZATIONS", None)
|
||||
subprocess.run(["sh", str(launcher)], env=env, check=True, capture_output=True, text=True)
|
||||
filtered = json.loads(log.read_text().splitlines()[-1])["flags"]
|
||||
assert filtered["WAYLAND_DISPLAY"] is None and filtered["SHELL"] == "/bin/zsh", "host session metadata survived"
|
||||
assert (filtered["XAUTHORITY"], filtered["DRI_PRIME"]) == ("/tmp/xauth", "1"), "X11/GPU selection lost"
|
||||
assert (filtered["MESA_NO_ERROR"], filtered["__GL_THREADED_OPTIMIZATIONS"]) == ("1", "0"), "launcher GPU tuning lost"
|
||||
print("LAUNCHER_OFFLINE pass=1 installed=PASS installer=PASS inherited_injection=absent runtime=preserved inherited_fds=closed host_env_filter=PASS")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -29,7 +29,9 @@ unset RCLOL_APP_ARG3 RCLOL_APP_ARG3_SLOT RCLOL_UX_ARG1 RCLOL_UX_ARG1_SLOT
|
||||
unset RCLOL_UX_ARG2 RCLOL_UX_ARG2_SLOT
|
||||
# The paired runtime presents completed GPU textures without CPU readback.
|
||||
# Explicit 0 or an empty value selects the CPU fallback for diagnostics.
|
||||
for option in RCLOL_GPU_DIRECT_PRESENT RCLOL_GPU_DIRECT_ASYNC RCLOL_GPU_DIRECT_VSYNC DARLING_CA_PRESENT_AT_COMMIT DARLING_CA_SKIP_LOCAL_IOSURFACE_RENDER RCLOL_FORWARD_MOUSE_MOVED RCLOL_MOUSE_CONFINEMENT; do
|
||||
# DARLING_METAL_MAC_GPU_NAME reports the MacBookPro16,1 GPU of the host vendor (AMD or Intel) as
|
||||
# MTLDevice.name, matching the model IOKit reports; memory figures stay the host's.
|
||||
for option in RCLOL_GPU_DIRECT_PRESENT RCLOL_GPU_DIRECT_ASYNC RCLOL_GPU_DIRECT_VSYNC DARLING_CA_PRESENT_AT_COMMIT DARLING_CA_SKIP_LOCAL_IOSURFACE_RENDER RCLOL_FORWARD_MOUSE_MOVED RCLOL_MOUSE_CONFINEMENT DARLING_METAL_MAC_GPU_NAME; do
|
||||
eval 'value=${'"$option"'-1}'
|
||||
case "$value" in 0|'') unset "$option";; *) export "$option=$value";; esac
|
||||
done
|
||||
@@ -104,7 +106,9 @@ fi
|
||||
# On affected setups Chromium's default temp directory is unusable. Electron
|
||||
# can copy app.asar entries to temporary files for async reads and report a
|
||||
# failed copy as "not found". Use a writable guest directory by default.
|
||||
exec env -u ELECTRON_RUN_AS_NODE -u DYLD_INSERT_LIBRARIES EGL_PLATFORM=x11 \
|
||||
# exec-guest.py closes host file descriptors above stderr before starting the guest. A shell
|
||||
# loop cannot: dash only accepts single-digit descriptors in redirections.
|
||||
exec python3 "$runner_dir/exec-guest.py" env -u ELECTRON_RUN_AS_NODE -u DYLD_INSERT_LIBRARIES EGL_PLATFORM=x11 \
|
||||
MAC_CHROMIUM_TMPDIR="${MAC_CHROMIUM_TMPDIR:-/private/tmp}" \
|
||||
LANG="${RCLOL_GUEST_LOCALE:-en_US.UTF-8}" LC_ALL="${RCLOL_GUEST_LOCALE:-en_US.UTF-8}" \
|
||||
mesa_glthread="${RCLOL_MESA_GLTHREAD-${mesa_glthread-false}}" \
|
||||
|
||||
@@ -49,6 +49,13 @@ int main() {
|
||||
expectName("", MTLDeviceInfo::kFallbackName);
|
||||
expectName(nullptr, MTLDeviceInfo::kFallbackName);
|
||||
|
||||
// -[MTLDevice registryID]: never 0, like a real Mac.
|
||||
{
|
||||
bool ok = MTLDeviceInfo::kDefaultRegistryID != 0;
|
||||
failures += !ok;
|
||||
std::printf("%s registry ID is non-zero\n", ok ? "PASS" : "FAIL");
|
||||
}
|
||||
|
||||
// DARLING_METAL_GPU_NAME override.
|
||||
{
|
||||
bool ok = MTLDeviceInfo::resolveName("My GPU", "AMD Radeon RX 6800 XT (RADV NAVI21)") == "My GPU"
|
||||
@@ -58,6 +65,19 @@ int main() {
|
||||
std::printf("%s name override\n", ok ? "PASS" : "FAIL");
|
||||
}
|
||||
|
||||
// DARLING_METAL_MAC_GPU_NAME: MacBookPro16,1 GPUs for AMD and Intel hosts; NVIDIA keeps its name.
|
||||
{
|
||||
using MTLDeviceInfo::macGpuName;
|
||||
bool ok = std::string(macGpuName(0x1002)) == "AMD Radeon Pro 5500M"
|
||||
&& std::string(macGpuName(0x8086)) == "Intel UHD Graphics 630"
|
||||
&& macGpuName(0x10de) == nullptr
|
||||
&& MTLDeviceInfo::resolveName(nullptr, "AMD Radeon RX 6600 (RADV NAVI23)", macGpuName(0x1002)) == "AMD Radeon Pro 5500M"
|
||||
&& MTLDeviceInfo::resolveName(nullptr, "NVIDIA GeForce GTX 1060 6GB", macGpuName(0x10de)) == "NVIDIA GeForce GTX 1060 6GB"
|
||||
&& MTLDeviceInfo::resolveName("My GPU", "AMD Radeon RX 6600 (RADV NAVI23)", macGpuName(0x1002)) == "My GPU";
|
||||
failures += !ok;
|
||||
std::printf("%s Mac GPU name\n", ok ? "PASS" : "FAIL");
|
||||
}
|
||||
|
||||
// DARLING_METAL_VRAM_MB parsing: positive decimal integer only.
|
||||
const unsigned long long MiB = 1024ull * 1024ull;
|
||||
expectBytes("vram \"8192\"", MTLDeviceInfo::parseMegabytes("8192"), 8192 * MiB);
|
||||
|
||||
Reference in new issue
Block a user