Merge pull request #19 from lucannez64/pr16-fixes-v2

Integrate runtime fixes with fullscreen and visible-cursor repairs
This commit is contained in:
Kalah authored and GitHub committed 2026-10-17 00:27:45 +02:00
commit e490e2e353
38 files changed
+2038 -523

No files matched your search

+2
View File
@@ -11,3 +11,5 @@ jobs:
with:
python-version: '3.12'
- run: python scripts/validate.py
- run: python tools/riot-input-offline-check.py
- run: python tools/riot-runtime-cleanup-offline-check.py
+3 -1
View File
@@ -18,6 +18,8 @@ The build uses Unix Makefiles because the incremental runtime helpers read CMake
The supported build is x86-64 only (`TARGET_i386=OFF`). The patched OpenDirectory uses Objective-C ivars that the 32-bit fragile ABI cannot compile. Keep the empty `CMAKE_BUILD_TYPE` used by `scripts/build.sh`: selecting `Release` introduces `NDEBUG` while objc4 defines `OBJC_IS_DEBUG_BUILD=1`, causing a debug-macro mismatch. The base patch requests the macOS 10.15 Objective-C runtime code generation to avoid undefined `__NSArray0__struct` / `__NSDictionary0__struct` symbols with newer Clang. Indium handles both the LLVM 22 and LLVM 23 branch opcode names.
`scripts/build.sh` compiles the whole tree with `-O3 -march=native -mtune=generic`, so the binaries only run on CPUs with the build machine's instruction set. Keep `-mtune=generic`: clang 23 tuning for AMD Zen (`-mtune=native` or `-mtune=znver*`) miscompiles Darwin thread-local variables, which crashes launchd at boot (see [troubleshooting](TROUBLESHOOTING.md#launchd-crashes-at-boot-after-changing-compiler-flags)). Two kernel files keep `-O0` because they depend on the unoptimized stack layout: `bsdthread_register.c` and `sigexc.c`.
Linux 5.14 or newer is required for `MADV_POPULATE_WRITE`, used when Darwin's allocator reuses lazily reclaimable pages. The memory installer checks this requirement; do not bypass it.
The runtime installer requires native `pkg-config` entries for `x11`, `x11-xcb`, `xcb-shm` and `xext`, plus the base Darling SDK.
@@ -69,7 +71,7 @@ sh build/runtime/tools/build-riot-libs-runtime.sh
python3 build/runtime/tools/install-riot-libs-runtime.py
```
The builders recompile libsystem_c (618 of its 641 objects), libc++ and libc++abi (38 and 18) and the libsystem_platform string primitives at `-O2`, each object with its own configured defines, includes and per-file options, and relink with the configured link line. `-fno-strict-aliasing` is added for this old BSD C and no `-DNDEBUG`, `-ffast-math` or LTO is. Files whose correctness depends on frame layout, on running in a forked child, or on the floating-point environment keep their configured flags: the fork paths and the stack guard handler in libc and the frame-pointer backtrace walker. libsystem_m is not rebuilt: its sources are built at `-O0` upstream, and an `-O2` rebuild made the Riot client UI render a black window. The libkqueue objects libsystem_c also links belong to the kernel library's target and are left alone. Each builder stops while a League game process is running, because compiling would skew the FPS the user measures.
The builders recompile libsystem_c (618 of its 641 objects), libc++ and libc++abi (38 and 18) and the libsystem_platform string primitives at `-O3 -march=native -mtune=generic`, each object with its own configured defines, includes and per-file options, and relink with the configured link line. `-fno-strict-aliasing` is added for this old BSD C and no `-DNDEBUG`, `-ffast-math` or LTO is. Files whose correctness depends on frame layout, on running in a forked child, or on the floating-point environment keep their configured flags: the fork paths and the stack guard handler in libc and the frame-pointer backtrace walker. libsystem_m is not rebuilt: its sources are built at `-O0` upstream, and an `-O2` rebuild made the Riot client UI render a black window. The libkqueue objects libsystem_c also links belong to the kernel library's target and are left alone. Each builder stops while a League game process is running, because compiling would skew the FPS the user measures. `scripts/install-runtime.sh` builds and installs these overlays, then runs `tools/check-darwin-tlv-calls.py` on everything in `RuntimeLibraries` and `RuntimeFrameworks` and fails if any binary has the thread-local miscompile.
The installer writes the four files into `~/.darling/Users/<user>/RuntimeLibraries`, the directory the launcher puts in `DYLD_LIBRARY_PATH`; dyld searches those directories by leaf name before the install path, which is why an overlay there replaces the prefix and SDK copies of `libsystem_c.dylib`, `libc++.1.dylib`, `libc++abi.dylib` and `libsystem_platform.dylib` without touching either. Every replaced file is backed up first (`build/libs-runtime-backups`), the staged file is verified against the built artifact, and the rename is atomic. The installer never stops a process, so a running guest keeps the library it already loaded; exit the game and both clients and restart the namespace for activation. One command restores the previous state:
+7 -3
View File
@@ -434,7 +434,7 @@ build/test-x11-relative-mouse
The native probe requires the existing per-user AppKit and CoreGraphics overlays
and a running X11/Xwayland display. It rebuilds the changed X11 and CoreGraphics
objects and tests them in a temporary framework overlay, without installing them.
Six `DISPLAY_GEOMETRY ...=pass` lines are required; full output is saved in
Seven `DISPLAY_GEOMETRY ...=pass` lines are required; full output is saved in
`build/pr12-display-check.log`.
It checks the live primary screen's origin, synthetic single-monitor and offset
@@ -445,8 +445,12 @@ relative deltas, both RandR cache-invalidation events, and the root-screen fallb
Warps are intercepted: the probe does not move the desktop pointer or send game
input. Synthetic layouts exercise coordinate conversion and display API behavior;
they do not simulate physical monitor hotplug or validate League rendering.
Fullscreen policy is unchanged. Manually verify game launch, window placement,
click alignment and edge panning on the affected monitor layout before rollout.
The probe also checks that managed borderless windows covering a whole monitor
request EWMH fullscreen, including on secondary monitors, and clear the property
when resized before mapping. Titled windows, unmanaged popups and windows sized
to the panel-reduced work area are excluded. Mapped-window transitions and panel
stacking require manual verification under KDE Plasma/Xwayland. Also verify game
launch, window placement, click alignment and edge panning before rollout.
After installing the tested X11 and CoreGraphics binaries locally, the user
reported that everything was working (2026-10-16). Fullscreen policy was unchanged.
+57 -1
View File
@@ -156,6 +156,20 @@ A separate multi-window path stopped the active async presenter when another dif
- **Prebuilt Swift file contains Git LFS pointer text:** install Git LFS and fetch the original upstream assets; pointer text is not a usable library.
- **Submodule fetch stops with no output right after `src/external/sqlite`:** the next submodule is `src/external/swift`, whose pinned `.lfsconfig` points at the retired `git-lfs.darlinghq.org`. The checkout is blocked on that host, not on sqlite. `prepare.py` redirects that submodule's LFS requests to GitHub. To finish a fetch that was interrupted before any patches were applied, run `git -C build/darling-src -c lfs.url=https://github.com/darlinghq/darling-swift.git/info/lfs submodule update --init --recursive --progress`, then re-run `./scripts/build.sh`. Do not run that submodule update once `prepare.py` has applied patches: it resets every submodule working tree to the pinned revision and discards the patches.
## launchd crashes at boot after changing compiler flags
`darling shell` fails with `Error connecting to shellspawn` and `/tmp/lolsig.log` shows `LOLSIGFAIL pid=1 sig=11 code=1 addr=0x0` in `memmove`, called from `snprintf` in libkqueue's `epoll_event_dump`. The binaries were tuned for AMD Zen: clang 23 with `-mtune=znver*` (also selected by `-march=native` or `-mtune=native` on Zen) loads a thread-local variable's descriptor into a register other than `%rdi`, but dyld's `tlv_get_addr` reads `%rdi`, so every `__thread` and `thread_local` access returns NULL. Rebuild with `-mtune=generic` (the default in `scripts/build.sh` and the overlay builders), and check any binary with:
```sh
python3 tools/check-darwin-tlv-calls.py <binary>...
```
It prints each thread-local access that calls its thunk without the descriptor in `%rdi` and exits 1. A plain `darling shell` boot does not load the `RuntimeLibraries` overlays; to boot the way the launcher does, run `env DYLD_LIBRARY_PATH=/Users/<user>/RuntimeLibraries DYLD_FRAMEWORK_PATH=/Users/<user>/RuntimeFrameworks darling shell /usr/bin/true`.
## Base install refuses to continue while Darling is not running
`sudo cmake --install` prints `Darling currently running for <user>, shutting it down...` several times and then `Darling launchd PID … remains alive after shutdown`. A guest launchd outlived its darlingserver, so `darling shutdown` sees no container to stop. `python3 build/runtime/tools/ensure-darling-runtime.py` stops such a launchd (your own, only when no darlingserver runs); `scripts/run.sh` and `scripts/install-runtime.sh` run it too.
## Lower fight FPS than Windows
The game code itself runs natively under Darling: the x86-64 slice of the universal binary executes unchanged, picks its instruction-set paths with `cpuid` on the real CPU, and sizes its worker pool from the real logical CPU count. What differs from Windows is the path around it. Every Metal call goes through Objective-C, the Metal wrapper, Indium and Vulkan instead of a native Direct3D driver, and kernel services go through the emulation and darlingserver. In an ARAM fight the game's own code now takes most of a frame; see [status](STATUS.md#game-fps-2026-10-1314) for measurements.
@@ -232,10 +246,40 @@ The X11 backend previously called `XSync` after every cursor-shape update, waiti
A later input trace showed League setting the cursor about 190 times and unhiding it about 100 times per second, each time sending the same cursor to the X server. The backend now skips the update when the window already has that cursor. The effect on latency has not been measured.
The backend's relative mouse mode (`CGAssociateMouseAndMouseCursorPosition(false)`) previously warped the pointer to the window center, treated the game's own cursor warps as motion and reported `deltaY` upside down. It now follows macOS: the cursor stays where it is, events carry the physical motion as deltas (positive `deltaY` downwards, also for ordinary motion), the game's warps move the cursor without a delta, and `+[NSEvent mouseLocation]` returns that cursor. On Xwayland a warp takes effect only while the cursor is hidden, so with a visible cursor the pointer is not held in place. `tools/test-x11-relative-mouse.c` checks this logic on the host (`cc tools/test-x11-relative-mouse.c && ./a.out` after `scripts/prepare.py`). League on the tested Hyprland setup never enters this mode, so it has not been confirmed in game.
The backend's relative mouse mode (`CGAssociateMouseAndMouseCursorPosition(false)`) previously warped the pointer to the window center, treated the game's own cursor warps as motion and reported `deltaY` upside down. It now follows macOS: the cursor stays where it is, events carry the physical motion as deltas (positive `deltaY` downwards, also for ordinary motion), the game's warps move the cursor without a delta, and `+[NSEvent mouseLocation]` returns that cursor. On Xwayland a warp takes effect only while the cursor is hidden, so with a visible cursor the pointer is not held in place. `python3 tools/riot-input-offline-check.py` checks the shipped patch on the host without fetching Darling. Add `--xvfb /path/to/Xvfb` to exercise real Xorg protocol on an isolated virtual display; it never uses your desktop display. For Xorg/XLibre (issue #17), relative capture now starts from a fresh `XQueryPointer` snapshot and ignores older queued motion. Warp echoes match both their X request serial and position, so a real motion crossing the anchor cannot be mistaken for an echo. Application warp targets are clipped to the grabbed window in root coordinates, including on a secondary monitor; events, `mouseLocation`, and rewarps share that target. Queued motion before a pending warp does not generate another rewarp. A failed pointer query leaves relative capture suspended, and focus loss releases the grab until the window receives keyboard focus again. Host regression tests cover these paths, but League on the tested Hyprland setup never enters this mode; in-game confirmation on XLibre is still required.
For a manual comparison, keep the scene, FPS cap, display refresh and compositor settings the same. Record whether the delay affects the cursor itself, camera movement, or actions after clicking, and compare the in-game VSync setting on and off. Include the desktop/compositor, X11 or Xwayland session, GPU/driver, monitor refresh and fullscreen/windowed mode in the report. Do not automate game input. The fullscreen cursor-shape problem below is a separate known symptom.
## Click offset grows towards the bottom of the screen
A growing vertical offset has been reported on KDE Plasma/Wayland with Xwayland.
The cause is unconfirmed, including whether it is related to commit `426fb75`.
The borderless fullscreen request has been restored separately; retest alignment
once the game covers the panel.
If it persists, enable the passive input trace before the next manual game launch:
```sh
darling shell touch /tmp/rclol-input-trace
```
The flag is checked once per process, so enable it before launching the game.
During a short manual test, compare clicks near the top, middle and bottom, and
note the direction and approximate size of the error. Include the in-game
resolution and window mode, Plasma's scale setting for each monitor, and the
host output of `xrandr --listmonitors`. Collect the guest log with:
```sh
darling shell cat /tmp/rclol-input-trace.log > rclol-input-trace.log
darling shell rm -f /tmp/rclol-input-trace
```
Removing the flag disables tracing for subsequently started processes. The log
records motion, warps, pointer-position queries and relative-mode transitions;
it can establish whether the relative path is active, but cannot by itself
establish where the game rendered a clicked target. If practical, compare the
same resolution in a smaller window and in borderless fullscreen.
## Cursor stops changing shape in fullscreen
On Hyprland the game's cursor can keep its first shape (for example, it never switches to the attack cursor) until you leave and re-enter fullscreen. Darling sends every change to the X server; what freezes the shape on screen is Hyprland's direct scanout of the fullscreen Xwayland window. Turn it off in `hyprland.conf`:
@@ -247,3 +291,15 @@ render {
```
`hyprctl keyword render:direct_scanout 0` applies it until Hyprland restarts.
### Black layer over a running Metal game
The X11 Chromium presenter searches shared Core Animation frame slots by surface size. Native League game windows now bypass that presenter, so a same-size client frame cannot create a presentation child over the game's Metal drawable. This also disables the remote-frame polling timer for the game while preserving Chromium client presentation.
`tools/riot-remote-frame-ownership-check.m` exercises the cached-frame path without creating a display or window. `tools/build-riot-x11-runtime.py` builds this probe. For an unidentified overlay, `python3 tools/trace-riot-windows.py --output build/riot-window-trace.jsonl` passively records Riot/League windows, including unmanaged windows and child stacking, for one hour. It requires the host `python-xlib` package and does not send input.
If a black fullscreen game has only its normal Metal child and becomes visible after removing and restoring fullscreen, inspect the initial fullscreen transition. Automatic fullscreen now requires the window to cover an active monitor, so temporary 1x1 startup windows and ordinary windowed sizes are not promoted. Explicit Cocoa fullscreen requests are preserved. `riot-fullscreen-geometry-check` (built by the X11 builder) checks the policy against active XRandR monitor geometry using only read requests; the next game launch is still required to validate the startup sequence.
### Visible cursor while relative on Xwayland (PR #18)
When Xwayland does not honour warps, the virtual cursor follows real pointer motion and application warps leave it in place. Expected warp echoes are still discarded using their request serial and coordinates. This keeps clicks and camera edge panning aligned with the visible cursor on GNOME/gamescope. The contributor confirmed this path in a gamescope practice-tool session; plain GNOME and NVIDIA still require manual verification.
+829 -416
View File
File diff suppressed because it is too large. Load diff
+42 -17
View File
@@ -3557,7 +3557,7 @@ diff --git a/src/frameworks/CoreVideo/src/CVDisplayLink.mm b/src/frameworks/Core
index 3e9cba226..99e0a5494 100644
--- a/src/frameworks/CoreVideo/src/CVDisplayLink.mm
+++ b/src/frameworks/CoreVideo/src/CVDisplayLink.mm
@@ -1,38 +1,878 @@
@@ -1,38 +1,887 @@
#include <CoreVideo/CVDisplayLink.h>
+#include <stdint.h>
+#include <string.h>
@@ -4377,17 +4377,26 @@ index 3e9cba226..99e0a5494 100644
+static void *displayClockThread(void *) {
+ struct { long sec, nsec; } deadline;
+ if (__lolsys_syscall(1,(long)&deadline,0,0,0,0,228)<0) return NULL;
+ static uint64_t cvIntervalNS = 0;
+ if (!cvIntervalNS) {
+ const char *hz = getenv("RCLOL_REFRESH_HZ");
+ int rate = hz ? atoi(hz) : 0;
+ cvIntervalNS = (rate > 0 && rate <= 1000) ? 1000000000ull / (uint64_t)rate : (1000000000ull / 60ull);
+ }
+ for (;;) {
+ deadline.nsec += NSEC_PER_SEC / 60;
+ deadline.nsec += cvIntervalNS;
+ if(deadline.nsec>=NSEC_PER_SEC){deadline.nsec-=NSEC_PER_SEC;++deadline.sec;}
+ long result;
+ do { result=__lolsys_syscall(1,1,(long)&deadline,0,0,0,230); } while(result==-4);
+ if(result<0) return NULL;
+ dispatchTimerFire(NULL);
+ struct { long sec, nsec; } now;
+ if(__lolsys_syscall(1,(long)&now,0,0,0,0,228)==0 &&
+ (now.sec>deadline.sec || (now.sec==deadline.sec && now.nsec-deadline.nsec>NSEC_PER_SEC/60))) {
+ deadline.sec=now.sec;deadline.nsec=now.nsec;
+ if(__lolsys_syscall(1,(long)&now,0,0,0,0,228)==0) {
+ long long diffNS = ((long long)now.sec - (long long)deadline.sec) * 1000000000LL + (now.nsec - deadline.nsec);
+ if (diffNS > (long long)cvIntervalNS) {
+ deadline.sec = now.sec;
+ deadline.nsec = now.nsec;
+ }
+ }
+ }
+}
@@ -4444,7 +4453,7 @@ index 3e9cba226..99e0a5494 100644
*displayLinkOut = (CVDisplayLinkRef) self;
return kCVReturnSuccess;
}
@@ -41,35 +881,128 @@ CVReturn CVDisplayLinkStart(CVDisplayLinkRef displayLink)
@@ -41,35 +890,128 @@ CVReturn CVDisplayLinkStart(CVDisplayLinkRef displayLink)
{
if (!displayLink)
return kCVReturnInvalidArgument;
@@ -4584,7 +4593,7 @@ index 3e9cba226..99e0a5494 100644
return kCVReturnSuccess;
}
@@ -91,7 +1024,7 @@ CVReturn CVDisplayLinkSetCurrentCGDisplayFromOpenGLContext(CVDisplayLinkRef disp
@@ -91,7 +1033,7 @@ CVReturn CVDisplayLinkSetCurrentCGDisplayFromOpenGLContext(CVDisplayLinkRef disp
CGDirectDisplayID displayID = [window.screen cgDirectDisplayID];
NSMutableDictionary* self = (NSMutableDictionary*) displayLink;
@@ -4593,7 +4602,7 @@ index 3e9cba226..99e0a5494 100644
forKey: kDirectDisplayArray];
return kCVReturnSuccess;
}
@@ -117,14 +1050,31 @@ CVReturn CVDisplayLinkCreateWithCGDisplay(
@@ -117,14 +1059,31 @@ CVReturn CVDisplayLinkCreateWithCGDisplay(
CGDirectDisplayID displayID,
CV_RETURNS_RETAINED_PARAMETER CVDisplayLinkRef CV_NULLABLE * CV_NONNULL displayLinkOut )
{
@@ -6248,7 +6257,7 @@ index deca16b87..d75d5e22d 100644
set_property(TARGET OpenGL APPEND_STRING PROPERTY LINK_FLAGS
diff --git a/src/frameworks/OpenGL/OpenGL.c b/src/frameworks/OpenGL/OpenGL.c
index ffd7fe054..a74ffc63f 100644
index ffd7fe054..1ff212d34 100644
--- a/src/frameworks/OpenGL/OpenGL.c
+++ b/src/frameworks/OpenGL/OpenGL.c
@@ -2,11 +2,22 @@
@@ -6725,7 +6734,7 @@ index ffd7fe054..a74ffc63f 100644
free(context);
}
@@ -383,9 +608,329 @@ CGLError CGLUnlockContext(CGLContextObj context) {
@@ -383,9 +608,330 @@ CGLError CGLUnlockContext(CGLContextObj context) {
return kCGLNoError;
}
@@ -6948,12 +6957,13 @@ index ffd7fe054..a74ffc63f 100644
+ if(__lolsys_syscall(1,(long)&now,0,0,0,0,228)==0){
+ uint64_t current=(uint64_t)now.tv_sec*1000000000ull+now.tv_nsec;
+ uint64_t deadline=ctx->native_next_present_ns;
+ if(!deadline || (current>deadline && current-deadline>16666667ull))deadline=current;
+ uint64_t paceNS=nativeRefreshIntervalNS();
+ if(!deadline || (current>deadline && current-deadline>paceNS))deadline=current;
+ if(deadline>current){
+ struct timespec wake={deadline/1000000000ull,deadline%1000000000ull};
+ while(__lolsys_syscall(1,1,(long)&wake,0,0,0,230)==-4){}
+ }
+ ctx->native_next_present_ns=deadline+16666667ull;
+ ctx->native_next_present_ns=deadline+paceNS;
+ }
+ }
+ if(getenv("RCLOL_GPU_DIRECT_ASYNC")){int queued=enqueueAsyncPresent(ctx,binding.texture,binding.target,width,height);if(!queued){stopAsyncPresent(ctx);__atomic_store_n(&target->ready,0,__ATOMIC_RELEASE);}CGLReleaseContext(ctx);return queued;}
@@ -7057,7 +7067,7 @@ index ffd7fe054..a74ffc63f 100644
}
CGLError CGLSetParameter(CGLContextObj context, CGLContextParameter parameter, const GLint *value) {
@@ -394,8 +939,9 @@ CGLError CGLSetParameter(CGLContextObj context, CGLContextParameter parameter, c
@@ -394,8 +940,9 @@ CGLError CGLSetParameter(CGLContextObj context, CGLContextParameter parameter, c
if (parameter == kCGLCPSwapInterval)
{
@@ -7068,7 +7078,7 @@ index ffd7fe054..a74ffc63f 100644
if (success)
context->swap_interval = v;
return success ? kCGLNoError : kCGLBadValue;
@@ -417,14 +963,19 @@ CGLError CGLGetParameter(CGLContextObj context, CGLContextParameter parameter, G
@@ -417,14 +964,19 @@ CGLError CGLGetParameter(CGLContextObj context, CGLContextParameter parameter, G
return kCGLNoError;
}
@@ -8418,12 +8428,26 @@ new file mode 100644
+}
diff --git a/src/frameworks/OpenGL/CGLAsyncPresent.inc b/src/frameworks/OpenGL/CGLAsyncPresent.inc
new file mode 100644
index 000000000..e5363c06a
--- /dev/null
+++ b/src/frameworks/OpenGL/CGLAsyncPresent.inc
@@ -0,0 +1,190 @@
@@ -0,0 +1,204 @@
+/* Bounded GPU-only staging: the producer never waits for display cadence.
+ * Three textures cover the currently displayed, pending, and writing frames.
+ * GPU fences order both the completed producer copy and subsequent reuse. */
+
+/* darling-lol: software refresh period for the paced paths, from RCLOL_REFRESH_HZ
+ * (1-1000 Hz). Unset or out of range keeps the previous 60 Hz period. */
+static uint64_t nativeRefreshIntervalNS(void){
+ static uint64_t interval;
+ if(!interval){
+ const char *hz=getenv("RCLOL_REFRESH_HZ");
+ long rate=hz?strtol(hz,NULL,10):0;
+ interval=(rate>0&&rate<=1000)?1000000000ull/(uint64_t)rate:16666667ull;
+ }
+ return interval;
+}
+
+struct DarlingAsyncPresent {
+ CGLContextObj owner;
+ pthread_mutex_t mutex;
@@ -8526,11 +8550,12 @@ new file mode 100644
+ /* Native VSync owns the refresh cadence; avoid a second clock that
+ * can miss the compositor's next refresh after a variable swap wait. */
+ if(nativeVSync)continue;
+ deadline+=16666667ull;
+ uint64_t refreshNS=nativeRefreshIntervalNS();
+ deadline += refreshNS;
+ uint64_t now=nativeMonotonicNS();
+ /* Preserve the refresh phase across small scheduling delays. Reset only
+ * after a full missed period, so lateness does not accumulate as drift. */
+ if(now>deadline && now-deadline>16666667ull){__atomic_fetch_add(&target->reserved[2],1,__ATOMIC_RELAXED);deadline=now;}
+ if(now>deadline && now-deadline>refreshNS){__atomic_fetch_add(&target->reserved[2],1,__ATOMIC_RELAXED);deadline=now;}
+ nativeWaitUntil(deadline);
+ }
+ if(fbo)glDeleteFramebuffers(1,&fbo);
+13 -11
View File
@@ -1931,7 +1931,7 @@ index 23fd2d3..1a48b81 100644
+ return _gpuAddress;
};
diff --git a/src/indium/command-buffer.cpp b/src/indium/command-buffer.cpp
index 43f4a84..46a4f72 100644
index 43f4a84..aaa1acb 100644
--- a/src/indium/command-buffer.cpp
+++ b/src/indium/command-buffer.cpp
@@ -1,3 +1,8 @@
@@ -2268,7 +2268,7 @@ index 43f4a84..46a4f72 100644
auto encoder = std::make_shared<Indium::PrivateComputeCommandEncoder>(shared_from_this(), descriptor);
{
std::scoped_lock lock(_mutex);
@@ -91,10 +354,237 @@ std::shared_ptr<Indium::ComputeCommandEncoder> Indium::PrivateCommandBuffer::com
@@ -91,10 +354,239 @@ std::shared_ptr<Indium::ComputeCommandEncoder> Indium::PrivateCommandBuffer::com
return computeCommandEncoder(ComputePassDescriptor { {}, dispatchType });
};
@@ -2400,10 +2400,12 @@ index 43f4a84..46a4f72 100644
+ for (;;) {
+ {
+ std::unique_lock<std::mutex> lock(wakeMutex);
+ submitBatchWake().wait_for(lock, std::chrono::milliseconds(2));
+ /* darling-lol perf: 500µs batch window balances submission
+ * merging with latency; the old 2ms added visible input lag. */
+ submitBatchWake().wait_for(lock, std::chrono::microseconds(500));
+ }
+ std::unique_lock<std::mutex> queueLock(device->graphicsQueueMutex());
+ if (!state.items.empty() && std::chrono::steady_clock::now() - state.since >= std::chrono::milliseconds(2)) flushSubmitBatchLocked();
+ if (!state.items.empty() && std::chrono::steady_clock::now() - state.since >= std::chrono::microseconds(500)) flushSubmitBatchLocked();
+ }
+ }).detach();
+}
@@ -2506,7 +2508,7 @@ index 43f4a84..46a4f72 100644
for (const auto& encoder: _commandEncoders) {
if (auto renderEncoder = std::dynamic_pointer_cast<PrivateRenderCommandEncoder>(encoder)) {
@@ -110,16 +600,28 @@ void Indium::PrivateCommandBuffer::commit() {
@@ -110,16 +602,28 @@ void Indium::PrivateCommandBuffer::commit() {
}
}
@@ -2536,7 +2538,7 @@ index 43f4a84..46a4f72 100644
// for the event loop
auto timelineSemaphore = _privateDevice->getWrappedTimelineSemaphore();
@@ -138,14 +640,49 @@ void Indium::PrivateCommandBuffer::commit() {
@@ -138,14 +642,49 @@ void Indium::PrivateCommandBuffer::commit() {
readWriteTextures.insert(readWriteTextures.end(), readWrite.begin(), readWrite.end());
}
@@ -2589,7 +2591,7 @@ index 43f4a84..46a4f72 100644
}
std::vector<VkSemaphoreSubmitInfo> signalInfos;
@@ -202,11 +739,13 @@ void Indium::PrivateCommandBuffer::commit() {
@@ -202,11 +741,13 @@ void Indium::PrivateCommandBuffer::commit() {
auto privateTexture = std::dynamic_pointer_cast<PrivateTexture>(texture);
const auto& presentSema = presentationSemaphores[i];
@@ -2608,7 +2610,7 @@ index 43f4a84..46a4f72 100644
handleTextureSemaphores(privateTexture);
}
@@ -215,7 +754,15 @@ void Indium::PrivateCommandBuffer::commit() {
@@ -215,7 +756,15 @@ void Indium::PrivateCommandBuffer::commit() {
// but it also leaves the resources for this command buffer tied up, essentially becoming a memory leak.
// i've observed this in the cube example, and it happens more than once (because the example display semaphore is exhausted and never signaled).
// UPDATE: upon further testing, it seems that this only occurs when the view is off-screen/hidden. weird.
@@ -2625,7 +2627,7 @@ index 43f4a84..46a4f72 100644
{
std::unique_lock lock(self->_mutex);
self->_completed = true;
@@ -234,8 +781,47 @@ void Indium::PrivateCommandBuffer::commit() {
@@ -234,8 +783,47 @@ void Indium::PrivateCommandBuffer::commit() {
// the handlers may have captured a reference to us (from their surrounding scope), so clear out handlers so those references go away
self->_scheduledHandlers.clear();
self->_completedHandlers.clear();
@@ -2673,7 +2675,7 @@ index 43f4a84..46a4f72 100644
commandBufferInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_SUBMIT_INFO;
commandBufferInfo.commandBuffer = _commandBuffer;
@@ -252,23 +838,41 @@ void Indium::PrivateCommandBuffer::commit() {
@@ -252,23 +840,41 @@ void Indium::PrivateCommandBuffer::commit() {
// the Device constructor tries to choose command queues that support as many operations as possible, but it's possible
// that a particular device only supports certain operations on certain queues (e.g. maybe it only supports transfer operations
// on an exclusive queue that doesn't support graphics or compute).
@@ -2720,7 +2722,7 @@ index 43f4a84..46a4f72 100644
};
void Indium::PrivateCommandBuffer::presentDrawable(std::shared_ptr<Drawable> drawable) {
@@ -307,9 +911,12 @@ void Indium::PrivateCommandBuffer::addCompletedHandler(std::function<void(std::s
@@ -307,9 +913,12 @@ void Indium::PrivateCommandBuffer::addCompletedHandler(std::function<void(std::s
};
void Indium::PrivateCommandBuffer::waitUntilCompleted() {
+35 -7
View File
@@ -29,7 +29,7 @@ index e14e7ea..024981b 100644
#target_link_libraries(system_kernel system_duct platform_static32 platform_static64)
make_fat(system_kernel)
diff --git a/darling/src/libsystem_kernel/emulation/CMakeLists.txt b/darling/src/libsystem_kernel/emulation/CMakeLists.txt
index 5b2e765..64d35f5 100644
index 5b2e765..40308eb 100644
--- a/darling/src/libsystem_kernel/emulation/CMakeLists.txt
+++ b/darling/src/libsystem_kernel/emulation/CMakeLists.txt
@@ -6,13 +6,12 @@ enable_language(ASM)
@@ -68,6 +68,18 @@ index 5b2e765..64d35f5 100644
src/xnu_syscall/bsd/impl/misc/proc_info.c
src/xnu_syscall/bsd/impl/misc/ptrace.c
src/xnu_syscall/bsd/impl/misc/reboot.c
@@ -359,6 +360,11 @@ set(emulation_sources
${CMAKE_BINARY_DIR}/src/external/darlingserver/src/rpc.c
)
+# darling-lol: keep these at -O0 even when the tree is built optimized. bsdthread_register.c
+# sets %rsp from the thread argument in inline assembly and sigexc.c runs on the fault and
+# suspend signal stacks; both depend on the unoptimized stack layout.
+set_source_files_properties(src/xnu_syscall/bsd/impl/bsdthread/bsdthread_register.c
+ src/linux_premigration/signal/sigexc.c PROPERTIES COMPILE_OPTIONS "-O0")
set_source_files_properties(src/legacy_path/signal/duct_signals.c PROPERTIES COMPILE_FLAGS "-nostdinc")
set_source_files_properties(src/legacy_path/signal/sigexc.c PROPERTIES OBJECT_DEPENDS ${CMAKE_CURRENT_BINARY_DIR}/src/legacy_path/signal/exc.h)
set_source_files_properties(${CMAKE_BINARY_DIR}/src/external/darlingserver/src/rpc.c PROPERTIES
diff --git a/darling/src/libsystem_kernel/emulation/include/common/base.h b/darling/src/libsystem_kernel/emulation/include/common/base.h
index 207dae6..f5e96db 100644
--- a/darling/src/libsystem_kernel/emulation/include/common/base.h
@@ -4812,7 +4824,7 @@ index c1b2d50..4e19963 100644
return ret;
}
diff --git a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/mach/impl/mach_traps.c b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/mach/impl/mach_traps.c
index b998456..2783a4a 100644
index b998456..c6281e6 100644
--- a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/mach/impl/mach_traps.c
+++ b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/mach/impl/mach_traps.c
@@ -12,9 +12,28 @@
@@ -5894,10 +5906,19 @@ index b998456..2783a4a 100644
return 0;
}
@@ -696,6 +1613,41 @@ kern_return_t syscall_thread_switch_impl(
.tv_sec = 0,
.tv_nsec = 1000000
@@ -692,12 +1609,53 @@ kern_return_t syscall_thread_switch_impl(
int option,
mach_msg_timeout_t option_time)
{
+ /* option_time is in milliseconds (mach_msg_timeout_t). */
struct timespec tv = {
- .tv_sec = 0,
- .tv_nsec = 1000000
+ .tv_sec = option_time / 1000,
+ .tv_nsec = (long) (option_time % 1000) * 1000000
};
- // Sleep for 1ms
- __linux_nanosleep(&tv, &tv);
+
+ /* darling-lol: opt-in trace (host file /tmp/rclol-select-trace) of who calls thread_switch and with which option:
+ * every 50th call writes thread id, option, time and up to 6 caller addresses (frame-pointer walk) to
@@ -5933,9 +5954,16 @@ index b998456..2783a4a 100644
+ }
+ }
+
// Sleep for 1ms
__linux_nanosleep(&tv, &tv);
+ /* darling-lol perf: a plain yield (option_time == 0) only gives up the CPU; sleeping a
+ * full millisecond there is longer than a frame budget at high refresh rates. Timed
+ * switches (spin locks pass 1 ms) keep sleeping for the requested time. */
+ if (option_time == 0)
+ __linux_sched_yield_site("THRSW");
+ else
+ __linux_nanosleep(&tv, &tv);
// TODO: we could implement this with yield_to() in LKM
return KERN_SUCCESS;
diff --git a/libsyscall/CMakeLists.txt b/libsyscall/CMakeLists.txt
index 57cebad..3014255 100755
--- a/libsyscall/CMakeLists.txt
+9 -1
View File
@@ -9,7 +9,15 @@ python3 scripts/prepare.py
# so Release's -DNDEBUG trips its "mismatch in debug-ness macros" #error.
# x86-64 only: the patched OpenDirectory declares ivars in @implementation, which the
# 32-bit (fragile ABI) slice cannot compile, and the macOS client is 64-bit.
# darling-lol perf: inject -O3 and -march=native even with empty build type,
# so the base Darling runtime is not compiled at -O0. The empty build type
# is required by objc4's debug-ness macro check.
# -mtune=generic, not native: clang 23 tuning for AMD Zen (znver*) passes the thread-local
# variable descriptor in the wrong register on Darwin targets, so every __thread access
# returns NULL (launchd crashed in libkqueue's epoll_event_dump).
cmake -S build/darling-src -B build/darling-src/build -G 'Unix Makefiles' \
-DCMAKE_BUILD_TYPE= -DTARGET_i386=OFF -DCMAKE_INSTALL_PREFIX=/usr/local -DENABLE_METAL=ON
-DCMAKE_BUILD_TYPE= -DTARGET_i386=OFF -DCMAKE_INSTALL_PREFIX=/usr/local -DENABLE_METAL=ON \
-DCMAKE_C_FLAGS="-O3 -march=native -mtune=generic -pipe" \
-DCMAKE_CXX_FLAGS="-O3 -march=native -mtune=generic -pipe"
cmake --build build/darling-src/build --parallel "${JOBS:-$(nproc)}"
echo 'Build complete. Install the base runtime with: sudo cmake --install build/darling-src/build'
+7
View File
@@ -0,0 +1,7 @@
#!/bin/sh
# Create/update GNOME desktop entry and launcher icon.
set -eu
repo=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
cd "$repo"
python3 scripts/configure.py >/dev/null
python3 build/runtime/tools/riot-desktop-entry.py
+11
View File
@@ -5,6 +5,8 @@ repo=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
cd "$repo"
python3 scripts/configure.py
if [ -d build/venv/bin ]; then PATH="$repo/build/venv/bin:$PATH"; export PATH; fi
# A server killed by the base install leaves a stale .init.pid that makes darling shell fail.
python3 build/runtime/tools/ensure-darling-runtime.py
# Initialize the normal user-owned prefix before installing overrides.
darling shell /usr/bin/true
# The prefix has no /private/tmp of its own, so the guest falls through to the root-owned
@@ -14,4 +16,13 @@ chmod 1777 "$HOME/.darling/private/tmp"
python3 build/runtime/tools/stop-darling-runtime.py
sh build/runtime/tools/install-riot-presentation-runtime.sh
sh build/runtime/tools/install-riot-gpu-runtime.sh
# darling-lol perf: build and install optimized libc/libc++/libplatform.
if [ -f build/runtime/tools/build-riot-libs-runtime.sh ]; then
sh build/runtime/tools/build-riot-libs-runtime.sh
python3 build/runtime/tools/install-riot-libs-runtime.py
fi
python3 build/runtime/tools/install-darling-system-profiler.py
# Every guest process loads these overlays; refuse a set the compiler broke (check-darwin-tlv-calls.py).
find "$HOME/.darling/Users/$(id -un)/RuntimeLibraries" "$HOME/.darling/Users/$(id -un)/RuntimeFrameworks" \
-type f ! -name '*.before-*' ! -name '*.rt-*' ! -name '*.llvm*' -size +4k -print0 |
xargs -0 python3 build/runtime/tools/check-darwin-tlv-calls.py
+5
View File
@@ -4,4 +4,9 @@ repo=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
cd "$repo"
python3 scripts/configure.py >/dev/null
if [ -d build/venv/bin ]; then PATH="$repo/build/venv/bin:$PATH"; export PATH; fi
# darling-lol perf: optionally wrap with gamemoderun for automatic
# CPU governor, nice level, and scheduler optimizations.
if [ "${RCLOL_GAMEMODE:-auto}" != "0" ] && command -v gamemoderun >/dev/null 2>&1; then
exec gamemoderun sh build/runtime/tools/run-riot-client.sh
fi
exec sh build/runtime/tools/run-riot-client.sh
+8 -1
View File
@@ -4,7 +4,10 @@ import ast,json,re,subprocess,sys
from pathlib import Path
from common import ROOT
subprocess.run([sys.executable,str(ROOT/'scripts/configure.py')],check=True)
files=subprocess.check_output(['git','-C',str(ROOT),'ls-files','-z']).decode().split('\0')
try:
files=subprocess.check_output(['git','-C',str(ROOT),'ls-files','-z'],stderr=subprocess.DEVNULL).decode().split('\0')
except (OSError, subprocess.CalledProcessError):
files=[str(p.relative_to(ROOT)) for p in ROOT.rglob('*') if p.is_file() and not any(part.startswith('.') or part in ['build','__pycache__'] for part in p.relative_to(ROOT).parts)]
errors=[]
secret_patterns=[r'gh[pousr]_[A-Za-z0-9]{30,}',r'AKIA[0-9A-Z]{16}',r'eyJ[A-Za-z0-9_-]{20,}\.[A-Za-z0-9_-]{20,}\.[A-Za-z0-9_-]{20,}',r'-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----']
for name in filter(None,files):
@@ -28,6 +31,10 @@ manifest=json.loads((ROOT/'upstream/manifest.json').read_text())
for c in manifest['components']:
if not re.fullmatch('[0-9a-f]{40}',c['revision']):errors.append(c['path']+': invalid pinned revision')
if not (ROOT/c['patch']).is_file():errors.append(c['path']+': patch missing')
else:
parsed=subprocess.run(['git','apply','--numstat','-'],input=(ROOT/c['patch']).read_text(),
text=True,capture_output=True)
if parsed.returncode:errors.append(c['patch']+': '+parsed.stderr.strip())
if errors:
print('\n'.join(errors));raise SystemExit(1)
print('PASS: tracked source audit, rendered Python/shell syntax and',len(manifest['components']),'pinned patch entries')
+2 -2
View File
@@ -15,7 +15,7 @@ link = shlex.split((framework / 'CMakeFiles/AppKit.dir/link.txt').read_text())
for name in ['NSResponder', 'NSApplication', 'NSWindow', 'NSDisplay', 'NSView', 'NSThemeFrame', 'NSTextInput', 'NSBitmapImageRep', 'NSEvent.subproj/NSEvent_mouse']:
obj = project / f'build/riot-{Path(name).name}.o'
subprocess.run(['clang', *flags['C_DEFINES'], *flags['C_INCLUDES'], *flags['C_FLAGS'],
'-O2', '-Wno-macro-redefined', '-c',
'-O3', '-march=native', '-mtune=generic', '-Wno-macro-redefined', '-c',
str(project / f'build/darling-src/src/external/cocotron/AppKit/{name}.m'),
'-o', str(obj)], check=True)
link[link.index(f'CMakeFiles/AppKit.dir/{name}.m.o')] = str(obj)
@@ -24,7 +24,7 @@ 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']:
probe = project / f'build/{name}.o'
subprocess.run(['clang', *flags['C_DEFINES'], *flags['C_INCLUDES'], *flags['C_FLAGS'],
'-O2', '-fblocks', '-Wno-macro-redefined', '-c',
'-O3', '-march=native', '-mtune=generic', '-fblocks', '-Wno-macro-redefined', '-c',
str(project / f'tools/{name}.m'), '-o', str(probe)], check=True)
subprocess.run(['clang', '-target', 'x86_64-apple-macosx11.0', '-fuse-ld=lld', '-nostdlib',
'-isysroot', '@DARLING_SDK@', str(probe), '-lSystem',
+4 -4
View File
@@ -1,11 +1,11 @@
#!/usr/bin/env python3
"""Rebuild libc++abi and libc++ with -O2. The CMake tree has no build type, so the stock objects (and the earlier
"""Rebuild libc++abi and libc++ with -O3. The CMake tree has no build type, so the stock objects (and the earlier
single-file libc++abi rebuild) are -O0: std::type_info::operator==, __dynamic_cast, shared_ptr internals ran 3-5x slower
than they should (about 2% of the League main thread). Outputs build/riot-opt-libc++abi.dylib / riot-opt-libc++.1.dylib.
"""
from concurrent.futures import ThreadPoolExecutor
from pathlib import Path
import re, shlex, subprocess, sys
import os, re, shlex, subprocess, sys
root = Path(__file__).resolve().parent.parent
resource_dir = subprocess.check_output(['clang', '-print-resource-dir'], text=True).strip()
def current_clang(values):
@@ -26,9 +26,9 @@ def build(lib, objdir, linkdir, output):
jobs.append((rel, obj)); link[i] = str(obj)
def compile_one(job):
rel, obj = job
subprocess.run(['clang++', *flags['CXX_DEFINES'], *flags['CXX_INCLUDES'], *flags['CXX_FLAGS'], '-O2', '-DNDEBUG',
subprocess.run(['clang++', *flags['CXX_DEFINES'], *flags['CXX_INCLUDES'], *flags['CXX_FLAGS'], '-O3', '-march=native', '-mtune=generic', '-DNDEBUG',
'-c', str(src / rel), '-o', str(obj)], check=True)
with ThreadPoolExecutor(max_workers=8) as pool:
with ThreadPoolExecutor(max_workers=os.cpu_count() or 8) as pool:
list(pool.map(compile_one, jobs))
link[link.index('-o') + 1] = str(root / 'build' / output)
subprocess.run(link, cwd=folder, check=True)
+9 -6
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
"""Rebuild libc++ and libc++abi with -O2.
"""Rebuild libc++ and libc++abi with -O3 -march=native -mtune=generic.
The CMake tree has no build type, so both libraries are clang's -O0 output: the
game imports 131 functions from libc++.1.dylib (out-of-line std::string,
@@ -15,6 +15,7 @@ generation only. Outputs build/riot-libc++.1.dylib and build/riot-libc++abi.dyli
"""
from concurrent.futures import ThreadPoolExecutor
from pathlib import Path
import os
import re
import shlex
import subprocess
@@ -47,20 +48,22 @@ def build(library, object_dir, link_dir, output):
obj = root / ("build/riot-opt-%s-%s.o" % (library, relative.replace("/", "_")))
jobs.append((relative, obj))
link[index] = str(obj)
print(library, "recompiling", len(jobs), "objects at -O2", flush=True)
print(library, "recompiling", len(jobs), "objects at -O3", flush=True)
def compile_one(job):
relative, obj = job
subprocess.run(["clang++", *flags["CXX_DEFINES"], *flags["CXX_INCLUDES"], *flags["CXX_FLAGS"],
"-O2", "-fno-strict-aliasing", "-c", str(source / relative), "-o", str(obj)],
cxx_flags = [f for f in flags["CXX_FLAGS"] if f not in ("-g", "-O0", "-O1", "-O2")]
subprocess.run(["clang++", *flags["CXX_DEFINES"], *flags["CXX_INCLUDES"], *cxx_flags,
"-O3", "-march=native", "-mtune=generic", "-fno-strict-aliasing",
"-c", str(source / relative), "-o", str(obj)],
check=True, capture_output=True)
riot_build_gate.wait_until_free()
with ThreadPoolExecutor(max_workers=6) as pool:
with ThreadPoolExecutor(max_workers=os.cpu_count() or 8) as pool:
list(pool.map(compile_one, jobs))
link[link.index("-o") + 1] = str(root / "build" / output)
subprocess.run(link, cwd=folder, check=True)
print("built build/" + output, len(jobs), "objects at -O2", flush=True)
print("built build/" + output, len(jobs), "objects at -O3", flush=True)
if __name__ == "__main__":
+10 -6
View File
@@ -19,6 +19,7 @@ def configured_flags(directory):
if line.startswith('C_') and ' = ' in line:
key,value=line.split(' = ',1)
result[key]=[re.sub(r'/usr/lib/clang/\d+/include',resource_dir+'/include',v) for v in shlex.split(value)]
if key == 'C_FLAGS': result[key].append('-mtune=generic')
return result
kqueue_flags=configured_flags(build/'src/external/libkqueue/CMakeFiles/kqueue.dir')
emulation_flags=configured_flags(kernel/'emulation/CMakeFiles/emulation.dir')
@@ -29,8 +30,8 @@ syscall_flags=configured_flags(kernel/'libsyscall/CMakeFiles/libsyscall.dir')
kqueue_flags={**kqueue_flags,'C_FLAGS':[*kqueue_flags['C_FLAGS'],'-O2','-fno-strict-aliasing']}
kqueue_sources=[value.split('/kqueue.dir/',1)[1][:-2] for value in objects if '/kqueue.dir/' in value and value.endswith('.c.o')]
assert kqueue_sources, 'Configured kqueue objects missing'
# The configured emulation target is a plain development build: its flags.make
# carries no -O flag, so every object recompiled with `emulation_flags` is -O0.
# The configured emulation flags carry scripts/build.sh's -O3; the -O2 appended below
# comes last and wins, and the stack-layout-dependent files below pin -O0.
# These files are on the per-call path of the guest (BSD trap dispatch, read/
# write, network, kqueue, psynch/ulock, clocks, madvise), so rebuild them
# optimized. -fno-strict-aliasing matches the other optimized rebuilds in this
@@ -56,13 +57,16 @@ repairs += [('src/xnu_syscall/bsd/impl/'+name,repair_flags,
# configured forced-include header, without changing unrelated kernel objects.
emulation_root=project/'build/darling-src/src/external/xnu/darling/src/libsystem_kernel/emulation'
rpc_flags={**emulation_flags,'C_FLAGS':[*emulation_flags['C_FLAGS'],'-O2','-fno-omit-frame-pointer']}
# bsdthread_register.c keeps emulation_flags (-O0): its pthread/wqueue entry
# bsdthread_register.c stays at -O0: its pthread/wqueue entry
# wrappers jump into _thread_start/_start_wqthread through inline assembly that
# sets %rsp from the thread argument, i.e. it depends on the stack layout.
# Signal entry with stack-layout-dependent frame walking stays unoptimized on
# purpose (emulation_flags): it runs on the fault/suspend signal stacks.
repairs += [('src/xnu_syscall/bsd/impl/bsdthread/bsdthread_register.c',emulation_flags,emulation_root)]
repairs += [('src/linux_premigration/signal/sigexc.c',emulation_flags,emulation_root)]
# purpose: it runs on the fault/suspend signal stacks.
# The configured flags are optimized now (scripts/build.sh), so pin -O0 explicitly, as the
# emulation CMakeLists does for these two files.
unoptimized_flags={**emulation_flags,'C_FLAGS':[*emulation_flags['C_FLAGS'],'-O0']}
repairs += [('src/xnu_syscall/bsd/impl/bsdthread/bsdthread_register.c',unoptimized_flags,emulation_root)]
repairs += [('src/linux_premigration/signal/sigexc.c',unoptimized_flags,emulation_root)]
repairs += [('src/xnu_syscall/mach/impl/mach_traps.c',rpc_flags,emulation_root)]
# User-space clock (per-CPU TSC calibration), its gettimeofday entry and the cached kern.boottime.
repairs += [('src/linux_premigration/ext/clock_gettime.c',rpc_flags,emulation_root)]
+5 -6
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
"""Rebuild libsystem_c (libc) with -O2.
"""Rebuild libsystem_c (libc) with -O3 -march=native -mtune=generic.
The CMake tree is configured with an empty build type, so the installed
libsystem_c.dylib is clang's -O0 output except for the three string files
@@ -7,7 +7,7 @@ libplatform marks optimized. The game imports 122 functions from this library
(stdio, string, stdlib, locale, time) and it is on the path of every frame;
this builder takes each object's own compile command out of the generated
build.make, keeps every define, include and per-file option it has, replaces
the optimization level with -O2 and relinks the result with the configured
the optimization level with -O3 and relinks the result with the configured
link line. -fno-strict-aliasing matches what the existing optimized rebuilds
use for this ten-year-old BSD C.
@@ -75,7 +75,7 @@ def source_of(head, relative):
def compile_command(target_folder, source, object_path):
"""The configured compile command for one object, with the object path and
the dependency flags taken out and -O2 -fno-strict-aliasing added."""
the dependency flags taken out and -O3 -fno-strict-aliasing added."""
for line in (target_folder / "build.make").read_text().splitlines():
if "$(C_DEFINES)" not in line or " -c " not in line or str(source) not in line:
continue
@@ -100,9 +100,8 @@ def compile_command(target_folder, source, object_path):
skip = True
continue
cleaned.append(token)
if "-O2" not in cleaned:
cleaned.append("-O2")
cleaned.append("-fno-strict-aliasing")
cleaned = [t for t in cleaned if t not in ("-g", "-O0", "-O1", "-O2")]
cleaned.extend(["-O3", "-march=native", "-mtune=generic", "-fno-strict-aliasing"])
return cleaned
return None
+1 -1
View File
@@ -1,7 +1,7 @@
#!/bin/sh
# Build every overlay of the optimized library batch, in order.
#
# Each builder recompiles the component's own objects at -O2 with the flags
# Each builder recompiles the component's own objects at -O3 -march=native -mtune=generic with the flags
# the configured build gives them and relinks with the configured link line;
# none of them runs a full cmake build. The builders stop while a League game
# process is running, because a compile job would skew the FPS the user is
+4 -1
View File
@@ -1,9 +1,12 @@
#!/usr/bin/env python3
"""Build the Metal layer presentation bridge and its window regression check."""
from pathlib import Path
import json
import shlex
import subprocess
root = Path(__file__).resolve().parent.parent
config_path = root / 'build/config.json'
sdk = json.loads(config_path.read_text())['sdk'] if config_path.exists() else '/usr/local/libexec/darling'
folder = root / 'build/darling-src/build/src/external/cocotron/QuartzCore'
source = root / 'build/darling-src/src/external/cocotron/QuartzCore'
flags = {}
@@ -28,6 +31,6 @@ obj = root / 'build/riot-metal-presentation-check.o'
subprocess.run(['clang++', *flags['CXX_DEFINES'], *flags['CXX_INCLUDES'], *flags['CXX_FLAGS'],
'-c', str(root / 'tools/riot-metal-presentation-check.mm'), '-o', str(obj)], check=True)
subprocess.run(['clang++', '-target', 'x86_64-apple-macosx11.0', '-fuse-ld=lld', '-nostdlib',
'-isysroot', '@DARLING_SDK@', str(obj), '-lSystem', '-lc++', '-lobjc',
'-isysroot', sdk, str(obj), '-lSystem', '-lc++', '-lobjc',
'-framework', 'AppKit', '-framework', 'OpenGL', '-framework', 'Foundation', str(root / 'build/riot-Metal'),
str(root / 'build/riot-QuartzCore'), '-o', str(root / 'build/riot-metal-presentation-check')], check=True)
+4 -3
View File
@@ -1,7 +1,7 @@
#!/usr/bin/env python3
"""Optimize configured string helpers, preserving every other platform object.
Every src/string/generic source is recompiled at -O2 -fno-builtin (and
Every src/string/generic source is recompiled at -O3 -fno-builtin (and
-fno-strict-aliasing for this old BSD C) with the object library's own defines
and includes, then relinked into build/riot-libsystem_platform.dylib. The AVX2
and REP paths for memcpy/memmove/memset/bzero/memcmp/memchr live in those same
@@ -27,10 +27,11 @@ for original in list(link):
if not original.startswith('CMakeFiles/platform_obj.dir/src/string/generic/') or not original.endswith('.c.o'):continue
relative=original.split('platform_obj.dir/',1)[1].removesuffix('.o')
obj=root/('build/riot-platform-'+Path(relative).name+'.o')
subprocess.run(['clang',*flags['C_DEFINES'],*flags['C_INCLUDES'],*flags['C_FLAGS'],'-O2','-fno-builtin','-fno-strict-aliasing','-c',str(root/'build/darling-src/src/external/libplatform'/relative),'-o',str(obj)],check=True)
c_flags = [f for f in flags['C_FLAGS'] if f not in ('-g', '-O0', '-O1', '-O2')]
subprocess.run(['clang',*flags['C_DEFINES'],*flags['C_INCLUDES'],*c_flags,'-O3','-march=native','-mtune=generic','-fno-builtin','-fno-strict-aliasing','-c',str(root/'build/darling-src/src/external/libplatform'/relative),'-o',str(obj)],check=True)
link[link.index(original)]=str(obj)
recompiled+=1
link[link.index('-o')+1]=str(root/'build/riot-libsystem_platform.dylib')
subprocess.run(link,cwd=folder,check=True)
print('built build/riot-libsystem_platform.dylib with',recompiled,'string objects at -O2')
print('built build/riot-libsystem_platform.dylib with',recompiled,'string objects at -O3')
p=root/'build/m810-platform-runtime';p.mkdir(exist_ok=True);shutil.copy2(root/'build/riot-libsystem_platform.dylib',p/'libsystem_platform.dylib')
+20 -2
View File
@@ -16,7 +16,7 @@ for line in (framework / 'CMakeFiles/X11_backend.dir/flags.make').read_text().sp
link = shlex.split((framework / 'CMakeFiles/X11_backend.dir/link.txt').read_text())
for name in ('X11Display', 'X11Window'):
obj = project / f'build/riot-{name}.o'
subprocess.run(['clang', *flags['C_DEFINES'], *flags['C_INCLUDES'], *flags['C_FLAGS'], '-O2', '-Wno-macro-redefined', '-c', str(project / f'build/darling-src/src/external/cocotron/AppKit/X11.backend/{name}.m'), '-o', str(obj)], check=True)
subprocess.run(['clang', *flags['C_DEFINES'], *flags['C_INCLUDES'], *flags['C_FLAGS'], '-O3', '-march=native', '-mtune=generic', '-Wno-macro-redefined', '-c', str(project / f'build/darling-src/src/external/cocotron/AppKit/X11.backend/{name}.m'), '-o', str(obj)], check=True)
link[link.index(f'CMakeFiles/X11_backend.dir/X11.backend/{name}.m.o')] = str(obj)
link[link.index('-o') + 1] = str(project / 'build/riot-X11')
subprocess.run(link, cwd=framework, check=True)
@@ -25,9 +25,27 @@ print('Built isolated X11 backend')
# Exercise actual backend modifier transitions without compositor shortcuts.
probe = project / 'build/riot-modifier-check.o'
subprocess.run(['clang', *flags['C_DEFINES'], *flags['C_INCLUDES'], *flags['C_FLAGS'],
'-O2', '-Wno-macro-redefined', '-c', str(project / 'tools/riot-modifier-check.m'),
'-O3', '-march=native', '-mtune=generic', '-Wno-macro-redefined', '-c', str(project / 'tools/riot-modifier-check.m'),
'-o', str(probe)], check=True)
subprocess.run(['clang', '-target', 'x86_64-apple-macosx11.0', '-fuse-ld=lld',
'-nostdlib', '-isysroot', sdk, str(probe),
'-lSystem', '-framework', 'AppKit', '-framework', 'Foundation', '-lobjc',
'-o', str(project / 'build/riot-modifier-check')], check=True)
# Verify that cached Chromium presentation cannot cover a native game drawable.
probe = project / 'build/riot-remote-frame-ownership-check.o'
subprocess.run(['clang', *flags['C_DEFINES'], *flags['C_INCLUDES'], *flags['C_FLAGS'],
'-O3', '-march=native', '-mtune=generic', '-UNDEBUG', '-Wno-macro-redefined', '-c',
str(project / 'tools/riot-remote-frame-ownership-check.m'), '-o', str(probe)], check=True)
subprocess.run(['clang', '-target', 'x86_64-apple-macosx11.0', '-fuse-ld=lld',
'-nostdlib', '-isysroot', sdk, str(probe), '-lSystem', '-lobjc',
'-framework', 'Foundation', '-framework', 'CoreFoundation',
'-o', str(project / 'build/riot-remote-frame-ownership-check')], check=True)
probe = project / 'build/riot-fullscreen-geometry-check.o'
subprocess.run(['clang', *flags['C_DEFINES'], *flags['C_INCLUDES'], *flags['C_FLAGS'],
'-O3', '-march=native', '-mtune=generic', '-UNDEBUG', '-Wno-macro-redefined', '-c',
str(project / 'tools/riot-fullscreen-geometry-check.m'), '-o', str(probe)], check=True)
subprocess.run(['clang', '-target', 'x86_64-apple-macosx11.0', '-fuse-ld=lld',
'-nostdlib', '-isysroot', sdk, str(probe), '-lSystem', '-lobjc', '-framework', 'Foundation',
'-o', str(project / 'build/riot-fullscreen-geometry-check')], check=True)
+62
View File
@@ -0,0 +1,62 @@
#!/usr/bin/env python3
"""Fail if a Mach-O binary calls a thread-local variable's thunk without its descriptor in %rdi.
dyld's tlv_get_addr reads the descriptor from %rdi. clang 23 tuning for AMD Zen (-mtune=znver*,
also implied by -march=native on Zen) can load the descriptor into another register and call
through it, leaving %rdi unrelated: the thunk then returns NULL and the first write crashes.
Usage: check-darwin-tlv-calls.py BINARY... ; exits 1 and lists every bad access it finds.
"""
import re
import subprocess
import sys
def thread_vars_ranges(path):
"""Address ranges of the __thread_vars sections, which hold the TLV descriptors."""
output = subprocess.run(['llvm-objdump', '--macho', '--section-headers', path],
capture_output=True, text=True, check=True).stdout
ranges = []
for line in output.splitlines():
fields = line.split()
if len(fields) >= 4 and fields[1] == '__thread_vars':
start, size = int(fields[3], 16), int(fields[2], 16)
ranges.append((start, start + size))
return ranges
def bad_accesses(path):
with open(path, 'rb') as binary:
if binary.read(4) not in (b'\xcf\xfa\xed\xfe', b'\xca\xfe\xba\xbe'):
return []
ranges = thread_vars_ranges(path)
if not ranges:
return []
disassembly = subprocess.run(['llvm-objdump', '-d', '--no-show-raw-insn', path],
capture_output=True, text=True, check=True).stdout
load = re.compile(r'(?:leaq|movq)\s+-?0x[0-9a-f]+\(%rip\), %(\w+)\s+#+ 0x([0-9a-f]+)')
found, function, pending = [], '?', {}
for line in disassembly.splitlines():
if line.endswith('>:'):
function, pending = line.split('<', 1)[-1][:-2], {}
continue
match = load.search(line)
if match and any(start <= int(match[2], 16) < end for start, end in ranges):
pending[match[1]] = line.strip()
continue
call = re.search(r'callq\s+\*\(%(\w+)\)', line)
if call and call[1] in pending:
if call[1] != 'rdi':
found.append('%s: %s | %s' % (function, pending[call[1]], line.strip()))
pending.pop(call[1])
return found
status = 0
for binary in sys.argv[1:]:
problems = bad_accesses(binary)
if problems:
status = 1
print('%s: %d thread-local access(es) without the descriptor in %%rdi' % (binary, len(problems)))
for problem in problems[:20]:
print(' ' + problem)
sys.exit(status)
+3 -2
View File
@@ -41,7 +41,8 @@ subprocess.run(['clang', *flags_for(frameworks / 'AppKit', 'X11_backend'),
subprocess.run(['clang', '-target', 'x86_64-apple-macosx11.0', '-fuse-ld=lld',
'-nostdlib', '-isysroot', config['sdk'], str(probe) + '.o',
'-lSystem', '-framework', 'AppKit', '-framework', 'Foundation',
'-framework', 'CoreGraphics', '-lobjc', '-o', str(probe)], check=True)
'-framework', 'CoreGraphics', '-lobjc',
'-L' + str(Path(config['sdk']) / 'usr/lib/native'), '-lX11', '-o', str(probe)], check=True)
original = Path(config['prefix']) / config['guest_home'].lstrip('/') / 'RuntimeFrameworks'
with tempfile.TemporaryDirectory(prefix='pr12-display-frameworks-', dir=root / 'build') as directory:
@@ -66,5 +67,5 @@ with tempfile.TemporaryDirectory(prefix='pr12-display-frameworks-', dir=root / '
output.seek(0)
lines = [line.rstrip() for line in output if line.startswith('DISPLAY_GEOMETRY')]
print('\n'.join(lines))
if result.returncode or len(lines) != 6:
if result.returncode or len(lines) != 7:
raise SystemExit(f'Display probe failed ({result.returncode}); inspect {log}')
+77 -5
View File
@@ -1,16 +1,16 @@
#!/usr/bin/env python3
"""Clear only a dead Darling server's stale PID marker before startup."""
"""Clear a dead Darling server's stale PID marker, and stale shared buffers once no server runs."""
import os
from pathlib import Path
import signal
import time
prefix = Path(os.environ.get('DPREFIX', str(Path.home() / '.darling')))
def recover_marker(prefix):
marker = prefix / '.init.pid'
try:
pid = int(marker.read_text().strip())
except (OSError, ValueError):
raise SystemExit(0)
if pid <= 0:
raise SystemExit(0)
pid = 0
if pid > 0:
status = Path('/proc') / str(pid) / 'status'
try:
state = next(line.split()[1] for line in status.read_text().splitlines() if line.startswith('State:'))
@@ -22,3 +22,75 @@ except (OSError, StopIteration):
if dead:
os.replace(marker, prefix / f'.init.pid.stale-{pid}-{time.time_ns()}')
print(f'Recovered stale Darling server marker for PID {pid}', flush=True)
def any_server_running(proc_root=Path('/proc')):
"""True if any darlingserver runs, for any prefix: /dev/shm buffers are not per prefix."""
for proc in proc_root.iterdir():
if not proc.name.isdigit():
continue
try:
if (proc / 'comm').read_text().strip() == 'darlingserver':
return True
except PermissionError:
# A hidden process might be a server; shared buffers are global, so fail closed.
return True
except OSError:
pass
return False
def stop_orphaned_launchd(proc_root=Path('/proc')):
"""A guest launchd that outlived its darlingserver cannot be shut down with `darling shutdown`
and makes the base install (tools/shutdown-user.sh) refuse to continue."""
uid = os.getuid()
for proc in proc_root.iterdir():
if not proc.name.isdigit():
continue
try:
if proc.stat().st_uid != uid or (proc / 'cmdline').read_bytes().rstrip(b'\0') != b'/sbin/launchd':
continue
os.kill(int(proc.name), signal.SIGKILL)
print(f'Stopped orphaned Darling launchd PID {proc.name}', flush=True)
except (OSError, ValueError):
pass
def stop_riot_crashpad(proc_root=Path('/proc')):
"""A Riot crash handler outliving its server holds the old session's ports; spare every other app's."""
uid = os.getuid()
for proc in proc_root.iterdir():
if not proc.name.isdigit():
continue
try:
if not (proc / 'comm').read_text().startswith('chrome_crashpad') or proc.stat().st_uid != uid:
continue
argv = (proc / 'cmdline').read_bytes().split(b'\0')
executable = argv[0].decode(errors='replace')
# Match the executable's bundle, not arbitrary arguments (a browser may mention Riot).
if any(bundle in executable for bundle in (
'/Riot Client.app/', '/League of Legends.app/', '/League of Legends (TM) Client.app/'
)):
os.kill(int(proc.name), signal.SIGTERM)
except (OSError, ValueError):
pass
def remove_owned(paths):
uid = os.getuid()
for path in paths:
try:
if path.is_file() and path.stat().st_uid == uid:
path.unlink()
except OSError:
pass
def main():
prefix = Path(os.environ.get('DPREFIX', str(Path.home() / '.darling')))
recover_marker(prefix)
# Frame and present buffers left by a killed session confuse the next one's publishers.
if not any_server_running():
stop_orphaned_launchd()
stop_riot_crashpad()
remove_owned((prefix / 'private/tmp').glob('.com.riotgames.*'))
remove_owned(Path('/dev/shm').glob('darling-*'))
if __name__ == '__main__':
main()
+4
View File
@@ -32,7 +32,11 @@ build/riot-host-message-check
cc -shared -fPIC -O2 -Wall -Wextra -Wl,-soname,"$project_dir/build/libdarling-xshm.so" tools/m58f299-xshm.c -o build/libdarling-xshm.so.next -pthread $(pkg-config --cflags --libs x11 x11-xcb xcb-shm xext)
python3 tools/stop-darling-runtime.py
# Host fileports own SCM_RIGHTS descriptors and must die with their last sender.
if sudo -n true 2>/dev/null; then
sudo make -C "$project_dir/build/darling-src/build/src/external/darlingserver" install -j12
else
echo 'Skipped the darlingserver install (needs sudo); the base install already ships it: sudo cmake --install build/darling-src/build' >&2
fi
# 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 CoreText CoreFoundation Metal; do
+2 -2
View File
@@ -12,11 +12,11 @@ make -C "$build_dir/src/external/cfnetwork/src" CFNetwork -j8
make -C "$build_dir/src/external/configd/SystemConfiguration.fproj" SystemConfiguration -j8
make -C "$build_dir/src/external/cocotron/QuartzCore" QuartzCore -j8
make -C "$build_dir/src/external/cocotron/AppKit" AppKit X11_backend -j8
cc -shared -fPIC -O2 -Wall -Wextra \
cc -shared -fPIC -O3 -march=native -mtune=generic -Wall -Wextra \
"-Wl,-soname,$project_dir/build/libdarling-xshm.so" \
"$project_dir/tools/m58f299-xshm.c" -o "$project_dir/build/libdarling-xshm.next.so" \
-pthread $(pkg-config --cflags --libs x11 x11-xcb xcb-shm xext)
clang -target x86_64-apple-macosx11.0 -fuse-ld=lld -dynamiclib -nostdlib \
clang -target x86_64-apple-macosx11.0 -fuse-ld=lld -dynamiclib -nostdlib -O3 -march=native -mtune=generic \
-isysroot @DARLING_SDK@ \
-I"$source_dir/src/startup/mldr/elfcalls" \
"-DDARLING_XSHM_HOST_LIBRARY=\"$project_dir/build/libdarling-xshm.so\"" \
+3 -1
View File
@@ -256,7 +256,9 @@ static void *pumpFrames(void *arg) {
XFlush(f->display);
}
}
deadline.tv_nsec += 2000000;
/* darling-lol perf: 500µs poll interval (2000 Hz) reduces average frame
* detection latency from 1ms to 0.25ms at negligible CPU cost. */
deadline.tv_nsec += 500000;
if (deadline.tv_nsec >= 1000000000) {
deadline.tv_nsec -= 1000000000;
deadline.tv_sec++;
+202
View File
@@ -0,0 +1,202 @@
#!/usr/bin/env python3
"""Create and install the GNOME / FreeDesktop desktop entry and launcher icon."""
import os
import shutil
import subprocess
import sys
from pathlib import Path
PROJECT_ROOT = Path("@PROJECT_ROOT@")
HOST_HOME = Path("@HOST_HOME@")
DPREFIX = Path("@DPREFIX@")
SVG_ICON_CONTENT = """<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="512" height="512">
<defs>
<linearGradient id="bgGrad" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#091428"/>
<stop offset="50%" stop-color="#0a1c36"/>
<stop offset="100%" stop-color="#030811"/>
</linearGradient>
<linearGradient id="borderGrad" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#f0e6d2"/>
<stop offset="40%" stop-color="#c8aa6e"/>
<stop offset="80%" stop-color="#785a28"/>
<stop offset="100%" stop-color="#c89b3c"/>
</linearGradient>
<linearGradient id="goldGrad" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#fff2c6"/>
<stop offset="25%" stop-color="#f0e6d2"/>
<stop offset="50%" stop-color="#c8aa6e"/>
<stop offset="85%" stop-color="#9a7a35"/>
<stop offset="100%" stop-color="#785a28"/>
</linearGradient>
<linearGradient id="goldHighlight" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" stop-color="#ffffff" stop-opacity="0.6"/>
<stop offset="100%" stop-color="#c8aa6e" stop-opacity="0"/>
</linearGradient>
<filter id="glow" x="-20%" y="-20%" width="140%" height="140%">
<feGaussianBlur stdDeviation="6" result="blur"/>
<feComposite in="SourceGraphic" in2="blur" operator="over"/>
</filter>
<filter id="shadow" x="-20%" y="-20%" width="140%" height="140%">
<feDropShadow dx="0" dy="8" stdDeviation="14" flood-color="#000000" flood-opacity="0.85"/>
</filter>
</defs>
<!-- Background Shield / Squircle -->
<rect x="24" y="24" width="464" height="464" rx="104" fill="url(#bgGrad)" stroke="url(#borderGrad)" stroke-width="12" filter="url(#shadow)"/>
<rect x="40" y="40" width="432" height="432" rx="90" fill="none" stroke="#c8aa6e" stroke-width="2" stroke-opacity="0.4"/>
<!-- Crest Emblem -->
<g filter="url(#glow)">
<path d="M172 108 L242 108 L242 316 L364 316 L352 388 L156 388 L156 124 Z"
fill="url(#goldGrad)" stroke="#463714" stroke-width="3"/>
<path d="M 180 120 L 230 120 L 230 250 L 180 200 Z"
fill="url(#goldHighlight)"/>
<path d="M 242 328 L 348 328 L 340 376 L 172 376 L 172 356 L 242 356 Z"
fill="#c8aa6e" opacity="0.45"/>
<polygon points="256,76 264,96 284,96 268,108 274,128 256,116 238,128 244,108 228,96 248,96" fill="#f0e6d2"/>
</g>
</svg>
"""
def extract_best_png_from_icns(icns_path: Path) -> bytes | None:
"""Extract the highest-resolution embedded PNG from an Apple ICNS file."""
try:
data = icns_path.read_bytes()
if not data.startswith(b"icns"):
return None
offset = 8
best_png = None
best_len = 0
while offset + 8 <= len(data):
size = int.from_bytes(data[offset + 4 : offset + 8], "big")
if size < 8 or offset + size > len(data):
break
chunk = data[offset + 8 : offset + size]
if chunk.startswith(b"\x89PNG\r\n\x1a\n") and len(chunk) > best_len:
best_png = chunk
best_len = len(chunk)
offset += size
return best_png
except Exception:
return None
def find_candidate_icns() -> Path | None:
"""Find any available Riot or League .icns icon files."""
candidates = [
*list((PROJECT_ROOT / "build/installer").glob("*League of Legends*.app/Contents/Resources/*.icns")),
*list(DPREFIX.glob("Users/Shared/Riot Games/Riot Client.app/Contents/Resources/*.icns")),
*list(DPREFIX.glob("Applications/Riot Client.app/Contents/Resources/*.icns")),
*list(DPREFIX.glob("Applications/League of Legends.app/Contents/Resources/*.icns")),
]
for c in candidates:
if c.is_file() and c.stat().st_size > 1024:
return c
return None
def main():
icons_base = HOST_HOME / ".local/share/icons"
apps_base = HOST_HOME / ".local/share/applications"
desktop_dir = HOST_HOME / "Desktop"
scalable_dir = icons_base / "hicolor/scalable/apps"
png_dir = icons_base / "hicolor/512x512/apps"
scalable_dir.mkdir(parents=True, exist_ok=True)
png_dir.mkdir(parents=True, exist_ok=True)
apps_base.mkdir(parents=True, exist_ok=True)
# 1. Install scalable SVG icon
svg_target = scalable_dir / "league-of-legends.svg"
svg_target.write_text(SVG_ICON_CONTENT)
shutil.copyfile(svg_target, icons_base / "league-of-legends.svg")
# 2. Extract or generate PNG icon
png_target = png_dir / "league-of-legends.png"
icns_file = find_candidate_icns()
extracted = False
if icns_file:
png_data = extract_best_png_from_icns(icns_file)
if png_data:
png_target.write_bytes(png_data)
shutil.copyfile(png_target, icons_base / "league-of-legends.png")
extracted = True
print(f"Extracted official icon from {icns_file.name} to {png_target}")
if not extracted and not png_target.exists():
# Try to rasterize SVG via rsvg-convert or magick
rsvg = shutil.which("rsvg-convert")
magick = shutil.which("magick") or shutil.which("convert")
if rsvg:
subprocess.run([rsvg, "-w", "512", "-h", "512", str(svg_target), "-o", str(png_target)], check=False)
if png_target.exists():
shutil.copyfile(png_target, icons_base / "league-of-legends.png")
elif magick:
subprocess.run([magick, "-background", "none", "-resize", "512x512", str(svg_target), str(png_target)], check=False)
if png_target.exists():
shutil.copyfile(png_target, icons_base / "league-of-legends.png")
# 3. Create .desktop file
run_script = PROJECT_ROOT / "scripts/run.sh"
exec_path = str(run_script).replace("%", "%%")
exec_cmd = f'"{exec_path}"' if " " in exec_path else exec_path
desktop_content = f"""[Desktop Entry]
Version=1.0
Type=Application
Name=League of Legends
GenericName=MOBA Game
Comment=Play League of Legends on Linux with darling-lol
Exec={exec_cmd}
Path={PROJECT_ROOT}
Icon=league-of-legends
Terminal=false
Categories=Game;ActionGame;StrategyGame;
Keywords=riot;league;lol;darling;games;
StartupWMClass=Riot Client
StartupNotify=false
"""
desktop_file = apps_base / "league-of-legends.desktop"
desktop_file.write_text(desktop_content)
os.chmod(desktop_file, 0o755)
print(f"Created application launcher: {desktop_file}")
target_desktop_dirs = []
try:
res = subprocess.run(["xdg-user-dir", "DESKTOP"], capture_output=True, text=True, check=False)
if res.returncode == 0 and res.stdout.strip():
d = Path(res.stdout.strip())
if d.is_dir() and d not in target_desktop_dirs:
target_desktop_dirs.append(d)
except Exception:
pass
for cand in [HOST_HOME / "Desktop"]:
if cand.is_dir() and cand not in target_desktop_dirs:
target_desktop_dirs.append(cand)
for ddir in target_desktop_dirs:
desktop_shortcut = ddir / "league-of-legends.desktop"
desktop_shortcut.write_text(desktop_content)
os.chmod(desktop_shortcut, 0o755)
gio = shutil.which("gio")
if gio:
subprocess.run([gio, "set", str(desktop_shortcut), "metadata::trusted", "true"], check=False)
print(f"Created desktop shortcut: {desktop_shortcut}")
# 4. Refresh icon and desktop databases
update_desktop = shutil.which("update-desktop-database")
if update_desktop:
subprocess.run([update_desktop, str(apps_base)], check=False)
update_icons = shutil.which("gtk-update-icon-cache")
if update_icons:
subprocess.run([update_icons, "-f", "-t", str(icons_base / "hicolor")], check=False)
print("GNOME application launcher and icon installed successfully.")
if __name__ == "__main__":
main()
+49 -3
View File
@@ -8,6 +8,7 @@
#import "../build/darling-src/src/external/cocotron/AppKit/X11.backend/X11Display.h"
#import "../build/darling-src/src/external/cocotron/AppKit/X11.backend/X11Window.h"
#include <X11/extensions/Xrandr.h>
#include <X11/Xatom.h>
#include <assert.h>
#include <string.h>
@@ -20,6 +21,27 @@ extern CGError CGWarpMouseCursorPosition(CGPoint position);
- (void)postXEvent:(XEvent *)event;
@end
@interface X11Window (FullscreenProbe)
- (BOOL)wantsFullscreenWithStyleMask:(NSUInteger)mask rootFrame:(O2Rect)frame;
@end
@interface FullscreenProbeWindow : NSWindow
@end
@implementation FullscreenProbeWindow
- (BOOL)canBecomeKeyWindow { return YES; }
@end
static BOOL fullscreenProperty(X11Display *display, X11Window *window) {
Display *d = [display display];
Atom actual, fs = XInternAtom(d,"_NET_WM_STATE_FULLSCREEN",False);
int format; unsigned long count, remaining; unsigned char *data = NULL;
assert(XGetWindowProperty(d,[window windowHandle],XInternAtom(d,"_NET_WM_STATE",False),
0,32,False,XA_ATOM,&actual,&format,&count,&remaining,&data) == Success);
BOOL found = NO;
assert(actual == XA_ATOM && format == 32 && remaining == 0);
for (unsigned long i=0; i<count; ++i) if (((Atom *)data)[i] == fs) found=YES;
if (data) XFree(data);
return found;
}
static void *slot(id obj, const char *name) {
Ivar ivar = class_getInstanceVariable([obj class], name);
assert(ivar);
@@ -102,16 +124,40 @@ int main(void) {
assert(CGGetDisplaysWithRect(CGRectMake(3900,-800,10,10),4,ids,&count) == 0 && count == 1 && ids[0] == 3);
puts("DISPLAY_GEOMETRY reordered_ids_bounds_enumeration_queries=pass");
// Use cached monitor frames so the policy is tested for the reporter's layout.
FullscreenProbeWindow *game = [[FullscreenProbeWindow alloc]
initWithContentRect:NSMakeRect(0,0,3840,2160) styleMask:NSBorderlessWindowMask
backing:NSBackingStoreBuffered defer:NO];
X11Window *gameNative = (id)[game platformWindow];
assert(!*(BOOL *)slot(gameNative,"_overrideRedirect"));
assert(fullscreenProperty(d,gameNative));
[gameNative setFrame:NSMakeRect(0,0,3840,2100)]; // panel-sized work area is not a monitor
assert(!fullscreenProperty(d,gameNative));
[gameNative setFrame:NSMakeRect(-3840,102,3840,2160)]; // secondary monitor
assert(fullscreenProperty(d,gameNative));
NSRect full = [d rootRectForFrame:NSMakeRect(0,0,3840,2160)];
assert(![gameNative wantsFullscreenWithStyleMask:NSTitledWindowMask rootFrame:full]);
assert([gameNative wantsFullscreenWithStyleMask:NSWindowStyleMaskFullScreen
rootFrame:NSMakeRect(1,2,640,480)]);
*(BOOL *)slot(gameNative,"_overrideRedirect") = YES;
assert(![gameNative wantsFullscreenWithStyleMask:0 rootFrame:full]);
*(BOOL *)slot(gameNative,"_overrideRedirect") = NO;
[gameNative setFrame:NSMakeRect(20,30,800,600)];
assert(!fullscreenProperty(d,gameNative));
[game release];
puts("DISPLAY_GEOMETRY borderless_fullscreen_monitor_and_resize_policy=pass");
Method method = class_getInstanceMethod([d class],@selector(warpPointerToRootX:y:));
assert(method);
IMP original = method_setImplementation(method,(IMP)captureWarp);
X11RelativeMouse *relative = slot(d,"_relativeMouse");
X11RelativeMouseBegin(relative,3900,1000);
assert(CGWarpMouseCursorPosition(CGPointMake(100,200)) == 0);
assert(relative->virtualX == 3940 && relative->virtualY == 1010);
BOOL honoured = !*(BOOL *)slot(d,"_isXwayland") || *(BOOL *)slot(d,"_cursorHidden");
assert(relative->virtualX == (honoured ? 3940 : 3900) && relative->virtualY == (honoured ? 1010 : 1000));
NSPoint location = [d mouseLocation];
assert(location.x == 100 && location.y == 1960);
X11RelativeMouseMotion motion = X11RelativeMouseHandleMotion(relative,3907,1009,false);
assert(location.x == (honoured ? 100 : 60) && location.y == (honoured ? 1960 : 1970));
X11RelativeMouseMotion motion = X11RelativeMouseHandleMotion(relative,3907,1009,false,NextRequest([d display]));
assert(motion.deltaX == 7 && motion.deltaY == 9);
X11RelativeMouseEnd(relative);
assert(CGWarpMouseCursorPosition(CGPointMake(100,200)) == 0);
+46
View File
@@ -0,0 +1,46 @@
/* Read-only XRandR geometry probe: never creates, maps or changes a window. */
#import <Foundation/Foundation.h>
#include <X11/Xlib.h>
#include <X11/extensions/Xrandr.h>
#include <dlfcn.h>
#include <assert.h>
#include <stdio.h>
int main(int argc, char **argv) {
@autoreleasepool {
assert(argc == 2);
void *backend = dlopen(argv[1], RTLD_LAZY);
assert(backend);
BOOL (*covers)(Display *, NSRect) = (BOOL (*)(Display *, NSRect))dlsym(backend,"X11RootRectCoversMonitor");
assert(covers);
assert(!covers(NULL, NSMakeRect(0,0,1,1)));
assert(!covers(NULL, NSMakeRect(0,0,0,0)));
void *xlib = dlopen("/usr/lib/native/libX11.dylib", RTLD_LAZY);
void *randr = dlopen("/usr/lib/native/libXRandR.dylib", RTLD_LAZY);
assert(xlib && randr);
Display *(*openDisplay)(const char *) = dlsym(xlib,"XOpenDisplay");
int (*closeDisplay)(Display *) = dlsym(xlib,"XCloseDisplay");
Window (*rootWindow)(Display *) = dlsym(xlib,"XDefaultRootWindow");
XRRScreenResources *(*getResources)(Display *,Window) = dlsym(randr,"XRRGetScreenResourcesCurrent");
XRRCrtcInfo *(*getCrtc)(Display *,XRRScreenResources *,RRCrtc) = dlsym(randr,"XRRGetCrtcInfo");
void (*freeCrtc)(XRRCrtcInfo *) = dlsym(randr,"XRRFreeCrtcInfo");
void (*freeResources)(XRRScreenResources *) = dlsym(randr,"XRRFreeScreenResources");
assert(openDisplay && closeDisplay && rootWindow && getResources && getCrtc && freeCrtc && freeResources);
Display *d = openDisplay(NULL); assert(d);
XRRScreenResources *resources = getResources(d,rootWindow(d)); assert(resources);
unsigned checked = 0;
for (int i=0; i<resources->ncrtc; ++i) {
XRRCrtcInfo *c = getCrtc(d,resources,resources->crtcs[i]); assert(c);
if (c->mode) {
assert(covers(d,NSMakeRect(c->x,c->y,c->width,c->height)));
assert(!covers(d,NSMakeRect(c->x,c->y,20,20)));
assert(!covers(d,NSMakeRect(c->x,c->y,c->width/2,c->height/2)));
++checked;
}
freeCrtc(c);
}
freeResources(resources); closeDisplay(d);
assert(checked);
printf("FULLSCREEN_GEOMETRY startup_rejected=1 windowed_rejected=1 active_monitors=%u\n",checked);
}
return 0;
}
+57
View File
@@ -0,0 +1,57 @@
#!/usr/bin/env python3
"""Test the shipped relative-mouse implementation without fetching Darling or opening X11."""
import argparse
import os
import time
from pathlib import Path
import shlex
import subprocess
import tempfile
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--xvfb', help='also exercise Xorg protocol on this private Xvfb executable')
args = parser.parse_args()
root = Path(__file__).resolve().parent.parent
name = 'AppKit/X11.backend/X11RelativeMouse.h'
patch = (root / 'patches/cocotron.patch').read_text()
section = patch.split(f'diff --git a/{name} b/{name}\n', 1)[1].split('\ndiff --git ', 1)[0]
# This header is a new file: all of its source lines are additions after the first hunk header.
body = section.split('@@\n', 1)[1]
assert all(line.startswith('+') for line in body.splitlines()), 'relative mouse header must be a complete added file'
with tempfile.TemporaryDirectory(prefix='darling-input-check-') as folder:
folder = Path(folder)
header = folder / 'X11RelativeMouse.h'
header.write_text('\n'.join(line[1:] for line in body.splitlines()) + '\n')
binary = folder / 'relative-mouse-check'
subprocess.run([*shlex.split(os.environ.get('CC', 'cc')), '-std=c99', '-Wall', '-Wextra', '-Werror',
f'-DX11_RELATIVE_MOUSE_HEADER="{header}"', str(root / 'tools/test-x11-relative-mouse.c'),
'-o', str(binary)], check=True)
subprocess.run([str(binary)], check=True)
if args.xvfb:
protocol = folder / 'relative-mouse-xorg-check'
subprocess.run([*shlex.split(os.environ.get('CC', 'cc')), '-std=c99', '-Wall', '-Wextra', '-Werror',
f'-DX11_RELATIVE_MOUSE_HEADER="{header}"', str(root / 'tools/test-x11-relative-mouse-xorg.c'),
'-lX11', '-o', str(protocol)], check=True)
# -displayfd probes low display numbers and can unlink an existing Xwayland socket.
# Pick a high unused display and let Xvfb's normal lock handling protect it instead.
number = next(n for n in range(1000, 1100)
if not Path(f'/tmp/.X{n}-lock').exists()
and not Path(f'/tmp/.X11-unix/X{n}').exists()
and not Path(f'/tmp/.X11-unix/X{n}_').exists())
private_display = ':' + str(number)
server = subprocess.Popen([args.xvfb, private_display, '-screen', '0', '3840x1080x24',
'-nolisten', 'tcp', '-ac', '-noreset'],
stdout=subprocess.DEVNULL, stderr=subprocess.PIPE)
try:
deadline = time.monotonic() + 15
while not Path(f'/tmp/.X11-unix/X{number}').is_socket():
if server.poll() is not None:
raise RuntimeError('private Xvfb failed: ' + server.stderr.read().decode())
if time.monotonic() >= deadline:
raise RuntimeError('private Xvfb did not become ready')
time.sleep(0.02)
subprocess.run([str(protocol), private_display], check=True, timeout=20)
finally:
server.terminate()
server.communicate(timeout=10)
+50
View File
@@ -0,0 +1,50 @@
/* No display connection or window is created. Exercise the real backend with a cached remote
* frame to ensure a client slot cannot take ownership of League's native Metal drawable. */
#import <Foundation/Foundation.h>
#import <CoreFoundation/CoreFoundation.h>
#import <objc/runtime.h>
#include <dlfcn.h>
#include <assert.h>
#include <stdio.h>
@interface FrameOwner : NSObject
@property(copy) NSString *title;
- (void)platformWindowDidInvalidateCGContext:(id)window;
@end
@implementation FrameOwner
@synthesize title;
- (void)platformWindowDidInvalidateCGContext:(id)window {}
@end
@interface NSObject (RemoteFrameProbe)
- (BOOL)presentRemoteCAFrame:(id)timer;
@end
static void *slot(id object, const char *name) {
Ivar ivar = class_getInstanceVariable([object class], name);
assert(ivar);
return (char *)object + ivar_getOffset(ivar);
}
int main(int argc, char **argv) {
@autoreleasepool {
assert(argc == 2);
void *backend = dlopen(argv[1], RTLD_LAZY);
if (!backend) { fprintf(stderr,"%s\n",dlerror()); return 1; }
Class cls = NSClassFromString(@"X11Window");
assert(cls);
id native = [cls alloc]; /* Deliberately skip X11 initialization: cached-frame path needs no display. */
FrameOwner *owner = [FrameOwner new];
*(id *)slot(native,"_delegate") = owner;
*(BOOL *)slot(native,"_mapped") = YES;
*(BOOL *)slot(native,"_remoteFrameValid") = YES;
*(unsigned *)slot(native,"_remoteFrameTargetCount") = 1;
*(double *)slot(native,"_remoteFrameTargetRefresh") = CFAbsoluteTimeGetCurrent() + 60;
*(double *)slot(native,"_remoteFrameSelectionRefresh") = CFAbsoluteTimeGetCurrent() + 60;
owner.title = @"Riot Client";
assert([native presentRemoteCAFrame:nil]); /* Client's cached presentation remains usable. */
owner.title = @"League of Legends (TM) Client";
assert(![native presentRemoteCAFrame:nil]); /* Same cached frame cannot cover the game. */
[native release];
[owner release];
puts("REMOTE_FRAME_OWNERSHIP client_preserved=1 native_game_protected=1");
}
return 0;
}
@@ -0,0 +1,61 @@
#!/usr/bin/env python3
"""Check stale-session cleanup against fake processes; never signal a real process."""
import importlib.util
import os
from pathlib import Path
import tempfile
from unittest.mock import patch, Mock, MagicMock
spec = importlib.util.spec_from_file_location('cleanup', Path(__file__).with_name('ensure-darling-runtime.py'))
cleanup = importlib.util.module_from_spec(spec)
spec.loader.exec_module(cleanup)
with tempfile.TemporaryDirectory(prefix='darling-cleanup-check-') as folder:
root = Path(folder)
def process(pid, comm, argv):
proc = root / str(pid)
proc.mkdir()
(proc / 'comm').write_text(comm + '\n')
(proc / 'cmdline').write_bytes(b'\0'.join(arg.encode() for arg in argv) + b'\0')
return proc
browser = process(101, 'chrome_crashpad', ['/opt/google/chrome/chrome_crashpad_handler', '--annotation=riot'])
riot = process(102, 'chrome_crashpad', ['/Applications/Riot Client.app/Contents/Frameworks/chrome_crashpad_handler'])
launchd = process(103, 'launchd', ['/sbin/launchd'])
(launchd / 'cmdline').write_bytes(b'/sbin/launchd' + b'\0' * 8)
process(104, 'unrelated', ['/usr/bin/unrelated', '/sbin/launchd'])
with patch.object(cleanup.os, 'kill') as kill:
cleanup.stop_riot_crashpad(root)
assert kill.call_args_list == [((102, cleanup.signal.SIGTERM),)]
kill.reset_mock()
cleanup.stop_orphaned_launchd(root)
assert kill.call_args_list == [((103, cleanup.signal.SIGKILL),)]
assert not cleanup.any_server_running(root)
process(105, 'darlingserver', ['darlingserver', '--prefix=another-prefix'])
assert cleanup.any_server_running(root)
hidden = MagicMock()
hidden.name = '106'
(hidden / 'comm').read_text.side_effect = PermissionError()
fake_root = Mock()
fake_root.iterdir.return_value = [hidden]
assert cleanup.any_server_running(fake_root)
# Foreign-owned Riot processes and buffers must survive even when no server runs.
with patch.object(Path, 'stat', return_value=Mock(st_uid=os.getuid() + 1)), patch.object(cleanup.os, 'kill') as kill:
cleanup.stop_riot_crashpad(root)
cleanup.stop_orphaned_launchd(root)
kill.assert_not_called()
buffer = root / 'darling-buffer'
buffer.write_text('stale')
with patch.object(Path, 'stat', return_value=Mock(st_uid=os.getuid() + 1, st_mode=0o100600)):
cleanup.remove_owned([buffer])
assert buffer.exists()
cleanup.remove_owned([buffer])
assert not buffer.exists()
# The entry point must preserve all sessions while ANY prefix has a server.
with patch.object(cleanup, 'recover_marker'), patch.object(cleanup, 'any_server_running', return_value=True), \
patch.object(cleanup, 'stop_riot_crashpad') as crashpad, \
patch.object(cleanup, 'stop_orphaned_launchd') as orphan, patch.object(cleanup, 'remove_owned') as remove:
cleanup.main()
crashpad.assert_not_called()
orphan.assert_not_called()
remove.assert_not_called()
print('RUNTIME_CLEANUP_OFFLINE pass=1 browser_preserved=1 other_prefix_preserved=1 foreign_owner_preserved=1 hidden_process_preserved=1 orphan_recovered=1')
+22 -1
View File
@@ -17,7 +17,7 @@ unset RCLOL_APP_ARG3 RCLOL_APP_ARG3_SLOT RCLOL_UX_ARG1 RCLOL_UX_ARG1_SLOT
unset RCLOL_UX_ARG2 RCLOL_UX_ARG2_SLOT
# The paired runtime presents completed GPU textures without CPU readback.
# Explicit 0 or an empty value selects the CPU fallback for diagnostics.
for option in RCLOL_GPU_DIRECT_PRESENT RCLOL_GPU_DIRECT_ASYNC RCLOL_GPU_DIRECT_VSYNC DARLING_CA_PRESENT_AT_COMMIT DARLING_CA_SKIP_LOCAL_IOSURFACE_RENDER RCLOL_FORWARD_MOUSE_MOVED; do
for option in RCLOL_GPU_DIRECT_PRESENT RCLOL_GPU_DIRECT_ASYNC RCLOL_GPU_DIRECT_VSYNC DARLING_CA_PRESENT_AT_COMMIT DARLING_CA_SKIP_LOCAL_IOSURFACE_RENDER RCLOL_FORWARD_MOUSE_MOVED RCLOL_MOUSE_CONFINEMENT; do
eval 'value=${'"$option"'-1}'
case "$value" in 0|'') unset "$option";; *) export "$option=$value";; esac
done
@@ -60,6 +60,27 @@ else
--launch-patchline="${RCLOL_LAUNCH_PATCHLINE-live}"
fi
fi
# --- darling-lol perf: scheduling & driver latency hints ---
# Refresh rate for display link, async presenter, and frame pacing: the primary monitor's
# current mode (the first monitor without a primary); unset falls back to 60 Hz.
if [ -z "${RCLOL_REFRESH_HZ-}" ] && command -v xrandr >/dev/null 2>&1; then
RCLOL_REFRESH_HZ=$(xrandr --current 2>/dev/null | awk '
/ connected/ { primary = / primary / }
/\*/ { for (i = 2; i <= NF; i++) if ($i ~ /\*/) { rate = $i; sub(/[*+]+/, "", rate)
if (primary) { print int(rate + 0.5); found = 1; exit } else if (first == "") first = int(rate + 0.5) } }
END { if (!found && first != "") print first }')
fi
if [ -n "${RCLOL_REFRESH_HZ-}" ]; then export RCLOL_REFRESH_HZ; fi
# Vulkan WSI & Mesa optimizations
export MESA_NO_ERROR="${MESA_NO_ERROR:-1}"
# NVIDIA proprietary driver: limit pre-rendered frames.
export __GL_MaxFramesAllowed="${__GL_MaxFramesAllowed:-1}"
export __GL_THREADED_OPTIMIZATIONS="${__GL_THREADED_OPTIMIZATIONS:-1}"
export __GL_YIELD="${__GL_YIELD:-NOTHING}"
# Reduce timer slack for tighter scheduling in the game process tree.
if [ -w /proc/self/timerslack_ns ]; then
echo 1 > /proc/self/timerslack_ns 2>/dev/null || true
fi
exec env -u ELECTRON_RUN_AS_NODE -u DYLD_INSERT_LIBRARIES EGL_PLATFORM=x11 \
LANG="${RCLOL_GUEST_LOCALE:-en_US.UTF-8}" LC_ALL="${RCLOL_GUEST_LOCALE:-en_US.UTF-8}" \
mesa_glthread="${RCLOL_MESA_GLTHREAD-${mesa_glthread-false}}" \
+96
View File
@@ -0,0 +1,96 @@
/* Protocol regression on a PRIVATE Xvfb server, never the user's DISPLAY. */
#ifndef X11_RELATIVE_MOUSE_HEADER
#define X11_RELATIVE_MOUSE_HEADER "../build/darling-src/src/external/cocotron/AppKit/X11.backend/X11RelativeMouse.h"
#endif
#include X11_RELATIVE_MOUSE_HEADER
#include <X11/Xlib.h>
#include <assert.h>
#include <stdio.h>
static XEvent nextMotion(Display *d) {
XEvent event;
do { XNextEvent(d, &event); } while (event.type != MotionNotify);
return event;
}
static void movePhysical(Display *other, int x, int y) {
XWarpPointer(other, None, DefaultRootWindow(other), 0, 0, 0, 0, x, y);
XSync(other, False);
}
static X11RelativeMouseMotion handle(X11RelativeMouse *mouse, XEvent event) {
return X11RelativeMouseHandleMotion(mouse, event.xmotion.x_root, event.xmotion.y_root, true,
event.xmotion.serial);
}
static unsigned warp(Display *d, int x, int y) {
XWarpPointer(d, None, DefaultRootWindow(d), 0, 0, 0, 0, x, y);
XSync(d, False);
return XEventsQueued(d, QueuedAlready);
}
int main(int argc, char **argv) {
assert(argc == 2); /* The runner supplies the dedicated server explicitly. */
Display *d = XOpenDisplay(argv[1]), *other = XOpenDisplay(argv[1]);
assert(d && other);
Window win = XCreateSimpleWindow(d, DefaultRootWindow(d), 1920, 24, 1920, 1056, 0, 0, 0);
XSelectInput(d, win, PointerMotionMask);
XMapWindow(d, win);
XSync(d, False);
movePhysical(other, 2500, 400);
XSync(d, True);
assert(XGrabPointer(d, win, False, PointerMotionMask, GrabModeAsync, GrabModeAsync,
win, None, CurrentTime) == GrabSuccess);
/* Leave an older motion queued while taking the fresh grab snapshot. */
movePhysical(other, 2510, 400);
Window root, child; int rx, ry, wx, wy; unsigned state;
unsigned long snapshot = NextRequest(d);
assert(XQueryPointer(d, win, &root, &child, &rx, &ry, &wx, &wy, &state));
X11RelativeMouse m = {0};
X11RelativeMouseBegin(&m, rx, ry); m.firstSerial = snapshot;
assert(rx == 2510 && ry == 400);
X11RelativeMouseMotion r = handle(&m, nextMotion(d));
assert(!r.post && !r.rewarp && m.physicalX == 2510);
movePhysical(other, 2520, 395);
r = handle(&m, nextMotion(d));
assert(r.post && r.deltaX == 10 && r.deltaY == -5 && r.rewarp);
unsigned long serial = NextRequest(d);
unsigned queued = warp(d, r.warpX, r.warpY);
X11RelativeMouseExpectEcho(&m, r.warpX, r.warpY, queued, serial);
XEvent echo = nextMotion(d);
assert(echo.xmotion.serial == serial);
r = handle(&m, echo);
assert(r.echo && !r.post && m.physicalX == 2510);
puts("PASS: real Xorg grab snapshot and warp echo request serial");
/* Queue real motion across the rewarp target BEFORE requesting a rewarp. */
movePhysical(other, 2530, 400);
movePhysical(other, 2510, 400);
serial = NextRequest(d);
queued = warp(d, 2510, 400);
X11RelativeMouseExpectEcho(&m, 2510, 400, queued, serial);
r = handle(&m, nextMotion(d));
assert(r.post && !r.echo && r.deltaX == 20 && !r.rewarp);
r = handle(&m, nextMotion(d));
assert(r.post && !r.echo && r.deltaX == -20);
r = handle(&m, nextMotion(d));
assert(r.echo && !r.post);
puts("PASS: real queued motion crossing the anchor is not swallowed as an echo");
m.bounded = true; m.minX = 1920; m.minY = 24; m.maxX = 3839; m.maxY = 1079;
double x = 3900, y = -100;
X11RelativeMouseClamp(&m, &x, &y);
/* Send the raw target here to verify the X server clips it exactly as the backend does. */
serial = NextRequest(d); queued = warp(d, 3900, -100);
X11RelativeMouseWarp(&m, x, y, queued, serial, true);
assert(XQueryPointer(d, win, &root, &child, &rx, &ry, &wx, &wy, &state));
assert(rx == 3839 && ry == 24 && m.virtualX == rx && m.virtualY == ry);
r = handle(&m, nextMotion(d));
assert(r.echo && !r.post);
movePhysical(other, 3830, 30);
r = handle(&m, nextMotion(d));
assert(r.post && r.deltaX == -9 && r.deltaY == 6);
puts("PASS: real Xorg confinement on a secondary monitor agrees with the virtual cursor");
X11RelativeMouseEnd(&m);
XUngrabPointer(d, CurrentTime);
XDestroyWindow(d, win);
XCloseDisplay(other); XCloseDisplay(d);
return 0;
}
+119 -20
View File
@@ -1,4 +1,7 @@
#include "../build/darling-src/src/external/cocotron/AppKit/X11.backend/X11RelativeMouse.h"
#ifndef X11_RELATIVE_MOUSE_HEADER
#define X11_RELATIVE_MOUSE_HEADER "../build/darling-src/src/external/cocotron/AppKit/X11.backend/X11RelativeMouse.h"
#endif
#include X11_RELATIVE_MOUSE_HEADER
#include <assert.h>
#include <stdio.h>
@@ -6,7 +9,7 @@
* A warp's echo is simulated as the MotionNotify the server queues at the target; `queued` is the number of
* X events queued after the backend's XSync. */
static X11RelativeMouseMotion motion(X11RelativeMouse *m, int x, int y, bool honoured) {
return X11RelativeMouseHandleMotion(m, x, y, honoured);
return X11RelativeMouseHandleMotion(m, x, y, honoured, 0);
}
static void xorgHonouredWarps(void) {
@@ -16,22 +19,22 @@ static void xorgHonouredWarps(void) {
X11RelativeMouseMotion r = motion(&m, 510, 395, true);
assert(r.post && !r.echo && r.deltaX == 10 && r.deltaY == -5);
assert(r.rewarp && r.warpX == 500 && r.warpY == 400);
X11RelativeMouseExpectEcho(&m, r.warpX, r.warpY, 1);
X11RelativeMouseExpectEcho(&m, r.warpX, r.warpY, 1, 0);
/* The rewarp's echo is consumed and is not a delta back to the centre. */
r = motion(&m, 500, 400, true);
assert(!r.post && r.echo && !r.rewarp && m.echoCount == 0);
assert(m.physicalX == 500 && m.physicalY == 400);
r = motion(&m, 503, 404, true);
assert(r.post && r.deltaX == 3 && r.deltaY == 4 && r.rewarp);
X11RelativeMouseExpectEcho(&m, r.warpX, r.warpY, 1);
X11RelativeMouseExpectEcho(&m, r.warpX, r.warpY, 1, 0);
/* A real motion queued before the echo still measures against the last physical position. */
X11RelativeMouseBegin(&m, 500, 400);
r = motion(&m, 520, 400, true);
X11RelativeMouseExpectEcho(&m, r.warpX, r.warpY, 2); /* the next motion was already queued */
X11RelativeMouseExpectEcho(&m, r.warpX, r.warpY, 2, 0); /* the next motion was already queued */
r = motion(&m, 530, 400, true);
assert(r.post && r.deltaX == 10 && r.deltaY == 0 && r.rewarp);
X11RelativeMouseExpectEcho(&m, r.warpX, r.warpY, 1); /* same target: merged, not a second echo */
X11RelativeMouseExpectEcho(&m, r.warpX, r.warpY, 1, 0); /* same target: merged, not a second echo */
assert(m.echoCount == 1);
r = motion(&m, 500, 400, true);
assert(r.echo && m.echoCount == 0);
@@ -43,8 +46,8 @@ static void xorgHonouredWarps(void) {
X11RelativeMouseBegin(&m, 500, 400);
r = motion(&m, 512, 400, true);
assert(r.deltaX == 12 && r.rewarp);
X11RelativeMouseExpectEcho(&m, r.warpX, r.warpY, 1);
X11RelativeMouseWarp(&m, 524.0, 400.0, 2); /* game: mouseLocation + 2 * delta */
X11RelativeMouseExpectEcho(&m, r.warpX, r.warpY, 1, 0);
X11RelativeMouseWarp(&m, 524.0, 400.0, 2, 0, true); /* game: mouseLocation + 2 * delta */
assert(m.virtualX == 524.0 && m.physicalX == 512);
r = motion(&m, 500, 400, true);
assert(r.echo && m.physicalX == 500);
@@ -52,6 +55,7 @@ static void xorgHonouredWarps(void) {
assert(r.echo && m.physicalX == 524 && m.echoCount == 0);
r = motion(&m, 530, 401, true);
assert(r.post && r.deltaX == 6 && r.deltaY == 1 && r.rewarp && r.warpX == 524 && r.warpY == 400);
X11RelativeMouseEnd(&m);
puts("PASS: Xorg honoured warps rewarp to the frozen cursor and drop their echoes");
}
@@ -61,27 +65,50 @@ static void xwaylandVisibleCursor(void) {
/* Warps are not honoured: no rewarp, the pointer moves freely, deltas follow it. */
X11RelativeMouseMotion r = motion(&m, 810, 450, false);
assert(r.post && r.deltaX == 10 && !r.rewarp);
X11RelativeMouseWarp(&m, 820.0, 450.0, 1);
X11RelativeMouseWarp(&m, 820.0, 450.0, 1, 0, false);
/* The echo at the target is dropped without moving the physical position... */
r = motion(&m, 820, 450, false);
assert(r.echo && !r.post && m.physicalX == 810 && m.physicalY == 450);
/* ...so the next real motion, at the real absolute position, gives the true delta. */
r = motion(&m, 812, 453, false);
assert(r.post && r.deltaX == 2 && r.deltaY == 3 && !r.rewarp);
assert(m.virtualX == 820.0 && m.virtualY == 450.0);
assert(m.virtualX == 812.0 && m.virtualY == 453.0);
/* An expected echo that never arrives expires after the queued events and cannot eat a later motion. */
X11RelativeMouseWarp(&m, 700.0, 400.0, 1);
X11RelativeMouseWarp(&m, 700.0, 400.0, 1, 0, false);
X11RelativeMouseTick(&m); /* some other X event was the one queued */
assert(m.echoCount == 0);
r = motion(&m, 700, 400, false);
assert(r.post && !r.echo && r.deltaX == -112 && r.deltaY == -53);
X11RelativeMouseEnd(&m);
puts("PASS: Xwayland visible cursor keeps the physical position across unhonoured warp echoes");
}
static void leagueVisibleCursorWarps(void) {
X11RelativeMouse m = {0};
X11RelativeMouseBegin(&m, 2888, 30);
int x = 2888, y = 30;
for (int step = 0; step < 40; ++step) {
x -= 20;
y += 16;
X11RelativeMouseMotion r = motion(&m, x, y, false);
assert(r.post && r.deltaX == -20 && r.deltaY == 16 && !r.rewarp);
double gameX = m.virtualX + r.deltaX / 2, gameY = m.virtualY + r.deltaY / 2;
X11RelativeMouseWarp(&m, gameX, gameY, 1, 0, false);
r = motion(&m, (int) gameX, (int) gameY, false); /* the unhonoured warp's echo */
assert(r.echo && !r.post);
assert(m.virtualX == x && m.virtualY == y);
}
/* The pointer stops at the right edge of a 3440-pixel screen; the cursor is there too. */
X11RelativeMouseMotion r = motion(&m, 3439, 700, false);
assert(r.post && m.virtualX == 3439 && m.virtualY == 700);
X11RelativeMouseEnd(&m);
puts("PASS: League's visible-cursor warps leave the virtual cursor on the pointer, up to the screen edge");
}
static void gameWarpUpdatesVirtual(void) {
X11RelativeMouse m = {0};
X11RelativeMouseBegin(&m, 100, 100);
X11RelativeMouseWarp(&m, 150.75, 120.5, 0); /* no event: the pointer was already there */
X11RelativeMouseWarp(&m, 150.75, 120.5, 0, 0, true); /* no event: the pointer was already there */
assert(m.virtualX == 150.75 && m.virtualY == 120.5 && m.echoCount == 0);
assert(m.physicalX == 100 && m.physicalY == 100);
/* mouseLocation reports the virtual cursor, Cocoa bottom-up global, for a 1080-pixel screen. */
@@ -89,6 +116,7 @@ static void gameWarpUpdatesVirtual(void) {
assert(cocoaX == 150.75 && cocoaY == 959.5);
X11RelativeMouseMotion r = motion(&m, 101, 100, true);
assert(r.post && r.deltaX == 1 && r.rewarp && r.warpX == 150 && r.warpY == 120);
X11RelativeMouseEnd(&m);
puts("PASS: application warps while relative move the virtual cursor that mouseLocation returns");
}
@@ -101,39 +129,110 @@ static void deltaSign(void) {
/* Absolute motion in Cocoa window coordinates (bottom-up): moving down lowers y. */
X11AbsoluteMouseDelta(10, 300, 14, 290, &dx, &dy);
assert(dx == 4 && dy == 10);
X11RelativeMouseEnd(&m);
puts("PASS: deltaY is positive downwards for relative and absolute motion");
}
static void ungrabRestores(void) {
X11RelativeMouse m = {0};
X11RelativeMouseBegin(&m, 10, 10);
X11RelativeMouseWarp(&m, 40, 40, 3);
X11RelativeMouseWarp(&m, 40, 40, 3, 0, true);
X11RelativeMouseEnd(&m);
assert(!m.active && m.echoCount == 0);
X11RelativeMouseBegin(&m, 60, 70);
assert(m.active && m.virtualX == 60 && m.virtualY == 70 && m.physicalX == 60 && m.physicalY == 70);
X11RelativeMouseMotion r = motion(&m, 40, 40, true);
assert(r.post && !r.echo && r.deltaX == -20 && r.deltaY == -30);
X11RelativeMouseEnd(&m);
puts("PASS: ungrab clears expected echoes and a new grab starts from the current pointer");
}
static void echoQueueBound(void) {
static void echoQueueGrowth(void) {
X11RelativeMouse m = {0};
X11RelativeMouseBegin(&m, 0, 0);
for (int i = 1; i <= X11_RELATIVE_MOUSE_MAX_ECHOES + 3; ++i)
X11RelativeMouseWarp(&m, i, 0, 100);
assert(m.echoCount == X11_RELATIVE_MOUSE_MAX_ECHOES && m.echoes[0].x == 4);
for (int i = 1; i <= X11_RELATIVE_MOUSE_INITIAL_ECHOES + 3; ++i)
X11RelativeMouseWarp(&m, i, 0, 100, 0, true);
assert(m.echoCount == X11_RELATIVE_MOUSE_INITIAL_ECHOES + 3 && m.echoes[0].x == 1);
X11RelativeMouseMotion r = motion(&m, 6, 0, true);
assert(r.echo && m.echoCount == X11_RELATIVE_MOUSE_MAX_ECHOES - 3 && m.echoes[0].x == 7);
puts("PASS: expected echoes are bounded and consumed in order");
assert(r.echo && m.echoCount == X11_RELATIVE_MOUSE_INITIAL_ECHOES - 3 && m.echoes[0].x == 7);
X11RelativeMouseEnd(&m);
puts("PASS: echo backlog grows without discarding warps and is consumed in order");
}
static void serialsSeparateMotionFromEchoes(void) {
X11RelativeMouse m = {0};
X11RelativeMouseBegin(&m, 500, 400);
m.firstSerial = 100;
/* Events queued before the grab snapshot must not turn into a jump from that snapshot. */
X11RelativeMouseMotion r = X11RelativeMouseHandleMotion(&m, 0, 0, true, 99);
assert(!r.post && !r.rewarp && m.physicalX == 500 && m.physicalY == 400);
X11RelativeMouseExpectEcho(&m, 500, 400, 4, 110);
/* Real pre-warp motion happens to cross the anchor. Coordinates alone would discard it. */
m.physicalX = 510;
r = X11RelativeMouseHandleMotion(&m, 500, 400, true, 105);
assert(r.post && !r.echo && r.deltaX == -10 && m.echoCount == 1);
r = X11RelativeMouseHandleMotion(&m, 500, 400, true, 110);
assert(r.echo && !r.post && m.echoCount == 0);
/* A later real event at the same target must not be swallowed either. */
X11RelativeMouseExpectEcho(&m, 520, 400, 2, 120);
r = X11RelativeMouseHandleMotion(&m, 520, 400, true, 121);
assert(r.post && !r.echo && r.deltaX == 20);
X11RelativeMouseEnd(&m);
puts("PASS: grab snapshot and warp request serials distinguish queued motion from echoes");
}
static void queuedMotionDoesNotMultiplyWarps(void) {
X11RelativeMouse m = {0};
X11RelativeMouseBegin(&m, 500, 400);
X11RelativeMouseMotion r = X11RelativeMouseHandleMotion(&m, 510, 400, true, 100);
assert(r.post && r.deltaX == 10 && r.rewarp);
X11RelativeMouseExpectEcho(&m, 500, 400, 4, 110);
r = X11RelativeMouseHandleMotion(&m, 520, 400, true, 101);
assert(r.post && r.deltaX == 10 && !r.rewarp);
r = X11RelativeMouseHandleMotion(&m, 530, 400, true, 102);
assert(r.post && r.deltaX == 10 && !r.rewarp && m.echoCount == 1);
r = X11RelativeMouseHandleMotion(&m, 500, 400, true, 110);
assert(r.echo && !r.post);
r = X11RelativeMouseHandleMotion(&m, 502, 401, true, 111);
assert(r.post && r.deltaX == 2 && r.deltaY == 1 && r.rewarp);
X11RelativeMouseEnd(&m);
puts("PASS: queued physical motion uses one pending rewarp without losing deltas");
}
static void confinedWarpAndApplicationReanchor(void) {
X11RelativeMouse m = {0};
X11RelativeMouseBegin(&m, 2500, 400);
/* A second monitor at +1920, with a 24-pixel panel above the game. */
m.bounded = true; m.minX = 1920; m.minY = 24; m.maxX = 3839; m.maxY = 1079;
double x = 3900, y = -100;
X11RelativeMouseClamp(&m, &x, &y);
assert(x == 3839 && y == 24);
X11RelativeMouseWarp(&m, x, y, 3, 210, true);
/* Another application warp must preserve both outstanding echoes, even at the same position. */
X11RelativeMouseWarp(&m, x, y, 4, 220, true);
assert(m.echoCount == 2);
X11RelativeMouseMotion r = X11RelativeMouseHandleMotion(&m, 3839, 24, true, 210);
assert(r.echo && !r.post && m.echoCount == 1);
r = X11RelativeMouseHandleMotion(&m, 3839, 24, true, 220);
assert(r.echo && !r.post && m.echoCount == 0);
r = X11RelativeMouseHandleMotion(&m, 3830, 30, true, 221);
assert(r.post && r.deltaX == -9 && r.deltaY == 6);
assert(r.rewarp && r.warpX == 3839 && r.warpY == 24);
assert(m.virtualX == 3839 && m.virtualY == 24);
X11RelativeMouseEnd(&m);
puts("PASS: confined application warps share one position with events and rewarping on a secondary monitor");
}
int main(void) {
serialsSeparateMotionFromEchoes();
queuedMotionDoesNotMultiplyWarps();
confinedWarpAndApplicationReanchor();
xorgHonouredWarps();
xwaylandVisibleCursor();
leagueVisibleCursorWarps();
gameWarpUpdatesVirtual();
deltaSign();
ungrabRestores();
echoQueueBound();
echoQueueGrowth();
return 0;
}
+98
View File
@@ -0,0 +1,98 @@
#!/usr/bin/env python3
"""Passively record Riot/League X11 window lifetimes, geometry and child stacking (requires python-xlib)."""
import argparse
from datetime import datetime
import json
from pathlib import Path
import select
import time
from Xlib import X, display, error
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--output', type=Path, required=True)
parser.add_argument('--duration', type=float, default=3600)
args = parser.parse_args()
connection = display.Display()
root = connection.screen().root
watched, known = set(), {}
connection.set_error_handler(lambda exc, request: None) # Windows may disappear between queries.
atoms = {name: connection.intern_atom(name) for name in (
'_NET_WM_PID', '_NET_WM_NAME', '_NET_WM_STATE', '_NET_WM_WINDOW_TYPE')}
args.output.parent.mkdir(parents=True, exist_ok=True)
with args.output.open('a', buffering=1) as output:
def emit(record):
output.write(json.dumps({'time': datetime.now().astimezone().isoformat(), **record}) + '\n')
def property_value(window, name):
value = window.get_full_property(atoms[name], X.AnyPropertyType)
if value is None:
return None
if name == '_NET_WM_NAME':
return bytes(value.value).decode(errors='replace')
return list(map(int, value.value))
def scan(window, event, inherited=False, depth=0):
if depth > 64:
return
try:
if window.id not in watched:
window.change_attributes(event_mask=X.SubstructureNotifyMask | X.StructureNotifyMask | X.PropertyChangeMask)
watched.add(window.id)
tree = window.query_tree()
cls = window.get_wm_class()
relevant = inherited or any(word in str(cls).lower() for word in ('riot', 'league'))
if relevant:
attrs, geom = window.get_attributes(), window.get_geometry()
point = root.translate_coords(window, 0, 0)
state = {
'window': hex(window.id), 'parent': hex(tree.parent.id),
'class': cls, 'title': property_value(window, '_NET_WM_NAME') or window.get_wm_name(),
'pid': property_value(window, '_NET_WM_PID'),
'size': [geom.width, geom.height], 'root_position': [point.x, point.y],
'mapped': attrs.map_state, 'override_redirect': bool(attrs.override_redirect),
'state': [connection.get_atom_name(n) for n in property_value(window, '_NET_WM_STATE') or []],
'type': [connection.get_atom_name(n) for n in property_value(window, '_NET_WM_WINDOW_TYPE') or []],
'children_bottom_to_top': [hex(child.id) for child in tree.children],
}
if known.get(window.id) != state or event in ('MapNotify', 'UnmapNotify'):
emit({'event': event, **state})
known[window.id] = state
for child in tree.children:
scan(child, event, relevant, depth + 1)
except (error.XError, OSError, AttributeError, ValueError):
pass
emit({'event': 'trace-start', 'display': connection.get_display_name()})
scan(root, 'initial')
connection.flush()
deadline = time.monotonic() + args.duration
while time.monotonic() < deadline:
if not connection.pending_events():
select.select([connection.fileno()], [], [], min(1, max(0, deadline - time.monotonic())))
while connection.pending_events():
event = connection.next_event()
window = getattr(event, 'window', None)
if window is None:
continue
if event.type == X.DestroyNotify:
if window.id in known:
emit({'event': 'DestroyNotify', **known.pop(window.id)})
watched.discard(window.id)
else:
# Recover the owning top-level so new descendants inherit the Riot/League filter.
ancestor = window
try:
for _ in range(64):
parent = ancestor.query_tree().parent
if not hasattr(parent, 'id') or parent.id == root.id or parent.id == 0:
break
ancestor = parent
scan(ancestor, type(event).__name__)
except error.XError:
pass
connection.flush()
emit({'event': 'trace-end'})
connection.close()
if __name__ == '__main__':
main()