diff --git a/docs/TESTING.md b/docs/TESTING.md index 8f266f8..6fff517 100644 --- a/docs/TESTING.md +++ b/docs/TESTING.md @@ -34,8 +34,15 @@ 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 +darling shell env DYLD_FRAMEWORK_PATH=/Users/me/RuntimeFrameworks \ + /Volumes/SystemRoot/path/to/darling-lol/build/riot-display-modes-check ``` +`riot-display-modes-check` (built by `build-riot-display-modes-check.py`) expects `DISPLAY_MODES_CHECK pass=1`. It checks two things for the main display, display 0 and an unknown display: + +- `CGDisplayCopyAllDisplayModes` never returns NULL. League passes the id from `[[window screen] deviceDescription]` (0 while the window is off every screen) straight to `CFArrayGetCount`, and NULL crashed it. +- `CGDisplayScreenSize` returns a nonzero size (`SCREEN_SIZE ... pass=1`). It returned 0×0 for any display with EDID. League divides by this size for its camera aspect ratio, so the view-projection matrix's x row became NaN and every 3D vertex was discarded. The scene then showed only its blue clear colour, and world clicks did nothing. This was found with a RenderDoc capture and vertex shader debugging. + 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. diff --git a/patches/cocotron.patch b/patches/cocotron.patch index 2b64a4c..11dbe0e 100644 --- a/patches/cocotron.patch +++ b/patches/cocotron.patch @@ -5781,7 +5781,7 @@ index db8869c8..6fea5107 100644 + CGContextSetTextPosition(c, savedPosition.x, savedPosition.y); } diff --git a/CoreGraphics/CGDirectDisplay.m b/CoreGraphics/CGDirectDisplay.m -index e213b066..569d77f2 100644 +index e213b066..ea0bc264 100644 --- a/CoreGraphics/CGDirectDisplay.m +++ b/CoreGraphics/CGDirectDisplay.m @@ -53,6 +53,29 @@ static NSDisplay *currentDisplay(void) { @@ -5939,7 +5939,26 @@ index e213b066..569d77f2 100644 } CGDirectDisplayID CGDisplayMirrorsDisplay(CGDirectDisplayID display) { -@@ -341,11 +366,10 @@ static NSData *edidForDisplay(CGDirectDisplayID displayId) { +@@ -319,10 +344,15 @@ CFStringRef CGDisplayModeCopyPixelEncoding(CGDisplayModeRef mode) { + CFArrayRef CGDisplayCopyAllDisplayModes(CGDirectDisplayID displayIndex, + CFDictionaryRef options) + { ++ /* League reads the id from [[window screen] deviceDescription], which is ++ * 0 while the window is off every screen, and passes the result straight ++ * to CFArrayGetCount. Fall back to the main display, then to an empty ++ * array, so callers never get NULL. */ + NSDisplay *display = currentDisplay(); +- if (!display) +- return NULL; +- return (CFArrayRef)[[display modesForScreen: displayIndex - 1] retain]; ++ NSArray *modes = [display modesForScreen: displayIndex - 1]; ++ if (modes == nil) ++ modes = [display modesForScreen: CGMainDisplayID() - 1]; ++ return (CFArrayRef)[(modes ? modes : @[]) retain]; + } + + CGError CGDisplaySetDisplayMode(CGDirectDisplayID displayId, +@@ -341,11 +371,10 @@ static NSData *edidForDisplay(CGDirectDisplayID displayId) { if (!display) return nil; @@ -5953,7 +5972,48 @@ index e213b066..569d77f2 100644 NSData *edid = [screen edid]; if (edid && [edid length] >= 16) return edid; -@@ -474,7 +498,9 @@ CGError CGAssociateMouseAndMouseCursorPosition(boolean_t connected) { +@@ -354,17 +383,33 @@ static NSData *edidForDisplay(CGDirectDisplayID displayId) { + } + + CGSize CGDisplayScreenSize(CGDirectDisplayID display) { +- printf("STUB %s\n", __PRETTY_FUNCTION__); ++ /* League divides by this size to get its camera aspect ratio; a zero size ++ * made every 3D vertex NaN (blue screen, world clicks ignored). Use the ++ * main display for an unknown id (an off-screen window reports 0). */ ++ if (CGDisplayBounds(display).size.width <= 0) ++ display = CGMainDisplayID(); + + NSData *edid = edidForDisplay(display); +- +- if (!edid) { +- CGRect bounds = CGDisplayBounds(display); +- +- return bounds.size; ++ if (edid && [edid length] >= 128) { ++ const uint8_t *bytes = [edid bytes]; ++ /* First detailed timing descriptor: image size in mm. */ ++ double width = bytes[66] | ((bytes[68] & 0xf0) << 4); ++ double height = bytes[67] | ((bytes[68] & 0x0f) << 8); ++ /* DTV timings may encode only an aspect ratio in these fields. */ ++ bool aspectRatioOnly = (width == 16 && height == 9) || ++ (width == 4 && height == 3); ++ /* Basic display parameters: maximum image size in cm. */ ++ if (width <= 0 || height <= 0 || aspectRatioOnly) { ++ width = bytes[21] * 10.0; ++ height = bytes[22] * 10.0; ++ } ++ if (width > 0 && height > 0) ++ return (CGSize){.width = width, .height = height}; + } + +- return (CGSize){.width = 0, .height = 0}; ++ /* No usable EDID size: assume 96 dpi. */ ++ CGSize pixels = CGDisplayBounds(display).size; ++ return (CGSize){.width = pixels.width * 25.4 / 96, .height = pixels.height * 25.4 / 96}; + } + + uint32_t CGDisplaySerialNumber(CGDirectDisplayID displayId) { +@@ -474,7 +519,9 @@ CGError CGAssociateMouseAndMouseCursorPosition(boolean_t connected) { if (!display) return kCGErrorInvalidConnection; @@ -5964,7 +6024,7 @@ index e213b066..569d77f2 100644 return kCGErrorSuccess; } -@@ -500,3 +526,38 @@ CFDictionaryRef CGDisplayCurrentMode(CGDirectDisplayID display) { +@@ -500,3 +547,38 @@ CFDictionaryRef CGDisplayCurrentMode(CGDirectDisplayID display) { size_t CGDisplayModeGetPixelWidth(CGDisplayModeRef mode) { return 0; } diff --git a/tools/build-riot-display-modes-check.py b/tools/build-riot-display-modes-check.py new file mode 100644 index 0000000..0b6392b --- /dev/null +++ b/tools/build-riot-display-modes-check.py @@ -0,0 +1,21 @@ +#!/usr/bin/env python3 +"""Build the CGDisplayCopyAllDisplayModes NULL-result probe against Darling's SDK.""" +from pathlib import Path +import shlex +import subprocess + +root = Path(__file__).resolve().parent.parent +folder = root / 'build/darling-src/build/src/external/cocotron/CoreGraphics' +flags = {} +for line in (folder / 'CMakeFiles/CoreGraphics.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-display-modes-check.o' +subprocess.run(['clang', *flags['C_DEFINES'], *flags['C_INCLUDES'], *flags['C_FLAGS'], + '-Wno-macro-redefined', '-c', str(root / 'tools/riot-display-modes-check.c'), + '-o', str(obj)], check=True) +subprocess.run(['clang', '-target', 'x86_64-apple-macosx11.0', '-fuse-ld=lld', '-nostdlib', + '-isysroot', '@DARLING_SDK@', str(obj), '-lSystem', + '-framework', 'CoreGraphics', '-framework', 'CoreFoundation', + '-o', str(root / 'build/riot-display-modes-check')], check=True) diff --git a/tools/riot-display-modes-check.c b/tools/riot-display-modes-check.c new file mode 100644 index 0000000..b05ed58 --- /dev/null +++ b/tools/riot-display-modes-check.c @@ -0,0 +1,34 @@ +#include +#include + +/* Exported by Darling's CoreGraphics but missing from its CGDirectDisplay.h. */ +CFArrayRef CGDisplayCopyAllDisplayModes(CGDirectDisplayID display, CFDictionaryRef options); + +/* League passes CGDisplayCopyAllDisplayModes' result straight to + * CFArrayGetCount; display id 0 comes from an off-screen window. */ +static int check(CGDirectDisplayID display) { + CFArrayRef modes = CGDisplayCopyAllDisplayModes(display, NULL); + if (!modes) { + printf("DISPLAY_MODES display=%u result=NULL pass=0\n", display); + return 0; + } + printf("DISPLAY_MODES display=%u count=%ld\n", display, (long)CFArrayGetCount(modes)); + CFRelease(modes); + return 1; +} + +/* League divides by the physical size for its camera aspect ratio; {0,0} + * made every 3D vertex NaN. */ +static int checkSize(CGDirectDisplayID display) { + CGSize size = CGDisplayScreenSize(display); + int ok = size.width > 0 && size.height > 0; + printf("SCREEN_SIZE display=%u size=%.0fx%.0fmm pass=%d\n", display, size.width, size.height, ok); + return ok; +} + +int main(void) { + int pass = check(CGMainDisplayID()) & check(0) & check(0xffff); + pass &= checkSize(CGMainDisplayID()) & checkSize(0) & checkSize(0xffff); + printf("DISPLAY_MODES_CHECK pass=%d\n", pass); + return !pass; +}