diff --git a/docs/TESTING.md b/docs/TESTING.md index 59acc11..7a78652 100644 --- a/docs/TESTING.md +++ b/docs/TESTING.md @@ -15,6 +15,46 @@ python3 tools/riot-foundation-installer-offline-check.py 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. +## CoreText font-list and AppKit resize regressions + +After preparing the source and configuring the runtime tools: + +```sh +python3 build/runtime/tools/build-riot-coretext-runtime.py +python3 build/runtime/tools/build-riot-appkit-runtime.py +``` + +After installing and activating the runtime with both clients and the game closed, run the standalone probes (replace `me` and the checkout path): + +```sh +darling shell env DYLD_FRAMEWORK_PATH=/Users/me/RuntimeFrameworks \ + /Volumes/SystemRoot/path/to/darling-lol/build/riot-coretext-font-list-check +darling shell env DYLD_FRAMEWORK_PATH=/Users/me/RuntimeFrameworks \ + /Volumes/SystemRoot/path/to/darling-lol/build/riot-window-layout-check +``` + +Expected: `CORETEXT_FONT_LIST pass=1` with nonzero family/face counts (306/1124 on the development host), `VIEW_LAYER_RESIZE initial=1600x900 pass=1`, `NATIVE_RESIZE_ORDER size=1600x900 pass=1` `CONTENT_SIBLING_RESIZE ... pass=1` and `WINDOW_LAYOUT_CHECK ... pass=1`. + +The font probe reproduces Chromium's installed-family enumeration, mandatory family matching and localized-name lookup with a null language output. It checks ownership of the single-match result and that a nonexistent mandatory family produces no substitute. The resize probe checks backing-layer bounds immediately after frame/bounds changes, including a scaled view and a child layer, without relying on input or coordinate conversion to refresh them. It also verifies that native parent size is current during child layout, and that adding an overlay sibling after the content view does not redirect subsequent content resizing into that overlay. The old sibling selection left content at 1600×900 while stretching the 30×20 overlay to 1280×720; the corrected layout resizes the content and preserves the overlay. Before the fixes, these checks report a stale 1280×720 layer and native parent after a 1600×900 resize. The `FRAME_SIZE_HOOK` check uses a subclass whose `setFrameSize:` publishes its viewport size. It checks growth and shrinkage through parent autoresizing, direct `setFrame:`, and direct `setFrameSize:`. Before component-setter dispatch was corrected only the final direct size setter reached the subclass (one callback); afterward all four updates do, with current geometry visible inside the callback. No probe launches or contacts Riot/League. The user confirmed that the language dropdown and League window resizing work after the final X11 correction (2026-10-15). The practice-tool crash remains unresolved. + +### Presentation publisher handoff + +With clients closed, run `build/riot-client-resize-check --handoff` through Darling with the normal runtime overrides. This creates an owned window and two fixture frame slots. The live software owner first supplies a 1280×720 main frame, then switches to a 262×131 popup; another slot supplies the resized main frame without the software flag. The test samples its own window beyond the original frame extent and checks generation colors across growth and shrinkage. The old selector reports black at the 1600×900 sample; the correction passes all four sizes. Repeat with `RCLOL_XSHM=0` and with `DARLING_XSHM_LIBRARY` pointing to the normal transport to cover both paths. The fixtures remove their own frame slots on exit; they do not read client pixels or send input. + +### Pixels after a window resize + +`python3 build/runtime/tools/build-riot-client-resize-check.py` builds an independent AppKit window and IOSurface publisher. With clients closed and a display at least 1600×900, run it with the same framework/library overrides and `DARLING_XSHM_LIBRARY=/Users/me/libDarlingXShm.dylib`, `DARLING_CA_PRESENT_AT_COMMIT=1`, `DARLING_CA_SKIP_LOCAL_IOSURFACE_RENDER=1`. The default mode publishes software pixels. For the GPU path add `--gpu` and set `RCLOL_GPU_DIRECT_PRESENT=1 RCLOL_GPU_DIRECT_ASYNC=1` in the guest environment. + +The probe cycles through 1280×720, 1600×900, 1024×576 and 1280×720. Each stage changes the frame color and samples its own X11 window near the bottom-right corner, beyond the original frame's extent when growing. It waits up to 2.5 seconds for presentation and prints `RESIZE_PIXEL ... pass=1` for every stage. This verifies presented pixels rather than only view properties. Success does not reproduce League's own resize callbacks; the reported League UI failure remains open, with the opt-in [geometry trace](TROUBLESHOOTING.md#language-dropdown-closes-riot-or-client-resize-leaves-a-clipped-ui) needed to locate the remaining mismatch. + +## Legacy operating-system version query + +```sh +python3 build/runtime/tools/build-riot-carboncore-runtime.py +``` + +After installing and activating CarbonCore, run the independent `build/riot-gestalt-version-check` through Darling with the normal framework/library overrides. Expected on the current kernel: `GESTALT_VERSION product=14.7 version=14.7.0 packed=0x1470 architecture=10 pass=1`. It compares the separate version selectors against `kern.osproductversion`, checks packed BCD, confirms architecture remains a separate result, and covers null outputs and unknown selectors. The previous implementation returned 10 for every version component and the packed selector. The probe does not launch or contact Riot. Passing it establishes the API correction, not Vanguard compatibility. + ## Standalone guest probes Build and install the base/runtime first. Native probes are built by the incremental runtime scripts. With the default guest username, run a probe like this (replace `me` and the host checkout path): diff --git a/docs/TROUBLESHOOTING.md b/docs/TROUBLESHOOTING.md index ae16f0f..c46d3f0 100644 --- a/docs/TROUBLESHOOTING.md +++ b/docs/TROUBLESHOOTING.md @@ -62,6 +62,63 @@ LaunchServices dereferenced a nullable error-output pointer in six paths. The pa LaunchServices now opens `http`, `https` and `mailto` links on the Linux desktop itself. It loads the host's `libsystemd.so.0` and asks `xdg-desktop-portal` (`org.freedesktop.portal.OpenURI`) on the user session bus to open the link, so your default browser starts outside Darling and survives `darling shutdown`. Without a portal, it starts `xdg-open` in a transient systemd user unit instead. It also reports a handler for these schemes, since Electron checks for one before opening a link. Other schemes, control characters and URLs over 4096 bytes are rejected. If a link still does nothing, check the client's stderr for a `LaunchServices:` line and that `busctl --user status org.freedesktop.portal.Desktop` works on the host. Both LaunchServices and AppKit must be rebuilt. The hand-off passed the host-side D-Bus regression and opened the host browser from a guest through both `open(1)` and `NSWorkspace`; a link clicked in the Riot client is not yet confirmed. The null-pointer fix does not explain every unrelated button crash. +## Language dropdown closes Riot, or client resize leaves a clipped UI + +The [report in issue #1](https://github.com/lucannez64/darling-lol/issues/1#issuecomment-6194498757) contains an Electron renderer abort with `Symbol not found: _CTFontDescriptorCreateMatchingFontDescriptors`. CoreText now exports font-descriptor matching and localized-attribute lookup, and enumerates installed font families through fontconfig instead of returning a null stub. Localized lookup currently falls back to canonical font names. The standalone font-list check exercises Chromium's sequence, including mandatory family matching and retained results. + +AppKit also left backing-layer geometry at the old size until a coordinate conversion occurred. It now updates that geometry during frame/bounds changes, and invalidates parent transforms before child resize callbacks. Native parent geometry is updated before resizing child windows, whose Y positions depend on the parent's current height. Standalone regressions reproduce both stale-size defects and pass with the corrections. A follow-up test confirmed that the language dropdown is fixed, but League still displays a 1280×720 image inside a 1600×900 window. The geometry regressions alone do not establish a fix for that remaining resize failure. + +Rebuild and install the runtime using the [update instructions](SETUP.md#updating-an-existing-runtime), with the game and both clients closed. CoreText is now included in the GPU/runtime installer alongside AppKit; existing installations need this new override too. Until then, a locale can be selected at launch, for example `RCLOL_REGION=BR RCLOL_LOCALE=pt_BR ./scripts/run.sh`. + +Live inspection subsequently confirmed a 1600×900 outer League window with both its CoreAnimation child and presentation target still at 1280×720. `NSThemeFrame` selected the last non-menu/toolbar child as the content view. A standalone reproduction showed that an overlay added after the real content received the resize instead. Frame layout now identifies the window's actual content owner (including a containing wrapper), and applies ordinary autoresizing masks to other decorations. A hidden-window regression reproduces the old behavior with an overlay sibling and passes after the fix. The user retested with this build loaded; League still failed to resize. This correction therefore does not resolve the remaining client failure. + +The subsequent file trace showed the browser view reaching the new frame size without a renderer resize, and a later shrink left `RenderWidgetHostViewCocoa` at 1600×900 inside 1280×720 parents. Darling's `NSView setFrame:` directly updated geometry without dispatching `setFrameSize:`. [Chromium's Cocoa implementation](https://chromium.googlesource.com/chromium/src/+/114a1ef9e848ccebec516681bcf1786f85b5cbd6/content/browser/renderer_host/render_widget_host_view_mac.mm#2273) explicitly relies on this dispatch to notify the renderer. AppKit now calls the component setters for changed origins/sizes, with a private geometry updater to avoid recursion. A standalone subclass regression failed before this correction (one callback from four size changes) and passes after it (four callbacks, including parent autoresizing in both directions). The user retested with this AppKit build loaded and still saw clipping. Passive inspection then showed the native CoreAnimation child and a published frame at 1600×900, with the presentation target remaining at 1280×720. + +The X11 presentation selector also retained a software publisher across popup updates even after the window outgrew its old presentation target. If that publisher switched to a small popup and a different publisher supplied the resized main frame, the selector kept following the popup slot and never resized the presentation child. It now retains that owner only while the old presentation dimensions still match a native target. A two-publisher standalone pixel regression reproduces the black enlarged area on the old backend; the new backend passes 1280×720 → 1600×900 → 1024×576 → 1280×720 with and without the shared-memory transport. After installing this backend locally, the user confirmed that League window resizing works (2026-10-15). + +For the AppKit and X11 corrections, exit the game and both clients manually, then use the small update instead of rebuilding the entire runtime: + +```sh +python3 scripts/configure.py +python3 build/runtime/tools/build-riot-appkit-runtime.py +python3 build/runtime/tools/build-riot-x11-runtime.py +darling shutdown +sh build/runtime/tools/install-riot-appkit-runtime.sh +sh scripts/run.sh +``` + +The installer requires the existing per-user AppKit overlay, saves a checksum-named backup under `build/appkit-runtime-backups`, and atomically replaces each binary, including the paired X11 backend. It does not stop processes or change the system SDK. After launch, change League between 1280×720 and 1600×900 and verify both the rendered extent and click alignment. + +The installer previously preferred an existing `build/riot-QuartzCore` over its newly built `build/m336-QuartzCore`. It now installs the artifact it just rebuilt, and that builder recompiles all patched compositor and Metal presentation objects. This avoids silently installing an older optional probe build. + +If resize clipping recurs, launch with size-only runtime tracing and resize the client manually: + +```sh +mkdir -p build +env DARLING_UI_GEOMETRY_TRACE=1 sh scripts/run.sh 2>&1 \ + | rg --line-buffered '^UI_GEOMETRY ' | tee build/ui-geometry.log +``` + +Change from 1280×720 to 1600×900 and leave the client open at the broken size. AppKit also writes a bounded window/view hierarchy to the guest `/tmp/darling-ui-geometry.log` (host `~/.darling/private/tmp/darling-ui-geometry.log`). Read that file as well as `build/ui-geometry.log`: League redirects stderr, so the terminal capture alone can omit all of its resize events. The traces record window/view classes, identities, dimensions and autoresizing masks, layer scale and IOSurface dimensions at size changes. Tracing is disabled on ordinary launches. It uses the runtime's own geometry setters and surface publisher, with no debugger, client API access or input automation. + +The practice-tool exit in the same report remains unresolved: the supplied League client log records the game starting and exiting about two seconds later, but contains no game-process crash reason. These font and resize fixes do not establish a fix for that exit. Further diagnosis needs the game's own log/crash report or stderr from the affected machine. + +## Client reports macOS 10.10 and VAN -101 + +The legacy CarbonCore `Gestalt` implementation read a Darwin kernel release as though it were a macOS product version, then fell through into `gestaltSysArchitecture`, overwriting every result with `gestaltIntel` (10). The standalone probe reproduced `gestalt=10.10.10` even though `kern.osproductversion` was already `14.7`. Version selectors now read that existing product version, return before the architecture case, initialize missing patch versions to zero and implement the documented packed-BCD limits. Null outputs and unknown selectors return errors. + +After installing this correction and restarting locally, the client reported macOS 14.7 and the user confirmed that Play became available. Later launches failed again, including a launch without `DARLING_UI_GEOMETRY_TRACE`. The running client was verified to have the corrected CarbonCore binary loaded. Both successful and failed sessions reported macOS 14.7; successful sessions logged `MvgState: 0`, while failed sessions logged `MvgState: 5`, despite `MvgInit` and `MvgRegisterLogin` returning zero in both. These state values are observations, not a diagnosis of their undocumented meaning. The version correction is therefore insufficient to resolve VAN -101, and tracing is not required to reproduce the failure. Entering a match remains unverified. [Riot describes VAN -101](https://support.riotgames.com/en-us/riot/performance/error-van--101) as an unsupported system configuration and recommends a supported operating system or a support ticket with logs. No verified Darling compatibility fix for this failure is available in this snapshot. A temporary test rollback of commit `ac4a832` restores `proc_listpids` returning `ENOTSUP`; it was followed by both a successful launch and another VAN failure, so it has not established that commit as the cause. Tracing was enabled on the latter launch; its contribution remains unproven. + +Build the small update with `python3 build/runtime/tools/build-riot-carboncore-runtime.py`. After manually exiting the game and both clients: + +```sh +darling shutdown +sh build/runtime/tools/install-riot-carboncore-runtime.sh +sh scripts/run.sh +``` + +The installer adds a CarbonCore framework override and the matching absolute CoreServices subframework binary in the guest prefix, with checksum-named backups in `build/carboncore-runtime-backups`. It does not modify the SDK or restart processes. Both forms are needed because framework lookup and explicit subframework loading can take different paths. Ordinary full runtime installation includes this update too. + ## `LOLSIGFAIL … addr=0x143` every 10 seconds `iokitd` crashed in host glibc's `pthread_mutex_lock` and launchd restarted it every 10 seconds. It called `eglGetDisplay()` with an Xlib display, so Mesa picked the platform from `EGL_PLATFORM`. Wayland sessions often set that to `wayland`, and the first `darling` command passes its environment to every guest service, including `iokitd`. `scripts/run.sh` sets `EGL_PLATFORM=x11` and was unaffected, but `darling shell` from a Wayland terminal (including the one in `scripts/install-runtime.sh`) started the loop. `iokitd` now requests the X11 platform explicitly. The fix is part of the base install (`sudo cmake --install`); until then, start Darling with `EGL_PLATFORM=x11`. diff --git a/patches/cocotron.patch b/patches/cocotron.patch index 0391d0a..bfd0cf3 100644 --- a/patches/cocotron.patch +++ b/patches/cocotron.patch @@ -1271,10 +1271,42 @@ index 9177a986..9c3c5ca3 100644 @end diff --git a/AppKit/NSThemeFrame.m b/AppKit/NSThemeFrame.m -index 0ec827c0..6f780225 100644 +index 0ec827c0..a24cd9f5 100644 --- a/AppKit/NSThemeFrame.m +++ b/AppKit/NSThemeFrame.m -@@ -133,8 +133,10 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +@@ -85,7 +85,14 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ + - (void) resizeSubviewsWithOldSize: (NSSize) oldSize { + NSView *menuView = nil; + NSToolbarView *toolbarView = nil; +- NSView *contentView = nil; ++ /* Browser windows can add overlay/titlebar siblings after their content. ++ * Identify the window's actual content owner rather than choosing the ++ * last ordinary subview. Full-size content wrappers can own it indirectly. */ ++ NSView *contentView = [[self window] contentView]; ++ while (contentView && [contentView superview] && [contentView superview] != self) ++ contentView = [contentView superview]; ++ if ([contentView superview] != self) ++ contentView = nil; + + // tile the subviews, when/if we add titlebars and such do it here + for (NSView *view in _subviews) { +@@ -93,10 +100,13 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ + menuView = view; + else if ([view isKindOfClass: [NSToolbarView class]]) + toolbarView = (NSToolbarView *) view; +- else +- contentView = view; + } + ++ // Other decorations still follow their autoresizing masks. ++ for (NSView *view in [[_subviews copy] autorelease]) ++ if (view != contentView && view != menuView && view != toolbarView) ++ [view resizeWithOldSuperviewSize: oldSize]; ++ + // subtracts menu height but not toolbar height + NSRect contentFrame = [[[self window] class] + contentRectForFrameRect: [self bounds] +@@ -133,8 +143,10 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ } - (void) mouseDown: (NSEvent *) event { @@ -1300,19 +1332,31 @@ index c3f5d056..dbd93174 100644 @"NSTouchBarItemIdentifierCharacterPicker"; diff --git a/AppKit/NSView.m b/AppKit/NSView.m -index 8ac2bbdc..7486ebf5 100644 +index 8ac2bbdc..08197fbc 100644 --- a/AppKit/NSView.m +++ b/AppKit/NSView.m -@@ -46,6 +46,8 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +@@ -46,6 +46,12 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ #import #import #import +#import +#import ++#include "NSGeometryTrace.h" ++#include ++#include ++#include @class IBMetricsTable; -@@ -955,6 +957,54 @@ static inline void buildTransformsIfNeeded(NSView *self) { +@@ -66,6 +72,7 @@ const NSViewFullScreenModeOptionKey NSFullScreenModeApplicationPresentationOptio + - (CGAffineTransform) transformToWindow; + - (CGAffineTransform) transformToLayer; + - (void) _trackingAreasChanged; ++- (void) _darlingSetFrame: (NSRect) frame; + @end + + @implementation NSView +@@ -955,6 +962,54 @@ static inline void buildTransformsIfNeeded(NSView *self) { CGPointApplyAffineTransform(point, toWindow), fromWindow); } @@ -1367,7 +1411,77 @@ index 8ac2bbdc..7486ebf5 100644 - (NSSize) convertSize: (NSSize) size fromView: (NSView *) viewOrNil { NSView *fromView = (viewOrNil != nil) ? viewOrNil : [[self window] _backgroundView]; -@@ -1254,7 +1304,11 @@ static inline void buildTransformsIfNeeded(NSView *self) { +@@ -1029,6 +1084,16 @@ static inline void buildTransformsIfNeeded(NSView *self) { + } + + - (void) setFrame: (NSRect) frame { ++ /* AppKit dispatches through the component setters. Subclasses use ++ * setFrameSize: to notify their renderer even when callers set a full ++ * frame (including autoresizing through resizeWithOldSuperviewSize:). */ ++ if (!NSEqualPoints(_frame.origin, frame.origin)) ++ [self setFrameOrigin: frame.origin]; ++ if (!NSEqualSizes(_frame.size, frame.size)) ++ [self setFrameSize: frame.size]; ++} ++ ++- (void) _darlingSetFrame: (NSRect) frame { + // Cocoa does not post the notification if the frames are equal + // Possible that resizeSubviewsWithOldSize is not called if the sizes are + // equal +@@ -1052,6 +1117,12 @@ static inline void buildTransformsIfNeeded(NSView *self) { + } + _frame = frame; + ++ /* Resize callbacks can convert child frames to window coordinates. They ++ * must see the new parent transform, and compositor submissions must see ++ * the new layer bounds even before the next coordinate conversion. */ ++ invalidateTransform(self); ++ configureLayerGeometry(self); ++ + [_window invalidateCursorRectsForView: self]; // this also invalidates + // tracking areas + +@@ -1068,6 +1139,14 @@ static inline void buildTransformsIfNeeded(NSView *self) { + + invalidateTransform(self); + ++ if (getenv("DARLING_UI_GEOMETRY_TRACE") && ++ (_frame.size.width >= 256 || _frame.size.height >= 256)) ++ DarlingGeometryTrace("UI_GEOMETRY view pid=%d class=%s parent=%s frame=%.0fx%.0f bounds=%.0fx%.0f mask=%lu autoresizes=%d\n", ++ getpid(), class_getName([self class]), ++ _superview ? class_getName([_superview class]) : "none", ++ _frame.size.width, _frame.size.height, _bounds.size.width, _bounds.size.height, ++ (unsigned long)_autoresizingMask, _autoresizesSubviews); ++ + if (_postsNotificationOnFrameChange) + [[NSNotificationCenter defaultCenter] + postNotificationName: NSViewFrameDidChangeNotification +@@ -1078,14 +1157,14 @@ static inline void buildTransformsIfNeeded(NSView *self) { + NSRect frame = _frame; + + frame.size = size; +- [self setFrame: frame]; ++ [self _darlingSetFrame: frame]; + } + + - (void) setFrameOrigin: (NSPoint) origin { + NSRect frame = [self frame]; + + frame.origin = origin; +- [self setFrame: frame]; ++ [self _darlingSetFrame: frame]; + } + + - (void) setFrameRotation: (CGFloat) angle { +@@ -1100,6 +1179,7 @@ static inline void buildTransformsIfNeeded(NSView *self) { + if (!NSEqualRects(bounds, _bounds)) { + _bounds = bounds; + invalidateTransform(self); ++ configureLayerGeometry(self); + + [_window invalidateCursorRectsForView: self]; // this also invalidates + // tracking areas +@@ -1254,7 +1334,11 @@ static inline void buildTransformsIfNeeded(NSView *self) { (NSComparisonResult (*)(id, id, void *)) compareFunction context: (void *) context { @@ -1380,7 +1494,7 @@ index 8ac2bbdc..7486ebf5 100644 } - (void) didAddSubview: (NSView *) subview { -@@ -1511,8 +1565,16 @@ static inline void buildTransformsIfNeeded(NSView *self) { +@@ -1511,8 +1595,16 @@ static inline void buildTransformsIfNeeded(NSView *self) { } - (NSTextInputContext *) inputContext { @@ -1399,7 +1513,7 @@ index 8ac2bbdc..7486ebf5 100644 } - (void) registerForDraggedTypes: (NSArray *) types { -@@ -1819,15 +1881,21 @@ static inline void buildTransformsIfNeeded(NSView *self) { +@@ -1819,15 +1911,21 @@ static inline void buildTransformsIfNeeded(NSView *self) { - (void) _createLayerContextIfNeeded { if ([_superview layer] == nil) { @@ -1423,7 +1537,7 @@ index 8ac2bbdc..7486ebf5 100644 [[_superview layer] addSublayer: _layer]; [self _createLayerContextIfNeeded]; } -@@ -1905,6 +1973,7 @@ static inline void buildTransformsIfNeeded(NSView *self) { +@@ -1905,6 +2003,7 @@ static inline void buildTransformsIfNeeded(NSView *self) { [[_superview layer] replaceSublayer: _layer with: value]; [_layer release]; _layer = value; @@ -1432,14 +1546,15 @@ index 8ac2bbdc..7486ebf5 100644 [_subviews diff --git a/AppKit/NSWindow.m b/AppKit/NSWindow.m -index d545120a..db2c12d9 100644 +index d545120a..b2845478 100644 --- a/AppKit/NSWindow.m +++ b/AppKit/NSWindow.m -@@ -45,6 +45,20 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +@@ -45,6 +45,21 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ #import #import #import +#import ++#include "NSGeometryTrace.h" +#include +#include + @@ -1456,7 +1571,7 @@ index d545120a..db2c12d9 100644 const NSNotificationName NSWindowDidBecomeKeyNotification = @"NSWindowDidBecomeKeyNotification"; -@@ -204,6 +218,25 @@ NSInteger NSBitsPerPixelFromDepth(NSWindowDepth depth) { +@@ -204,6 +219,25 @@ NSInteger NSBitsPerPixelFromDepth(NSWindowDepth depth) { @end @@ -1482,7 +1597,7 @@ index d545120a..db2c12d9 100644 @implementation NSWindow @synthesize identifier = _identifier; -@@ -218,7 +251,8 @@ static BOOL _allowsAutomaticWindowTabbing; +@@ -218,7 +252,8 @@ static BOOL _allowsAutomaticWindowTabbing; /* This method is Cococtron specific and can be override by subclasses, do not * change method name */ + (BOOL) hasMainMenuForStyleMask: (NSWindowStyleMask) styleMask { @@ -1492,7 +1607,7 @@ index d545120a..db2c12d9 100644 } /* This method is Cococtron specific and can be override by subclasses, do not -@@ -318,6 +352,7 @@ static BOOL _allowsAutomaticWindowTabbing; +@@ -318,6 +353,7 @@ static BOOL _allowsAutomaticWindowTabbing; [NSApplication sharedApplication]; } _styleMask = styleMask; @@ -1500,7 +1615,7 @@ index d545120a..db2c12d9 100644 _backingType = backing; _isDeferred = defer; -@@ -351,7 +386,7 @@ static BOOL _allowsAutomaticWindowTabbing; +@@ -351,7 +387,7 @@ static BOOL _allowsAutomaticWindowTabbing; [_backgroundView _setWindow: self]; [_backgroundView setNextResponder: self]; @@ -1509,7 +1624,7 @@ index d545120a..db2c12d9 100644 _contentView = [[NSView alloc] initWithFrame: contentViewFrame]; [_contentView setAutoresizesSubviews: YES]; [_contentView -@@ -399,6 +434,8 @@ static BOOL _allowsAutomaticWindowTabbing; +@@ -399,6 +435,8 @@ static BOOL _allowsAutomaticWindowTabbing; } - (void) dealloc { @@ -1518,7 +1633,51 @@ index d545120a..db2c12d9 100644 [[NSNotificationCenter defaultCenter] removeObserver: self]; [_childWindows release]; [_title release]; -@@ -1025,7 +1062,34 @@ static BOOL _allowsAutomaticWindowTabbing; +@@ -934,6 +972,22 @@ static BOOL _allowsAutomaticWindowTabbing; + return _animationContext; + } + ++/* Dump geometry only, including children which did not receive a resize. ++ * The node/depth bounds keep malformed or unexpectedly large trees bounded. */ ++static void traceGeometryTree(NSView *view, unsigned depth, unsigned *remaining) { ++ if (!view || depth > 12 || !*remaining) ++ return; ++ --*remaining; ++ NSRect frame = [view frame], bounds = [view bounds]; ++ DarlingGeometryTrace("UI_GEOMETRY tree pid=%d depth=%u view=%p class=%s parent=%p frame=%.0f,%.0f,%.0fx%.0f bounds=%.0f,%.0f,%.0fx%.0f mask=%lu autoresizes=%d layer=%d\n", ++ getpid(), depth, view, class_getName([view class]), [view superview], ++ frame.origin.x, frame.origin.y, frame.size.width, frame.size.height, ++ bounds.origin.x, bounds.origin.y, bounds.size.width, bounds.size.height, ++ (unsigned long)[view autoresizingMask], [view autoresizesSubviews], [view wantsLayer]); ++ for (NSView *child in [view subviews]) ++ traceGeometryTree(child, depth + 1, remaining); ++} ++ + - (void) setFrame: (NSRect) newFrame + display: (BOOL) display + animate: (BOOL) animate +@@ -944,8 +998,19 @@ static BOOL _allowsAutomaticWindowTabbing; + _frame = newFrame; + _makeSureIsOnAScreen = YES; + +- [_backgroundView setFrameSize: _frame.size]; ++ /* Native child windows convert their Y origin using the platform ++ * window's height. Update it before laying out/resizing those children. */ + [_platformWindow setFrame: _frame]; ++ [_backgroundView setFrameSize: _frame.size]; ++ ++ if (didSize && getenv("DARLING_UI_GEOMETRY_TRACE")) { ++ NSSize contentSize = [_contentView frame].size; ++ DarlingGeometryTrace("UI_GEOMETRY window pid=%d class=%s frame=%.0fx%.0f content=%.0fx%.0f style=%lu window=%p contentView=%p\n", ++ getpid(), class_getName([self class]), _frame.size.width, _frame.size.height, ++ contentSize.width, contentSize.height, (unsigned long)_styleMask, self, [self contentView]); ++ unsigned remaining = 128; ++ traceGeometryTree(_backgroundView, 0, &remaining); ++ } + + if (didSize) { + [self resetCursorRects]; +@@ -1025,7 +1090,34 @@ static BOOL _allowsAutomaticWindowTabbing; } - (void) setMovable: (BOOL) movable { @@ -1554,7 +1713,7 @@ index d545120a..db2c12d9 100644 } - (void) setBackingType: (NSBackingStoreType) value { -@@ -1234,6 +1298,7 @@ static BOOL _allowsAutomaticWindowTabbing; +@@ -1234,6 +1326,7 @@ static BOOL _allowsAutomaticWindowTabbing; - (void) setIgnoresMouseEvents: (BOOL) value { _ignoresMouseEvents = value; @@ -1562,7 +1721,7 @@ index d545120a..db2c12d9 100644 } - (void) setAspectRatio: (NSSize) value { -@@ -1413,8 +1478,14 @@ static BOOL _allowsAutomaticWindowTabbing; +@@ -1413,8 +1506,14 @@ static BOOL _allowsAutomaticWindowTabbing; } - (NSButton *) standardWindowButton: (NSWindowButton) value { @@ -1579,7 +1738,7 @@ index d545120a..db2c12d9 100644 } - (NSButtonCell *) defaultButtonCell { -@@ -1425,6 +1496,17 @@ static BOOL _allowsAutomaticWindowTabbing; +@@ -1425,6 +1524,17 @@ static BOOL _allowsAutomaticWindowTabbing; return [_sheetContext sheet]; } @@ -1597,7 +1756,7 @@ index d545120a..db2c12d9 100644 - (id) windowController { return _windowController; } -@@ -1511,10 +1593,7 @@ static BOOL _allowsAutomaticWindowTabbing; +@@ -1511,10 +1621,7 @@ static BOOL _allowsAutomaticWindowTabbing; return [_platformWindow isMiniaturized]; } @@ -1609,7 +1768,7 @@ index d545120a..db2c12d9 100644 - (BOOL) inLiveResize { return _isInLiveResize; -@@ -1561,6 +1640,117 @@ static BOOL _allowsAutomaticWindowTabbing; +@@ -1561,6 +1668,117 @@ static BOOL _allowsAutomaticWindowTabbing; return point; } @@ -1727,7 +1886,7 @@ index d545120a..db2c12d9 100644 - (NSRect) frameRectForContentRect: (NSRect) contentRect { // hasMainMenu is an instance method so we can't just use the class method // frameRectForContentRect:styleMask: -@@ -1580,6 +1761,10 @@ static BOOL _allowsAutomaticWindowTabbing; +@@ -1580,6 +1798,10 @@ static BOOL _allowsAutomaticWindowTabbing; } - (NSRect) contentRectForFrameRect: (NSRect) frameRect { @@ -1738,7 +1897,7 @@ index d545120a..db2c12d9 100644 NSRect result = [[NSDisplay currentDisplay] insetRect: frameRect forNativeWindowBorderWithStyle: [self styleMask]]; -@@ -2155,6 +2340,7 @@ static BOOL _allowsAutomaticWindowTabbing; +@@ -2155,6 +2377,7 @@ static BOOL _allowsAutomaticWindowTabbing; - (void) orderWindow: (NSWindowOrderingMode) place relativeTo: (NSInteger) relativeTo { @@ -1746,7 +1905,7 @@ index d545120a..db2c12d9 100644 // The move notifications are sent under unknown conditions around // orderFront: in the Apple AppKit, we do them all the time here until it's // figured out. I suspect it is a side effect of off-screen windows being at -@@ -2208,6 +2394,8 @@ static BOOL _allowsAutomaticWindowTabbing; +@@ -2208,6 +2431,8 @@ static BOOL _allowsAutomaticWindowTabbing; break; } @@ -1755,7 +1914,7 @@ index d545120a..db2c12d9 100644 [self postNotificationName: NSWindowDidMoveNotification]; } -@@ -2257,6 +2445,12 @@ static BOOL _allowsAutomaticWindowTabbing; +@@ -2257,6 +2482,12 @@ static BOOL _allowsAutomaticWindowTabbing; } - (void) sendEvent: (NSEvent *) event { @@ -1768,7 +1927,7 @@ index d545120a..db2c12d9 100644 // Some events can cause our window to be destroyed // So make sure self lives at least through this current run loop... [[self retain] autorelease]; -@@ -2300,6 +2494,10 @@ static BOOL _allowsAutomaticWindowTabbing; +@@ -2300,6 +2531,10 @@ static BOOL _allowsAutomaticWindowTabbing; case NSLeftMouseDown: { NSView *view = [_backgroundView hitTest: [event locationInWindow]]; @@ -1779,7 +1938,7 @@ index d545120a..db2c12d9 100644 if ([view acceptsFirstResponder]) { if ([view needsPanelToBecomeKey]) { -@@ -2307,6 +2505,27 @@ static BOOL _allowsAutomaticWindowTabbing; +@@ -2307,6 +2542,27 @@ static BOOL _allowsAutomaticWindowTabbing; } } @@ -1807,7 +1966,7 @@ index d545120a..db2c12d9 100644 // Event goes to view, not first responder. [view mouseDown: event]; _mouseDownLocationInWindow = [event locationInWindow]; -@@ -2330,6 +2549,31 @@ static BOOL _allowsAutomaticWindowTabbing; +@@ -2330,6 +2586,31 @@ static BOOL _allowsAutomaticWindowTabbing; _mouseDownLocationInWindow = NSMakePoint(NAN, NAN); break; @@ -1839,7 +1998,7 @@ index d545120a..db2c12d9 100644 case NSMouseMoved: { NSView *hit = [_backgroundView hitTest: [event locationInWindow]]; -@@ -2536,6 +2780,7 @@ static BOOL _allowsAutomaticWindowTabbing; +@@ -2536,6 +2817,7 @@ static BOOL _allowsAutomaticWindowTabbing; } else { [[self platformWindow] hideWindow]; } @@ -1847,7 +2006,21 @@ index d545120a..db2c12d9 100644 } - (void) orderFront: (id) sender { -@@ -3260,8 +3505,7 @@ static BOOL _allowsAutomaticWindowTabbing; +@@ -3064,6 +3346,13 @@ static BOOL _allowsAutomaticWindowTabbing; + if (didSize) { + // Don't redraw everything unless we really have to. + [_backgroundView setFrameSize: _frame.size]; ++ if (getenv("DARLING_UI_GEOMETRY_TRACE")) { ++ DarlingGeometryTrace("UI_GEOMETRY native_window pid=%d window=%p contentView=%p frame=%.0fx%.0f\n", ++ getpid(), self, [self contentView], ++ _frame.size.width, _frame.size.height); ++ unsigned remaining = 128; ++ traceGeometryTree(_backgroundView, 0, &remaining); ++ } + [_backgroundView setNeedsDisplay: YES]; + [self resetCursorRects]; + [self saveFrameUsingName: _autosaveFrameName]; +@@ -3260,8 +3549,7 @@ static BOOL _allowsAutomaticWindowTabbing; mouseWasInside && !mouseIsInside) { [exited addObject: area]; } @@ -1857,7 +2030,7 @@ index d545120a..db2c12d9 100644 [moved addObject: area]; } if (options & NSTrackingCursorUpdate && !mouseWasInside && -@@ -3459,7 +3703,7 @@ static BOOL _allowsAutomaticWindowTabbing; +@@ -3459,7 +3747,7 @@ static BOOL _allowsAutomaticWindowTabbing; } - (CGSubWindow *) _createSubWindowWithFrame: (CGRect) frame { @@ -2957,7 +3130,7 @@ index bfe3839a..2521ffd5 100644 - (void) frameChanged; - (void) setLastKnownCursorPosition: (CGPoint) point; diff --git a/AppKit/X11.backend/X11Window.m b/AppKit/X11.backend/X11Window.m -index f4925b66..8553a8b4 100644 +index f4925b66..67b084a0 100644 --- a/AppKit/X11.backend/X11Window.m +++ b/AppKit/X11.backend/X11Window.m @@ -38,6 +38,34 @@ @@ -3114,7 +3287,7 @@ index f4925b66..8553a8b4 100644 [_context release]; _context = nil; -@@ -421,6 +499,379 @@ static NSData *makeWindowIcon() { +@@ -421,6 +499,388 @@ static NSData *makeWindowIcon() { } } @@ -3280,9 +3453,18 @@ index f4925b66..8553a8b4 100644 + closedir(dir); + } + -+ /* Keep the software compositor owner across its other window updates. -+ * Otherwise a temporary size mismatch selects stale browser readback. */ -+ if (_remoteFrameSoftwarePublisher && _remoteFrameMapping && !bestSoftware) { ++ /* Keep a software owner across popup updates while the presented size ++ * still belongs to this window. After a real resize, retaining that owner ++ * can strand us on its popup slot even though a matching readback frame ++ * is ready from another publisher. Let normal candidate selection make ++ * that handoff when the old presentation no longer matches a target. */ ++ BOOL presentationMatchesTarget = NO; ++ for (unsigned i = 0; i < frameTargetCount; ++i) ++ if (_remoteFrameWidth == (unsigned)frameTargets[i].width && ++ _remoteFrameHeight == (unsigned)frameTargets[i].height) ++ presentationMatchesTarget = YES; ++ if (_remoteFrameSoftwarePublisher && _remoteFrameMapping && !bestSoftware && ++ presentationMatchesTarget) { + const char *pidText = strrchr(_remoteFrameMappingPath, '-'); + int publisher = pidText ? atoi(pidText + 1) : 0; + if (publisher > 0 && (kill(publisher, 0) == 0 || errno != ESRCH)) { @@ -3494,7 +3676,7 @@ index f4925b66..8553a8b4 100644 - (Window) windowHandle { return _window; } -@@ -478,6 +929,15 @@ static NSData *makeWindowIcon() { +@@ -478,6 +938,15 @@ static NSData *makeWindowIcon() { frame.size.width, frame.size.height); [self invalidateContextWithNewSize: frame.size]; _frame = frame; @@ -3510,7 +3692,7 @@ index f4925b66..8553a8b4 100644 } - (void) setHasShadow: (BOOL) value { -@@ -559,7 +1019,29 @@ static NSData *makeWindowIcon() { +@@ -559,7 +1028,29 @@ static NSData *makeWindowIcon() { XRaiseWindow(_display, _window); } @@ -3540,7 +3722,7 @@ index f4925b66..8553a8b4 100644 Atom wmState = XInternAtom(_display, "_NET_WM_STATE", FALSE); Atom hiddenState = XInternAtom(_display, "_NET_WM_STATE_HIDDEN", FALSE); -@@ -615,6 +1097,12 @@ static NSData *makeWindowIcon() { +@@ -615,6 +1106,12 @@ static NSData *makeWindowIcon() { } - (void) openGLFlushBuffer { @@ -3553,7 +3735,7 @@ index f4925b66..8553a8b4 100644 CGLError error; CGLContextObj prevContext = CGLGetCurrentContext(); -@@ -666,6 +1154,7 @@ static int ignoreBadWindow(Display *display, XErrorEvent *errorEvent) { +@@ -666,6 +1163,7 @@ static int ignoreBadWindow(Display *display, XErrorEvent *errorEvent) { } - (void) frameChanged { @@ -4587,7 +4769,7 @@ index e1112f40..3a4fa2d6 100644 COREGRAPHICS_EXPORT CGImageRef CGWindowListCreateImageFromArray(CGRect screenBounds, CFArrayRef windowArray, CGWindowImageOption imageOption); diff --git a/CoreText/CMakeLists.txt b/CoreText/CMakeLists.txt -index 1ec11620..08a238ca 100644 +index 1ec11620..af21f5b3 100644 --- a/CoreText/CMakeLists.txt +++ b/CoreText/CMakeLists.txt @@ -28,7 +28,6 @@ set(CoreText_sources @@ -4598,6 +4780,12 @@ index 1ec11620..08a238ca 100644 ) set_source_files_properties(${CoreText_sources} LANGUAGE C) +@@ -58,4 +57,5 @@ add_framework(CoreText + Onyx2D + # native + FreeType ++ fontconfig + ) diff --git a/CoreText/CTFont.m b/CoreText/CTFont.m index 94ddabf9..fa018d3d 100644 --- a/CoreText/CTFont.m @@ -5236,10 +5424,19 @@ index 94ddabf9..fa018d3d 100644 CFTypeID CTFontGetTypeID(void) diff --git a/CoreText/CTFontDescriptor.m b/CoreText/CTFontDescriptor.m -index 8866ce59..eb8a101c 100644 +index 8866ce59..62dcf226 100644 --- a/CoreText/CTFontDescriptor.m +++ b/CoreText/CTFontDescriptor.m -@@ -24,9 +24,23 @@ const CFStringRef kCTFontEnabledAttribute = CFSTR("NSCTFontEnabledAttribute"); +@@ -1,6 +1,7 @@ + #import + +-#include ++#import ++#import + + const CFStringRef kCTFontURLAttribute = CFSTR("NSCTFontFileURLAttribute"); + const CFStringRef kCTFontNameAttribute = CFSTR("NSFontNameAttribute"); +@@ -24,9 +25,103 @@ const CFStringRef kCTFontEnabledAttribute = CFSTR("NSCTFontEnabledAttribute"); const CFStringRef kCTFontFormatAttribute = CFSTR("NSCTFontFormatAttribute"); const CFStringRef kCTFontRegistrationScopeAttribute = CFSTR("NSCTFontRegistrationScopeAttribute"); const CFStringRef kCTFontPriorityAttribute = CFSTR("NSCTFontPriorityAttribute"); @@ -5264,17 +5461,99 @@ index 8866ce59..eb8a101c 100644 + if (value) + CFRetain(value); + return value; ++} ++ ++/* Enumerate actual installed faces, rather than returning the query itself. ++ * FcFontList does not substitute an unrelated font for a missing family. */ ++CFArrayRef CTFontDescriptorCreateMatchingFontDescriptors(CTFontDescriptorRef descriptor, ++ CFSetRef mandatoryAttributes) ++{ ++ if (!descriptor) ++ return NULL; ++ NSDictionary *query = (NSDictionary *)descriptor; ++ FcPattern *pattern = FcPatternCreate(); ++ const CFStringRef keys[] = { kCTFontFamilyNameAttribute, kCTFontNameAttribute, ++ kCTFontStyleNameAttribute, kCTFontDisplayNameAttribute }; ++ const char *properties[] = { FC_FAMILY, FC_POSTSCRIPT_NAME, FC_STYLE, FC_FULLNAME }; ++ for (unsigned i = 0; i < sizeof(keys) / sizeof(keys[0]); ++i) { ++ id value = [query objectForKey:(id)keys[i]]; ++ if ([value isKindOfClass:[NSString class]]) ++ FcPatternAddString(pattern, properties[i], (const FcChar8 *)[value UTF8String]); ++ } ++ FcObjectSet *objects = FcObjectSetBuild(FC_FAMILY, FC_POSTSCRIPT_NAME, ++ FC_STYLE, FC_FULLNAME, FC_FILE, NULL); ++ FcFontSet *fonts = FcFontList(O2FontSharedFontConfig(), pattern, objects); ++ CFMutableArrayRef matchesArray = CFArrayCreateMutable(NULL, 0, &kCFTypeArrayCallBacks); ++ for (int i = 0; fonts && i < fonts->nfont; ++i) { ++ NSMutableDictionary *attributes = [NSMutableDictionary dictionary]; ++ for (unsigned j = 0; j < sizeof(keys) / sizeof(keys[0]); ++j) { ++ FcChar8 *text; ++ if (FcPatternGetString(fonts->fonts[i], properties[j], 0, &text) == FcResultMatch) { ++ NSString *value = [NSString stringWithUTF8String:(const char *)text]; ++ if (value) [attributes setObject:value forKey:(id)keys[j]]; ++ } ++ } ++ FcChar8 *file; ++ if (FcPatternGetString(fonts->fonts[i], FC_FILE, 0, &file) == FcResultMatch) { ++ NSURL *url = [NSURL fileURLWithPath:[NSString stringWithUTF8String:(const char *)file]]; ++ if (url) [attributes setObject:url forKey:(id)kCTFontURLAttribute]; ++ } ++ /* Point size is a request, not an installed-face property. */ ++ id size = [query objectForKey:(id)kCTFontSizeAttribute]; ++ if (size) [attributes setObject:size forKey:(id)kCTFontSizeAttribute]; ++ BOOL matches = YES; ++ for (id key in (NSSet *)mandatoryAttributes) { ++ id expected = [query objectForKey:key]; ++ if (!expected || ![expected isEqual:[attributes objectForKey:key]]) { ++ matches = NO; ++ break; ++ } ++ } ++ if (matches) { ++ CTFontDescriptorRef match = CTFontDescriptorCreateWithAttributes((CFDictionaryRef)attributes); ++ CFArrayAppendValue(matchesArray, match); ++ CFRelease(match); ++ } ++ } ++ if (fonts) FcFontSetDestroy(fonts); ++ FcObjectSetDestroy(objects); ++ FcPatternDestroy(pattern); ++ return matchesArray; ++} ++ ++CTFontDescriptorRef CTFontDescriptorCreateMatchingFontDescriptor(CTFontDescriptorRef descriptor, ++ CFSetRef mandatoryAttributes) ++{ ++ CFArrayRef matches = CTFontDescriptorCreateMatchingFontDescriptors(descriptor, mandatoryAttributes); ++ CTFontDescriptorRef result = NULL; ++ if (matches) { ++ if (CFArrayGetCount(matches)) ++ result = (CTFontDescriptorRef)CFRetain(CFArrayGetValueAtIndex(matches, 0)); ++ CFRelease(matches); ++ } ++ return result; ++} ++ ++CFTypeRef CTFontDescriptorCopyLocalizedAttribute(CTFontDescriptorRef descriptor, ++ CFStringRef attribute, CFStringRef *language) ++{ ++ /* Fall back to the canonical name when localized font metadata is absent. ++ * A NULL language output is valid (Chromium uses it for family names). */ ++ if (language) *language = NULL; ++ return CTFontDescriptorCopyAttribute(descriptor, attribute); } diff --git a/CoreText/CTFontManager.m b/CoreText/CTFontManager.m -index 943e1b78..5c295cbf 100644 +index 943e1b78..049665fb 100644 --- a/CoreText/CTFontManager.m +++ b/CoreText/CTFontManager.m -@@ -1,17 +1,24 @@ +@@ -1,23 +1,43 @@ #import +#import +#import +#import +#import ++#import ++#import const CFStringRef kCTFontManagerRegisteredFontsChangedNotification = CFSTR("CTFontManagerFontChangedNotification"); @@ -5298,7 +5577,26 @@ index 943e1b78..5c295cbf 100644 } CFArrayRef CTFontManagerCopyAvailableFontFamilyNames(void) -@@ -25,3 +32,118 @@ bool CTFontManagerRegisterFontsForURL(CFURLRef fontURL, CTFontManagerScope scope + { +- printf("STUB %s\n", __PRETTY_FUNCTION__); +- return nil; ++ CFDictionaryRef empty = CFDictionaryCreate(NULL, NULL, NULL, 0, ++ &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks); ++ CTFontDescriptorRef query = CTFontDescriptorCreateWithAttributes(empty); ++ CFArrayRef descriptors = CTFontDescriptorCreateMatchingFontDescriptors(query, NULL); ++ NSMutableSet *families = [NSMutableSet set]; ++ for (NSDictionary *descriptor in (NSArray *)descriptors) { ++ id family = [descriptor objectForKey:(id)kCTFontFamilyNameAttribute]; ++ if (family) [families addObject:family]; ++ } ++ CFRelease(query); ++ CFRelease(empty); ++ if (descriptors) CFRelease(descriptors); ++ return (CFArrayRef)[[[families allObjects] sortedArrayUsingSelector:@selector(compare:)] copy]; + } + + bool CTFontManagerRegisterFontsForURL(CFURLRef fontURL, CTFontManagerScope scope, CFErrorRef * error) +@@ -25,3 +45,118 @@ bool CTFontManagerRegisterFontsForURL(CFURLRef fontURL, CTFontManagerScope scope printf("STUB %s\n", __PRETTY_FUNCTION__); return false; } @@ -5575,9 +5873,15 @@ index 33dc18c6..78887dd8 100644 @end diff --git a/CoreText/include/CoreText/CTFontDescriptor.h b/CoreText/include/CoreText/CTFontDescriptor.h -index b2688960..61dfef1a 100644 +index b2688960..fee68509 100644 --- a/CoreText/include/CoreText/CTFontDescriptor.h +++ b/CoreText/include/CoreText/CTFontDescriptor.h +@@ -1,4 +1,4 @@ +-#import ++#import + #import + #import + @@ -24,6 +24,19 @@ CORETEXT_EXPORT const CFStringRef kCTFontEnabledAttribute; CORETEXT_EXPORT const CFStringRef kCTFontFormatAttribute; CORETEXT_EXPORT const CFStringRef kCTFontRegistrationScopeAttribute; @@ -5598,11 +5902,15 @@ index b2688960..61dfef1a 100644 CF_IMPLICIT_BRIDGING_ENABLED -@@ -39,5 +52,6 @@ typedef enum CTFontOrientation : uint32_t { +@@ -39,5 +52,10 @@ typedef enum CTFontOrientation : uint32_t { } CTFontOrientation; CORETEXT_EXPORT CFTypeRef CTFontDescriptorCopyAttribute(CTFontDescriptorRef descriptor, CFStringRef attribute); +CORETEXT_EXPORT CTFontDescriptorRef CTFontDescriptorCreateWithAttributes(CFDictionaryRef attributes); ++ ++CORETEXT_EXPORT CFArrayRef CTFontDescriptorCreateMatchingFontDescriptors(CTFontDescriptorRef descriptor, CFSetRef mandatoryAttributes); ++CORETEXT_EXPORT CTFontDescriptorRef CTFontDescriptorCreateMatchingFontDescriptor(CTFontDescriptorRef descriptor, CFSetRef mandatoryAttributes); ++CORETEXT_EXPORT CFTypeRef CTFontDescriptorCopyLocalizedAttribute(CTFontDescriptorRef descriptor, CFStringRef attribute, CFStringRef *language); CF_IMPLICIT_BRIDGING_DISABLED diff --git a/CoreText/include/CoreText/KTFont.h b/CoreText/include/CoreText/KTFont.h @@ -5957,10 +6265,10 @@ index 039d8c94..b518c961 100644 #import diff --git a/QuartzCore/CALayer.m b/QuartzCore/CALayer.m -index 5dcac115..7cc63b0b 100644 +index 5dcac115..5de4f03f 100644 --- a/QuartzCore/CALayer.m +++ b/QuartzCore/CALayer.m -@@ -1,8 +1,234 @@ +@@ -1,8 +1,243 @@ #import +#import +#import @@ -6032,6 +6340,15 @@ index 5dcac115..7cc63b0b 100644 + } + size_t width = IOSurfaceGetWidth(surface); + size_t height = IOSurfaceGetHeight(surface); ++ if (getenv("DARLING_UI_GEOMETRY_TRACE")) { ++ static __thread size_t previousWidth, previousHeight; ++ if (width != previousWidth || height != previousHeight) { ++ fprintf(stderr, "UI_GEOMETRY surface pid=%d pixels=%zux%zu route=%s\n", ++ getpid(), width, height, readback ? "gpu-readback" : "software"); ++ previousWidth = width; ++ previousHeight = height; ++ } ++ } + size_t rowBytes = IOSurfaceGetBytesPerRow(surface); + size_t dataSize = rowBytes * height; + void *base = IOSurfaceGetBaseAddress(surface); @@ -6195,7 +6512,7 @@ index 5dcac115..7cc63b0b 100644 NSString *const kCAFilterLinear = @"linear"; NSString *const kCAFilterNearest = @"nearest"; -@@ -61,13 +287,22 @@ NSString *const kCAContentsFormatGray8Uint = @"Gray8"; +@@ -61,13 +296,22 @@ NSString *const kCAContentsFormatGray8Uint = @"Gray8"; } - (void) setSublayers: (NSArray *) sublayers { @@ -6219,7 +6536,19 @@ index 5dcac115..7cc63b0b 100644 } - (id) delegate { -@@ -171,14 +406,73 @@ NSString *const kCAContentsFormatGray8Uint = @"Gray8"; +@@ -117,6 +361,11 @@ NSString *const kCAContentsFormatGray8Uint = @"Gray8"; + [self addAnimation: action forKey: @"bounds"]; + } + ++ if (!CGRectEqualToRect(_bounds, value) && getenv("DARLING_UI_GEOMETRY_TRACE") && ++ (value.size.width >= 256 || value.size.height >= 256)) ++ fprintf(stderr, "UI_GEOMETRY layer pid=%d class=%s bounds=%.0fx%.0f transform=%.3fx%.3f\n", ++ getpid(), class_getName([self class]), value.size.width, value.size.height, ++ _transform.m11, _transform.m22); + _bounds = value; + } + +@@ -171,14 +420,73 @@ NSString *const kCAContentsFormatGray8Uint = @"Gray8"; _opaque = value; } @@ -6293,7 +6622,19 @@ index 5dcac115..7cc63b0b 100644 } - (CATransform3D) transform { -@@ -236,6 +530,10 @@ NSString *const kCAContentsFormatGray8Uint = @"Gray8"; +@@ -186,6 +494,11 @@ NSString *const kCAContentsFormatGray8Uint = @"Gray8"; + } + + - (void) setTransform: (CATransform3D) value { ++ if (getenv("DARLING_UI_GEOMETRY_TRACE") && ++ (_bounds.size.width >= 256 || _bounds.size.height >= 256)) ++ fprintf(stderr, "UI_GEOMETRY transform pid=%d class=%s bounds=%.0fx%.0f scale=%.3fx%.3f\n", ++ getpid(), class_getName([self class]), _bounds.size.width, _bounds.size.height, ++ value.m11, value.m22); + _transform = value; + } + +@@ -236,6 +549,10 @@ NSString *const kCAContentsFormatGray8Uint = @"Gray8"; } - (void) dealloc { @@ -6304,7 +6645,7 @@ index 5dcac115..7cc63b0b 100644 [_sublayers release]; [_animations release]; [_minificationFilter release]; -@@ -258,6 +556,33 @@ NSString *const kCAContentsFormatGray8Uint = @"Gray8"; +@@ -258,6 +575,33 @@ NSString *const kCAContentsFormatGray8Uint = @"Gray8"; [self setSublayers: [_sublayers arrayByAddingObject: layer]]; } @@ -6338,7 +6679,7 @@ index 5dcac115..7cc63b0b 100644 - (void) replaceSublayer: (CALayer *) layer with: (CALayer *) other { NSMutableArray *layers = [_sublayers mutableCopy]; NSUInteger index = [_sublayers indexOfObjectIdenticalTo: layer]; -@@ -298,9 +623,14 @@ NSString *const kCAContentsFormatGray8Uint = @"Gray8"; +@@ -298,9 +642,14 @@ NSString *const kCAContentsFormatGray8Uint = @"Gray8"; } - (void) removeFromSuperlayer { @@ -6354,7 +6695,7 @@ index 5dcac115..7cc63b0b 100644 } - (void) setNeedsDisplay { -@@ -319,6 +649,19 @@ NSString *const kCAContentsFormatGray8Uint = @"Gray8"; +@@ -319,6 +668,19 @@ NSString *const kCAContentsFormatGray8Uint = @"Gray8"; [_context startTimerIfNeeded]; } @@ -6374,7 +6715,7 @@ index 5dcac115..7cc63b0b 100644 - (CAAnimation *) animationForKey: (NSString *) key { return [_animations objectForKey: key]; } -@@ -364,4 +707,119 @@ NSString *const kCAContentsFormatGray8Uint = @"Gray8"; +@@ -364,4 +726,119 @@ NSString *const kCAContentsFormatGray8Uint = @"Gray8"; _textureId = value; } @@ -7881,3 +8222,34 @@ index ad5e4f9d..dbd997aa 100644 - -@end \ No newline at end of file +diff --git a/AppKit/NSGeometryTrace.h b/AppKit/NSGeometryTrace.h +new file mode 100644 +index 00000000..a51a9241 +--- /dev/null ++++ b/AppKit/NSGeometryTrace.h +@@ -0,0 +1,25 @@ ++/* Opt-in runtime geometry diagnostics; never log view text or client data. */ ++#ifndef DARLING_NS_GEOMETRY_TRACE_H ++#define DARLING_NS_GEOMETRY_TRACE_H ++#include ++#include ++#include ++ ++static inline void DarlingGeometryTrace(const char *format, ...) { ++ if (!getenv("DARLING_UI_GEOMETRY_TRACE")) ++ return; ++ va_list args, copy; ++ va_start(args, format); ++ va_copy(copy, args); ++ vfprintf(stderr, format, args); ++ va_end(args); ++ /* Clients may redirect stderr to a non-log descriptor. Keep a copy in ++ * the guest's temporary directory so their resize events remain visible. */ ++ FILE *file = fopen("/tmp/darling-ui-geometry.log", "a"); ++ if (file) { ++ vfprintf(file, format, copy); ++ fclose(file); ++ } ++ va_end(copy); ++} ++#endif diff --git a/patches/darling.patch b/patches/darling.patch index 9b1c133..f1017ae 100644 --- a/patches/darling.patch +++ b/patches/darling.patch @@ -1,5 +1,5 @@ diff --git a/src/CoreAudio/AudioToolbox/AUComponent.cpp b/src/CoreAudio/AudioToolbox/AUComponent.cpp -index d48eec1ef..c86959d55 100644 +index d48eec1ef..1deb304e9 100644 --- a/src/CoreAudio/AudioToolbox/AUComponent.cpp +++ b/src/CoreAudio/AudioToolbox/AUComponent.cpp @@ -22,13 +22,16 @@ along with Darling. If not, see . @@ -88,6 +88,38 @@ index d48eec1ef..c86959d55 100644 + return status; } + OSStatus +@@ -121,7 +148,14 @@ AudioUnitGetParameter(AudioUnit inUnit, + AudioUnitElement inElement, + AudioUnitParameterValue * outValue) + { +- return dispatchCall(inUnit, kAudioUnitGetParameterSelect, inID, inScope, inElement, outValue); ++ OSStatus status = dispatchCall(inUnit, kAudioUnitGetParameterSelect, inID, inScope, inElement, outValue); ++ { ++ static unsigned count = 0; ++ if (rclolAudioTraceEnabled() && (count++ < 200 || count % 500 == 0)) ++ rclolAudioTrace("UNIT_GET_PARAM unit=%p id=%u scope=%u elem=%u value=%g status=%d\n", (void*) inUnit, ++ (unsigned) inID, (unsigned) inScope, (unsigned) inElement, (outValue && !status) ? (double) *outValue : 0.0, (int) status); ++ } ++ return status; + } + + OSStatus +@@ -132,7 +166,14 @@ AudioUnitSetParameter(AudioUnit inUnit, + AudioUnitParameterValue inValue, + UInt32 inBufferOffsetInFrames) + { +- return dispatchCall(inUnit, kAudioUnitSetParameterSelect, inID, inScope, inElement, inValue, inBufferOffsetInFrames); ++ OSStatus status = dispatchCall(inUnit, kAudioUnitSetParameterSelect, inID, inScope, inElement, inValue, inBufferOffsetInFrames); ++ { ++ static unsigned count = 0; ++ if (rclolAudioTraceEnabled() && (count++ < 200 || count % 500 == 0)) ++ rclolAudioTrace("UNIT_SET_PARAM unit=%p id=%u scope=%u elem=%u value=%g offset=%u status=%d\n", (void*) inUnit, ++ (unsigned) inID, (unsigned) inScope, (unsigned) inElement, (double) inValue, (unsigned) inBufferOffsetInFrames, (int) status); ++ } ++ return status; + } + OSStatus diff --git a/src/CoreAudio/AudioToolbox/AUGraph.cpp b/src/CoreAudio/AudioToolbox/AUGraph.cpp index ca20b90df..e90b37163 100644 @@ -643,7 +675,7 @@ index ca20b90df..e90b37163 100644 + return status; +} diff --git a/src/CoreAudio/AudioToolbox/AudioComponentManager.mm b/src/CoreAudio/AudioToolbox/AudioComponentManager.mm -index 5fa33817d..4c1f25a33 100644 +index 5fa33817d..8588e69d8 100644 --- a/src/CoreAudio/AudioToolbox/AudioComponentManager.mm +++ b/src/CoreAudio/AudioToolbox/AudioComponentManager.mm @@ -1,3 +1,4 @@ @@ -677,7 +709,7 @@ index 5fa33817d..4c1f25a33 100644 RegisteredComponent& regd = it->second; + static unsigned traceCount=0; -+ if(getenv("RCLOL_AUDIO_TRACE") && traceCount++<16) ++ if(rclolAudioTraceEnabled() && traceCount++<16) + rclolAudioTrace("AUDIO_COMPONENT version=%u subtype=%08x user_override=%d\n", + regd.version,regd.desc.componentSubType, + regd.bundlePath.find("DarlingRiotAudio.component")!=std::string::npos); @@ -685,10 +717,14 @@ index 5fa33817d..4c1f25a33 100644 { // Load the bundle diff --git a/src/CoreAudio/AudioToolbox/AudioConverter.cpp b/src/CoreAudio/AudioToolbox/AudioConverter.cpp -index 202121cd9..c40ba22c2 100644 +index 202121cd9..5672fcba1 100644 --- a/src/CoreAudio/AudioToolbox/AudioConverter.cpp +++ b/src/CoreAudio/AudioToolbox/AudioConverter.cpp -@@ -22,13 +22,35 @@ along with Darling. If not, see . +@@ -19,16 +19,39 @@ along with Darling. If not, see . + + #include "AudioConverter.h" + #include "AudioConverterImpl.h" ++#include "../AudioTrace.h" #include #include #include @@ -702,7 +738,7 @@ index 202121cd9..c40ba22c2 100644 +static std::unordered_set liveConverters; + +static bool audioTrace() { -+ static const bool enabled = std::getenv("RCLOL_AUDIO_TRACE") != nullptr; ++ static const bool enabled = rclolAudioTraceEnabled(); + return enabled; +} + @@ -726,7 +762,7 @@ index 202121cd9..c40ba22c2 100644 { // FIXME return AudioConverterNew(inSourceFormat, inDestinationFormat, outAudioConverter); -@@ -47,6 +69,15 @@ OSStatus AudioConverterReset(AudioConverterRef inAudioConverter) +@@ -47,6 +70,15 @@ OSStatus AudioConverterReset(AudioConverterRef inAudioConverter) OSStatus AudioConverterDispose(AudioConverterRef inAudioConverter) { @@ -881,7 +917,7 @@ index 000000000..6e85e8230 + return noErr; +} diff --git a/src/CoreAudio/CoreAudio/AudioHardware.cpp b/src/CoreAudio/CoreAudio/AudioHardware.cpp -index 7243e6a62..fb48486c6 100644 +index 7243e6a62..647c8aff7 100644 --- a/src/CoreAudio/CoreAudio/AudioHardware.cpp +++ b/src/CoreAudio/CoreAudio/AudioHardware.cpp @@ -1,3 +1,4 @@ @@ -944,25 +980,59 @@ index 7243e6a62..fb48486c6 100644 void AudioObjectShow(AudioObjectID inObjectID) { AudioHardwareImpl* obj = GetObject(inObjectID); -@@ -68,6 +101,9 @@ void AudioObjectShow(AudioObjectID inObjectID) +@@ -68,45 +101,94 @@ void AudioObjectShow(AudioObjectID inObjectID) Boolean AudioObjectHasProperty(AudioObjectID inObjectID, const AudioObjectPropertyAddress* inAddress) { + if((inObjectID&0xffff0000u)==0x44410000u) { -+ UInt32 size=0;return GetStreamPropertyData(inObjectID,inAddress,&size,nullptr)==noErr; ++ UInt32 size=0;bool has=GetStreamPropertyData(inObjectID,inAddress,&size,nullptr)==noErr; ++ rclolAudioTrace("HAS stream=%08x selector=%08x scope=%08x has=%d\n",inObjectID,inAddress->mSelector,inAddress->mScope,has); ++ return has; + } AudioHardwareImpl* obj = GetObject(inObjectID); - if (!obj) +- if (!obj) ++ if (!obj) { ++ rclolAudioTrace("HAS object=%u selector=%08x BADOBJECT\n",inObjectID,inAddress->mSelector); return 0; -@@ -89,24 +125,50 @@ OSStatus AudioObjectGetPropertyDataSize(AudioObjectID inObjectID, ++ } + +- return obj->hasProperty(inAddress); ++ bool has=obj->hasProperty(inAddress); ++ rclolAudioTrace("HAS object=%u selector=%08x scope=%08x elem=%u has=%d\n",inObjectID,inAddress->mSelector,inAddress->mScope,inAddress->mElement,has); ++ return has; + } + + OSStatus AudioObjectIsPropertySettable(AudioObjectID inObjectID, + const AudioObjectPropertyAddress* inAddress, Boolean* outIsSettable) + { + AudioHardwareImpl* obj = GetObject(inObjectID); +- if (!obj) ++ if (!obj) { ++ rclolAudioTrace("SETTABLE object=%08x selector=%08x BADOBJECT\n",inObjectID,inAddress->mSelector); + return kAudioHardwareBadObjectError; ++ } + +- return obj->isPropertySettable(inAddress, outIsSettable); ++ OSStatus status=obj->isPropertySettable(inAddress, outIsSettable); ++ rclolAudioTrace("SETTABLE object=%u selector=%08x scope=%08x elem=%u status=%d settable=%d\n",inObjectID,inAddress->mSelector,inAddress->mScope,inAddress->mElement,status,status?0:*outIsSettable); ++ return status; + } + + OSStatus AudioObjectGetPropertyDataSize(AudioObjectID inObjectID, const AudioObjectPropertyAddress* inAddress, UInt32 inQualifierDataSize, const void* inQualifierData, UInt32* outDataSize) { -+ if((inObjectID&0xffff0000u)==0x44410000u) -+ return GetStreamPropertyData(inObjectID,inAddress,outDataSize,nullptr); ++ if((inObjectID&0xffff0000u)==0x44410000u) { ++ OSStatus status=GetStreamPropertyData(inObjectID,inAddress,outDataSize,nullptr); ++ rclolAudioTrace("SIZE stream=%08x selector=%08x scope=%08x status=%d\n",inObjectID,inAddress->mSelector,inAddress->mScope,status); ++ return status; ++ } AudioHardwareImpl* obj = GetObject(inObjectID); - if (!obj) +- if (!obj) ++ if (!obj) { ++ rclolAudioTrace("SIZE object=%08x selector=%08x BADOBJECT\n",inObjectID,inAddress->mSelector); return kAudioHardwareBadObjectError; ++ } - return obj->getPropertyDataSize(inAddress, inQualifierDataSize, - inQualifierData, outDataSize); @@ -992,36 +1062,49 @@ index 7243e6a62..fb48486c6 100644 + *static_cast(value->mOutputData)=found; + value->mOutputDataSize=sizeof(found);*ioDataSize=sizeof(*value);return noErr; + } -+ if((inObjectID&0xffff0000u)==0x44410000u) -+ return GetStreamPropertyData(inObjectID,inAddress,ioDataSize,outData); ++ if((inObjectID&0xffff0000u)==0x44410000u) { ++ OSStatus status=GetStreamPropertyData(inObjectID,inAddress,ioDataSize,outData); ++ rclolAudioTrace("GET stream=%08x selector=%08x scope=%08x status=%d\n",inObjectID,inAddress->mSelector,inAddress->mScope,status); ++ return status; ++ } AudioHardwareImpl* obj = GetObject(inObjectID); - if (!obj) +- if (!obj) ++ if (!obj) { ++ rclolAudioTrace("GET object=%08x selector=%08x BADOBJECT\n",inObjectID,inAddress->mSelector); return kAudioHardwareBadObjectError; ++ } - return obj->getPropertyData(inAddress, inQualifierDataSize, inQualifierData, - ioDataSize, outData); + static unsigned traceCount=0; -+ if(getenv("RCLOL_AUDIO_TRACE") && traceCount++<100) ++ if(rclolAudioTraceEnabled() && traceCount++<100) + fprintf(stderr,"AUDIO_HARDWARE_GET object=%u property=%08x scope=%08x\n",inObjectID,inAddress->mSelector,inAddress->mScope); + OSStatus status=obj->getPropertyData(inAddress,inQualifierDataSize,inQualifierData,ioDataSize,outData); -+ rclolAudioTrace("GET object=%u selector=%08x scope=%08x status=%d size=%u\n",inObjectID,inAddress->mSelector,inAddress->mScope,status,status?0:*ioDataSize); ++ rclolAudioTrace("GET object=%u selector=%08x scope=%08x elem=%u status=%d size=%u\n",inObjectID,inAddress->mSelector,inAddress->mScope,inAddress->mElement,status,status?0:*ioDataSize); + return status; } OSStatus AudioObjectSetPropertyData(AudioObjectID inObjectID, -@@ -117,8 +179,9 @@ OSStatus AudioObjectSetPropertyData(AudioObjectID inObjectID, - if (!obj) +@@ -114,11 +196,15 @@ OSStatus AudioObjectSetPropertyData(AudioObjectID inObjectID, + const void* inQualifierData, UInt32 inDataSize, const void* inData) + { + AudioHardwareImpl* obj = GetObject(inObjectID); +- if (!obj) ++ if (!obj) { ++ rclolAudioTrace("SET object=%08x selector=%08x scope=%08x elem=%u BADOBJECT\n",inObjectID,inAddress->mSelector,inAddress->mScope,inAddress->mElement); return kAudioHardwareBadObjectError; ++ } - return obj->setPropertyData(inAddress, inQualifierDataSize, inQualifierData, - inDataSize, inData); + OSStatus status=obj->setPropertyData(inAddress,inQualifierDataSize,inQualifierData,inDataSize,inData); -+ rclolAudioTrace("SET object=%u selector=%08x scope=%08x status=%d\n",inObjectID,inAddress->mSelector,inAddress->mScope,status); ++ rclolAudioTrace("SET object=%u selector=%08x scope=%08x elem=%u size=%u value=%g status=%d\n",inObjectID,inAddress->mSelector,inAddress->mScope,inAddress->mElement,inDataSize, ++ (inData && inDataSize==sizeof(Float32))?(double)*(const Float32*)inData:0.0,status); + return status; } OSStatus AudioObjectAddPropertyListener(AudioObjectID inObjectID, -@@ -129,7 +192,9 @@ OSStatus AudioObjectAddPropertyListener(AudioObjectID inObjectID, +@@ -129,7 +215,9 @@ OSStatus AudioObjectAddPropertyListener(AudioObjectID inObjectID, if (!obj) return kAudioHardwareBadObjectError; @@ -1455,19 +1538,20 @@ index 85315086f..c43382686 100644 pa_sample_spec AudioHardwareImplPA::paSampleSpecForASBD(const AudioStreamBasicDescription& asbd, bool* convertSignedUnsigned) diff --git a/src/CoreAudio/CoreAudio/pulse/AudioHardwareImplPA.h b/src/CoreAudio/CoreAudio/pulse/AudioHardwareImplPA.h -index 8421491cd..3fe1c4e2c 100644 +index 8421491cd..03a530bfd 100644 --- a/src/CoreAudio/CoreAudio/pulse/AudioHardwareImplPA.h +++ b/src/CoreAudio/CoreAudio/pulse/AudioHardwareImplPA.h -@@ -38,6 +38,8 @@ public: +@@ -38,6 +38,9 @@ public: const void* inQualifierData, UInt32 inDataSize, const void* inData) override; void getPAContext(void (^cb)(pa_context*)); + void syncPA(void (^block)(void)); ++ dispatch_queue_t paQueue() const { return m_loop ? m_loop->queue() : nullptr; } + OSStatus stop(AudioDeviceIOProcID inProcID) override; static pa_sample_spec paSampleSpecForASBD(const AudioStreamBasicDescription& asbd, bool* convertSignedUnsigned = nullptr); protected: AudioHardwareStream* createStream(AudioDeviceIOProc callback, void* clientData) override; -@@ -49,4 +51,3 @@ private: +@@ -49,4 +52,3 @@ private: }; #endif @@ -1586,14 +1670,54 @@ index d6825f1d5..739b70011 100644 if (status != noErr) diff --git a/src/CoreAudio/CoreAudio/pulse/AudioHardwareStreamPAOutput.cpp b/src/CoreAudio/CoreAudio/pulse/AudioHardwareStreamPAOutput.cpp -index 278630f02..00db02f9c 100644 +index 278630f02..54b463c2f 100644 --- a/src/CoreAudio/CoreAudio/pulse/AudioHardwareStreamPAOutput.cpp +++ b/src/CoreAudio/CoreAudio/pulse/AudioHardwareStreamPAOutput.cpp -@@ -21,16 +21,17 @@ along with Darling. If not, see . +@@ -21,71 +21,318 @@ along with Darling. If not, see . #include "AudioHardwareImplPA.h" #include #include +#include ++#include "../../AudioTrace.h" ++ ++// Real CoreAudio calls an output IOProc once per IO cycle: evenly spaced at the buffer ++// period, with inOutputTime.mHostTime advancing by one buffer duration per call. Clients ++// such as Chromium's AUHALStream rely on that (they read the previous renderer buffer and ++// request the next one on every call), so PulseAudio's bursty write requests must not ++// drive the IOProc directly. Instead a dispatch timer on the PA queue ticks at the block ++// period and renders at most one block per tick (plus bounded catch-up after a late wakeup). ++ ++static const unsigned kPrefillBlocks = 8; // == tlength ++static const unsigned kMaxCatchUpBlocks = 2; // extra blocks after a late wakeup ++static const unsigned kServoWindowTicks = 32; ++ ++static bool rateLimited(unsigned& counter, unsigned head, unsigned every) { ++ if (!rclolAudioTraceEnabled()) return false; ++ unsigned n = counter++; ++ return n < head || n % every == 0; ++} ++static unsigned g_wcbEvents = 0, g_iopEvents = 0, g_tickEvents = 0, g_paceEvents = 0; ++static bool wcbShouldTrace() { return rateLimited(g_wcbEvents, 50, 1000); } ++static bool iopShouldTrace() { return rateLimited(g_iopEvents, 200, 500); } ++static bool tickShouldTrace() { return rateLimited(g_tickEvents, 200, 500); } ++static bool paceShouldTrace() { return rateLimited(g_paceEvents, 100, 100); } ++ ++static const mach_timebase_info_data_t& timebase() { ++ static mach_timebase_info_data_t tb = [] { ++ mach_timebase_info_data_t t = {0, 0}; ++ mach_timebase_info(&t); ++ if (!t.numer || !t.denom) t.numer = t.denom = 1; ++ return t; ++ }(); ++ return tb; ++} ++static uint64_t hostToNs(uint64_t host) { ++ return uint64_t((unsigned __int128) host * timebase().numer / timebase().denom); ++} ++static uint64_t nsToHost(uint64_t ns) { ++ return uint64_t((unsigned __int128) ns * timebase().denom / timebase().numer); ++} ++static uint64_t nowNs() { return hostToNs(mach_absolute_time()); } AudioHardwareStreamPAOutput::AudioHardwareStreamPAOutput(AudioHardwareImplPA* hw, AudioDeviceIOProc callback, void* clientData) : AudioHardwareStreamPA(hw, callback, clientData) @@ -1601,65 +1725,420 @@ index 278630f02..00db02f9c 100644 + initialize(); } - void AudioHardwareStreamPAOutput::paStreamWriteCB(pa_stream* s, size_t length, void* self) +-void AudioHardwareStreamPAOutput::paStreamWriteCB(pa_stream* s, size_t length, void* self) ++AudioHardwareStreamPAOutput::~AudioHardwareStreamPAOutput() { - AudioHardwareStreamPAOutput* This = static_cast(self); +- AudioHardwareStreamPAOutput* This = static_cast(self); - std::unique_lock l(This->m_stopMutex); ++ // Normally already done by stop(); the timer must never outlive this object. ++ static_cast(m_hw)->syncPA(^{ m_running = false; cancelTimer(); }); ++} - if (!This->m_running) +- if (!This->m_running) ++void AudioHardwareStreamPAOutput::stop() ++{ ++ // Runs on (or synchronously with) the PA queue, so no tick is in flight unless stop() ++ // was called from inside the IOProc, which renderBlock() handles via m_running. ++ // Cancellation guarantees the timer handler is never invoked again. ++ static_cast(m_hw)->syncPA(^{ m_running = false; cancelTimer(); }); ++ AudioHardwareStreamPA::stop(); ++} ++ ++void AudioHardwareStreamPAOutput::cancelTimer() ++{ ++ if (m_timer) { -@@ -55,17 +56,26 @@ void AudioHardwareStreamPAOutput::paStreamWriteCB(pa_stream* s, size_t length, v - - // std::cout << "AudioHardwareStreamPAOutput() length req=" << length << ", done=" << done << std::endl; - size_t rqsize = std::min(This->m_bufferSize, length-done); -- pa_stream_begin_write(This->m_stream, (void**) &abl->mBuffers[0].mData, &rqsize); -+ if(pa_stream_begin_write(This->m_stream,(void**)&abl->mBuffers[0].mData,&rqsize)<0 || !rqsize) break; - - abl->mBuffers[0].mDataByteSize = rqsize; -+ fake.mHostTime=mach_absolute_time(); -+ fake.mSampleTime=This->m_frames; -+ fake.mFlags=kAudioTimeStampHostTimeValid|kAudioTimeStampSampleTimeValid; - - // Call the client for more data - OSStatus status = This->m_callback(This->m_hw->id(), &fake, nullptr, nullptr, abl, &fake, This->m_clientData); -+ if (!This->m_running) { -+ // The client stopped/disconnected the stream during its IOProc. -+ pa_stream_cancel_write(s); -+ return; -+ } - if (status != noErr || !abl->mBuffers[0].mDataByteSize) - { - // std::cerr << "AudioDeviceIOProc returned " << status << ", corking...\n"; - - pa_stream_cork(This->m_stream, true, [](pa_stream*, int, void*) {}, nullptr); -+ pa_stream_cancel_write(This->m_stream); - break; - } - else -@@ -85,6 +95,7 @@ void AudioHardwareStreamPAOutput::paStreamWriteCB(pa_stream* s, size_t length, v - } - - done += abl->mBuffers[0].mDataByteSize; -+ if(This->m_hw->asbd().mBytesPerFrame) This->m_frames+=abl->mBuffers[0].mDataByteSize/This->m_hw->asbd().mBytesPerFrame; +- pa_stream_cork(This->m_stream, true, [](pa_stream*, int, void*) {}, nullptr); +- return; ++ dispatch_source_cancel(m_timer); ++ dispatch_release(m_timer); ++ m_timer = nullptr; ++ } ++} ++ ++uint64_t AudioHardwareStreamPAOutput::outputLatencyNs(size_t writable, bool* fromPA) ++{ ++ pa_usec_t usec = 0; ++ int negative = 0; ++ if (pa_stream_get_latency(m_stream, &usec, &negative) == 0) ++ { ++ *fromPA = true; ++ return negative ? 0 : uint64_t(usec) * 1000; } ++ // No timing info yet: at least account for what is queued in the PA buffer. ++ *fromPA = false; ++ const pa_buffer_attr* attr = pa_stream_get_buffer_attr(m_stream); ++ const pa_sample_spec* spec = pa_stream_get_sample_spec(m_stream); ++ if (!attr || !spec || writable == size_t(-1) || attr->tlength <= writable) ++ return 0; ++ return uint64_t(pa_bytes_to_usec(attr->tlength - writable, spec)) * 1000; ++} ++ ++AudioHardwareStreamPAOutput::RenderResult AudioHardwareStreamPAOutput::renderBlock(bool trace) ++{ ++ if (!m_running || m_corked || !m_stream || !m_blockBytes) ++ return RenderResult::Stopped; + +- // std::cout << "AudioHardwareStreamPAOutput::paStreamWriteCB()\n"; ++ const size_t writable = pa_stream_writable_size(m_stream); ++ if (writable == size_t(-1) || writable < m_blockBytes) ++ return RenderResult::NoSpace; + +- AudioTimeStamp fake = {0}; + AudioBufferList* abl = static_cast(alloca(sizeof(AudioBufferList) + sizeof(AudioBuffer))); ++ // Non-interleaved (planar) audio would have multiple buffers, but PA doesn't even support that AFAIK ++ abl->mNumberBuffers = 1; ++ abl->mBuffers[0].mNumberChannels = m_hw->asbd().mChannelsPerFrame; ++ ++ size_t rqsize = m_blockBytes; ++ if (pa_stream_begin_write(m_stream, (void**)&abl->mBuffers[0].mData, &rqsize) < 0 || !rqsize) ++ { ++ if (paceShouldTrace()) rclolAudioTrace("PA_OUT begin_write failed rqsize=%zu\n", rqsize); ++ return RenderResult::NoSpace; ++ } ++ if (rqsize < m_blockBytes) ++ { ++ // Never hand the client a partial block (real HALs use a fixed frame count). ++ if (paceShouldTrace()) rclolAudioTrace("PA_OUT granted rqsize=%zu < block=%zu: cancel\n", rqsize, m_blockBytes); ++ pa_stream_cancel_write(m_stream); ++ return RenderResult::NoSpace; ++ } ++ abl->mBuffers[0].mDataByteSize = m_blockBytes; ++ ++ bool latencyFromPA = false; ++ const uint64_t latencyNs = outputLatencyNs(writable, &latencyFromPA); ++ const double rate = m_hw->asbd().mSampleRate; ++ const double latencyFrames = rate > 0 ? double(latencyNs) * rate / 1e9 : 0; ++ const uint64_t nowHost = mach_absolute_time(); + +- size_t done = 0; ++ AudioTimeStamp now = {0}; ++ now.mHostTime = nowHost; ++ // Device position currently being played: the block about to be rendered starts ++ // latencyFrames after it, matching the host-time difference below. ++ now.mSampleTime = std::max(0.0, double(m_frames) - latencyFrames); ++ now.mRateScalar = 1.0; ++ now.mFlags = kAudioTimeStampHostTimeValid | kAudioTimeStampSampleTimeValid | kAudioTimeStampRateScalarValid; + +- while (done < length && This->m_running) ++ AudioTimeStamp output = now; ++ output.mHostTime = nowHost + nsToHost(latencyNs); ++ output.mSampleTime = double(m_frames); ++ ++ const double dHostMs = m_lastIOProcHost ? double(hostToNs(nowHost - m_lastIOProcHost)) / 1e6 : 0; ++ m_lastIOProcHost = nowHost; ++ ++ OSStatus status = m_callback(m_hw->id(), &now, nullptr, nullptr, abl, &output, m_clientData); ++ if (trace) ++ rclolAudioTrace("PA_OUT iop dHostMs=%.3f writable=%zu latencyMs=%.2f(%s) frames=%llu status=%d size=%u\n", ++ dHostMs, writable, double(latencyNs) / 1e6, latencyFromPA ? "pa" : "est", ++ (unsigned long long) m_frames, (int) status, (unsigned) abl->mBuffers[0].mDataByteSize); ++ ++ if (!m_running) + { +- // Non-interleaved (planar) audio would have multiple buffers, but PA doesn't even support that AFAIK +- abl->mNumberBuffers = 1; ++ // The client stopped/disconnected the stream during its IOProc. ++ pa_stream_cancel_write(m_stream); ++ return RenderResult::Stopped; ++ } ++ if (status != noErr || !abl->mBuffers[0].mDataByteSize) ++ { ++ if (paceShouldTrace()) rclolAudioTrace("PA_OUT IOProc failed/empty status=%d size=%u: CORK\n", (int) status, ++ (unsigned) abl->mBuffers[0].mDataByteSize); ++ pa_stream_cancel_write(m_stream); ++ pa_stream_cork(m_stream, true, [](pa_stream*, int, void*) {}, nullptr); ++ m_corked = true; ++ return RenderResult::Stopped; ++ } + +- abl->mBuffers[0].mNumberChannels = 2; +- // abl->mBuffers[0].mData = This->m_buffer; ++ if (m_convertSignedUnsigned) ++ transformSignedUnsigned(abl); + +- // std::cout << "AudioHardwareStreamPAOutput() length req=" << length << ", done=" << done << std::endl; +- size_t rqsize = std::min(This->m_bufferSize, length-done); +- pa_stream_begin_write(This->m_stream, (void**) &abl->mBuffers[0].mData, &rqsize); ++ // Always submit exactly one block so the sample clock stays block-aligned. ++ int rv = pa_stream_write(m_stream, abl->mBuffers[0].mData, m_blockBytes, nullptr, 0, PA_SEEK_RELATIVE); ++ if (rv != 0) ++ { ++ if (paceShouldTrace()) rclolAudioTrace("PA_OUT pa_stream_write rv=%d\n", rv); ++ return RenderResult::NoSpace; ++ } ++ m_frames += m_blockFrames; ++ return RenderResult::Written; ++} + +- abl->mBuffers[0].mDataByteSize = rqsize; ++uint64_t AudioHardwareStreamPAOutput::deadlineNs() const ++{ ++ return m_anchorNs + uint64_t(std::max(0.0, double(m_tickIndex) * m_periodNs - m_advanceNs)); ++} + +- // Call the client for more data +- OSStatus status = This->m_callback(This->m_hw->id(), &fake, nullptr, nullptr, abl, &fake, This->m_clientData); +- if (status != noErr || !abl->mBuffers[0].mDataByteSize) +- { +- // std::cerr << "AudioDeviceIOProc returned " << status << ", corking...\n"; ++void AudioHardwareStreamPAOutput::armTimer(uint64_t now) ++{ ++ if (!m_timer) ++ return; ++ const uint64_t deadline = deadlineNs(); ++ const int64_t delta = deadline > now ? int64_t(deadline - now) : 0; ++ dispatch_source_set_timer(m_timer, dispatch_time(DISPATCH_TIME_NOW, delta), DISPATCH_TIME_FOREVER, 100 * NSEC_PER_USEC); ++} + +- pa_stream_cork(This->m_stream, true, [](pa_stream*, int, void*) {}, nullptr); +- break; +- } +- else +- { +- if (This->m_convertSignedUnsigned) +- This->transformSignedUnsigned(abl); +- +- // std::cout << "AudioHardwareStreamPAOutput::paStreamWriteCB(): got " << abl->mBuffers[0].mDataByteSize << " bytes\n"; +- int rv = pa_stream_write(This->m_stream, abl->mBuffers[0].mData, abl->mBuffers[0].mDataByteSize, nullptr, 0, PA_SEEK_RELATIVE); +- if (rv != 0) +- { +- // std::cerr << "pa_stream_write() failed\n"; +- break; +- } +- //else +- // std::cout << "pa_stream_write() OK\n"; +- } ++void AudioHardwareStreamPAOutput::prefill() ++{ ++ if (!m_running || m_corked || m_primed || !m_stream || pa_stream_get_state(m_stream) != PA_STREAM_READY) ++ return; ++ ++ // The only burst: fill the target buffer once at start. ++ unsigned blocks = 0; ++ RenderResult r = RenderResult::Written; ++ while (blocks < kPrefillBlocks && (r = renderBlock(iopShouldTrace())) == RenderResult::Written) ++ ++blocks; ++ if (rclolAudioTraceEnabled()) ++ rclolAudioTrace("PA_OUT prefill blocks=%u result=%d periodMs=%.3f blockFrames=%u\n", blocks, (int) r, ++ m_periodNs / 1e6, m_blockFrames); ++ if (r == RenderResult::Stopped || !blocks) ++ return; ++ ++ m_primed = true; ++ m_anchorNs = nowNs(); ++ m_tickIndex = 1; ++ m_advanceNs = 0; ++ armTimer(m_anchorNs); ++} ++ ++void AudioHardwareStreamPAOutput::tick() ++{ ++ if (!m_running || m_corked || !m_timer) ++ return; ++ if (!m_primed) ++ { ++ prefill(); ++ return; ++ } ++ ++ uint64_t now = nowNs(); ++ uint64_t deadline = deadlineNs(); ++ if (now < deadline) ++ { ++ // Early wakeup (leeway); wait for the real deadline. ++ armTimer(now); ++ return; ++ } ++ ++ const bool trace = tickShouldTrace(); ++ const size_t writable = pa_stream_writable_size(m_stream); ++ if (writable != size_t(-1)) ++ m_windowMinWritable = std::min(m_windowMinWritable, writable); ++ ++ const uint64_t behind = uint64_t(double(now - deadline) / m_periodNs); ++ unsigned toRender = 1; ++ if (behind > kMaxCatchUpBlocks) ++ { ++ // Far behind (stalled queue, suspended process): don't burst, restart the clock. ++ if (paceShouldTrace()) ++ rclolAudioTrace("PA_OUT reanchor lateMs=%.3f behindBlocks=%llu\n", double(now - deadline) / 1e6, ++ (unsigned long long) behind); ++ m_anchorNs = now; ++ m_tickIndex = 0; ++ m_advanceNs = 0; ++ } ++ else if (behind) ++ { ++ toRender += unsigned(behind); ++ if (paceShouldTrace()) ++ rclolAudioTrace("PA_OUT catchup lateMs=%.3f blocks=%u\n", double(now - deadline) / 1e6, toRender); ++ } ++ ++ if (trace) ++ rclolAudioTrace("PA_OUT tick lateMs=%.3f writable=%zu render=%u\n", double(now - deadline) / 1e6, writable, toRender); ++ ++ for (unsigned i = 0; i < toRender; i++) ++ { ++ RenderResult r = renderBlock(iopShouldTrace()); ++ if (r == RenderResult::Stopped) ++ return; // also covers stop() from inside the IOProc (timer already cancelled) ++ if (r == RenderResult::NoSpace && paceShouldTrace()) ++ rclolAudioTrace("PA_OUT skip: writable=%zu < block=%zu\n", pa_stream_writable_size(m_stream), m_blockBytes); ++ // The IO cycle elapses either way; a skipped cycle means PA is consuming slower than us. ++ ++m_tickIndex; ++ } + +- done += abl->mBuffers[0].mDataByteSize; ++ // Drift servo: the PA sink clock isn't our clock. If the buffer stayed under-filled for a ++ // whole window (min writable >= 2 blocks), run ~6% faster for the next window. Each tick ++ // stays a single, evenly spaced IOProc call. ++ if (++m_windowTicks >= kServoWindowTicks) ++ { ++ const bool low = m_windowMinWritable != SIZE_MAX && m_windowMinWritable >= 2 * m_blockBytes; ++ const double step = low ? m_periodNs / 16 : 0; ++ if (step != m_servoStepNs && paceShouldTrace()) ++ rclolAudioTrace("PA_OUT servo minWritable=%zu step=%s\n", m_windowMinWritable, low ? "fast" : "off"); ++ m_servoStepNs = step; ++ m_windowMinWritable = SIZE_MAX; ++ m_windowTicks = 0; + } ++ m_advanceNs += m_servoStepNs; ++ ++ armTimer(nowNs()); ++} ++ ++void AudioHardwareStreamPAOutput::paStreamWriteCB(pa_stream* s, size_t length, void* self) ++{ ++ AudioHardwareStreamPAOutput* This = static_cast(self); ++ ++ if (!This->m_running) ++ { ++ if (wcbShouldTrace()) rclolAudioTrace("PA_WRITE_CB length=%zu not-running: corking\n", length); ++ pa_stream_cork(This->m_stream, true, [](pa_stream*, int, void*) {}, nullptr); ++ return; ++ } ++ ++ // Rendering is paced by the IO timer; the write request only triggers the initial prefill. ++ if (wcbShouldTrace()) ++ rclolAudioTrace("PA_WRITE_CB length=%zu writable=%zu primed=%d corked=%d\n", length, ++ pa_stream_writable_size(s), This->m_primed, This->m_corked); ++ if (!This->m_timer) ++ { ++ // No pacing timer could be created: fall back to rendering whole blocks on demand. ++ while (This->renderBlock(iopShouldTrace()) == RenderResult::Written) {} ++ return; ++ } ++ if (!This->m_primed) ++ This->prefill(); } -@@ -108,4 +119,3 @@ void AudioHardwareStreamPAOutput::start() - // pa_stream_cork(m_stream, false, [](pa_stream*, int, void*) {}, nullptr); + void AudioHardwareStreamPAOutput::start() +@@ -93,10 +340,44 @@ void AudioHardwareStreamPAOutput::start() + struct pa_buffer_attr battr; + + battr.fragsize = uint32_t(-1); +- battr.maxlength = uint32_t(m_bufferSize); +- battr.minreq = uint32_t(-1); +- battr.prebuf = battr.maxlength; +- battr.tlength = uint32_t(-1); ++ // Size the server buffer in whole IOProc blocks so every IO cycle can supply one. ++ uint32_t bpf = m_hw->asbd().mBytesPerFrame; ++ uint32_t block = bpf ? (m_hw->bufferSize() / bpf) * bpf : m_bufferSize; ++ if (!block) ++ block = m_bufferSize; ++ // ~46 ms target at 256 frames/44.1 kHz: a 2-block target underran, since each ++ // block waits on a cross-process round trip to the client's renderer. ++ battr.maxlength = block * 16; ++ battr.minreq = block; ++ battr.prebuf = block * 2; ++ battr.tlength = block * kPrefillBlocks; ++ ++ // Real HALs always call the IOProc with the same frame count (kAudioDevicePropertyBufferFrameSize). ++ m_blockFrames = bpf ? m_hw->bufferSize() / bpf : 0; ++ m_blockBytes = size_t(m_blockFrames) * bpf; ++ const double rate = m_hw->asbd().mSampleRate; ++ m_periodNs = rate > 0 ? double(m_blockFrames) * 1e9 / rate : 0; ++ ++ // start() runs on the PA queue, so the timer shares its serialization with all pa_* calls. ++ // Disabled: Darling's dispatch timers fire 3-35 ms late (one stalled 30 s), which starved ++ // PA into silence. Render on PA's write requests instead; the output timestamps still ++ // come from PA's latency, so blocks within one request get correctly spaced play times. ++ static const bool kUsePacingTimer = false; ++ dispatch_queue_t queue = static_cast(m_hw)->paQueue(); ++ if (kUsePacingTimer && queue && m_blockBytes && m_periodNs > 0) ++ { ++ m_timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue); ++ if (m_timer) ++ { ++ dispatch_source_set_event_handler(m_timer, ^{ tick(); }); ++ // Resumed immediately (a suspended source can't be released); armed after prefill. ++ dispatch_source_set_timer(m_timer, DISPATCH_TIME_FOREVER, DISPATCH_TIME_FOREVER, 0); ++ dispatch_resume(m_timer); ++ } ++ } ++ if (rclolAudioTraceEnabled()) ++ rclolAudioTrace("PA_OUT start block=%zu frames=%u periodMs=%.3f timer=%d\n", m_blockBytes, m_blockFrames, ++ m_periodNs / 1e6, m_timer != nullptr); + + pa_stream_set_write_callback(m_stream, paStreamWriteCB, this); + +@@ -105,7 +386,5 @@ void AudioHardwareStreamPAOutput::start() + |PA_STREAM_AUTO_TIMING_UPDATE) /* PA_STREAM_START_CORKED */, + nullptr, nullptr); + +- // pa_stream_cork(m_stream, false, [](pa_stream*, int, void*) {}, nullptr); AudioHardwareStreamPA::start(); } - diff --git a/src/CoreAudio/CoreAudio/pulse/AudioHardwareStreamPAOutput.h b/src/CoreAudio/CoreAudio/pulse/AudioHardwareStreamPAOutput.h -index 735474128..a92e64cd5 100644 +index 735474128..8d73f9902 100644 --- a/src/CoreAudio/CoreAudio/pulse/AudioHardwareStreamPAOutput.h +++ b/src/CoreAudio/CoreAudio/pulse/AudioHardwareStreamPAOutput.h -@@ -26,9 +26,9 @@ class AudioHardwareStreamPAOutput : public AudioHardwareStreamPA +@@ -20,15 +20,46 @@ along with Darling. If not, see . + #ifndef AUDIOHARDWARESTREAMPAOUTPUT_H + #define AUDIOHARDWARESTREAMPAOUTPUT_H + #include "AudioHardwareStreamPA.h" ++#include ++#include + + class AudioHardwareStreamPAOutput : public AudioHardwareStreamPA + { public: AudioHardwareStreamPAOutput(AudioHardwareImplPA* hw, AudioDeviceIOProc callback, void* clientData); ++ ~AudioHardwareStreamPAOutput(); ++ void stop() override; private: ++ enum class RenderResult { Written, NoSpace, Stopped }; ++ + uint64_t m_frames = 0; void start() override; static void paStreamWriteCB(pa_stream* s, size_t length, void* self); ++ ++ // IO cycle pacing; everything below is only touched on the PA queue. ++ void cancelTimer(); ++ void prefill(); ++ void tick(); ++ void armTimer(uint64_t nowNs); ++ uint64_t deadlineNs() const; ++ RenderResult renderBlock(bool trace); ++ uint64_t outputLatencyNs(size_t writable, bool* fromPA); ++ ++ dispatch_source_t m_timer = nullptr; ++ size_t m_blockBytes = 0; ++ uint32_t m_blockFrames = 0; ++ double m_periodNs = 0; ++ bool m_primed = false; ++ bool m_corked = false; ++ // Deadline k is m_anchorNs + k * m_periodNs - m_advanceNs. ++ uint64_t m_anchorNs = 0; ++ uint64_t m_tickIndex = 0; ++ double m_advanceNs = 0; ++ // Drift servo: smallest writable size seen over the current window of ticks. ++ size_t m_windowMinWritable = SIZE_MAX; ++ unsigned m_windowTicks = 0; ++ double m_servoStepNs = 0; ++ uint64_t m_lastIOProcHost = 0; }; #endif /* AUDIOHARDWARESTREAMPA_H */ @@ -1886,10 +2365,10 @@ index 63f3545e2..4a2d8ed62 100644 }); } diff --git a/src/CoreAudio/CoreAudio/pulse/PADispatchMainLoop.h b/src/CoreAudio/CoreAudio/pulse/PADispatchMainLoop.h -index ff83d1e15..0d84a2e0d 100644 +index ff83d1e15..1e86f9e99 100644 --- a/src/CoreAudio/CoreAudio/pulse/PADispatchMainLoop.h +++ b/src/CoreAudio/CoreAudio/pulse/PADispatchMainLoop.h -@@ -30,6 +30,8 @@ public: +@@ -30,8 +30,12 @@ public: void suspend(); void resume(); @@ -1897,8 +2376,12 @@ index ff83d1e15..0d84a2e0d 100644 + void async(void (^block)(void)); pa_mainloop_api* getAPI(); ++ // The serial queue all pa_* calls and callbacks run on. ++ dispatch_queue_t queue() const { return m_queue; } private: -@@ -50,6 +52,7 @@ private: + static pa_io_event* io_new(pa_mainloop_api *a, int fd, pa_io_event_flags_t events, pa_io_event_cb_t cb, void *userdata); + static void io_enable(pa_io_event *e, pa_io_event_flags_t events); +@@ -50,6 +54,7 @@ private: static void defer_event_fire(void* context); private: dispatch_queue_t m_queue; @@ -1907,7 +2390,7 @@ index ff83d1e15..0d84a2e0d 100644 bool m_suspended = true; }; diff --git a/src/CoreAudio/CoreAudioComponent/AUHAL.cpp b/src/CoreAudio/CoreAudioComponent/AUHAL.cpp -index fe4d1eaa1..172f751ba 100644 +index fe4d1eaa1..9321edcdf 100644 --- a/src/CoreAudio/CoreAudioComponent/AUHAL.cpp +++ b/src/CoreAudio/CoreAudioComponent/AUHAL.cpp @@ -1,3 +1,4 @@ @@ -1915,7 +2398,7 @@ index fe4d1eaa1..172f751ba 100644 /* This file is part of Darling. -@@ -19,6 +20,11 @@ along with Darling. If not, see . +@@ -19,6 +20,15 @@ along with Darling. If not, see . #include "AUHAL.h" #include @@ -1924,10 +2407,14 @@ index fe4d1eaa1..172f751ba 100644 +#include +#include +#include ++#include ++#include ++#include ++#include #pragma GCC visibility push(default) AUDIOCOMPONENT_ENTRY(AUOutputBaseFactory, AUHAL); -@@ -42,6 +48,37 @@ AUHAL::~AUHAL() +@@ -42,6 +52,37 @@ AUHAL::~AUHAL() Stop(); } @@ -1965,7 +2452,7 @@ index fe4d1eaa1..172f751ba 100644 bool AUHAL::CanScheduleParameters() const { return true; -@@ -49,7 +86,7 @@ bool AUHAL::CanScheduleParameters() const +@@ -49,7 +90,7 @@ bool AUHAL::CanScheduleParameters() const bool AUHAL::StreamFormatWritable(AudioUnitScope scope, AudioUnitElement element) { @@ -1974,11 +2461,11 @@ index fe4d1eaa1..172f751ba 100644 } OSStatus AUHAL::Version() -@@ -76,19 +113,26 @@ OSStatus AUHAL::Render(AudioUnitRenderActionFlags& ioActionFlags, const AudioTim +@@ -76,19 +117,26 @@ OSStatus AUHAL::Render(AudioUnitRenderActionFlags& ioActionFlags, const AudioTim OSStatus AUHAL::Start() { -+ if(getenv("RCLOL_AUDIO_TRACE")) rclolAudioTrace("AUDIO_UNIT_NATIVE_START output=%d input=%d\n",m_enableOutput,m_enableInput); ++ if(rclolAudioTraceEnabled()) rclolAudioTrace("AUDIO_UNIT_NATIVE_START output=%d input=%d\n",m_enableOutput,m_enableInput); if (m_running) return noErr; @@ -2004,7 +2491,7 @@ index fe4d1eaa1..172f751ba 100644 } if (m_enableInput) { -@@ -111,13 +155,15 @@ OSStatus AUHAL::Stop() +@@ -111,20 +159,42 @@ OSStatus AUHAL::Stop() { AudioDeviceStop(m_outputDevice, m_outputProcID); AudioDeviceDestroyIOProcID(m_outputDevice, m_outputProcID); @@ -2021,8 +2508,24 @@ index fe4d1eaa1..172f751ba 100644 return noErr; } -@@ -125,6 +171,10 @@ OSStatus AUHAL::SetProperty(AudioUnitPropertyID inID, AudioUnitScope inScope, Au ++// Volume/mute/gain related property selectors (HAL device properties and AUHAL parameters). ++static bool rclolVolumeRelated(UInt32 id) ++{ ++ switch (id) ++ { ++ case 'volm': case 'vold': case 'vdb#': case 'mute': case 'thru': ++ case 'pvol': case 'pvod': case 'mvol': case 'mbal': case 'span': ++ case 'svlm': case 'smut': case 'v2db': case 'db2v': ++ return true; ++ } ++ return false; ++} ++ + OSStatus AUHAL::SetProperty(AudioUnitPropertyID inID, AudioUnitScope inScope, AudioUnitElement inElement, const void* inData, UInt32 inDataSize) { ++ if (rclolAudioTraceEnabled() && rclolVolumeRelated(inID)) ++ rclolAudioTrace("AUHAL_SET_VOLPROP property=%08x scope=%u element=%u size=%u value=%g\n",inID,inScope,inElement,inDataSize, ++ (inData && inDataSize==sizeof(Float32))?(double)*(const Float32*)inData:0.0); switch (inID) { + case kAudioDevicePropertyBufferFrameSize: { @@ -2032,21 +2535,24 @@ index fe4d1eaa1..172f751ba 100644 case kAudioOutputUnitProperty_SetInputCallback: { ca_require(inDataSize == sizeof(AURenderCallbackStruct), InvalidPropertyValue); -@@ -176,7 +226,11 @@ OSStatus AUHAL::SetProperty(AudioUnitPropertyID inID, AudioUnitScope inScope, Au +@@ -176,15 +246,23 @@ OSStatus AUHAL::SetProperty(AudioUnitPropertyID inID, AudioUnitScope inScope, Au return noErr; } } - return AUBase::SetProperty(inID, inScope, inElement, inData, inDataSize); + { + OSStatus status=AUBase::SetProperty(inID,inScope,inElement,inData,inDataSize); -+ if(status && getenv("RCLOL_AUDIO_TRACE")) rclolAudioTrace("AUDIO_UNIT_SET property=%u scope=%u element=%u status=%d\n",inID,inScope,inElement,status); ++ if(status && rclolAudioTraceEnabled()) rclolAudioTrace("AUDIO_UNIT_SET property=%u scope=%u element=%u status=%d\n",inID,inScope,inElement,status); + return status; + } InvalidPropertyValue: return kAudioUnitErr_InvalidPropertyValue; } -@@ -185,6 +239,8 @@ OSStatus AUHAL::GetPropertyInfo(AudioUnitPropertyID inID, AudioUnitScope inScope + + OSStatus AUHAL::GetPropertyInfo(AudioUnitPropertyID inID, AudioUnitScope inScope, AudioUnitElement inElement, UInt32& outDataSize, Boolean& outWritable) { ++ if (rclolAudioTraceEnabled() && rclolVolumeRelated(inID)) ++ rclolAudioTrace("AUHAL_INFO_VOLPROP property=%08x scope=%u element=%u\n",inID,inScope,inElement); switch (inID) { + case kAudioDevicePropertyBufferFrameSize: @@ -2054,18 +2560,20 @@ index fe4d1eaa1..172f751ba 100644 case kAudioOutputUnitProperty_SetInputCallback: { outDataSize = sizeof(AURenderCallbackStruct); -@@ -210,13 +266,20 @@ OSStatus AUHAL::GetPropertyInfo(AudioUnitPropertyID inID, AudioUnitScope inScope +@@ -210,13 +288,22 @@ OSStatus AUHAL::GetPropertyInfo(AudioUnitPropertyID inID, AudioUnitScope inScope return noErr; } } - return AUBase::GetPropertyInfo(inID, inScope, inElement, outDataSize, outWritable); + OSStatus status=AUBase::GetPropertyInfo(inID,inScope,inElement,outDataSize,outWritable); -+ if(status && getenv("RCLOL_AUDIO_TRACE")) rclolAudioTrace("AUDIO_UNIT_INFO property=%u scope=%u element=%u status=%d\n",inID,inScope,inElement,status); ++ if(status && rclolAudioTraceEnabled()) rclolAudioTrace("AUDIO_UNIT_INFO property=%u scope=%u element=%u status=%d\n",inID,inScope,inElement,status); + return status; } OSStatus AUHAL::GetProperty(AudioUnitPropertyID inID, AudioUnitScope inScope, AudioUnitElement inElement, void* outData) { ++ if (rclolAudioTraceEnabled() && rclolVolumeRelated(inID)) ++ rclolAudioTrace("AUHAL_GET_VOLPROP property=%08x scope=%u element=%u\n",inID,inScope,inElement); switch (inID) { + case kAudioDevicePropertyBufferFrameSize: { @@ -2076,7 +2584,7 @@ index fe4d1eaa1..172f751ba 100644 case kAudioOutputUnitProperty_SetInputCallback: { memcpy(outData, &m_outputAvailableCb, sizeof(m_outputAvailableCb)); -@@ -228,12 +291,12 @@ OSStatus AUHAL::GetProperty(AudioUnitPropertyID inID, AudioUnitScope inScope, Au +@@ -228,12 +315,12 @@ OSStatus AUHAL::GetProperty(AudioUnitPropertyID inID, AudioUnitScope inScope, Au if (inElement == kOutputBus) { UInt32 value = m_enableOutput; @@ -2091,17 +2599,97 @@ index fe4d1eaa1..172f751ba 100644 } else return kAudioUnitErr_InvalidElement; -@@ -280,6 +343,9 @@ OSStatus AUHAL::recordCallback(AudioObjectID inObjectID, +@@ -277,9 +364,89 @@ OSStatus AUHAL::recordCallback(AudioObjectID inObjectID, + return This->doRecord(inNow, inInputData, inInputTime); + } + ++// Diagnostic only: classify each rendered block (see AUDIO_BLOCK_STATE). ++void AUHAL::traceBlock(const AudioTimeStamp* inNow, const AudioTimeStamp* inOutputTime, OSStatus result, ++ const CAStreamBasicDescription& desc, UInt32 nFrames, AudioBufferList* out) ++{ ++ static std::atomic stateLines(0); ++ static mach_timebase_info_data_t tb={0,0}; ++ if(!tb.denom) mach_timebase_info(&tb); ++ BlockTrace& t=m_blockTrace; ++ const unsigned long long cb=++t.callbacks; ++ if(result) { ++ if(++t.pullFailures<=50 || t.pullFailures%200==0) ++ rclolAudioTrace("AUDIO_PULL_FAIL unit=%p cb=%llu status=%d frames=%u count=%llu\n", ++ (void*)this,cb,(int)result,nFrames,t.pullFailures); ++ return; ++ } ++ if(!desc.IsFloat() || desc.mBitsPerChannel!=32 || !nFrames || !desc.mChannelsPerFrame) return; ++ const bool planar=desc.mFormatFlags&kAudioFormatFlagIsNonInterleaved; ++ const UInt32 ch=desc.mChannelsPerFrame; ++ const float* base=planar?(const float*)m_playbackSamples.data():(const float*)out->mBuffers[0].mData; ++ if(!base) return; ++ auto at=[&](UInt32 c,UInt32 f)->float { return planar?base[(size_t)c*nFrames+f]:base[(size_t)f*ch+c]; }; ++ float peak[2]={0,0}; double sq[2]={0,0}; bool halves[2]={true,true}; bool zero=true; ++ const UInt32 half=nFrames/2; ++ for(UInt32 c=0;c1?sqrt(sq[1]/nFrames):0; ++ bool same=false; ++ const size_t total=(size_t)nFrames*ch; ++ if(t.havePrev && t.prev.size()==total) { ++ same=true; ++ for(UInt32 c=0;cmSampleTime; ++ if(t.haveTime) { ++ dSample=sample-t.lastSample; ++ dHostMs=(double)(inOutputTime->mHostTime-t.lastHost)*tb.numer/tb.denom/1e6; ++ if(dHostMs>t.maxGapMs) t.maxGapMs=dHostMs; ++ } ++ t.haveTime=true; t.lastSample=sample; t.lastHost=inOutputTime->mHostTime; ++ const bool transition=state!=t.state; ++ if(transition && t.state>=0 && stateLines.fetch_add(1)<2000) ++ rclolAudioTrace("AUDIO_BLOCK_STATE unit=%p from=%s to=%s cb=%llu frames=%u peakL=%g peakR=%g rmsL=%g rmsR=%g\n", ++ (void*)this,names[t.state],names[state],cb,nFrames,peak[0],peak[1],rms0,rms1); ++ if(cb<=40 || cb%200==0 || transition) ++ rclolAudioTrace("AUDIO_BLOCK unit=%p cb=%llu state=%s frames=%u peakL=%g peakR=%g rmsL=%g rmsR=%g " ++ "halvesL=%d halvesR=%d same=%d zero=%d outSample=%.0f nowSample=%.0f dSample=%.0f outHost=%llu nowHost=%llu " ++ "dHostMs=%.3f maxGapMs=%.3f\n", ++ (void*)this,cb,names[state],nFrames,peak[0],peak[1],rms0,rms1,(int)halves[0],(int)halves[1],(int)same,(int)zero, ++ sample,inNow->mSampleTime,dSample,(unsigned long long)inOutputTime->mHostTime, ++ (unsigned long long)inNow->mHostTime,dHostMs,t.maxGapMs); ++ if(cb%2000==0) ++ rclolAudioTrace("AUDIO_BLOCK_SUMMARY unit=%p cb=%llu silent=%llu live=%llu repeat=%llu halves=%llu pullFail=%llu maxGapMs=%.3f\n", ++ (void*)this,cb,t.stateCounts[0],t.stateCounts[1],t.stateCounts[2],t.stateCounts[3],t.pullFailures,t.maxGapMs); ++ t.state=state; ++} ++ OSStatus AUHAL::doPlayback(const AudioTimeStamp* inNow, AudioBufferList* outOutputData, const AudioTimeStamp* inOutputTime) { // std::cout << "AUHAL::DoPlayback()\n"; + static unsigned stackTraces=0; -+ if(getenv("RCLOL_AUDIO_TRACE") && stackTraces++<2) ++ if(rclolAudioTraceEnabled() && stackTraces++<2) + rclolAudioTrace("AUDIO_CALLBACK_STACK bytes=%zu\n",pthread_get_stacksize_np(pthread_self())); if (!HasInput(0)) { // std::cerr << "No connection\n"; -@@ -290,9 +356,44 @@ OSStatus AUHAL::doPlayback(const AudioTimeStamp* inNow, AudioBufferList* outOutp +@@ -290,9 +457,75 @@ OSStatus AUHAL::doPlayback(const AudioTimeStamp* inNow, AudioBufferList* outOutp AudioUnitRenderActionFlags flags = kAudioUnitRenderAction_PreRender; const CAStreamBasicDescription& desc = GetStreamFormat(kAudioUnitScope_Input, kOutputBus); @@ -2136,8 +2724,39 @@ index fe4d1eaa1..172f751ba 100644 + result=GetInput(kOutputBus)->PullInputWithBufferList(flags,*inNow,kOutputBus,nFrames,outOutputData); + } + ++ if(rclolAudioTraceEnabled()) traceBlock(inNow,inOutputTime,result,desc,nFrames,outOutputData); ++ ++ // Diagnostic: Chromium's audio source can start handing out one identical ++ // block forever (heard as static). Log the timestamp Darling supplied at the ++ // moment that begins, so we can tell whether our clock inputs look sane. ++ { ++ const unsigned char* produced; ++ size_t producedBytes; ++ if(desc.mFormatFlags&kAudioFormatFlagIsNonInterleaved) { ++ produced=m_playbackSamples.data(); ++ producedBytes=(size_t)nFrames*frameBytes; ++ } else { ++ produced=(const unsigned char*)outOutputData->mBuffers[0].mData; ++ producedBytes=outOutputData->mBuffers[0].mDataByteSize; ++ } ++ uint64_t sum=1469598103934665603ull; ++ for(size_t i=0;imHostTime,inOutputTime->mSampleTime, ++ (unsigned)inOutputTime->mFlags); ++ } else { ++ repeatRun=0; ++ } ++ lastSum=sum; ++ } ++ + static unsigned traceRenders=0; -+ if (getenv("RCLOL_AUDIO_TRACE") && traceRenders++ < 16) { ++ if (rclolAudioTraceEnabled() && traceRenders++ < 16) { + float peak=0; + if (!result && desc.IsFloat() && sampleBytes==4) { + const float* samples=static_cast(outOutputData->mBuffers[0].mData); @@ -2150,7 +2769,7 @@ index fe4d1eaa1..172f751ba 100644 // std::cout << "Bytes: " << outOutputData->mBuffers[0].mDataByteSize << std::endl; // m_auhalData.write((char*) outOutputData->mBuffers[0].mData, outOutputData->mBuffers[0].mDataByteSize); diff --git a/src/CoreAudio/CoreAudioComponent/AUHAL.h b/src/CoreAudio/CoreAudioComponent/AUHAL.h -index bc21bcbbb..190a0ffc8 100644 +index bc21bcbbb..e369f95d1 100644 --- a/src/CoreAudio/CoreAudioComponent/AUHAL.h +++ b/src/CoreAudio/CoreAudioComponent/AUHAL.h @@ -25,6 +25,7 @@ along with Darling. If not, see . @@ -2171,7 +2790,15 @@ index bc21bcbbb..190a0ffc8 100644 bool CanScheduleParameters() const override; bool StreamFormatWritable(AudioUnitScope scope, AudioUnitElement element) override; OSStatus Version() override; -@@ -76,6 +80,11 @@ protected: +@@ -59,6 +63,7 @@ private: + void* inClientData); + + OSStatus doPlayback(const AudioTimeStamp* inNow, AudioBufferList* outOutputData, const AudioTimeStamp* inOutputTime); ++ void traceBlock(const AudioTimeStamp* inNow, const AudioTimeStamp* inOutputTime, OSStatus result, const CAStreamBasicDescription& desc, UInt32 nFrames, AudioBufferList* out); + OSStatus doRecord(const AudioTimeStamp* inNow, const AudioBufferList* inInputData, const AudioTimeStamp* inInputTime); + + protected: +@@ -76,6 +81,24 @@ protected: // TODO: Prepare for non-interleaved audio std::unique_ptr m_buffer; size_t m_bufferSize = 0, m_bufferUsed = 0; @@ -2180,6 +2807,19 @@ index bc21bcbbb..190a0ffc8 100644 + // Keep planar conversion storage across callbacks and start/stop cycles. + std::vector m_playbackSamples; + std::vector m_playbackBufferList; ++ ++ // Diagnostic-only block statistics (touched only when audio tracing is on). ++ struct BlockTrace { ++ unsigned long long callbacks = 0, pullFailures = 0; ++ unsigned long long stateCounts[4] = {0,0,0,0}; ++ int state = -1; ++ std::vector prev; ++ bool havePrev = false; ++ bool haveTime = false; ++ double lastSample = 0; ++ unsigned long long lastHost = 0; ++ double maxGapMs = 0; ++ } m_blockTrace; }; #endif @@ -7241,16 +7881,39 @@ diff --git a/src/CoreAudio/AudioTrace.h b/src/CoreAudio/AudioTrace.h new file mode 100644 --- /dev/null +++ b/src/CoreAudio/AudioTrace.h -@@ -0,0 +1,14 @@ +@@ -0,0 +1,37 @@ +#ifndef RCLOL_AUDIO_TRACE_H +#define RCLOL_AUDIO_TRACE_H +#include +#include +#include ++#include ++#include +#include +// Opt-in API/status diagnostics only; never records audio samples or names. ++// Enabled by env RCLOL_AUDIO_TRACE or by the existence of /tmp/rclol-audio-trace ++// or /Users/Shared/rclol-audio-trace (the latter survives /tmp wipes at launch). ++// A negative result is re-checked at most once every ~2 seconds; a positive ++// result is cached for the life of the process. ++static inline bool rclolAudioTraceEnabled() { ++ static std::atomic enabled{false}; ++ static std::atomic nextCheckMs{0}; ++ if (enabled.load(std::memory_order_relaxed)) return true; ++ struct timespec ts; ++ clock_gettime(CLOCK_MONOTONIC, &ts); ++ long long nowMs = (long long) ts.tv_sec * 1000 + ts.tv_nsec / 1000000 + 1; ++ if (nowMs < nextCheckMs.load(std::memory_order_relaxed)) return false; ++ nextCheckMs.store(nowMs + 2000, std::memory_order_relaxed); ++ if (getenv("RCLOL_AUDIO_TRACE") != nullptr || ++ access("/tmp/rclol-audio-trace", F_OK) == 0 || ++ access("/Users/Shared/rclol-audio-trace", F_OK) == 0) { ++ enabled.store(true, std::memory_order_relaxed); ++ return true; ++ } ++ return false; ++} +static inline void rclolAudioTrace(const char* format, ...) { -+ if (!getenv("RCLOL_AUDIO_TRACE")) return; ++ if (!rclolAudioTraceEnabled()) return; + static FILE* file=[] { FILE* f=fopen("/tmp/rclol-audio-api.log","a"); if(f) setvbuf(f,nullptr,_IOLBF,0); return f; }(); + if(!file) return; + va_list args;va_start(args,format);fprintf(file,"pid=%d ",getpid());vfprintf(file,format,args);va_end(args); @@ -7344,7 +8007,7 @@ new file mode 100644 + OSStatus SetParameter(AudioUnitParameterID id, AudioUnitScope scope, AudioUnitElement element, + AudioUnitParameterValue gain, UInt32) override { + static std::atomic traceCount{0}; -+ if (getenv("RCLOL_AUDIO_TRACE") && traceCount++ < 64) ++ if (rclolAudioTraceEnabled() && traceCount++ < 64) + rclolAudioTrace("AUDIO_MATRIX_PARAM id=%u scope=%u element=%08x gain=%g\n",id,scope,element,gain); + std::lock_guard lock(mMutex); + if (!std::isfinite(gain)) return kAudioUnitErr_InvalidParameter; @@ -8801,3 +9464,67 @@ new file mode 100644 + api->sd_bus_flush_close_unref(bus); + return r < 0 ? r : 0; +} +diff --git a/src/frameworks/CoreServices/src/CarbonCore/Gestalt.cpp b/src/frameworks/CoreServices/src/CarbonCore/Gestalt.cpp +index dffdd4cb2..7430f587b 100644 +--- a/src/frameworks/CoreServices/src/CarbonCore/Gestalt.cpp ++++ b/src/frameworks/CoreServices/src/CarbonCore/Gestalt.cpp +@@ -25,6 +25,9 @@ along with Darling. If not, see . + + OSStatus Gestalt(uint32_t type, int* value) + { ++ if (!value) ++ return paramErr; ++ *value = 0; + switch (type) + { + case gestaltSystemVersion: +@@ -32,24 +35,31 @@ OSStatus Gestalt(uint32_t type, int* value) + case gestaltSystemVersionMinor: + case gestaltSystemVersionPatch: + { +- // We may be mixing the Darwin and OS version, but this API is deprecated anyway +- int name[2] = { CTL_KERN, KERN_OSRELEASE }; +- char version[32]; +- size_t version_len = sizeof(version) - 1; +- int maj, min, patch; +- +- sysctl(name, 2, version, &version_len, nullptr, 0); +- sscanf(version, "%d.%d.%d", &maj, &min, &patch); +- +- if (type == gestaltSystemVersion) +- *value = 0x100 * maj + 0x10 * min; +- else if (type == gestaltSystemVersionMajor) +- *value = maj; +- else if (type == gestaltSystemVersionMinor) +- *value = min; +- else if (type == gestaltSystemVersionPatch) +- *value = patch; +- } ++ // Darwin's kernel release is not the macOS product version. ++ char version[64] = {}; ++ size_t version_len = sizeof(version); ++ int major = 0, minor = 0, patch = 0; ++ if (sysctlbyname("kern.osproductversion", version, &version_len, nullptr, 0) != 0) ++ return gestaltUnknownErr; ++ version[sizeof(version) - 1] = '\0'; ++ if (sscanf(version, "%d.%d.%d", &major, &minor, &patch) < 1 || ++ major < 0 || major > 99 || minor < 0 || patch < 0) ++ return gestaltUnknownErr; ++ ++ if (type == gestaltSystemVersion) { ++ // Legacy packed BCD: minor and bug-fix components saturate at 9. ++ const int packedMajor = ((major / 10) << 4) | (major % 10); ++ *value = (packedMajor << 8) | ((minor > 9 ? 9 : minor) << 4) | ++ (patch > 9 ? 9 : patch); ++ } else if (type == gestaltSystemVersionMajor) ++ *value = major; ++ else if (type == gestaltSystemVersionMinor) ++ *value = minor; ++ else ++ *value = patch; ++ // Version selectors must not fall through to the architecture case. ++ return 0; ++ } + case gestaltSysArchitecture: + #ifdef __powerpc__ + *value = gestaltPowerPC; diff --git a/patches/xnu.patch b/patches/xnu.patch index 482a3f6..7b95368 100644 --- a/patches/xnu.patch +++ b/patches/xnu.patch @@ -3221,18 +3221,10 @@ index 0000000..479294a + return -ENOSYS; +} diff --git a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/misc/proc_info.c b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/misc/proc_info.c -index c2f70fb..73c2a5a 100644 +index c2f70fb..af904ed 100644 --- a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/misc/proc_info.c +++ b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/misc/proc_info.c -@@ -35,6 +35,7 @@ - #define LINUX_PR_SET_NAME 15 - - static long _proc_pidinfo(int32_t pid, uint32_t flavor, uint64_t arg, void* buffer, int32_t bufsize); -+static long _proc_listpids(uint32_t type, uint32_t typeinfo, void* buffer, int32_t bufsize); - - extern __SIZE_TYPE__ strlen(const char *s); - extern void *memset(void *s, int c, __SIZE_TYPE__ n); -@@ -83,7 +84,42 @@ long sys_proc_info(uint32_t callnum, int32_t pid, uint32_t flavor, +@@ -83,6 +83,38 @@ long sys_proc_info(uint32_t callnum, int32_t pid, uint32_t flavor, case 2: // proc_pidinfo return _proc_pidinfo(pid, flavor, arg, buffer, bufsize); case 3: // proc_pidfdinfo @@ -3269,160 +3261,8 @@ index c2f70fb..73c2a5a 100644 + return -ENOTSUP; + } case 1: // proc_listpids -+ // `pid` carries the process type and `flavor` carries the type -+ // info (e.g. the pgid, ppid or uid to match). -+ return _proc_listpids(pid, flavor, buffer, bufsize); default: __simple_printf("sys_proc_info(): Unsupported callnum: %d\n", - callnum); -@@ -827,3 +863,147 @@ bail: - close_internal(fd); - return count * sizeof(uint64_t); - } -+ -+// Returns whether `pid` should be included in a proc_listpids() query of the -+// given `type`, matching the semantics of XNU's bsd/kern/proc_info.c. -+static bool _proc_listpids_match(uint32_t type, uint32_t typeinfo, int32_t pid, -+ int32_t uid) -+{ -+ char path[64], stat[1024]; -+ char* statptr; -+ const char* elem; -+ int32_t ppid, pgrp, tty; -+ -+ switch (type) -+ { -+ case PROC_ALL_PIDS: -+ return true; -+ -+ case PROC_UID_ONLY: -+ case PROC_RUID_ONLY: -+ // Darling's container runs every guest process as our own -+ // user, which is also what _sysctl_proc() reports. -+ return typeinfo == (uint32_t) uid; -+ -+ case PROC_PGRP_ONLY: -+ case PROC_PPID_ONLY: -+ case PROC_TTY_ONLY: -+ break; -+ -+ default: -+ // PROC_KDBG_ONLY and anything else: no process carries the -+ // corresponding state under Darling. -+ return false; -+ } -+ -+ __simple_sprintf(path, "/proc/%d/stat", pid); -+ if (!read_string(path, stat, sizeof(stat))) -+ return false; -+ -+ statptr = stat; -+ skip_stat_elems(&statptr, 1); // pid -+ -+ if (!(elem = next_stat_elem(&statptr))) -+ return false; -+ // comm -+ -+ if (!(elem = next_stat_elem(&statptr))) -+ return false; -+ // state -+ -+ if (!(elem = next_stat_elem(&statptr))) -+ return false; -+ ppid = __simple_atoi(elem, NULL); -+ -+ if (!(elem = next_stat_elem(&statptr))) -+ return false; -+ pgrp = __simple_atoi(elem, NULL); -+ -+ if (type == PROC_PPID_ONLY) -+ return ppid == (int32_t) typeinfo; -+ -+ if (type == PROC_PGRP_ONLY) -+ return pgrp == (int32_t) typeinfo; -+ -+ // PROC_TTY_ONLY: the (Linux-encoded) controlling terminal device. -+ if (!(elem = next_stat_elem(&statptr))) -+ return false; -+ // session -+ -+ if (!(elem = next_stat_elem(&statptr))) -+ return false; -+ tty = __simple_atoi(elem, NULL); -+ -+ return tty != 0 && (uint32_t) tty == typeinfo; -+} -+ -+// Emulates XNU's proc_listpids(), i.e. proc_info(PROC_INFO_CALL_LISTPIDS). -+// Guest pids are Linux pids, so walking /proc gives the same set that the -+// other proc_info flavors and sysctl(KERN_PROC_*) already operate on. -+static long _proc_listpids(uint32_t type, uint32_t typeinfo, void* buffer, int32_t bufsize) -+{ -+ char dents[256]; -+ long basep = 0; -+ int fd, ret; -+ int32_t maxpids, count = 0; -+ static int uid = -1; -+ -+ if (type != PROC_ALL_PIDS && type != PROC_PGRP_ONLY && type != PROC_TTY_ONLY && -+ type != PROC_UID_ONLY && type != PROC_RUID_ONLY && -+ type != PROC_PPID_ONLY && type != PROC_KDBG_ONLY) -+ return -EINVAL; -+ -+ if (buffer && bufsize < (int32_t) sizeof(int)) -+ return -ENOMEM; -+ -+ // A null buffer asks for the size a caller should allocate. -+ maxpids = buffer ? (bufsize / (int32_t) sizeof(int)) : 0; -+ -+ if (uid == -1) -+ uid = sys_getuid(); -+ -+ fd = sys_open_nocancel("/proc", BSD_O_RDONLY | BSD_O_DIRECTORY, 0); -+ if (fd < 0) -+ return fd; -+ -+ while ((ret = sys_getdirentries(fd, (char*) dents, sizeof(dents), &basep)) > 0) -+ { -+ int pos = 0; -+ -+ while (pos < ret) -+ { -+ struct bsd_dirent* dent = (struct bsd_dirent*) &dents[pos]; -+ -+ pos += dent->d_reclen; -+ -+ if (dent->d_type != DT_DIR || !isdigit(dent->d_name[0])) -+ continue; -+ -+ int pid = __simple_atoi(dent->d_name, NULL); -+ -+ if (!_proc_listpids_match(type, typeinfo, pid, uid)) -+ continue; -+ -+ if (buffer) -+ { -+ // Truncate rather than overflow, like XNU does. -+ if (count >= maxpids) -+ goto bail; -+ -+ ((int*) buffer)[count] = pid; -+ } -+ -+ ++count; -+ } -+ } -+ -+bail: -+ close_internal(fd); -+ -+ // XNU reports a little extra headroom so that a size query followed by a -+ // fill call cannot come up short when processes appear in between. -+ if (!buffer) -+ return (long)(count + 20) * sizeof(int); -+ -+ return (long) count * sizeof(int); -+} diff --git a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/misc/ptrace.c b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/misc/ptrace.c index 0a8f1e4..55aadbe 100644 --- a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/misc/ptrace.c diff --git a/tools/build-m58f336-quartzcore.py b/tools/build-m58f336-quartzcore.py index dc372e1..f83589e 100644 --- a/tools/build-m58f336-quartzcore.py +++ b/tools/build-m58f336-quartzcore.py @@ -1,18 +1,28 @@ #!/usr/bin/env python3 -"""Build only the changed GPU frame publisher and relink QuartzCore.""" -import shlex,subprocess +"""Rebuild the patched QuartzCore compositor and Metal presentation objects.""" +import shlex +import subprocess from pathlib import Path -project=Path(__file__).resolve().parent.parent -framework=project/'build/darling-src/build/src/external/cocotron/QuartzCore' -flags={} -for line in (framework/'CMakeFiles/QuartzCore.dir/flags.make').read_text().splitlines(): - if line.startswith('C_') and ' = ' in line: - k,v=line.split(' = ',1);flags[k]=shlex.split(v) -link=shlex.split((framework/'CMakeFiles/QuartzCore.dir/link.txt').read_text()) -for name in ('CALayer','CALayerContext','CATransaction'): - obj=project/('build/m336-'+name+'.o') - subprocess.run(['clang',*flags['C_DEFINES'],*flags['C_INCLUDES'],*flags['C_FLAGS'],'-O2','-Wno-macro-redefined','-c',str(project/('build/darling-src/src/external/cocotron/QuartzCore/'+name+'.m')),'-o',str(obj)],check=True) - link[link.index('CMakeFiles/QuartzCore.dir/'+name+'.m.o')]=str(obj) -link[link.index('-o')+1]=str(project/'build/m336-QuartzCore') -subprocess.run(link,cwd=framework,check=True) -print('Built isolated QuartzCore frame publisher') + +project = Path(__file__).resolve().parent.parent +framework = project / 'build/darling-src/build/src/external/cocotron/QuartzCore' +flags = {} +for line in (framework / 'CMakeFiles/QuartzCore.dir/flags.make').read_text().splitlines(): + if line.startswith(('C_', 'CXX_')) and ' = ' in line: + key, value = line.split(' = ', 1) + flags[key] = shlex.split(value) +link = shlex.split((framework / 'CMakeFiles/QuartzCore.dir/link.txt').read_text()) +sources = ['CALayer.m', 'CALayerContext.m', 'CARenderer.m', 'CATransaction.m', + 'CATransactionGroup.m', 'CATransformLayer.m', 'CAMetalLayer.mm', 'CAMetalDrawable.mm'] +for source in sources: + kind = 'CXX' if source.endswith('.mm') else 'C' + obj = project / f'build/m336-{Path(source).stem}.o' + subprocess.run(['clang++' if kind == 'CXX' else 'clang', *flags[kind + '_DEFINES'], + *flags[kind + '_INCLUDES'], *flags[kind + '_FLAGS'], '-O2', + '-Wno-macro-redefined', '-c', + str(project / 'build/darling-src/src/external/cocotron/QuartzCore' / source), + '-o', str(obj)], check=True) + link[link.index(f'CMakeFiles/QuartzCore.dir/{source}.o')] = str(obj) +link[link.index('-o') + 1] = str(project / 'build/m336-QuartzCore') +subprocess.run(link, cwd=framework, check=True) +print('Built isolated QuartzCore compositor and Metal presentation') diff --git a/tools/build-riot-appkit-runtime.py b/tools/build-riot-appkit-runtime.py index 096976e..f4df0f7 100644 --- a/tools/build-riot-appkit-runtime.py +++ b/tools/build-riot-appkit-runtime.py @@ -21,7 +21,7 @@ for name in ['NSResponder', 'NSApplication', 'NSWindow', 'NSDisplay', 'NSView', link[link.index(f'CMakeFiles/AppKit.dir/{name}.m.o')] = str(obj) link[link.index('-o') + 1] = str(project / 'build/riot-AppKit') subprocess.run(link, cwd=framework, check=True) -for name in ['riot-event-coalescing-check', 'riot-scroll-event-check', 'riot-appkit-pump-check', 'riot-window-layout-check', 'riot-game-input-check', 'riot-mouse-input-check']: +for name in ['riot-event-coalescing-check', 'riot-scroll-event-check', 'riot-appkit-pump-check', 'riot-window-layout-check']: probe = project / f'build/{name}.o' subprocess.run(['clang', *flags['C_DEFINES'], *flags['C_INCLUDES'], *flags['C_FLAGS'], '-O2', '-fblocks', '-Wno-macro-redefined', '-c', diff --git a/tools/build-riot-carboncore-runtime.py b/tools/build-riot-carboncore-runtime.py new file mode 100644 index 0000000..2d76142 --- /dev/null +++ b/tools/build-riot-carboncore-runtime.py @@ -0,0 +1,29 @@ +#!/usr/bin/env python3 +"""Rebuild CarbonCore's legacy Gestalt version query and its standalone probe.""" +from pathlib import Path +import shlex +import subprocess + +root = Path(__file__).resolve().parent.parent +folder = root / 'build/darling-src/build/src/frameworks/CoreServices/src/CarbonCore' +flags = {} +for line in (folder / 'CMakeFiles/CarbonCore.dir/flags.make').read_text().splitlines(): + if line.startswith(('C_', 'CXX_')) and ' = ' in line: + key, value = line.split(' = ', 1) + flags[key] = shlex.split(value) +obj = root / 'build/riot-Gestalt.o' +subprocess.run(['clang++', *flags['CXX_DEFINES'], *flags['CXX_INCLUDES'], *flags['CXX_FLAGS'], + '-O2', '-Wno-macro-redefined', '-c', + str(root / 'build/darling-src/src/frameworks/CoreServices/src/CarbonCore/Gestalt.cpp'), + '-o', str(obj)], check=True) +link = shlex.split((folder / 'CMakeFiles/CarbonCore.dir/link.txt').read_text()) +link[link.index('CMakeFiles/CarbonCore.dir/Gestalt.cpp.o')] = str(obj) +link[link.index('-o') + 1] = str(root / 'build/riot-CarbonCore') +subprocess.run(link, cwd=folder, check=True) +probe = root / 'build/riot-gestalt-version-check.o' +subprocess.run(['clang++', *flags['CXX_DEFINES'], *flags['CXX_INCLUDES'], *flags['CXX_FLAGS'], + '-O2', '-Wno-macro-redefined', '-c', str(root / 'tools/riot-gestalt-version-check.cpp'), + '-o', str(probe)], check=True) +subprocess.run(['clang++', '-target', 'x86_64-apple-macosx11.0', '-fuse-ld=lld', '-nostdlib', + '-isysroot', '@DARLING_SDK@', str(probe), str(root / 'build/riot-CarbonCore'), + '-lSystem', '-lc++', '-o', str(root / 'build/riot-gestalt-version-check')], check=True) diff --git a/tools/build-riot-client-resize-check.py b/tools/build-riot-client-resize-check.py new file mode 100644 index 0000000..03338fe --- /dev/null +++ b/tools/build-riot-client-resize-check.py @@ -0,0 +1,23 @@ +#!/usr/bin/env python3 +"""Build an owned X11/IOSurface window that verifies pixels after resizing.""" +from pathlib import Path +import shlex +import subprocess + +root = Path(__file__).resolve().parent.parent +folder = root / 'build/darling-src/build/src/external/cocotron/AppKit' +flags = {} +for line in (folder / 'CMakeFiles/X11_backend.dir/flags.make').read_text().splitlines(): + if line.startswith('C_') and ' = ' in line: + key, value = line.split(' = ', 1) + flags[key] = shlex.split(value) +obj = root / 'build/riot-client-resize-check.o' +subprocess.run(['clang', *flags['C_DEFINES'], *flags['C_INCLUDES'], *flags['C_FLAGS'], + '-O2', '-Wno-macro-redefined', '-c', str(root / 'tools/riot-client-resize-check.m'), + '-o', str(obj)], check=True) +subprocess.run(['clang', '-target', 'x86_64-apple-macosx11.0', '-fuse-ld=lld', '-nostdlib', + '-isysroot', '@DARLING_SDK@', str(obj), '-lSystem', '-lobjc', + '-framework', 'AppKit', '-framework', 'QuartzCore', '-framework', 'OpenGL', + '-framework', 'IOSurface', '-framework', 'CoreFoundation', '-framework', 'Foundation', + str(root / 'build/darling-src/build/src/native/libX11.dylib'), + '-o', str(root / 'build/riot-client-resize-check')], check=True) diff --git a/tools/build-riot-coretext-runtime.py b/tools/build-riot-coretext-runtime.py new file mode 100644 index 0000000..e4187b3 --- /dev/null +++ b/tools/build-riot-coretext-runtime.py @@ -0,0 +1,34 @@ +#!/usr/bin/env python3 +"""Build CoreText's font-list compatibility override and standalone regression.""" +from pathlib import Path +import shlex +import subprocess + +root = Path(__file__).resolve().parent.parent +folder = root / 'build/darling-src/build/src/external/cocotron/CoreText' +flags = {} +for line in (folder / 'CMakeFiles/CoreText.dir/flags.make').read_text().splitlines(): + if line.startswith('C_') and ' = ' in line: + key, value = line.split(' = ', 1) + flags[key] = shlex.split(value) +link = shlex.split((folder / 'CMakeFiles/CoreText.dir/link.txt').read_text()) +for name in ['CTFontDescriptor', 'CTFontManager']: + obj = root / f'build/riot-{name}.o' + subprocess.run(['clang', *flags['C_DEFINES'], *flags['C_INCLUDES'], *flags['C_FLAGS'], + '-O2', '-Wno-macro-redefined', '-c', + str(root / f'build/darling-src/src/external/cocotron/CoreText/{name}.m'), + '-o', str(obj)], check=True) + link[link.index(f'CMakeFiles/CoreText.dir/{name}.m.o')] = str(obj) +link[link.index('-o') + 1] = str(root / 'build/riot-CoreText') +fontconfig = str(root / 'build/darling-src/build/src/native/libfontconfig.dylib') +if not any(value.endswith('/libfontconfig.dylib') and not value.startswith('-') for value in link): + link.append(fontconfig) +subprocess.run(link, cwd=folder, check=True) +obj = root / 'build/riot-coretext-font-list-check.o' +subprocess.run(['clang', *flags['C_DEFINES'], *flags['C_INCLUDES'], *flags['C_FLAGS'], + '-O2', '-Wno-macro-redefined', '-c', str(root / 'tools/riot-coretext-font-list-check.m'), + '-o', str(obj)], check=True) +subprocess.run(['clang', '-target', 'x86_64-apple-macosx11.0', '-fuse-ld=lld', '-nostdlib', + '-isysroot', '@DARLING_SDK@', str(obj), str(root / 'build/riot-CoreText'), + '-lSystem', '-lobjc', '-framework', 'Foundation', '-framework', 'CoreFoundation', + '-o', str(root / 'build/riot-coretext-font-list-check')], check=True) diff --git a/tools/install-riot-appkit-runtime.sh b/tools/install-riot-appkit-runtime.sh new file mode 100755 index 0000000..18ecc50 --- /dev/null +++ b/tools/install-riot-appkit-runtime.sh @@ -0,0 +1,44 @@ +#!/bin/sh +# Install the rebuilt AppKit overlay and its paired X11 backend. +# Manually exit the game and both clients, then shut down Darling before use. +set -eu +project_dir=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) +guest_home=${RIOT_GUEST_HOME:-$HOME/.darling@GUEST_HOME@} +framework="$guest_home/RuntimeFrameworks/AppKit.framework/Versions/C" +backup_dir=${RIOT_APPKIT_BACKUP_DIR:-$project_dir/build/appkit-runtime-backups} +# Preflight both artifacts and destinations before replacing either binary. +for name in AppKit X11; do + case "$name" in + AppKit) artifact=${RIOT_APPKIT_ARTIFACT:-$project_dir/build/riot-AppKit}; binary="$framework/AppKit";; + X11) artifact=${RIOT_X11_ARTIFACT:-$project_dir/build/riot-X11}; binary="$framework/Resources/Backends/X11.backend/Contents/MacOS/X11";; + esac + [ -f "$artifact" ] || { echo "Build $name first: $artifact" >&2; exit 1; } + [ -f "$binary" ] && [ ! -L "$binary" ] || { + echo "Expected an existing $name overlay: $binary" >&2; exit 1; + } +done +mkdir -p "$backup_dir" +for name in AppKit X11; do + case "$name" in + AppKit) artifact=${RIOT_APPKIT_ARTIFACT:-$project_dir/build/riot-AppKit}; binary="$framework/AppKit";; + X11) artifact=${RIOT_X11_ARTIFACT:-$project_dir/build/riot-X11}; binary="$framework/Resources/Backends/X11.backend/Contents/MacOS/X11";; + esac + if cmp -s "$artifact" "$binary"; then + echo "$name is already current." + continue + fi + old_hash=$(sha256sum "$binary" | cut -d ' ' -f 1) + backup="$backup_dir/$name-$old_hash" + if [ -e "$backup" ]; then + cmp -s "$binary" "$backup" || { echo "Backup verification failed: $backup" >&2; exit 1; } + else + cp -p "$binary" "$backup" + fi + staged=$(mktemp "$binary.staged.XXXXXX") + trap 'rm -f "$staged"' EXIT HUP INT TERM + install -m755 "$artifact" "$staged" + cmp -s "$artifact" "$staged" || { echo "Staged $name verification failed" >&2; exit 1; } + mv -f "$staged" "$binary" + echo "$name installed. Backup: $backup" +done +echo "Launch scripts/run.sh to use the new runtime." diff --git a/tools/install-riot-carboncore-runtime.sh b/tools/install-riot-carboncore-runtime.sh new file mode 100755 index 0000000..de9d6b0 --- /dev/null +++ b/tools/install-riot-carboncore-runtime.sh @@ -0,0 +1,39 @@ +#!/bin/sh +# Install the corrected legacy system-version API into the stopped guest prefix. +# Exit the game and both clients, then run darling shutdown before this script. +set -eu +project_dir=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) +prefix_dir=${DPREFIX:-@DPREFIX@} +guest_home=${RIOT_GUEST_HOME:-$prefix_dir@GUEST_HOME@} +artifact=${RIOT_CARBONCORE_ARTIFACT:-$project_dir/build/riot-CarbonCore} +backup_dir=${RIOT_CARBONCORE_BACKUP_DIR:-$project_dir/build/carboncore-runtime-backups} +sdk_framework="@DARLING_SDK@/System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/CarbonCore.framework" +framework="$guest_home/RuntimeFrameworks/CarbonCore.framework" +prefix_binary="$prefix_dir/System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/CarbonCore.framework/Versions/A/CarbonCore" +[ -f "$artifact" ] || { echo "Build CarbonCore first: $artifact" >&2; exit 1; } +[ -f "$sdk_framework/Versions/A/CarbonCore" ] || { echo "Missing SDK CarbonCore framework" >&2; exit 1; } +mkdir -p "$guest_home/RuntimeFrameworks" "$backup_dir" +if [ ! -d "$framework" ]; then + cp -a "$sdk_framework" "$framework" +fi +for binary in "$framework/Versions/A/CarbonCore" "$prefix_binary"; do + if [ -L "$binary" ]; then echo "Expected a regular binary: $binary" >&2; exit 1; fi + if [ -f "$binary" ] && cmp -s "$artifact" "$binary"; then continue; fi + previous="$binary" + if [ ! -f "$previous" ]; then previous="$sdk_framework/Versions/A/CarbonCore"; fi + old_hash=$(sha256sum "$previous" | cut -d ' ' -f 1) + backup="$backup_dir/CarbonCore-$old_hash" + if [ -e "$backup" ]; then + cmp -s "$previous" "$backup" || { echo "Backup verification failed: $backup" >&2; exit 1; } + else + cp -p "$previous" "$backup" + fi + mkdir -p "$(dirname -- "$binary")" + staged=$(mktemp "$binary.staged.XXXXXX") + trap 'rm -f "$staged"' EXIT HUP INT TERM + install -m755 "$artifact" "$staged" + cmp -s "$artifact" "$staged" || { echo "Staged CarbonCore verification failed" >&2; exit 1; } + mv -f "$staged" "$binary" +done +echo "CarbonCore installed; backups: $backup_dir" +echo "Launch scripts/run.sh to activate it." diff --git a/tools/install-riot-gpu-runtime.sh b/tools/install-riot-gpu-runtime.sh index 2b1d35b..80b9461 100755 --- a/tools/install-riot-gpu-runtime.sh +++ b/tools/install-riot-gpu-runtime.sh @@ -14,6 +14,8 @@ python3 tools/build-m58f341-corevideo.py python3 tools/build-riot-x11-runtime.py python3 tools/build-riot-appkit-runtime.py python3 tools/build-riot-coregraphics-runtime.py +python3 tools/build-riot-coretext-runtime.py +python3 tools/build-riot-carboncore-runtime.py python3 tools/build-riot-corefoundation-runtime.py python3 tools/build-riot-kqueue-runtime.py python3 tools/build-riot-malloc-runtime.py @@ -31,7 +33,7 @@ python3 tools/stop-darling-runtime.py sudo make -C "$project_dir/build/darling-src/build/src/external/darlingserver" install -j12 # Electron's window-visibility correction lives in the AppKit overlay # (NSWindow -occlusionState, cocotron.patch); no Riot binary is modified here. -for name in IOSurface OpenGL QuartzCore CoreVideo CoreAudio AudioToolbox CoreGraphics CoreFoundation Metal; do +for name in IOSurface OpenGL QuartzCore CoreVideo CoreAudio AudioToolbox CoreGraphics CoreText CoreFoundation Metal; do framework="$guest_home/RuntimeFrameworks/$name.framework" if [ ! -d "$framework" ]; then cp -a "@DARLING_SDK@/System/Library/Frameworks/$name.framework" "$framework"; fi binary="$framework/Versions/A/$name" @@ -39,13 +41,13 @@ for name in IOSurface OpenGL QuartzCore CoreVideo CoreAudio AudioToolbox CoreGra case "$name" in IOSurface) source=build/m333-IOSurface;; OpenGL) source=build/m327-OpenGL;; - QuartzCore) - if [ -f build/riot-QuartzCore ]; then source=build/riot-QuartzCore; - else source=build/m336-QuartzCore; fi;; + # Use the artifact rebuilt by this invocation, not an older optional probe build. + QuartzCore) source=build/m336-QuartzCore;; CoreVideo) source=build/m341-CoreVideo;; CoreAudio) source=build/riot-CoreAudio;; AudioToolbox) source=build/riot-AudioToolbox;; CoreGraphics) source=build/riot-CoreGraphics;; + CoreText) source=build/riot-CoreText;; CoreFoundation) source=build/riot-CoreFoundation;; Metal) source=build/riot-Metal;; esac @@ -54,6 +56,8 @@ for name in IOSurface OpenGL QuartzCore CoreVideo CoreAudio AudioToolbox CoreGra # Dyld resolves Versions/A. Preserve the standard outer symlink as well. if [ ! -L "$framework/$name" ]; then rm -f "$framework/$name"; ln -s "Versions/Current/$name" "$framework/$name"; fi done +# Legacy version queries must also reach the corrected absolute subframework. +sh tools/install-riot-carboncore-runtime.sh # Re-exported GL symbols use the native library's absolute install name. # DYLD_LIBRARY_PATH must see this rebuilt wrapper as well as the framework copy. mkdir -p "$guest_home/RuntimeLibraries" diff --git a/tools/riot-client-resize-check.m b/tools/riot-client-resize-check.m new file mode 100644 index 0000000..b778026 --- /dev/null +++ b/tools/riot-client-resize-check.m @@ -0,0 +1,147 @@ +#import +#import +#import +#import +#import +#import +#import +#import +#include +#include +#include +#include +#include +#include + +@interface NSObject (NativeWindowID) +- (unsigned long)windowHandle; +@end +static void pump(double seconds) { + NSDate *end=[NSDate dateWithTimeIntervalSinceNow:seconds]; + while([end timeIntervalSinceNow]>0) { + NSEvent *e=[NSApp nextEventMatchingMask:NSAnyEventMask untilDate:[NSDate distantPast] inMode:NSDefaultRunLoopMode dequeue:YES]; + if(e)[NSApp sendEvent:e]; + [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.005]]; + } +} +static IOSurfaceRef surface(int width,int height,int generation) { + int dims[]={width,height,4,width*4,0x42475241}; + const void *keys[]={kIOSurfaceWidth,kIOSurfaceHeight,kIOSurfaceBytesPerElement,kIOSurfaceBytesPerRow,kIOSurfacePixelFormat},*values[5]; + for(int i=0;i<5;i++)values[i]=CFNumberCreate(NULL,kCFNumberIntType,&dims[i]); + CFDictionaryRef options=CFDictionaryCreate(NULL,keys,values,5,&kCFTypeDictionaryKeyCallBacks,&kCFTypeDictionaryValueCallBacks); + IOSurfaceRef s=IOSurfaceCreate(options);CFRelease(options);for(int i=0;i<5;i++)CFRelease(values[i]); + if(!s)return NULL; + unsigned char *pixels=IOSurfaceGetBaseAddress(s);size_t stride=IOSurfaceGetBytesPerRow(s); + for(int y=0;y=0 && ftruncate(fd,32+64u*1024u*1024u)==0; + static uint32_t sequence=0;sequence+=2; + FixtureFrame head={0x43414652u,sequence|1,width,height,(uint32_t)stride, + 0x42475241u|(software?0x80000000u:0),bytes}; + if(ok)ok=pwrite(fd,&head,sizeof(head),0)==sizeof(head) && + pwrite(fd,IOSurfaceGetBaseAddress(s),bytes,sizeof(head))==(ssize_t)bytes; + head.sequence=sequence; + if(ok)ok=pwrite(fd,&head,sizeof(head),0)==sizeof(head); + if(fd>=0)close(fd);CFRelease(s);return ok; +} +static int checkPublisherHandoff(void) { + char owner[128],readback[128]; + snprintf(owner,sizeof(owner),"/dev/shm/darling-ca-frame-%u-%d",getuid(),getpid()); + snprintf(readback,sizeof(readback),"/dev/shm/darling-ca-frame-%u-%d",getuid(),getpid()+100000000); + NSWindow *w=[[NSWindow alloc] initWithContentRect:NSMakeRect(0,0,1280,720) + styleMask:NSBorderlessWindowMask backing:NSBackingStoreBuffered defer:NO]; + [w setTitle:@"Darling standalone publisher handoff"]; + [w makeKeyAndOrderFront:nil]; + Display *d=XOpenDisplay(NULL);Window xid=[(id)[w platformWindow] windowHandle]; + BOOL pass=d!=NULL; + int sizes[][2]={{1280,720},{1600,900},{1024,576},{1280,720}}; + for(int n=0;pass && n<4;n++) { + int width=sizes[n][0],height=sizes[n][1]; + [w setContentSize:NSMakeSize(width,height)]; + if(n==0)pass=writeFixture(owner,width,height,n,YES); + else pass=writeFixture(owner,262,131,0,YES) && + writeFixture(readback,width,height,n,NO); + BOOL ok=NO;unsigned long pixel=0; + NSDate *deadline=[NSDate dateWithTimeIntervalSinceNow:3.0]; + while(pass && !ok && [deadline timeIntervalSinceNow]>0) { + pump(.05);XSync(d,False); + XImage *im=XGetImage(d,xid,0,0,width,height,AllPlanes,ZPixmap); + if(im){pixel=XGetPixel(im,width-8,height-8)&0xffffff; + ok=pixel==(((unsigned long)(40+n*40)<<16)|0xff);XDestroyImage(im);} + } + printf("PUBLISHER_HANDOFF size=%dx%d bottom_right=%06lx pass=%d\n",width,height,pixel,ok); + pass &=ok; + } + [w orderOut:nil];[w release];if(d)XCloseDisplay(d); + unlink(owner);unlink(readback);return pass?0:1; +} +int main(int argc, const char **argv) { + @autoreleasepool { + [NSApplication sharedApplication]; + if(argc>1 && strcmp(argv[1],"--handoff")==0)return checkPublisherHandoff(); + NSWindow *w=[[NSWindow alloc] initWithContentRect:NSMakeRect(0,0,1280,720) styleMask:NSBorderlessWindowMask backing:NSBackingStoreBuffered defer:NO]; + [w setTitle:@"Darling standalone resize pixels"]; + NSView *view=[w contentView];[view setWantsLayer:YES]; + [w makeKeyAndOrderFront:nil];pump(.15); + Display *d=XOpenDisplay(NULL);Window xid=[(id)[w platformWindow] windowHandle]; + void (*publish)(IOSurfaceRef)=dlsym(RTLD_DEFAULT,"DarlingCAPublishIOSurfacePixels"); + if(!d||!publish)return 2; + BOOL useGPU = argc > 1 && strcmp(argv[1], "--gpu") == 0; + IOSurfaceRef bootstrap=surface(1280,720,0); + [view.layer setContents:(id)bootstrap];publish(bootstrap);pump(.5);CFRelease(bootstrap); + CGLPixelFormatAttribute attrs[]={0};CGLPixelFormatObj format;GLint screens;CGLContextObj gpu; + GLuint texture=0; + if(useGPU) { + if(CGLChoosePixelFormat(attrs,&format,&screens)||CGLCreateContext(format,NULL,&gpu))return 4; + CGLSetCurrentContext(gpu);glGenTextures(1,&texture); + } + BOOL pass=YES; + int sizes[][2]={{1280,720},{1600,900},{1024,576},{1280,720}}; + for(int n=0;n<4;n++) { + int width=sizes[n][0],height=sizes[n][1]; + [w setContentSize:NSMakeSize(width,height)]; + IOSurfaceRef s=surface(width,height,n);if(!s)return 3; + if(useGPU) { + CGLSetCurrentContext(gpu);glBindTexture(GL_TEXTURE_RECTANGLE,texture); + if(CGLTexImageIOSurface2D(gpu,GL_TEXTURE_RECTANGLE,GL_RGBA8,width,height,GL_BGRA,GL_UNSIGNED_INT_8_8_8_8_REV,s,0))return 5; + } + for(int f=0;f<8;f++) { + if(useGPU)CGLSetCurrentContext(gpu); + [view.layer setContents:(id)s]; + if(!useGPU)publish(s); + [CATransaction flush];pump(.1); + } + BOOL ok=NO; + unsigned long pixel=0; + NSDate *deadline=[NSDate dateWithTimeIntervalSinceNow:2.5]; + do { + XSync(d,False); + XImage *im=XGetImage(d,xid,0,0,width,height,AllPlanes,ZPixmap); + if(im) { + pixel=XGetPixel(im,width-8,height-8)&0xffffff; + ok=pixel==(((unsigned long)(40+n*40)<<16)|0xff); + XDestroyImage(im); + } + if(!ok)pump(.05); + } while(!ok && [deadline timeIntervalSinceNow]>0); + printf("RESIZE_PIXEL mode=%s size=%dx%d view=%.0fx%.0f bottom_right=%06lx pass=%d\n", + useGPU?"gpu":"software",width,height,view.frame.size.width,view.frame.size.height,pixel,ok); + pass &=ok;CFRelease(s); + } + if(useGPU){CGLSetCurrentContext(gpu);glDeleteTextures(1,&texture);CGLSetCurrentContext(NULL);CGLReleaseContext(gpu);CGLReleasePixelFormat(format);} + [w orderOut:nil];[w release];XCloseDisplay(d); + return pass?0:1; + } +} diff --git a/tools/riot-coretext-font-list-check.m b/tools/riot-coretext-font-list-check.m new file mode 100644 index 0000000..1e0f537 --- /dev/null +++ b/tools/riot-coretext-font-list-check.m @@ -0,0 +1,53 @@ +#import +#import +#import +#include +#include + +static void verifyFontList(BOOL pass, const char *message) { + if (!pass) { fprintf(stderr, "CORETEXT_FONT_LIST FAIL: %s\n", message); exit(1); } +} + +int main(void) { + @autoreleasepool { + CFArrayRef families = CTFontManagerCopyAvailableFontFamilyNames(); + verifyFontList(families && CFArrayGetCount(families) > 0, "installed font families"); + const void *key = kCTFontFamilyNameAttribute; + CFSetRef mandatory = CFSetCreate(NULL, &key, 1, &kCFTypeSetCallBacks); + unsigned faces = 0; + for (NSString *family in (NSArray *)families) { + NSDictionary *attributes = [NSDictionary dictionaryWithObject:family forKey:(id)key]; + CTFontDescriptorRef query = CTFontDescriptorCreateWithAttributes((CFDictionaryRef)attributes); + CFArrayRef matches = CTFontDescriptorCreateMatchingFontDescriptors(query, mandatory); + verifyFontList(matches && CFArrayGetCount(matches) > 0, "family resolves to installed faces"); + for (CFIndex i = 0; i < CFArrayGetCount(matches); ++i) { + CTFontDescriptorRef match = (CTFontDescriptorRef)CFArrayGetValueAtIndex(matches, i); + CFStringRef name = CTFontDescriptorCopyAttribute(match, kCTFontFamilyNameAttribute); + verifyFontList(name && CFEqual(name, (CFStringRef)family), "mandatory family matches exactly"); + CFStringRef localized = CTFontDescriptorCopyLocalizedAttribute(match, kCTFontFamilyNameAttribute, NULL); + verifyFontList(localized && CFGetTypeID(localized) == CFStringGetTypeID(), "localized lookup with null output"); + CFRelease(localized); CFRelease(name); ++faces; + } + CTFontDescriptorRef first = CTFontDescriptorCreateMatchingFontDescriptor(query, mandatory); + verifyFontList(first != NULL, "single matching descriptor"); + CFRelease(query); CFRelease(matches); + /* The single-match result must outlive its temporary array. */ + CFTypeRef name = CTFontDescriptorCopyAttribute(first, kCTFontNameAttribute); + verifyFontList(name != NULL, "retained single descriptor and normalized PostScript name"); + CFRelease(name); CFRelease(first); + } + NSDictionary *missing = [NSDictionary dictionaryWithObject:@"DarlingMissingFont-7259AEB1" forKey:(id)key]; + CTFontDescriptorRef query = CTFontDescriptorCreateWithAttributes((CFDictionaryRef)missing); + CFArrayRef matches = CTFontDescriptorCreateMatchingFontDescriptors(query, mandatory); + verifyFontList(!matches || !CFArrayGetCount(matches), "missing mandatory family has no substitutes"); + verifyFontList(!CTFontDescriptorCreateMatchingFontDescriptor(query, mandatory), "no single match for missing font"); + CFStringRef language = CFSTR("sentinel"); + verifyFontList(!CTFontDescriptorCopyLocalizedAttribute(query, kCTFontStyleNameAttribute, &language) && !language, + "missing attribute initializes language output"); + if (matches) CFRelease(matches); + CFRelease(query); CFRelease(mandatory); + printf("CORETEXT_FONT_LIST pass=1 families=%ld faces=%u\n", CFArrayGetCount(families), faces); + CFRelease(families); + } + return 0; +} diff --git a/tools/riot-gestalt-version-check.cpp b/tools/riot-gestalt-version-check.cpp new file mode 100644 index 0000000..9277b8b --- /dev/null +++ b/tools/riot-gestalt-version-check.cpp @@ -0,0 +1,27 @@ +#include +#include +#include +#include + +int main() { + char product[64] = {}; + size_t size = sizeof(product); + int expectedMajor = 0, expectedMinor = 0, expectedPatch = 0; + if (sysctlbyname("kern.osproductversion", product, &size, nullptr, 0) || + sscanf(product, "%d.%d.%d", &expectedMajor, &expectedMinor, &expectedPatch) < 1) + return 2; + int major = -1, minor = -1, patch = -1, packed = -1, architecture = -1; + bool pass = !Gestalt(gestaltSystemVersionMajor, &major) && major == expectedMajor && + !Gestalt(gestaltSystemVersionMinor, &minor) && minor == expectedMinor && + !Gestalt(gestaltSystemVersionPatch, &patch) && patch == expectedPatch; + int expectedPacked = ((((expectedMajor / 10) << 4) | (expectedMajor % 10)) << 8) | + ((expectedMinor > 9 ? 9 : expectedMinor) << 4) | (expectedPatch > 9 ? 9 : expectedPatch); + pass &= !Gestalt(gestaltSystemVersion, &packed) && packed == expectedPacked; + pass &= !Gestalt(gestaltSysArchitecture, &architecture) && architecture == gestaltIntel; + pass &= Gestalt(gestaltSystemVersionMajor, nullptr) == paramErr; + int unknown = 123; + pass &= Gestalt(0, &unknown) == unimpErr && unknown == 0; + printf("GESTALT_VERSION product=%s version=%d.%d.%d packed=0x%x architecture=%d pass=%d\n", + product, major, minor, patch, packed, architecture, pass); + return pass ? 0 : 1; +} diff --git a/tools/riot-window-layout-check.m b/tools/riot-window-layout-check.m index 0484421..c65d0ae 100644 --- a/tools/riot-window-layout-check.m +++ b/tools/riot-window-layout-check.m @@ -1,6 +1,36 @@ #import +#import #include +@interface ResizeOrderView : NSView { +@public + NSSize nativeSizeDuringResize; +} +@end +@implementation ResizeOrderView +- (void)setFrame:(NSRect)frame { + if ([self window]) + nativeSizeDuringResize = [(id)[[self window] platformWindow] frame].size; + [super setFrame:frame]; +} +@end + +/* Browser compositors publish their new viewport from setFrameSize:, even + * when AppKit or their parent sets a complete frame rectangle. */ +@interface FrameSizeHookView : NSView { +@public + NSUInteger sizeCallbacks; + NSSize publishedSize; +} +@end +@implementation FrameSizeHookView +- (void)setFrameSize:(NSSize)size { + [super setFrameSize:size]; + sizeCallbacks++; + publishedSize = [self frame].size; +} +@end + int main(void) { @autoreleasepool { [NSApplication sharedApplication]; @@ -21,6 +51,69 @@ int main(void) { pass &= NSEqualSizes(changed, [ordinary frame].size); [full setFrame:NSMakeRect(20, 20, 900, 500) display:NO]; pass &= NSEqualSizes([[full contentView] frame].size, NSMakeSize(900, 500)); + /* Layer geometry must be current before any coordinate conversion or + * input event. Chromium submits compositor surfaces during resize. */ + NSView *view = [[NSView alloc] initWithFrame:NSMakeRect(0, 0, 1280, 720)]; + [view setWantsLayer:YES]; + [view setFrameSize:NSMakeSize(1600, 900)]; + NSSize layerSize = [[view layer] bounds].size; + BOOL layers = NSEqualSizes(layerSize, NSMakeSize(1600, 900)); + [view setBounds:NSMakeRect(10, 20, 800, 450)]; + layers &= NSEqualRects([[view layer] bounds], NSMakeRect(10, 20, 800, 450)); + [view setFrameSize:NSMakeSize(1280, 720)]; + layers &= NSEqualSizes([[view layer] bounds].size, NSMakeSize(640, 360)); + NSView *child = [[NSView alloc] initWithFrame:NSMakeRect(5, 6, 20, 30)]; + [view addSubview:child]; + [child setFrame:NSMakeRect(12, 18, 40, 50)]; + layers &= NSEqualRects([[child layer] frame], NSMakeRect(12, 18, 40, 50)); + printf("VIEW_LAYER_RESIZE initial=%.0fx%.0f pass=%d\n", layerSize.width, layerSize.height, layers); + pass &= layers; + NSWindow *native = [[NSWindow alloc] initWithContentRect:rect styleMask:NSBorderlessWindowMask + backing:NSBackingStoreBuffered defer:NO]; + ResizeOrderView *content = [[ResizeOrderView alloc] initWithFrame:rect]; + [native setContentView:content]; + [native platformWindow]; + [native setFrame:NSMakeRect(20, 20, 1600, 900) display:NO]; + BOOL nativeOrder = NSEqualSizes(content->nativeSizeDuringResize, NSMakeSize(1600, 900)); + printf("NATIVE_RESIZE_ORDER size=%.0fx%.0f pass=%d\n", content->nativeSizeDuringResize.width, + content->nativeSizeDuringResize.height, nativeOrder); + pass &= nativeOrder; + /* Chromium adds titlebar/overlay siblings after the actual content. + * Frame layout must not mistake the last sibling for the content. */ + NSView *overlay = [[NSView alloc] initWithFrame:NSMakeRect(4, 5, 30, 20)]; + [[[native contentView] superview] addSubview:overlay]; + [native setContentSize:NSMakeSize(1280, 720)]; + BOOL siblings = NSEqualSizes([[native contentView] frame].size, NSMakeSize(1280, 720)) && + NSEqualSizes([overlay frame].size, NSMakeSize(30, 20)); + printf("CONTENT_SIBLING_RESIZE content=%.0fx%.0f overlay=%.0fx%.0f pass=%d\n", + [[native contentView] frame].size.width, [[native contentView] frame].size.height, + [overlay frame].size.width, [overlay frame].size.height, siblings); + pass &= siblings; + NSView *host = [[NSView alloc] initWithFrame:NSMakeRect(0, 0, 1280, 720)]; + FrameSizeHookView *viewport = [[FrameSizeHookView alloc] + initWithFrame:NSMakeRect(0, 0, 1280, 720)]; + [viewport setAutoresizingMask:NSViewWidthSizable | NSViewHeightSizable]; + [host addSubview:viewport]; + [host setFrame:NSMakeRect(0, 0, 1600, 900)]; + BOOL hooks = viewport->sizeCallbacks == 1 && + NSEqualSizes(viewport->publishedSize, NSMakeSize(1600, 900)); + [host setFrame:NSMakeRect(0, 0, 1024, 576)]; + hooks &= viewport->sizeCallbacks == 2 && + NSEqualSizes(viewport->publishedSize, NSMakeSize(1024, 576)); + [viewport setFrame:NSMakeRect(3, 4, 800, 450)]; + hooks &= viewport->sizeCallbacks == 3 && + NSEqualSizes(viewport->publishedSize, NSMakeSize(800, 450)); + [viewport setFrameSize:NSMakeSize(640, 360)]; + hooks &= viewport->sizeCallbacks == 4 && + NSEqualSizes(viewport->publishedSize, NSMakeSize(640, 360)); + printf("FRAME_SIZE_HOOK callbacks=%lu published=%.0fx%.0f pass=%d\n", + (unsigned long)viewport->sizeCallbacks, viewport->publishedSize.width, + viewport->publishedSize.height, hooks); + pass &= hooks; + [viewport release]; [host release]; + [overlay release]; + [content release]; [native release]; + [child release]; [view release]; printf("WINDOW_LAYOUT_CHECK full=%.0fx%.0f changed=%.0fx%.0f pass=%d\n", size.width, size.height, changed.width, changed.height, pass); [ordinary release]; [full release]; diff --git a/upstream/manifest.json b/upstream/manifest.json index 016154d..63d753c 100644 --- a/upstream/manifest.json +++ b/upstream/manifest.json @@ -8,7 +8,7 @@ "revision": "60ba801decee7a00782f74f6be4c8ffb013f79ff", "repository": "https://github.com/darlinghq/darling.git", "patch": "patches/darling.patch", - "modified_files": 87, + "modified_files": 88, "new_files": [ "src/CoreAudio/CoreAudio/AudioConverterShim.c", "src/CoreAudio/AudioTrace.h", @@ -56,9 +56,10 @@ "revision": "c8d38d16a9f613d300157bebbab2b9501bc0c274", "repository": "https://github.com/darlinghq/darling-cocotron.git", "patch": "patches/cocotron.patch", - "modified_files": 103, + "modified_files": 104, "new_files": [ - "QuartzCore/include/QuartzCore/CATransformLayer.h" + "QuartzCore/include/QuartzCore/CATransformLayer.h", + "AppKit/NSGeometryTrace.h" ] }, {