Merge pull request #43 from lucannez64/test-batch-2

Merge tested runtime, GPU reporting, certificate, and frame-wait fixes
This commit is contained in:
Kalah authored and GitHub committed 2026-10-17 22:55:17 +02:00
commit 5dd00ed1ae
17 files changed
+1229 -64

No files matched your search

+11
View File
@@ -12,8 +12,19 @@ python3 tools/riot-launcher-offline-check.py
python3 tools/riot-memory-installer-offline-check.py
python3 tools/riot-foundation-installer-offline-check.py
python3 tools/riot-llvm-version-offline-check.py
python3 -m unittest tests.test_root_certificates
```
The Metal device-report check compiles on the host and needs no Darling:
```sh
c++ -std=c++17 -I build/darling-src/src/external/metal/private-include \
tools/test-metal-device-info.cpp -o build/test-metal-device-info
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.
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.
+24
View File
@@ -56,6 +56,18 @@ The installer fetcher no longer treats a missing archive `Info.plist` or the inf
There is also a separate known compatibility gap: native LaunchAgent watch registration can return HTTP 409 after the League install path is accepted, leaving the download idle. A historical inspector hook handles this and interrupted `pending_verify` states in 28 mock cases, but that hook is disabled in normal launches and excluded from the manual workflow. There is no verified runtime-only replacement for that handoff in this snapshot. Do not assume the native permission and launcher fixes resolve every idle Install case, or enable debugger/API automation to work around it.
## `Unable to read system root certificates` or `Error -25263`
Riot logs `CertBundle: Unable to read system root certificates` and Chromium logs `[known_roots_mac.cc(60)] Unable to determine trusted roots; assuming all roots are trusted! Error -25263` when the prefix has no system trust domain. Security reads that domain from `/System/Library/Keychains/SystemRootCertificates.keychain` and `SystemTrustSettings.plist`, which Darling does not ship, so `SecTrustSettingsCopyCertificates` returns `errSecNoTrustSettings`.
`tools/ensure-darling-root-certificates.py` builds both from the host CA bundle (`/etc/ssl/cert.pem` or whichever bundle Python's `ssl` reports; `--bundle` overrides it). The launcher and `scripts/install-runtime.sh` run it. Each self-signed root gets an unrestricted trust entry. The keychain is built with the guest `security` tool in a single `darling shell`. A stamp in the same directory records the bundle's digest, so later launches skip it without starting Darling until the host bundle changes. If Darling was already running and the guest could not write the directory, the script installs the files on the host and says they apply after `darling shutdown`. A failure prints `Darling system root certificates were not installed: …` and the launch continues. To rebuild by hand:
```sh
python3 build/runtime/tools/ensure-darling-root-certificates.py --force
```
`python3 -m unittest tests.test_root_certificates` compares the generated SHA-1 keys, issuer names and serial numbers with OpenSSL, and checks that an unchanged bundle is skipped. It uses stub `darling` and `security` commands. Reading the keychain and the system trust domain inside a real guest (`security find-certificate -a /System/Library/Keychains/SystemRootCertificates.keychain`, `security dump-trust-settings -s`) is not yet confirmed.
## Riot closes after a network lookup is cancelled
Two local crashes were traced to `CFNetwork::_HostCancel` calling `CFRetain(NULL)`.
@@ -234,6 +246,18 @@ A non-zero error or `FAILED` means the frames cannot reach the window. Before 20
An additional NVIDIA report on `68f816d` showed successful optimal-tiling import but repeated `Metal Device timed out giving us a Drawable to present` messages. Its [presentation trace](https://github.com/lucannez64/darling-lol/issues/3#issuecomment-6214732591) shows all three drawables queued and all three main-thread presentation callbacks running, followed by timeouts with three frames still queued. The compositor only prepared a Metal layer when it was the root layer, skipping nested Metal layers. It now prepares Metal layers throughout the tree. A standalone nested-layer reproduction exhausts the pool before this correction and passes ten presentations with pixel readback after it; the root-layer probe also passes. Confirmation on the reporter's NVIDIA system is still needed.
### What Metal reports about the GPU
`MTLDevice.name` is the selected Vulkan device's name in macOS style: the trailing driver note in brackets and the `(R)`/`(TM)` marks are removed (`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`, `llvmpipe (LLVM …)` becomes `llvmpipe`). NVIDIA names are unchanged. The name is never replaced with another vendor's GPU. Before 2026-10-17 it was always `Darling Indium Device`.
`recommendedMaxWorkingSetSize` is the size of the device-local Vulkan heaps (the VRAM) on a discrete GPU. On integrated and software GPUs it is the size of all heaps, because their device-local heap can be just the firmware carve-out while they also use system RAM; macOS likewise reports a large share of system RAM for unified-memory GPUs. It falls back to the old 1 GiB when Vulkan reports no usable heap. `hasUnifiedMemory` stays `YES` by default: with `NO`, apps switch to managed and private resource paths (`synchronizeResource:`, private buffers created with data) that are not implemented here. Set these in the game's environment to override:
- `DARLING_METAL_GPU_NAME=<name>`: report this name.
- `DARLING_METAL_VRAM_MB=<positive integer>`: report this working-set size in MiB. Other values are ignored.
- `DARLING_METAL_HOST_UNIFIED_MEMORY=1`: report `hasUnifiedMemory` from the GPU type (`YES` for integrated and software GPUs, `NO` for discrete ones). This is experimental; on a discrete GPU it can break rendering.
Each process reads these once.
### 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.
+165 -30
View File
@@ -1127,6 +1127,22 @@ index 5ca58772..54b60ef5 100644
}
@end
diff --git a/AppKit/NSScroller.m b/AppKit/NSScroller.m
index 6c361fa3..55027e7e 100644
--- a/AppKit/NSScroller.m
+++ b/AppKit/NSScroller.m
@@ -34,8 +34,10 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
return [[NSDisplay currentDisplay] scrollerWidth];
}
+// Chromium forwards this to its renderer, which rejects an out-of-range value and leaves
+// the Riot Client black; macOS reports legacy scrollers when no trackpad is attached.
+ (NSScrollerStyle) preferredScrollerStyle {
- NSUnimplementedMethod();
+ return NSScrollerStyleLegacy;
}
/* OS X has a global default "AppleScrollBarVariant" with the values: Single,
DoubleMin, DoubleMax, and DoubleBoth This controls the default position of the
diff --git a/AppKit/NSScrubberItemView.m b/AppKit/NSScrubberItemView.m
index dea5e1d3..7b3a877b 100644
--- a/AppKit/NSScrubberItemView.m
@@ -2069,7 +2085,7 @@ index d545120a..b2845478 100644
+ (BOOL) allowsAutomaticWindowTabbing {
diff --git a/AppKit/NSWorkspace.m b/AppKit/NSWorkspace.m
index ad5e4f9d..dbd997aa 100644
index ad5e4f9d..6e9cb1c9 100644
--- a/AppKit/NSWorkspace.m
+++ b/AppKit/NSWorkspace.m
@@ -20,9 +20,58 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
@@ -2131,7 +2147,7 @@ index ad5e4f9d..dbd997aa 100644
NSString *const NSWorkspaceWillPowerOffNotification =
@"NSWorkspaceWillPowerOffNotification";
@@ -161,6 +210,24 @@ NSNotificationName NSWorkspaceWillUnmountNotification = @"NSWorkspaceWillUnmount
@@ -161,6 +210,41 @@ NSNotificationName NSWorkspaceWillUnmountNotification = @"NSWorkspaceWillUnmount
return 0;
}
@@ -2152,11 +2168,28 @@ index ad5e4f9d..dbd997aa 100644
+ }
+ });
+}
+
+/* Electron's shell.openExternal({activate: false}) (Riot's "Sign in with Google") uses this
+ * pre-10.15 form. Launch options and the bundle identifier are ignored: the host decides
+ * which application opens the URL. */
+- (BOOL) openURLs: (NSArray *) urls
+ withAppBundleIdentifier: (NSString *) bundleIdentifier
+ options: (NSUInteger) options
+ additionalEventParamDescriptor: (NSAppleEventDescriptor *) descriptor
+ launchIdentifiers: (NSArray **) identifiers
+{
+ BOOL opened = [urls count] > 0;
+ for (NSURL *url in urls)
+ opened = [self openURL: url] && opened;
+ if (identifiers != NULL)
+ *identifiers = nil;
+ return opened;
+}
+
- (BOOL) selectFile: (NSString *) path
inFileViewerRootedAtPath: (NSString *) rootedAtPath
{
@@ -317,16 +384,3 @@ NSNotificationName NSWorkspaceWillUnmountNotification = @"NSWorkspaceWillUnmount
@@ -317,16 +401,3 @@ NSNotificationName NSWorkspaceWillUnmountNotification = @"NSWorkspaceWillUnmount
}
@end
@@ -4528,7 +4561,7 @@ index bfe3839a..e8c8828f 100644
- (void) frameChanged;
- (void) setLastKnownCursorPosition: (CGPoint) point;
diff --git a/AppKit/X11.backend/X11Window.m b/AppKit/X11.backend/X11Window.m
index f4925b66..f9dd38cc 100644
index f4925b66..0296203b 100644
--- a/AppKit/X11.backend/X11Window.m
+++ b/AppKit/X11.backend/X11Window.m
@@ -23,6 +23,7 @@
@@ -4583,7 +4616,27 @@ index f4925b66..f9dd38cc 100644
BOOL isPanel = [delegate isKindOfClass: [NSPanel class]];
if (isPanel && _styleMask & NSDocModalWindowMask)
_styleMask = NSBorderlessWindowMask;
@@ -193,8 +222,16 @@ static NSData *makeWindowIcon() {
@@ -177,7 +206,18 @@ static NSData *makeWindowIcon() {
int screen = DefaultScreen(_display);
- if ((_visualInfo = glXChooseVisual(_display, screen, att)) == NULL) {
+ // Rendering goes through EGL. NVIDIA's glXChooseVisual can return a GLX-only visual that every
+ // eglCreateWindowSurface rejects (EGL_BAD_CONFIG), so prefer the default visual when usable.
+ XVisualInfo visualTemplate;
+ int visualCount = 0;
+ visualTemplate.visualid = XVisualIDFromVisual(DefaultVisual(_display, screen));
+ visualTemplate.screen = screen;
+ _visualInfo = XGetVisualInfo(_display, VisualIDMask | VisualScreenMask, &visualTemplate, &visualCount);
+ if (_visualInfo != NULL && (_visualInfo->class != TrueColor || _visualInfo->depth < 24)) {
+ XFree(_visualInfo);
+ _visualInfo = NULL;
+ }
+ if (_visualInfo == NULL && (_visualInfo = glXChooseVisual(_display, screen, att)) == NULL) {
NSLog(@"glXChooseVisual failed at %s %d", __FILE__, __LINE__);
}
@@ -193,8 +233,16 @@ static NSData *makeWindowIcon() {
XSetWindowAttributes xattr;
unsigned long xattr_mask;
@@ -4602,7 +4655,7 @@ index f4925b66..f9dd38cc 100644
xattr_mask = CWOverrideRedirect | CWColormap;
xattr.colormap = cmap;
@@ -206,6 +243,12 @@ static NSData *makeWindowIcon() {
@@ -206,6 +254,12 @@ static NSData *makeWindowIcon() {
(_visualInfo == NULL) ? CopyFromParent : _visualInfo->visual,
xattr_mask, &xattr);
@@ -4615,7 +4668,7 @@ index f4925b66..f9dd38cc 100644
[self syncDelegateProperties];
Atom atm = XInternAtom(_display, "WM_DELETE_WINDOW", False);
@@ -217,6 +260,15 @@ static NSData *makeWindowIcon() {
@@ -217,6 +271,15 @@ static NSData *makeWindowIcon() {
.res_class = (char *) name};
XSetClassHint(_display, _window, &classHint);
@@ -4631,7 +4684,7 @@ index f4925b66..f9dd38cc 100644
XSetWindowBackgroundPixmap(_display, _window, None);
_isModal = [delegate isSheet] || [NSApp modalWindow] == delegate;
@@ -266,6 +318,13 @@ static NSData *makeWindowIcon() {
@@ -266,6 +329,13 @@ static NSData *makeWindowIcon() {
[self setWindowIcon];
@@ -4645,7 +4698,7 @@ index f4925b66..f9dd38cc 100644
return self;
}
@@ -279,24 +338,30 @@ static NSData *makeWindowIcon() {
@@ -279,24 +349,30 @@ static NSData *makeWindowIcon() {
return _styleMask;
}
@@ -4692,7 +4745,7 @@ index f4925b66..f9dd38cc 100644
if (!_mapped) {
long states[2];
int states_cnt = 0;
@@ -304,7 +369,7 @@ static NSData *makeWindowIcon() {
@@ -304,7 +380,7 @@ static NSData *makeWindowIcon() {
states[states_cnt++] =
(long) XInternAtom(_display, "_NET_WM_STATE_MODAL", False);
}
@@ -4701,7 +4754,7 @@ index f4925b66..f9dd38cc 100644
states[states_cnt++] = (long) XInternAtom(
_display, "_NET_WM_STATE_FULLSCREEN", False);
}
@@ -312,14 +377,13 @@ static NSData *makeWindowIcon() {
@@ -312,14 +388,13 @@ static NSData *makeWindowIcon() {
XInternAtom(_display, "_NET_WM_STATE", False), XA_ATOM,
32, PropModeReplace, (const unsigned char *) states,
states_cnt);
@@ -4718,7 +4771,7 @@ index f4925b66..f9dd38cc 100644
event.data.l[1] =
(long) XInternAtom(_display, "_NET_WM_STATE_FULLSCREEN", False);
event.data.l[3] = 1;
@@ -327,6 +391,35 @@ static NSData *makeWindowIcon() {
@@ -327,6 +402,35 @@ static NSData *makeWindowIcon() {
SubstructureNotifyMask | SubstructureRedirectMask,
(XEvent *) &event);
}
@@ -4754,7 +4807,7 @@ index f4925b66..f9dd38cc 100644
}
- (void) setStyleMask: (NSUInteger) mask {
@@ -338,7 +431,6 @@ static NSData *makeWindowIcon() {
@@ -338,7 +442,6 @@ static NSData *makeWindowIcon() {
}
+ (void) removeDecorationForWindow: (Window) w onDisplay: (Display *) dpy {
@@ -4762,7 +4815,7 @@ index f4925b66..f9dd38cc 100644
struct {
unsigned long flags;
unsigned long functions;
@@ -358,6 +450,7 @@ static NSData *makeWindowIcon() {
@@ -358,6 +461,7 @@ static NSData *makeWindowIcon() {
if (!_mapped) {
XMapWindow(_display, _window);
_mapped = YES;
@@ -4770,7 +4823,7 @@ index f4925b66..f9dd38cc 100644
}
}
@@ -377,15 +470,27 @@ static NSData *makeWindowIcon() {
@@ -377,15 +481,27 @@ static NSData *makeWindowIcon() {
- (void) syncDelegateProperties {
long mask = KeyPressMask | KeyReleaseMask | ExposureMask |
StructureNotifyMask | EnterWindowMask | LeaveWindowMask |
@@ -4803,7 +4856,7 @@ index f4925b66..f9dd38cc 100644
// TODO: background color
}
@@ -393,6 +498,28 @@ static NSData *makeWindowIcon() {
@@ -393,6 +509,28 @@ static NSData *makeWindowIcon() {
// This is essentially dealloc; we release our contexts
// and windows, but unlike dealloc, this method can be called
// several times, so set everything to nil/NULL/0.
@@ -4832,7 +4885,7 @@ index f4925b66..f9dd38cc 100644
[_context release];
_context = nil;
@@ -421,6 +548,392 @@ static NSData *makeWindowIcon() {
@@ -421,6 +559,392 @@ static NSData *makeWindowIcon() {
}
}
@@ -5225,7 +5278,7 @@ index f4925b66..f9dd38cc 100644
- (Window) windowHandle {
return _window;
}
@@ -470,14 +983,32 @@ static NSData *makeWindowIcon() {
@@ -470,14 +994,32 @@ static NSData *makeWindowIcon() {
const char *text = [title cString];
XStringListToTextProperty((char **) &text, 1, &prop);
XSetWMName(_display, _window, &prop);
@@ -5259,7 +5312,7 @@ index f4925b66..f9dd38cc 100644
}
- (void) setHasShadow: (BOOL) value {
@@ -488,6 +1019,25 @@ static NSData *makeWindowIcon() {
@@ -488,6 +1030,25 @@ static NSData *makeWindowIcon() {
_level = value;
}
@@ -5285,7 +5338,7 @@ index f4925b66..f9dd38cc 100644
- (void) setOpaque: (BOOL) value {
_isOpaque = value;
}
@@ -543,6 +1093,21 @@ static NSData *makeWindowIcon() {
@@ -543,6 +1104,21 @@ static NSData *makeWindowIcon() {
- (void) makeKey {
[self ensureMapped];
XRaiseWindow(_display, _window);
@@ -5307,7 +5360,7 @@ index f4925b66..f9dd38cc 100644
}
- (void) makeMain {
@@ -559,7 +1124,29 @@ static NSData *makeWindowIcon() {
@@ -559,7 +1135,29 @@ static NSData *makeWindowIcon() {
XRaiseWindow(_display, _window);
}
@@ -5337,7 +5390,7 @@ index f4925b66..f9dd38cc 100644
Atom wmState = XInternAtom(_display, "_NET_WM_STATE", FALSE);
Atom hiddenState = XInternAtom(_display, "_NET_WM_STATE_HIDDEN", FALSE);
@@ -615,6 +1202,12 @@ static NSData *makeWindowIcon() {
@@ -615,6 +1213,12 @@ static NSData *makeWindowIcon() {
}
- (void) openGLFlushBuffer {
@@ -5350,7 +5403,7 @@ index f4925b66..f9dd38cc 100644
CGLError error;
CGLContextObj prevContext = CGLGetCurrentContext();
@@ -652,7 +1245,7 @@ static NSData *makeWindowIcon() {
@@ -652,7 +1256,7 @@ static NSData *makeWindowIcon() {
}
- (O2Rect) frame {
@@ -5359,7 +5412,7 @@ index f4925b66..f9dd38cc 100644
}
static int ignoreBadWindow(Display *display, XErrorEvent *errorEvent) {
@@ -666,6 +1259,7 @@ static int ignoreBadWindow(Display *display, XErrorEvent *errorEvent) {
@@ -666,6 +1270,7 @@ static int ignoreBadWindow(Display *display, XErrorEvent *errorEvent) {
}
- (void) frameChanged {
@@ -5367,7 +5420,7 @@ index f4925b66..f9dd38cc 100644
XErrorHandler previousHandler = XSetErrorHandler(ignoreBadWindow);
@try {
Window root, parent;
@@ -710,14 +1304,6 @@ static int ignoreBadWindow(Display *display, XErrorEvent *errorEvent) {
@@ -710,14 +1315,6 @@ static int ignoreBadWindow(Display *display, XErrorEvent *errorEvent) {
[_deviceDictionary addEntriesFromDictionary: entries];
}
@@ -8703,7 +8756,7 @@ index 5dcac115..5de4f03f 100644
+
@end
diff --git a/QuartzCore/CALayerContext.m b/QuartzCore/CALayerContext.m
index 5dce8c56..7f9125e3 100644
index 5dce8c56..d325ba5f 100644
--- a/QuartzCore/CALayerContext.m
+++ b/QuartzCore/CALayerContext.m
@@ -1,47 +1,69 @@
@@ -8870,7 +8923,7 @@ index 5dce8c56..7f9125e3 100644
}
- (void) assignTextureIdsToLayerTree: (CALayer *) layer {
@@ -93,7 +159,22 @@
@@ -93,8 +159,27 @@
[self assignTextureIdsToLayerTree: child];
}
@@ -8891,9 +8944,14 @@ index 5dce8c56..7f9125e3 100644
- (void) renderLayer: (CALayer *) layer {
+ CFTimeInterval renderLayerStart = CACurrentMediaTime();
CGLContextMakeCurrentAndAttachToWindow(_glContext, _cglWindow);
+ // CAMetalLayer first makes this context current without a surface, which leaves GL_DRAW_BUFFER
+ // at GL_NONE on NVIDIA even once the window is attached (Mesa resets it).
+ glDrawBuffer(GL_BACK);
+ glReadBuffer(GL_BACK);
glEnable(GL_DEPTH_TEST);
@@ -118,27 +199,63 @@
glShadeModel(GL_SMOOTH);
@@ -118,27 +203,63 @@
[_deleteTextureIds removeAllObjects];
@@ -8962,7 +9020,7 @@ index 5dce8c56..7f9125e3 100644
}
- (void) startTimerIfNeeded {
@@ -147,7 +264,7 @@
@@ -147,7 +268,7 @@
target: self
selector: @selector(timer:)
userInfo: nil
@@ -8971,7 +9029,7 @@ index 5dce8c56..7f9125e3 100644
}
- (void) deleteTextureId: (NSNumber *) textureId {
@@ -155,7 +272,10 @@
@@ -155,7 +276,10 @@
}
- (void) flush {
@@ -10378,3 +10436,80 @@ index 00000000..19cd9ea9
+- (instancetype) init;
+
+@end
diff --git a/AppKit/NSSavePanel.subproj/NSSavePanel.m b/AppKit/NSSavePanel.subproj/NSSavePanel.m
index 50c3d59e..5872e594 100644
--- a/AppKit/NSSavePanel.subproj/NSSavePanel.m
+++ b/AppKit/NSSavePanel.subproj/NSSavePanel.m
@@ -124,8 +124,48 @@ static NSSavePanel *_newPanel = nil;
[NSApp stopModalWithCode: NSCancelButton];
}
+// Electron (Riot Client UI) opens its file dialogs through these and waits
+// for the handler; run the panel modally and report the result.
- (void) beginWithCompletionHandler: (void (^)(NSModalResponse result)) handler {
- NSUnimplementedMethod();
+ NSModalResponse result = [self runModal];
+ if (handler)
+ handler(result);
+}
+
+- (void) beginSheetModalForWindow: (NSWindow *) window
+ completionHandler: (void (^)(NSModalResponse result)) handler
+{
+ [self beginWithCompletionHandler: handler];
+}
+
+// Electron sets these unconditionally; Cocotron's panel has no tag field,
+// name label or extension toggle, so they are accepted and ignored.
+- (void) setNameFieldLabel: (NSString *) label {
+}
+
+- (NSString *) nameFieldLabel {
+ return nil;
+}
+
+- (void) setShowsTagField: (BOOL) flag {
+}
+
+- (BOOL) showsTagField {
+ return NO;
+}
+
+- (void) setExtensionHidden: (BOOL) flag {
+}
+
+- (BOOL) isExtensionHidden {
+ return NO;
+}
+
+- (void) setCanSelectHiddenExtension: (BOOL) flag {
+}
+
+- (BOOL) canSelectHiddenExtension {
+ return NO;
}
- (NSInteger) runModalForDirectory: (NSString *) directory
diff --git a/AppKit/include/AppKit/NSSavePanel.h b/AppKit/include/AppKit/NSSavePanel.h
index 53bacc46..3c6adfb7 100644
--- a/AppKit/include/AppKit/NSSavePanel.h
+++ b/AppKit/include/AppKit/NSSavePanel.h
@@ -88,6 +88,18 @@ enum {
- (void) setPrompt: (NSString *) message;
- (NSString *) prompt;
+- (void) setNameFieldLabel: (NSString *) label;
+- (NSString *) nameFieldLabel;
+- (void) setShowsTagField: (BOOL) flag;
+- (BOOL) showsTagField;
+- (void) setExtensionHidden: (BOOL) flag;
+- (BOOL) isExtensionHidden;
+- (void) setCanSelectHiddenExtension: (BOOL) flag;
+- (BOOL) canSelectHiddenExtension;
+
+- (void) beginSheetModalForWindow: (NSWindow *) window
+ completionHandler: (void (^)(NSModalResponse result)) handler;
+
- (void) beginSheetForDirectory: (NSString *) path
file: (NSString *) name
modalForWindow: (NSWindow *) docWindow
+77 -3
View File
@@ -7796,7 +7796,7 @@ index fee1bde87..bee86604b 100644
#endif
-
diff --git a/src/startup/mldr/mldr.c b/src/startup/mldr/mldr.c
index 6617ef82b..1d12043c2 100644
index 6617ef82b..a227abee8 100644
--- a/src/startup/mldr/mldr.c
+++ b/src/startup/mldr/mldr.c
@@ -154,7 +154,7 @@ int main(int argc, char** argv, char** envp)
@@ -7825,7 +7825,19 @@ index 6617ef82b..1d12043c2 100644
static int socket_bitmap_get(socket_bitmap_t* bitmap) {
int fd = -1;
bool updated = false;
@@ -673,7 +683,13 @@ static void socket_bitmap_put(socket_bitmap_t* bitmap, int socket) {
@@ -648,6 +658,11 @@ static void socket_bitmap_put(socket_bitmap_t* bitmap, int socket) {
pthread_mutex_lock(&bitmap->mutex);
+ // darling-lol: placed above the slots because its slot was taken (socket_bitmap_place).
+ if (socket > bitmap->highest) {
+ goto out;
+ }
+
index = bitmap->highest - socket;
bitmap->bits[index / 8] &= ~(1 << (index % 8));
@@ -673,7 +688,13 @@ static void socket_bitmap_put(socket_bitmap_t* bitmap, int socket) {
}
}
@@ -7840,7 +7852,7 @@ index 6617ef82b..1d12043c2 100644
// this bit is in-use, so we can't shrink any further
break;
}
@@ -685,12 +701,20 @@ static void socket_bitmap_put(socket_bitmap_t* bitmap, int socket) {
@@ -685,12 +706,20 @@ static void socket_bitmap_put(socket_bitmap_t* bitmap, int socket) {
if (old_byte_size != new_byte_size) {
// we can free one or more bytes from the bitmap
@@ -7866,6 +7878,68 @@ index 6617ef82b..1d12043c2 100644
}
}
@@ -698,6 +727,22 @@ out:
pthread_mutex_unlock(&bitmap->mutex);
};
+/* darling-lol: move `fd` to the next slot. dup2() silently closed whatever the guest or a native library (NVIDIA's
+ * driver: one fd per allocation) already held on that number, and its owner later closed our socket. F_DUPFD never
+ * replaces a descriptor: if the slot is taken, it returns the lowest free number above it, which is ours alone. */
+static int socket_bitmap_place(int fd) {
+ int slot = socket_bitmap_get(&socket_bitmap);
+ if (slot < 0) {
+ return -1;
+ }
+
+ int placed = fcntl(fd, F_DUPFD, slot);
+ if (placed != slot) {
+ socket_bitmap_put(&socket_bitmap, slot);
+ }
+ return placed;
+}
+
int __mldr_create_rpc_socket(void) {
int pre_fd = -1;
int fd = -1;
@@ -707,18 +752,11 @@ int __mldr_create_rpc_socket(void) {
goto err_out;
}
- fd = socket_bitmap_get(&socket_bitmap);
+ fd = socket_bitmap_place(pre_fd);
if (fd < 0) {
goto err_out;
}
- if (dup2(pre_fd, fd) < 0) {
- // we have to put it away ourselves here because `fd` is not yet valid, so we can't close() it in the error handler
- socket_bitmap_put(&socket_bitmap, fd);
- fd = -1;
- goto err_out;
- }
-
close(pre_fd);
pre_fd = -1;
@@ -772,17 +810,11 @@ int __mldr_create_process_lifetime_pipe(int* fds) {
}
for (int i = 0; i < 2; ++i) {
- fds[i] = socket_bitmap_get(&socket_bitmap);
+ fds[i] = socket_bitmap_place(pre_fds[i]);
if (fds[i] < 0) {
goto err_out;
}
- if (dup2(pre_fds[i], fds[i]) < 0) {
- socket_bitmap_put(&socket_bitmap, fds[i]);
- fds[i] = -1;
- goto err_out;
- }
-
close(pre_fds[i]);
pre_fds[i] = -1;
}
diff --git a/src/tools/open.m b/src/tools/open.m
index 126bd394c..f62c2e0e8 100644
--- a/src/tools/open.m
+49 -8
View File
@@ -663,6 +663,7 @@ index 91d11a1..37551e1 100644
-}
diff --git a/src/IOPlatformExpertDevice.h b/src/IOPlatformExpertDevice.h
new file mode 100644
index 0000000..38ad635
--- /dev/null
+++ b/src/IOPlatformExpertDevice.h
@@ -0,0 +1,21 @@
@@ -673,8 +674,8 @@ new file mode 100644
+#include "ServiceRegistry.h"
+
+// The root platform device (what IOServiceMatching("IOPlatformExpertDevice") finds on macOS).
+// Its properties describe the host machine as reported by the Linux kernel's DMI tables.
+// Identifiers the host does not expose to us (serial number, UUID) are left out.
+// Its model strings name a fixed MacBookPro16,1. IOPlatformUUID and IOPlatformSerialNumber are
+// derived from the host machine-id, so they differ per host and stay stable across reinstalls.
+class IOPlatformExpertDevice : public IOService
+{
+public:
@@ -689,14 +690,19 @@ new file mode 100644
+#endif
diff --git a/src/IOPlatformExpertDevice.mm b/src/IOPlatformExpertDevice.mm
new file mode 100644
index 0000000..e330569
--- /dev/null
+++ b/src/IOPlatformExpertDevice.mm
@@ -0,0 +1,54 @@
@@ -0,0 +1,93 @@
+#include "IOPlatformExpertDevice.h"
+#import <Foundation/NSString.h>
+#import <Foundation/NSData.h>
+#import <Foundation/NSArray.h>
+#import <Foundation/NSCharacterSet.h>
+#import <Foundation/NSDictionary.h>
+#include <CommonCrypto/CommonDigest.h>
+#include <uuid/uuid.h>
+#include <unistd.h>
+#include <cstring>
+
+// Platform expert strings are NUL-terminated data
@@ -719,17 +725,52 @@ new file mode 100644
+ };
+}
+
+// Per-host identifiers, stable like a real Mac's: a SHA-256 of the host machine-id and a fixed
+// string, so the raw machine-id (which machine-id(5) says not to expose) never reaches apps.
+// The serial's "DARLING-" form is longer than any Apple serial and cannot match a real Mac.
+static bool hostIdentifiers(NSString** uuid, NSString** serial)
+{
+ static const char app[] = "darling-lol IOPlatformExpertDevice";
+ uuid_t machineID;
+ struct timespec wait = { 0, 0 };
+ if (gethostuuid(machineID, &wait) != 0)
+ return false;
+
+ unsigned char input[sizeof(machineID) + sizeof(app)];
+ std::memcpy(input, machineID, sizeof(machineID));
+ std::memcpy(input + sizeof(machineID), app, sizeof(app));
+ unsigned char hash[CC_SHA256_DIGEST_LENGTH];
+ CC_SHA256(input, sizeof(input), hash);
+
+ hash[6] = (hash[6] & 0x0f) | 0x40; // version 4
+ hash[8] = (hash[8] & 0x3f) | 0x80; // RFC 4122 variant
+ uuid_string_t text;
+ uuid_unparse_upper(hash, text);
+ // Owned (not autoreleased): the caller keeps them in statics for the daemon's lifetime
+ *uuid = [[NSString alloc] initWithUTF8String: text];
+ *serial = [[NSString alloc] initWithFormat: @"DARLING-%02X%02X%02X%02X", hash[16], hash[17], hash[18], hash[19]];
+ return true;
+}
+
+NSDictionary* IOPlatformExpertDevice::getProperties()
+{
+ // Fixed MacBookPro16,1 identity; the Linux DMI values describe the host hardware, not the Mac we emulate
+ return @{
+ // MacBookPro16,1 model strings; the Linux DMI values describe the host hardware, not the Mac we emulate
+ NSMutableDictionary* properties = [NSMutableDictionary dictionaryWithDictionary: @{
+ @"manufacturer": dataForString(@"Apple Inc."),
+ @"model": dataForString(@"MacBookPro16,1"),
+ @"product-name": dataForString(@"MacBookPro16,1"),
+ @"board-id": dataForString(@"Mac-E1008331FDC96864"),
+ @"IOPlatformUUID": @"6F3B2A1E-8C4D-4E5F-9A7B-1D2C3E4F5A6B",
+ @"IOPlatformSerialNumber": @"C02ZL0GVMD6N",
+ };
+ }];
+
+ static NSString* uuid;
+ static NSString* serial;
+ static bool known = hostIdentifiers(&uuid, &serial);
+ if (known)
+ {
+ properties[@"IOPlatformUUID"] = uuid;
+ properties[@"IOPlatformSerialNumber"] = serial;
+ }
+ return properties;
+}
+
+bool IOPlatformExpertDevice::conformsTo(const char* className)
+22 -6
View File
@@ -2447,7 +2447,7 @@ index 43f4a84..aaa1acb 100644
+ if ((calls.fetch_add(1,std::memory_order_relaxed) & 63)==0)
+ bindingReuse.store(access("/tmp/metal-no-binding-reuse",F_OK)!=0,std::memory_order_relaxed);
+}
+// OFF by default (round-3 behaviour: a presenting commit waits for the whole worker queue with drain()). Guest flag file
+// Frame-end drain waits for a snapshot of queued work. The opt-in guest flag file
+// /tmp/metal-present-dispatch-wait (checked once per process) makes the frame-end sync narrower: the caller waits only
+// until this commit's own task has run (its present dispatch, and by FIFO order every replay queued before it), not for
+// tasks queued after it. Round 4 had this on by default; that round regressed in matches, so it is opt-in now.
@@ -2487,7 +2487,7 @@ index 43f4a84..aaa1acb 100644
+ token.wait("presentDispatchWait");
+ return;
+ }
+ // Default (round 3): post first, then check the flag file and wait for the whole worker queue.
+ // Post first, then wait for the work already queued; later producers cannot extend this wait.
+ SubmitThread::post([self]() {
+ AsyncEncode::PoolScope pool;
+ self->commit();
@@ -8050,7 +8050,7 @@ diff --git a/private-include/indium/submit-thread.private.hpp b/private-include/
new file mode 100644
--- /dev/null
+++ b/private-include/indium/submit-thread.private.hpp
@@ -0,0 +1,159 @@
@@ -0,0 +1,175 @@
+#pragma once
+
+// Ordered queue-submission worker.
@@ -8067,6 +8067,8 @@ new file mode 100644
+
+#include <condition_variable>
+#include <deque>
+#include <cstdint>
+#include <limits>
+#include <functional>
+#include <mutex>
+#include <thread>
@@ -8080,6 +8082,9 @@ new file mode 100644
+ std::condition_variable idle;
+ std::deque<std::function<void()>> queue;
+ size_t active = 0;
+ uint64_t posted = 0, completed = 0;
+ size_t drainWaiters = 0;
+ uint64_t drainTarget = std::numeric_limits<uint64_t>::max();
+ bool started = false;
+ const bool enabled = access("/tmp/metal-submit-thread", F_OK) == 0;
+ };
@@ -8095,8 +8100,10 @@ new file mode 100644
+ {
+ std::lock_guard<std::mutex> lock(mutex);
+ done = true;
+ // The waiter may destroy this stack token as soon as it sees done.
+ // Finish notifying while the token's mutex still protects its lifetime.
+ condvar.notify_all();
+ }
+ condvar.notify_all();
+ }
+ // blocks until the task carrying this token has finished (counting the block under `site`)
+ void wait(const char* site) {
@@ -8133,7 +8140,8 @@ new file mode 100644
+ {
+ std::lock_guard<std::mutex> lock(s.mutex);
+ --s.active;
+ if (s.queue.empty() && s.active == 0) s.idle.notify_all();
+ ++s.completed;
+ if (s.drainWaiters && s.completed >= s.drainTarget) s.idle.notify_all();
+ }
+ }
+ }
@@ -8158,6 +8166,7 @@ new file mode 100644
+ // mutex as the worker's wait to avoid losing that transition.
+ needsWake = s.active == 0 && s.queue.empty();
+ s.queue.push_back(std::move(task));
+ ++s.posted;
+ }
+ if (needsWake) s.wake.notify_one();
+ }
@@ -8190,6 +8199,7 @@ new file mode 100644
+ // mutex as the worker's wait to avoid losing that transition.
+ needsWake = s.active == 0 && s.queue.empty();
+ s.queue.push_back(std::move(task));
+ ++s.posted;
+ }
+ if (needsWake) s.wake.notify_one();
+ }
@@ -8206,7 +8216,13 @@ new file mode 100644
+ auto& s = state();
+ if (onWorker()) return;
+ std::unique_lock<std::mutex> lock(s.mutex);
+ Waits::blockOn(site, s.idle, lock, [&] { return s.queue.empty() && s.active == 0; });
+ const uint64_t target = s.posted;
+ if (s.completed >= target) return;
+ ++s.drainWaiters;
+ if (target < s.drainTarget) s.drainTarget = target;
+ // FIFO completion is sufficient: later producers must not extend this wait.
+ Waits::blockOn(site, s.idle, lock, [&] { return s.completed >= target; });
+ if (--s.drainWaiters == 0) s.drainTarget = std::numeric_limits<uint64_t>::max();
+ }
+ }
+}
+263 -13
View File
@@ -1,5 +1,5 @@
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 431f29a..7f10875 100644
index 431f29a..eb3eec8 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -42,8 +42,10 @@ add_framework(Metal
@@ -13,6 +13,17 @@ index 431f29a..7f10875 100644
src/Metal/MTLCommandQueue.mm
src/Metal/MTLCompileOptions.mm
src/Metal/MTLComputeCommandEncoder.mm
@@ -97,6 +99,10 @@ if (BUILD_METAL)
target_compile_definitions(Metal PUBLIC
DARLING_METAL_ENABLED=1
)
+ # MTLDevice.mm reads the Vulkan device properties from Indium's private headers.
+ target_link_libraries(Metal PRIVATE
+ indium_private
+ )
endif()
#
diff --git a/include/Metal/MTLCommandBuffer.h b/include/Metal/MTLCommandBuffer.h
index ef43df8..9045a84 100644
--- a/include/Metal/MTLCommandBuffer.h
@@ -1123,10 +1134,10 @@ index 2a57796..0cbedf2 100644
-
@end
diff --git a/src/Metal/MTLDevice.mm b/src/Metal/MTLDevice.mm
index f58184a..6cfc697 100644
index f58184a..a80a625 100644
--- a/src/Metal/MTLDevice.mm
+++ b/src/Metal/MTLDevice.mm
@@ -2,13 +2,26 @@
@@ -2,14 +2,32 @@
// SPDX-License-Identifier: MPL-2.0
#import <Metal/MTLDeviceInternal.h>
@@ -1151,9 +1162,15 @@ index f58184a..6cfc697 100644
+#import <Metal/MTLVertexDescriptor.h>
+#include <type_traits>
#import <Metal/MTLRenderPipelineInternal.h>
+#import <Metal/MTLDeviceInfo.h>
+#if DARLING_METAL_ENABLED
+#include <indium/device.private.hpp>
+#include <cstdlib>
+#endif
MTL_EXTERN const MTLDeviceNotificationName MTLDeviceWasAddedNotification = @"MTLDeviceWasAdded";
@@ -88,6 +101,37 @@ void MTLRemoveDeviceObserver(id<NSObject> observer) {
MTL_EXTERN const MTLDeviceNotificationName MTLDeviceRemovalRequestedNotification = @"MTLDeviceRemovalRequested";
@@ -88,6 +106,37 @@ void MTLRemoveDeviceObserver(id<NSObject> observer) {
[observer release];
};
@@ -1191,7 +1208,73 @@ index f58184a..6cfc697 100644
@implementation MTLDeviceInternal
#if DARLING_METAL_ENABLED
@@ -153,12 +197,46 @@ void MTLRemoveDeviceObserver(id<NSObject> observer) {
@@ -96,6 +145,9 @@ void MTLRemoveDeviceObserver(id<NSObject> observer) {
NSThread* _pollingThread;
NSCondition* _threadExitCondition;
BOOL _threadIsRunning;
+ NSString* _name;
+ uint64_t _workingSetSize;
+ BOOL _sharesSystemMemory;
}
@synthesize device = _device;
@@ -116,6 +168,39 @@ void MTLRemoveDeviceObserver(id<NSObject> observer) {
[_threadExitCondition unlock];
}
+// Real host GPU facts, computed once per device in -initWithDevice: (games may query them per frame).
+- (void)loadHostDeviceInfo
+{
+ // 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"));
+
+ std::string vulkanName = _device->name();
+ _name = [[NSString alloc] initWithUTF8String: MTLDeviceInfo::resolveName(nameOverride, vulkanName.c_str()).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())) {
+ 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();
+ MTLDeviceInfo::Heap heaps[VK_MAX_MEMORY_HEAPS];
+ const size_t count = memory.memoryHeapCount < VK_MAX_MEMORY_HEAPS ? memory.memoryHeapCount : VK_MAX_MEMORY_HEAPS;
+ for (size_t i = 0; i < count; ++i) {
+ heaps[i] = { memory.memoryHeaps[i].size, (memory.memoryHeaps[i].flags & VK_MEMORY_HEAP_DEVICE_LOCAL_BIT) != 0 };
+ }
+ workingSet = MTLDeviceInfo::workingSetBytes(heaps, count, _sharesSystemMemory);
+ }
+ if (vramOverride != 0) {
+ workingSet = vramOverride;
+ }
+ // 1 GiB was the previous fixed value; keep it when Vulkan reports nothing usable.
+ _workingSetSize = workingSet != 0 ? workingSet : (1ull << 30);
+}
+
- (instancetype)initWithDevice: (std::shared_ptr<Indium::Device>)device
{
self = [super init];
@@ -123,6 +208,7 @@ void MTLRemoveDeviceObserver(id<NSObject> observer) {
_device = device;
_threadExitCondition = [NSCondition new];
_threadIsRunning = NO;
+ [self loadHostDeviceInfo];
_pollingThread = [[NSThread alloc] initWithTarget: self selector: @selector(pollingLoop) object: nil];
[_pollingThread start];
}
@@ -133,6 +219,7 @@ void MTLRemoveDeviceObserver(id<NSObject> observer) {
{
[_pollingThread release];
[_threadExitCondition release];
+ [_name release];
[super dealloc];
}
@@ -153,12 +240,60 @@ void MTLRemoveDeviceObserver(id<NSObject> observer) {
[_threadExitCondition unlock];
}
@@ -1207,13 +1290,27 @@ index f58184a..6cfc697 100644
+
+/* darling-lol M57: feature queries ANGLE's Metal backend needs. */
+- (BOOL)isDepth24Stencil8PixelFormatSupported { return _device->supportsDepth24Stencil8PixelFormat(); }
+- (BOOL)hasUnifiedMemory { return YES; }
+- (BOOL)hasUnifiedMemory
+{
+ // Stays YES by default: with NO, ANGLE and games take the Managed/Private paths
+ // (synchronizeResource:, Private uploads) that this Metal/Indium does not fully implement.
+ // DARLING_METAL_HOST_UNIFIED_MEMORY=1 reports the host GPU type instead
+ // (YES for integrated and CPU devices, NO for discrete ones).
+ static const bool reportHost = [] {
+ const char* value = getenv("DARLING_METAL_HOST_UNIFIED_MEMORY");
+ return value && value[0] == '1' && value[1] == '\0';
+ }();
+ return reportHost ? _sharesSystemMemory : YES;
+}
+- (NSUInteger)maxBufferLength { return 256u * 1024u * 1024u; }
+- (NSUInteger)recommendedMaxWorkingSetSize { return 1ull << 30; }
+- (NSUInteger)recommendedMaxWorkingSetSize
+{
+ return _workingSetSize > NSUIntegerMax ? NSUIntegerMax : static_cast<NSUInteger>(_workingSetSize);
+}
+- (BOOL)isLowPower { return NO; }
+- (BOOL)isRemovable { return NO; }
+- (BOOL)isHeadless { return NO; }
+- (NSString*)name { return @"Darling Indium Device"; }
+- (NSString*)name { return _name; }
+- (uint64_t)registryID { return 0; }
+- (BOOL)supportsFamily: (NSUInteger)family { return YES; }
+- (BOOL)supportsFeatureSet: (NSUInteger)featureSet { return YES; }
@@ -1239,7 +1336,7 @@ index f58184a..6cfc697 100644
if (!pso) {
if (error) {
// TODO: better error and/or match what the official Metal method does
@@ -166,6 +244,7 @@ void MTLRemoveDeviceObserver(id<NSObject> observer) {
@@ -166,6 +301,7 @@ void MTLRemoveDeviceObserver(id<NSObject> observer) {
}
return nil;
}
@@ -1247,7 +1344,7 @@ index f58184a..6cfc697 100644
return [[MTLComputePipelineStateInternal alloc] initWithState: pso device: self label: descriptor.label];
}
@@ -183,7 +262,13 @@ void MTLRemoveDeviceObserver(id<NSObject> observer) {
@@ -183,7 +319,13 @@ void MTLRemoveDeviceObserver(id<NSObject> observer) {
reflection: (MTLAutoreleasedComputePipelineReflection*)reflection
error: (NSError**)error
{
@@ -1262,7 +1359,7 @@ index f58184a..6cfc697 100644
if (!pso) {
if (error) {
// TODO: better error and/or match what the official Metal method does
@@ -191,13 +276,44 @@ void MTLRemoveDeviceObserver(id<NSObject> observer) {
@@ -191,13 +333,44 @@ void MTLRemoveDeviceObserver(id<NSObject> observer) {
}
return nil;
}
@@ -1308,7 +1405,7 @@ index f58184a..6cfc697 100644
if (!pso) {
if (error) {
// TODO: better error and/or match what the official Metal method does
@@ -205,7 +321,114 @@ void MTLRemoveDeviceObserver(id<NSObject> observer) {
@@ -205,7 +378,114 @@ void MTLRemoveDeviceObserver(id<NSObject> observer) {
}
return nil;
}
@@ -1424,7 +1521,7 @@ index f58184a..6cfc697 100644
}
- (id<MTLCommandQueue>)newCommandQueue
@@ -275,8 +498,21 @@ void MTLRemoveDeviceObserver(id<NSObject> observer) {
@@ -275,8 +555,21 @@ void MTLRemoveDeviceObserver(id<NSObject> observer) {
- (id<MTLLibrary>)newLibraryWithData: (dispatch_data_t)data
error: (NSError**)error
{
@@ -3369,6 +3466,159 @@ new file mode 100644
+const std::shared_ptr<Indium::DepthStencilState>& MTLDepthStencilGetIndium(id object);
+
+#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 @@
+// SPDX-FileCopyrightText: 2026 Darling Developers
+// SPDX-License-Identifier: MPL-2.0
+
+#ifndef _METAL_MTLDEVICEINFO_H_
+#define _METAL_MTLDEVICEINFO_H_
+
+// Host-testable helpers for the values MTLDevice reports about the GPU.
+// Plain C++ only (no Objective-C, Vulkan or Indium) so tools/test-metal-device-info.cpp
+// can compile this header on the host.
+
+#include <cstddef>
+#include <cstdint>
+#include <string>
+
+namespace MTLDeviceInfo {
+
+// Used when neither the cleaned nor the raw Vulkan name has any characters.
+static constexpr const char* kFallbackName = "Darling Indium Device";
+
+static inline bool isSpace(char c) {
+ return c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == '\f' || c == '\v';
+}
+
+static inline std::string collapseWhitespace(const std::string& text) {
+ std::string out;
+ bool pendingSpace = false;
+ for (char c: text) {
+ if (isSpace(c)) {
+ pendingSpace = !out.empty();
+ continue;
+ }
+ if (pendingSpace) {
+ out.push_back(' ');
+ pendingSpace = false;
+ }
+ out.push_back(c);
+ }
+ return out;
+}
+
+static inline char lower(char c) {
+ return (c >= 'A' && c <= 'Z') ? static_cast<char>(c - 'A' + 'a') : c;
+}
+
+// Removes "(R)" and "(TM)" in any letter case (Intel's Vulkan names use both).
+static inline std::string removeTrademarks(const std::string& text) {
+ std::string out;
+ for (size_t i = 0; i < text.size();) {
+ if (text[i] == '(') {
+ if (i + 2 < text.size() && lower(text[i + 1]) == 'r' && text[i + 2] == ')') {
+ i += 3;
+ continue;
+ }
+ if (i + 3 < text.size() && lower(text[i + 1]) == 't' && lower(text[i + 2]) == 'm' && text[i + 3] == ')') {
+ i += 4;
+ continue;
+ }
+ }
+ out.push_back(text[i++]);
+ }
+ return out;
+}
+
+// Drops one trailing parenthetical group (Mesa/driver annotation such as
+// " (RADV NAVI21)", " (CML GT2)" or " (LLVM 22.1.0, 256 bits)"), but never when
+// the group is the whole name. Expects whitespace already collapsed and trimmed.
+static inline std::string removeTrailingGroup(const std::string& text) {
+ if (text.empty() || text.back() != ')') {
+ return text;
+ }
+ int depth = 0;
+ for (size_t i = text.size(); i-- > 0;) {
+ if (text[i] == ')') {
+ ++depth;
+ } else if (text[i] == '(' && --depth == 0) {
+ std::string prefix = collapseWhitespace(text.substr(0, i));
+ return prefix.empty() ? text : prefix;
+ }
+ }
+ return text; // unbalanced: leave it alone
+}
+
+// Converts a VkPhysicalDeviceProperties::deviceName into macOS Metal style,
+// e.g. "AMD Radeon RX 6800 XT (RADV NAVI21)" -> "AMD Radeon RX 6800 XT" and
+// "Intel(R) UHD Graphics 630 (CML GT2)" -> "Intel UHD Graphics 630".
+// This only cleans the real name; it never substitutes another GPU.
+static inline std::string metalNameFromVulkanName(const char* vulkanName) {
+ std::string raw = vulkanName ? vulkanName : "";
+ std::string cleaned = removeTrailingGroup(collapseWhitespace(removeTrademarks(raw)));
+ if (!cleaned.empty()) {
+ return cleaned;
+ }
+ std::string rawTrimmed = collapseWhitespace(raw);
+ return rawTrimmed.empty() ? std::string(kFallbackName) : rawTrimmed;
+}
+
+// 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) {
+ if (overrideName && overrideName[0] != '\0') {
+ return overrideName;
+ }
+ return metalNameFromVulkanName(vulkanName);
+}
+
+// Parses DARLING_METAL_VRAM_MB: a positive decimal integer of mebibytes, digits only.
+// Returns 0 (meaning "ignore") for anything else, including overflow.
+static inline uint64_t parseMegabytes(const char* text) {
+ if (!text || text[0] == '\0') {
+ return 0;
+ }
+ uint64_t megabytes = 0;
+ for (const char* p = text; *p; ++p) {
+ if (*p < '0' || *p > '9') {
+ return 0;
+ }
+ uint64_t digit = static_cast<uint64_t>(*p - '0');
+ if (megabytes > (UINT64_MAX / (1024ull * 1024ull) - digit) / 10) {
+ return 0;
+ }
+ megabytes = megabytes * 10 + digit;
+ }
+ return megabytes * 1024ull * 1024ull;
+}
+
+struct Heap {
+ uint64_t size;
+ bool deviceLocal;
+};
+
+// Working-set size from the Vulkan memory heaps; 0 when nothing usable is reported.
+// Discrete GPUs report their device-local (VRAM) heaps. GPUs sharing system RAM
+// (integrated/CPU device types) report every heap: their device-local heap may be
+// only the firmware carve-out (RADV APUs) while the GPU can also use the system heap,
+// which matches macOS reporting a large share of system RAM for unified-memory GPUs.
+static inline uint64_t workingSetBytes(const Heap* heaps, size_t count, bool sharesSystemMemory) {
+ uint64_t total = 0;
+ for (size_t i = 0; i < count; ++i) {
+ if (!sharesSystemMemory && !heaps[i].deviceLocal) {
+ continue;
+ }
+ total = heaps[i].size > UINT64_MAX - total ? UINT64_MAX : total + heaps[i].size;
+ }
+ return total;
+}
+
+} // namespace MTLDeviceInfo
+
+#endif // _METAL_MTLDEVICEINFO_H_
diff --git a/private-include/Metal/MTLSamplerStateInternal.h b/private-include/Metal/MTLSamplerStateInternal.h
new file mode 100644
--- /dev/null
+3
View File
@@ -13,6 +13,9 @@ darling shell /usr/bin/true
# installed layer and every write there (the Riot installer's temp files) fails with EACCES.
mkdir -p "$HOME/.darling/private/tmp"
chmod 1777 "$HOME/.darling/private/tmp"
# Build the system root store now that the prefix exists; Darling is stopped next anyway.
python3 build/runtime/tools/ensure-darling-root-certificates.py ||
echo 'Darling system root store not installed; the launcher retries it' >&2
python3 build/runtime/tools/stop-darling-runtime.py
sh build/runtime/tools/install-riot-presentation-runtime.sh
sh build/runtime/tools/install-riot-gpu-runtime.sh
+162
View File
@@ -0,0 +1,162 @@
"""Check the system root store generator against OpenSSL without starting Darling.
A stub `darling` runs the guest script on the host with a stub `security` tool, so
the generated trust record, the guest/host hand-off and the idempotency stamp are
exercised; the real keychain database is not.
"""
import contextlib
import hashlib
import io
import os
from pathlib import Path
import plistlib
import re
import runpy
import shutil
import subprocess
import tempfile
import textwrap
import unittest
SCRIPT = Path(__file__).resolve().parents[1] / 'tools/ensure-darling-root-certificates.py'
tool = runpy.run_path(str(SCRIPT))
DARLING_STUB = textwrap.dedent('''\
#!/usr/bin/env python3
import os, subprocess, sys
with open(os.environ['STUB_LOG'], 'a') as log:
log.write(' '.join(sys.argv[1:]) + '\\n')
if os.environ.get('STUB_FAIL'):
sys.exit(3)
assert sys.argv[1] == 'shell'
strip = lambda value: value[len('/Volumes/SystemRoot'):] if value.startswith('/Volumes/SystemRoot/') else value
env = dict(os.environ, PATH=os.environ['STUB_BIN'] + ':' + os.environ['PATH'])
sys.exit(subprocess.run([strip(arg) for arg in sys.argv[2:]], env=env).returncode)
''')
SECURITY_STUB = textwrap.dedent('''\
#!/usr/bin/env python3
import hashlib, sys
command, args = sys.argv[1], sys.argv[2:]
if command == 'create-keychain':
open(args[-1], 'wb').close()
elif command == 'add-certificates':
keychain = args[args.index('-k') + 1]
with open(keychain, 'a') as stream:
for path in args[args.index('-k') + 2:]:
data = open(path, 'rb').read()
stream.write(hashlib.sha1(data).hexdigest().upper() + '\\n')
elif command == 'find-certificate':
for line in open(args[-1]):
print('keychain: "%s"' % args[-1])
print('SHA-1 hash: ' + line.strip())
''')
def openssl(der, *options):
return subprocess.run(['openssl', 'x509', '-inform', 'DER', '-noout', *options], input=der,
capture_output=True, check=True).stdout.decode()
def asn1_issuer(der):
"""Issuer bytes located by OpenSSL's own ASN.1 parser: the fourth TBSCertificate field
(third without an explicit version)."""
lines = subprocess.run(['openssl', 'asn1parse', '-inform', 'DER'], input=der,
capture_output=True, check=True).stdout.decode().splitlines()
fields = []
for line in lines:
match = re.match(r'\s*(\d+):d=(\d+)\s+hl=(\d+)\s+l=\s*(\d+)\s+(\w+):\s*(.*)', line)
if match and match.group(2) == '2':
fields.append(match)
if fields[0].group(6).startswith('cont [ 0 ]'):
fields = fields[1:]
offset, header, length = (int(fields[2].group(index)) for index in (1, 3, 4))
return der[offset:offset + header + length]
@unittest.skipUnless(shutil.which('openssl'), 'openssl is required')
class RootCertificateTests(unittest.TestCase):
def setUp(self):
temporary = tempfile.TemporaryDirectory()
self.addCleanup(temporary.cleanup)
self.root = Path(temporary.name)
self.bundle = tool['default_bundle']()
self.data = self.bundle.read_bytes()
self.prefix = self.root / 'prefix'
self.prefix.mkdir()
stubs = self.root / 'bin'
stubs.mkdir()
for name, text in (('darling', DARLING_STUB), ('security', SECURITY_STUB)):
(stubs / name).write_text(text)
(stubs / name).chmod(0o755)
self.darling = [str(stubs / 'darling')]
self.log = self.root / 'darling.log'
self.log.touch()
self.environment = {'STUB_LOG': str(self.log), 'STUB_BIN': str(stubs)}
def run_tool(self, bundle=None, **extra):
output = io.StringIO()
saved = dict(os.environ)
os.environ.update(self.environment, **extra)
try:
with contextlib.redirect_stdout(output), contextlib.redirect_stderr(output):
status = tool['main'](['--prefix', str(self.prefix), '--bundle', str(bundle or self.bundle),
'--darling', ' '.join(self.darling)])
finally:
os.environ.clear()
os.environ.update(saved)
return status, output.getvalue()
def test_fields_match_openssl(self):
roots = tool['load_roots'](self.data, report=lambda line: None)
self.assertGreater(len(roots), 50)
samples = [roots[0], roots[len(roots) // 2], roots[-1]]
# Include a serial whose INTEGER needs a leading zero octet, if the bundle has one.
samples += [cert for cert in roots if cert['serial'][0] == 0][:1]
for cert in samples:
fingerprint = openssl(cert['der'], '-fingerprint', '-sha1').split('=', 1)[1].strip()
self.assertEqual(cert['sha1'], fingerprint.replace(':', ''))
serial = openssl(cert['der'], '-serial').split('=', 1)[1].strip()
self.assertEqual(int.from_bytes(cert['serial'], 'big'), int(serial, 16))
self.assertEqual(cert['issuer'], asn1_issuer(cert['der']))
def test_trust_record_and_idempotency(self):
status, output = self.run_tool()
self.assertEqual(status, 0, output)
folder = self.prefix / 'System/Library/Keychains'
record = plistlib.loads((folder / 'SystemTrustSettings.plist').read_bytes())
self.assertEqual(record['trustVersion'], 1)
roots = tool['load_roots'](self.data, report=lambda line: None)
self.assertEqual(set(record['trustList']), {cert['sha1'] for cert in roots})
for cert in roots[:5]:
entry = record['trustList'][cert['sha1']]
self.assertEqual(entry['issuerName'], cert['issuer'])
self.assertEqual(entry['serialNumber'], cert['serial'])
self.assertEqual(entry['trustSettings'], [])
self.assertEqual(hashlib.sha1(cert['der']).hexdigest().upper(), cert['sha1'])
self.assertEqual(len((folder / 'SystemRootCertificates.keychain').read_text().split()), len(roots))
self.assertEqual(len(self.log.read_text().splitlines()), 1)
status, output = self.run_tool()
self.assertEqual((status, output), (0, ''))
self.assertEqual(len(self.log.read_text().splitlines()), 1, 'unchanged bundle restarted Darling')
# A changed bundle is rebuilt.
smaller = self.root / 'smaller.pem'
smaller.write_bytes(b''.join(re.findall(rb'-----BEGIN CERTIFICATE-----.*?-----END CERTIFICATE-----\n?',
self.data, re.S)[:3]))
status, output = self.run_tool(smaller)
self.assertEqual(status, 0, output)
self.assertEqual(len(self.log.read_text().splitlines()), 2)
self.assertEqual(len(plistlib.loads((folder / 'SystemTrustSettings.plist').read_bytes())['trustList']), 3)
def test_failure_is_reported_and_not_stamped(self):
status, output = self.run_tool(STUB_FAIL='1')
self.assertEqual(status, 1)
self.assertIn('Darling system root certificates were not installed', output)
self.assertFalse((self.prefix / 'System/Library/Keychains' / tool['STAMP']).exists())
kept = re.search(r'see (\S+)/guest\.log', output)
self.assertTrue(kept, output)
shutil.rmtree(kept.group(1))
if __name__ == '__main__':
unittest.main()
+2 -1
View File
@@ -13,6 +13,8 @@ for line in (folder / 'CMakeFiles/Metal.dir/flags.make').read_text().splitlines(
flags[key] = shlex.split(value)
resource_include = subprocess.check_output(['clang++', '-print-resource-dir'], text=True).strip() + '/include'
flags['CXX_INCLUDES'] = [resource_include if value.startswith('/usr/lib/clang/') and value.endswith('/include') else value for value in flags['CXX_INCLUDES']]
# MTLDevice.mm reads Vulkan device properties through Indium's private headers.
flags['CXX_INCLUDES'].append('-I' + str(root / 'build/darling-src/src/external/metal/deps/indium/private-include'))
link = shlex.split((folder / 'CMakeFiles/Metal.dir/link.txt').read_text())
# These objects passed live League validation. Keep the broader resource/queue
# optimization experiment out of the default build after fresh-match stalls.
@@ -28,7 +30,6 @@ for name in ('MTLBuffer', 'MTLTexture', 'MTLDevice', 'MTLSamplerDescriptor', 'MT
link.append(str(obj))
link[link.index('-o') + 1] = str(root / 'build/riot-Metal')
subprocess.run(link, cwd=folder, check=True)
flags['CXX_INCLUDES'].append('-I' + str(root / 'build/darling-src/src/external/metal/deps/indium/private-include'))
obj = root / 'build/riot-metal-sampler-check.o'
subprocess.run(['clang++', *flags['CXX_DEFINES'], *flags['CXX_INCLUDES'],
*flags['CXX_FLAGS'], '-c', str(root / 'tools/riot-metal-sampler-check.mm'),
+285
View File
@@ -0,0 +1,285 @@
#!/usr/bin/env python3
"""Give Darling's system trust domain the host's CA roots.
Security.framework reads the system domain from two immutable files
(libsecurity_keychain TrustSettingsSchema.h): the root store
/System/Library/Keychains/SystemRootCertificates.keychain and its trust record
SystemTrustSettings.plist. Darling ships neither, so SecTrustSettingsCopyCertificates
for the system domain fails with errSecNoTrustSettings (-25263) and Chromium and
Riot's CertBundle report that they cannot read the system roots.
The trust record is generated here from the host CA bundle. The keychain uses the
guest `security` tool, since no host-side writer for its database format exists.
Darling is only started when the bundle (or this script) changed; a stamp next to
the outputs records the input digest.
"""
import argparse
import base64
import binascii
import datetime
import hashlib
import json
import os
from pathlib import Path
import plistlib
import re
import shutil
import ssl
import subprocess
import sys
import tempfile
VERSION = 1
KEYCHAIN = 'SystemRootCertificates.keychain'
TRUST_SETTINGS = 'SystemTrustSettings.plist'
STAMP = '.darling-lol-root-certificates.json'
SYSTEM_ROOT = '/Volumes/SystemRoot'
PEM = re.compile(rb'-----BEGIN CERTIFICATE-----\s*(.*?)\s*-----END CERTIFICATE-----', re.S)
# One guest invocation: build the keychain in the private work directory, list what
# it holds, then publish both files from inside the guest so a running Darling sees
# them (host writes to the prefix are invisible to a mounted overlay). The guest
# cannot create directories in the root-owned base layer; the host then publishes.
GUEST_SCRIPT = r'''#!/bin/sh
set -u
PATH=$PATH:/usr/bin:/bin
work=$1
kc=$work/SystemRootCertificates.keychain
security create-keychain -p '' "$kc" || exit 10
security unlock-keychain -p '' "$kc" || exit 11
# Certificates the guest cannot parse are reported and left out; the host checks found.txt.
security add-certificates -k "$kc" "$work"/certs/*.cer 2> "$work/add-errors.txt"
security find-certificate -a -Z "$kc" > "$work/found.txt" || exit 12
grep -q 'SHA-1 hash:' "$work/found.txt" || exit 13
dest=/System/Library/Keychains
if [ -d "$dest" ] && [ -w "$dest" ] &&
cp "$kc" "$dest/.SystemRootCertificates.keychain.next" 2>/dev/null &&
cp "$work/SystemTrustSettings.plist" "$dest/.SystemTrustSettings.plist.next" 2>/dev/null &&
mv -f "$dest/.SystemRootCertificates.keychain.next" "$dest/SystemRootCertificates.keychain" &&
mv -f "$dest/.SystemTrustSettings.plist.next" "$dest/SystemTrustSettings.plist"; then
: > "$work/placed"
else
rm -f "$dest/.SystemRootCertificates.keychain.next" "$dest/.SystemTrustSettings.plist.next" 2>/dev/null
fi
exit 0
'''
class Failure(Exception):
pass
def default_bundle():
paths = ssl.get_default_verify_paths()
for candidate in (paths.cafile, paths.openssl_cafile, '/etc/ssl/certs/ca-certificates.crt',
'/etc/pki/tls/certs/ca-bundle.crt', '/etc/ssl/ca-bundle.pem', '/etc/ssl/cert.pem'):
if candidate and Path(candidate).is_file():
return Path(candidate)
raise Failure('no host CA bundle found; pass --bundle')
def der_element(data, offset):
"""Return (tag, content start, end) of the DER element at offset."""
if offset + 2 > len(data):
raise ValueError('truncated element')
tag = data[offset]
if tag & 0x1f == 0x1f:
raise ValueError('high tag numbers are not used in certificates')
length = data[offset + 1]
start = offset + 2
if length & 0x80:
count = length & 0x7f
if count == 0 or count > 4 or start + count > len(data):
raise ValueError('unsupported length')
length = int.from_bytes(data[start:start + count], 'big')
start += count
end = start + length
if end > len(data):
raise ValueError('truncated element')
return tag, start, end
def parse_certificate(der):
"""Fields TrustSettings::copyIssuerAndSerial stores: the issuer Name as encoded
(CSSMOID_X509V1IssuerNameStd) and the serial INTEGER's content octets."""
tag, start, end = der_element(der, 0)
if tag != 0x30 or end != len(der):
raise ValueError('not a single DER certificate')
tag, offset, tbs_end = der_element(der, start)
if tag != 0x30:
raise ValueError('missing TBSCertificate')
tag, content, end = der_element(der, offset)
if tag == 0xa0: # explicit version
offset = end
tag, content, end = der_element(der, offset)
if tag != 0x02 or content == end:
raise ValueError('missing serial number')
serial = der[content:end]
offset = end
for name in ('signature', 'issuer', 'validity', 'subject'):
tag, content, end = der_element(der, offset)
if tag != 0x30 or end > tbs_end:
raise ValueError(f'missing {name}')
if name == 'issuer':
issuer = der[offset:end]
elif name == 'subject':
subject = der[offset:end]
offset = end
return {'sha1': hashlib.sha1(der).hexdigest().upper(), 'issuer': issuer,
'serial': serial, 'subject': subject, 'der': der}
def load_roots(data, report=print):
roots, seen, identities, skipped = [], set(), {}, []
for index, match in enumerate(PEM.finditer(data)):
try:
der = base64.b64decode(b''.join(match.group(1).split()), validate=True)
cert = parse_certificate(der)
except (binascii.Error, ValueError) as error:
skipped.append(f'#{index + 1}: unreadable ({error})')
continue
if cert['sha1'] in seen:
continue
seen.add(cert['sha1'])
if cert['subject'] != cert['issuer']:
skipped.append(f'{cert["sha1"]}: not self-issued, not a root')
continue
# The keychain keys certificates by issuer and serial; a second one is a duplicate item.
identity = (cert['issuer'], cert['serial'])
if identity in identities:
skipped.append(f'{cert["sha1"]}: same issuer and serial as {identities[identity]}')
continue
identities[identity] = cert['sha1']
roots.append(cert)
for line in skipped:
report(f'Root certificates: skipped {line}')
return roots
def trust_settings(roots, now):
# An empty trustSettings array means "trusted as root for every use"
# (TrustSettings::evaluateCert / qualifyUsageWithCertDict, numSpecs == 0).
return plistlib.dumps({
'trustVersion': 1,
'trustList': {cert['sha1']: {'issuerName': cert['issuer'], 'serialNumber': cert['serial'],
'modDate': now, 'trustSettings': []} for cert in roots},
}, fmt=plistlib.FMT_XML, sort_keys=True)
def bundle_digest(data):
return hashlib.sha256(b'darling-root-certificates-v%d\0' % VERSION + data).hexdigest()
def current(folder, digest):
try:
stamp = json.loads((folder / STAMP).read_text())
except (OSError, ValueError):
return False
return (stamp.get('digest') == digest and (folder / KEYCHAIN).is_file()
and (folder / TRUST_SETTINGS).is_file())
def server_running(prefix):
try:
pid = int((prefix / '.init.pid').read_text().strip())
status = (Path('/proc') / str(pid) / 'status').read_text()
except (OSError, ValueError):
return False
return not any(line.split()[1:2] == ['Z'] for line in status.splitlines() if line.startswith('State:'))
def publish_host(work, folder):
folder.mkdir(parents=True, exist_ok=True)
for name in (KEYCHAIN, TRUST_SETTINGS):
temporary = folder / f'.{name}.next'
shutil.copyfile(work / name, temporary)
os.chmod(temporary, 0o644)
os.replace(temporary, folder / name)
def write_stamp(folder, digest, bundle, roots, present):
temporary = folder / (STAMP + '.next')
temporary.write_text(json.dumps({'version': VERSION, 'digest': digest, 'bundle': str(bundle),
'roots': len(roots), 'in_keychain': len(present)}, indent=1) + '\n')
os.replace(temporary, folder / STAMP)
def ensure(prefix, bundle, darling, force=False, timeout=600):
data = bundle.read_bytes()
digest = bundle_digest(data)
folder = prefix / 'System/Library/Keychains'
if not force and current(folder, digest):
return 0
roots = load_roots(data)
if not roots:
raise Failure(f'no root certificates in {bundle}')
now = datetime.datetime.now(datetime.timezone.utc).replace(microsecond=0, tzinfo=None)
work = Path(tempfile.mkdtemp(prefix='darling-root-certificates-')).resolve()
keep = True
try:
(work / 'certs').mkdir()
for index, cert in enumerate(roots):
(work / 'certs' / f'{index:04d}.cer').write_bytes(cert['der'])
(work / TRUST_SETTINGS).write_bytes(trust_settings(roots, now))
script = work / 'build.sh'
script.write_text(GUEST_SCRIPT)
# Before Darling starts, the overlay picks up a host-created directory, which
# lets the guest publish into it.
running = server_running(prefix)
if not running:
folder.mkdir(parents=True, exist_ok=True)
print(f'Building Darling system root store from {len(roots)} certificates in {bundle}', flush=True)
try:
with open(work / 'guest.log', 'wb') as log:
result = subprocess.run([*darling, 'shell', '/bin/sh', SYSTEM_ROOT + str(script),
SYSTEM_ROOT + str(work)], stdin=subprocess.DEVNULL,
stdout=log, stderr=subprocess.STDOUT, timeout=timeout)
except (OSError, subprocess.SubprocessError) as error:
raise Failure(f'could not run the guest security tool: {error}; work files kept in {work}')
if result.returncode or not (work / 'found.txt').is_file():
raise Failure(f'guest keychain build failed (exit {result.returncode}); see {work}/guest.log')
found = set(re.findall(r'SHA-1 hash:\s*([0-9A-Fa-f]{40})', (work / 'found.txt').read_text(errors='replace')))
found = {value.upper() for value in found}
wanted = {cert['sha1'] for cert in roots}
present = wanted & found
if not present:
raise Failure(f'the guest keychain holds none of the certificates; see {work}')
if present != wanted:
print(f'Root certificates: the guest rejected {len(wanted - present)} of {len(wanted)}; '
f'see {work}/add-errors.txt', file=sys.stderr, flush=True)
# Kept for inspection; the outputs are still published.
else:
keep = False
if not (work / 'placed').exists():
# A running guest could not see a new directory; publish on the host for
# the next Darling start.
publish_host(work, folder)
if running:
print('Darling system root store installed; it takes effect after `darling shutdown`',
file=sys.stderr, flush=True)
write_stamp(folder, digest, bundle, roots, present)
print(f'Darling system root store has {len(present)} certificates', flush=True)
return 0
except OSError as error:
raise Failure(f'{error}; work files kept in {work}')
finally:
if not keep:
shutil.rmtree(work, ignore_errors=True)
def main(argv=None):
parser = argparse.ArgumentParser(description=__doc__.split('\n\n')[0])
parser.add_argument('--bundle', type=Path, help='PEM CA bundle (default: the host system bundle)')
parser.add_argument('--prefix', type=Path, default=Path(os.environ.get('DPREFIX', str(Path.home() / '.darling'))))
parser.add_argument('--darling', default='darling', help='Darling command (for tests)')
parser.add_argument('--force', action='store_true', help='rebuild even when the stamp is current')
args = parser.parse_args(argv)
try:
return ensure(args.prefix.expanduser(), args.bundle or default_bundle(), args.darling.split(), args.force)
except (Failure, OSError) as error:
print(f'Darling system root certificates were not installed: {error}', file=sys.stderr, flush=True)
return 1
if __name__ == '__main__':
sys.exit(main())
+2 -1
View File
@@ -60,7 +60,8 @@ with open({str(log)!r}, "a") as output:
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] == [
"ensure-darling-runtime.py", "ensure-darling-service-accounts.py", "ensure-league-runtime-layout.py"
"ensure-darling-runtime.py", "ensure-darling-service-accounts.py", "ensure-league-runtime-layout.py",
"ensure-darling-root-certificates.py",
], "unexpected automatic helper"
launches = [r for r in records if r["command"] == "darling"]
assert len(launches) == 1, "unexpected launch count"
+3 -2
View File
@@ -3,8 +3,9 @@
#
# tools/test-deferred-handoff.cpp the ordered worker's FIFO, the present-dispatch token, the wait counters
# tools/test-deferred-encoder-model.cpp the deferred blit/render op hand-off: order, resource lifetime, drop safety
# tools/test-submit-drain.cpp overlapping waits complete despite blocked later producers
#
# Both compile the real headers from deps/indium/private-include with the host compiler. The counter test writes its
# These compile the real headers from deps/indium/private-include with the host compiler. The counter test writes its
# summary to /tmp/opencode/waits-test.log (DARLING_WAITS_LOG) and needs /tmp/metal-debug-waits for the length of the
# run; it removes the flag again if it created it (and first checks, in a forked child, that the counters stay off and
# write nothing without it).
@@ -17,7 +18,7 @@ project_dir=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
out_dir=${RIOT_TEST_OUT:-/tmp/opencode}
include="$project_dir/build/darling-src/src/external/metal/deps/indium/private-include"
mkdir -p "$out_dir"
for name in test-deferred-handoff test-deferred-encoder-model; do
for name in test-deferred-handoff test-deferred-encoder-model test-submit-drain; do
c++ -std=c++17 -O1 -pthread -I "$include" "$project_dir/tools/$name.cpp" -o "$out_dir/$name"
echo "== $name"
"$out_dir/$name"
+5
View File
@@ -11,6 +11,11 @@ runner_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
python3 "$runner_dir/ensure-darling-runtime.py"
python3 "$runner_dir/ensure-darling-service-accounts.py"
python3 "$runner_dir/ensure-league-runtime-layout.py"
# Security's system trust domain (SecTrustSettingsCopyCertificates) needs a root keychain and
# trust record built from the host CA bundle. Only a changed bundle starts Darling; a failure
# is reported but does not stop the launch.
python3 "$runner_dir/ensure-darling-root-certificates.py" ||
echo 'Continuing without a Darling system root store; TLS clients may report untrusted roots' >&2
# Discard argument-rewriting settings from the former injected shim.
unset RCLOL_APP_ARGS RCLOL_APP_ARGS_SLOT RCLOL_APP_ARG2 RCLOL_APP_ARG2_SLOT
unset RCLOL_APP_ARG3 RCLOL_APP_ARG3_SLOT RCLOL_UX_ARG1 RCLOL_UX_ARG1_SLOT
+89
View File
@@ -0,0 +1,89 @@
// Host check for the GPU name and working-set helpers behind -[MTLDevice name] and
// -[MTLDevice recommendedMaxWorkingSetSize] (private-include/Metal/MTLDeviceInfo.h).
// Build and run after preparing the sources (see docs/TESTING.md):
// c++ -std=c++17 -I build/darling-src/src/external/metal/private-include
// tools/test-metal-device-info.cpp -o build/test-metal-device-info
// build/test-metal-device-info
#include <Metal/MTLDeviceInfo.h>
#include <cstdio>
#include <string>
static int failures = 0;
static void expectName(const char* vulkan, const char* expected) {
std::string actual = MTLDeviceInfo::metalNameFromVulkanName(vulkan);
bool ok = actual == expected;
failures += !ok;
std::printf("%s name \"%s\" -> \"%s\"%s%s%s\n", ok ? "PASS" : "FAIL", vulkan ? vulkan : "(null)",
actual.c_str(), ok ? "" : " (expected \"", ok ? "" : expected, ok ? "" : "\")");
}
static void expectBytes(const char* what, unsigned long long actual, unsigned long long expected) {
bool ok = actual == expected;
failures += !ok;
std::printf("%s %s -> %llu%s\n", ok ? "PASS" : "FAIL", what, actual, ok ? "" : " (mismatch)");
if (!ok) std::printf(" expected %llu\n", expected);
}
int main() {
// Mesa and proprietary Vulkan names -> macOS Metal style.
expectName("AMD Radeon RX 6800 XT (RADV NAVI21)", "AMD Radeon RX 6800 XT");
expectName("AMD Radeon RX 6650 XT (RADV NAVI23)", "AMD Radeon RX 6650 XT");
expectName("AMD Radeon Graphics (RADV RENOIR)", "AMD Radeon Graphics");
expectName("AMD Radeon Pro 5500M (RADV NAVI14)", "AMD Radeon Pro 5500M");
expectName("AMD RADV NAVI21 (ACO)", "AMD RADV NAVI21");
expectName("Intel(R) UHD Graphics 630 (CML GT2)", "Intel UHD Graphics 630");
expectName("Intel(R) Arc(TM) A770 Graphics (DG2)", "Intel Arc A770 Graphics");
expectName("NVIDIA GeForce GTX 680", "NVIDIA GeForce GTX 680");
expectName("NVIDIA GeForce RTX 2070 SUPER", "NVIDIA GeForce RTX 2070 SUPER");
expectName("NVIDIA GeForce RTX 4090", "NVIDIA GeForce RTX 4090");
expectName("llvmpipe (LLVM 22.1.0, 256 bits)", "llvmpipe");
// Whitespace, nesting and edge cases.
expectName(" AMD Radeon\tRX 7900 XTX (RADV NAVI31) ", "AMD Radeon RX 7900 XTX");
expectName("Foo GPU (driver (nested) x)", "Foo GPU");
expectName("Foo (Bar) GPU", "Foo (Bar) GPU"); // not trailing: kept
expectName("(RADV NAVI21)", "(RADV NAVI21)"); // whole name: kept
expectName("Broken (unbalanced", "Broken (unbalanced");
expectName("(R)(TM)", "(R)(TM)"); // cleaned empty -> raw name
expectName(" ", MTLDeviceInfo::kFallbackName); // raw empty -> neutral fallback
expectName("", MTLDeviceInfo::kFallbackName);
expectName(nullptr, MTLDeviceInfo::kFallbackName);
// DARLING_METAL_GPU_NAME override.
{
bool ok = MTLDeviceInfo::resolveName("My GPU", "AMD Radeon RX 6800 XT (RADV NAVI21)") == "My GPU"
&& MTLDeviceInfo::resolveName("", "AMD Radeon RX 6800 XT (RADV NAVI21)") == "AMD Radeon RX 6800 XT"
&& MTLDeviceInfo::resolveName(nullptr, "NVIDIA GeForce RTX 4090") == "NVIDIA GeForce RTX 4090";
failures += !ok;
std::printf("%s name override\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);
expectBytes("vram \"1\"", MTLDeviceInfo::parseMegabytes("1"), MiB);
expectBytes("vram \"0\"", MTLDeviceInfo::parseMegabytes("0"), 0);
expectBytes("vram \"\"", MTLDeviceInfo::parseMegabytes(""), 0);
expectBytes("vram null", MTLDeviceInfo::parseMegabytes(nullptr), 0);
expectBytes("vram \"-5\"", MTLDeviceInfo::parseMegabytes("-5"), 0);
expectBytes("vram \"8G\"", MTLDeviceInfo::parseMegabytes("8G"), 0);
expectBytes("vram \" 8\"", MTLDeviceInfo::parseMegabytes(" 8"), 0);
expectBytes("vram \"1.5\"", MTLDeviceInfo::parseMegabytes("1.5"), 0);
expectBytes("vram max", MTLDeviceInfo::parseMegabytes("17592186044415"), 17592186044415ull * MiB);
expectBytes("vram overflow", MTLDeviceInfo::parseMegabytes("17592186044416"), 0);
expectBytes("vram huge", MTLDeviceInfo::parseMegabytes("99999999999999999999999"), 0);
// Working set from Vulkan heaps.
const MTLDeviceInfo::Heap discrete[] = {{8192 * MiB, true}, {16384 * MiB, false}, {256 * MiB, true}};
expectBytes("discrete heaps", MTLDeviceInfo::workingSetBytes(discrete, 3, false), 8448 * MiB);
const MTLDeviceInfo::Heap apu[] = {{512 * MiB, true}, {15000 * MiB, false}};
expectBytes("integrated heaps", MTLDeviceInfo::workingSetBytes(apu, 2, true), 15512 * MiB);
const MTLDeviceInfo::Heap hostOnly[] = {{4096 * MiB, false}};
expectBytes("discrete without device-local heap", MTLDeviceInfo::workingSetBytes(hostOnly, 1, false), 0);
expectBytes("no heaps", MTLDeviceInfo::workingSetBytes(nullptr, 0, false), 0);
const MTLDeviceInfo::Heap huge[] = {{UINT64_MAX, true}, {UINT64_MAX, true}};
expectBytes("saturating sum", MTLDeviceInfo::workingSetBytes(huge, 2, false), UINT64_MAX);
std::printf("METAL_DEVICE_INFO failures=%d pass=%d\n", failures, failures == 0);
return failures == 0 ? 0 : 1;
}
+66
View File
@@ -0,0 +1,66 @@
// Exercise the real worker with overlapping drains and producers. A frame's
// drain must finish even while later work is blocked indefinitely.
#include <indium/submit-thread.private.hpp>
#include <atomic>
#include <cassert>
#include <chrono>
#include <cstdio>
#include <future>
#include <thread>
using namespace std::chrono_literals;
using namespace Indium;
static void awaitDrains(size_t count) {
const auto deadline = std::chrono::steady_clock::now() + 5s;
for (;;) {
{
auto& s = SubmitThread::state();
std::lock_guard<std::mutex> lock(s.mutex);
if (s.drainWaiters == count) return;
}
assert(std::chrono::steady_clock::now() < deadline);
std::this_thread::yield();
}
}
int main() {
std::promise<void> releaseFirst, releaseSecond, releaseThird;
auto first = releaseFirst.get_future().share();
auto second = releaseSecond.get_future().share();
auto third = releaseThird.get_future().share();
std::atomic<unsigned> completed{0};
SubmitThread::post([&] { first.wait(); ++completed; });
auto drain1 = std::async(std::launch::async, [] { SubmitThread::drain(); });
awaitDrains(1); // drain1 has taken its snapshot before the next post
SubmitThread::post([&] { second.wait(); ++completed; });
auto drain2 = std::async(std::launch::async, [] { SubmitThread::drain(); });
awaitDrains(2);
SubmitThread::post([&] { third.wait(); ++completed; });
releaseFirst.set_value();
assert(drain1.wait_for(2s) == std::future_status::ready);
drain1.get();
assert(completed == 1);
assert(drain2.wait_for(0s) == std::future_status::timeout);
releaseSecond.set_value();
assert(drain2.wait_for(2s) == std::future_status::ready);
drain2.get();
assert(completed == 2);
assert(SubmitThread::pending());
releaseThird.set_value();
SubmitThread::drain();
assert(completed == 3);
// The worker itself cannot wait for a snapshot containing its active task.
SubmitThread::post([] { SubmitThread::drain(); });
SubmitThread::drain();
// Repeated idle/active transitions exercise notification registration races.
for (unsigned i=0; i<100000; ++i) {
SubmitThread::post([&] { ++completed; });
SubmitThread::drain();
}
assert(completed == 100003);
std::puts("PASS submit drain: overlapping snapshots, blocked later producer, reentrant drain, 100000 handoffs");
std::fflush(stdout);
// The production worker is detached and process-lived.
std::_Exit(0);
}
+1
View File
@@ -244,6 +244,7 @@
"private-include/Metal/MTLArgumentInternal.h",
"private-include/Metal/MTLBlitCommandEncoderInternal.h",
"private-include/Metal/MTLDepthStencilInternal.h",
"private-include/Metal/MTLDeviceInfo.h",
"private-include/Metal/MTLSamplerStateInternal.h",
"private-include/Metal/MTLVertexDescriptorInternal.h",
"src/Metal/MTLArgument.mm",