Files
darling-lol/docs/SETUP.md
T
lucannez64andClaude Opus 5.5 4370f375cd Cut Darling overhead in League fights and add passive match capture
Game process (ARAM Hexgate: 84 -> 127 FPS average, slowest stretches ~110):
- Local pthread mutexes, condition variables and Mach semaphores are on by
  default in the game process only, with /tmp/rclol-no-local-* opt-outs.
  The main thread's darlingserver waits fell from 1.36 to 0.10 ms per frame.
- Local semaphores: the count and the slot generation change in one
  compare-exchange, a destroy leaves a negative marker, the table is reset
  after fork, and names the table did not create go to the server.
- Metal: drop identical re-binds (keyed on the Indium object, depth-stencil
  retained), pass blit resources by reference, and stop resetting vertex
  descriptors twice.
- Core isolation of the main thread is opt-in (/tmp/rclol-core-isolation);
  it lowered FPS in its only match.
- Optimized libc/libc++/Foundation overlays, AVX2 string primitives, real CPU
  topology and their installers and offline checks.

Tools and docs:
- tools/riot-match-capture: passive /proc sampler, user-space IP profiler and
  reports, plus a comparison against the game's netstats.
- tools/riot-lsem-model and tools/riot-metal-bind-model: host models that
  extract the shipped code and check it.
- tools/install-riot-metal-pair.sh installs Metal and Indium together.
- Status, setup, testing and troubleshooting docs updated with the measured
  matches and the switch table; manifest file counts match the patches.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-14 16:48:34 +02:00

14 KiB

Setup and portability

The patch manifest pins Darling to 60ba801decee7a00782f74f6be4c8ffb013f79ff and records every changed submodule's revision. scripts/prepare.py checks each revision before applying patches and never resets an existing checkout to make a patch apply. Re-running it accepts an already-applied patch.

Git LFS must be available before fetching the upstream submodules. The local development checkout had removed prebuilt Swift files from its working directory; that deletion is deliberately not exported. A fresh upstream checkout keeps its original Swift assets and licensing. Do not install Git LFS pointer text as a library.

configure.py renders @PROJECT_ROOT@, @HOST_HOME@, @GUEST_HOME@, @GUEST_USER@, @DPREFIX@ and @DARLING_SDK@ into ignored local copies. A build-directory symlink preserves the original scripts' relative build layout. The public source files remain machine-independent.

python3 scripts/configure.py --guest-user my-linux-user
# A custom SDK can be rendered for advanced setups:
python3 scripts/configure.py --sdk /usr/local/libexec/darling

The normal wrappers currently support the default ~/.darling prefix and /usr/local base installation. The configurator exposes a custom prefix for developers, but several archived tools assume the default layout. Custom prefixes/install locations have not been tested end to end.

The build uses Unix Makefiles because the incremental runtime helpers read CMake flags.make and link.txt. Ninja is not a drop-in replacement for those helpers.

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.

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 vpx, x11, x11-xcb, xcb-shm and xext, plus the base Darling SDK. Some bridge guards intentionally fall back to Chromium's own implementation when the installed client changes.

Run configuration, builds and the per-user runtime installation as your ordinary account. Use sudo only for the base CMake installation and the incremental server installer. Running the whole build or per-user install under sudo can create root-owned files and configure the wrong guest home. scripts/install-runtime.sh initializes the default user prefix and creates ~/.darling/private/tmp with mode 1777, so the installer can write guest temporary files. It then shuts down Darling before replacing the paired runtime: first exit the game and both clients manually.

The normal launcher starts neither an Electron inspector nor client API/UI helpers. It retains the existing runtime libraries and compatibility shims, disables incompatible Chromium features, supplies an absolute log directory, and selects the League product. The first-time installer uses --skip-to-install and omits the installed client's --app-root, avoiding an immediate exit or inaccessible intro screen. Legacy inspector hooks, API helpers, input automation and client binary patches remain historical tools; do not use them in the manual workflow. Keep account data, logs and captures private.

Updating an existing runtime

These commands assume the pinned, patched source tree and its configured Makefiles already exist. From the repository root, build the two components with one worker to reduce resource contention:

cmake --build build/darling-src/build --target darlingserver system_kernel --parallel 1
python3 build/runtime/tools/build-riot-kqueue-runtime.py
sudo python3 tools/install-riot-darling-server.py
python3 tools/install-riot-memory-runtime.py

These two Python installers derive paths from the checkout/configuration and may be run directly from tools/; tokenized framework builders still require their rendered copies. The server installer defaults to /usr/local/bin/darlingserver, saves a checksum-named backup under build/backups/darlingserver, and preserves active server mappings. The CMake tree is configured without an optimization level, so build-riot-kqueue-runtime.py relinks build/riot-libsystem_kernel.dylib with the per-call paths rebuilt at -O2; the memory installer installs that relink by default into the guest RuntimeLibraries and the per-prefix bootstrap copy, and never overwrites it with a different library. It leaves the system SDK alone. Each replacement is staged and verified before installation; the memory installer prepares every destination before replacing any. Replacement is atomic per file, not a transaction across all files.

Memory backups default to build/memory-runtime-backups, separate from the potentially root-owned directory created by the sudo server installer. If a custom backup directory is unwritable, choose another with --backup-dir /path/to/writable/backups. See permission troubleshooting. Neither installer stops or starts any process.

Real CPU topology and the Foundation overlay

The kernel relink also reports the host's real CPU counts for hw.*/machdep.cpu.* (previously a fixed 8 logical / 8 physical MacBookPro16,1 configuration, which made the game create 7 worker threads on a 12-thread host). The memory installer above installs that library; no extra step is needed for the sysctl side. The rebuild also produces the commpage fix (kNumCPUs, physical/logical counts) as build/riot-mldr, whose SDK copy is root-owned:

sudo install -m755 build/riot-mldr /usr/local/libexec/darling/usr/libexec/darling/mldr

The optimized Foundation overlay is built and installed per user:

python3 build/runtime/tools/build-riot-foundation-runtime.py
python3 tools/install-riot-foundation-runtime.py

The installer seeds ~/.darling/Users/<user>/RuntimeFrameworks/Foundation.framework from the SDK (the same overlay mechanism CoreFoundation/AppKit use), replaces Versions/C/Foundation atomically after staging a checksum-named backup in build/foundation-runtime-backups, and prints JSON; --binary/--backup-dir override the defaults. Do not run it while a client is mid-update; a restart is required for activation. To A/B the old fixed CPU counts without reinstalling, run touch /tmp/rclol-fixed-cpu-count before starting a guest process and rm /tmp/rclol-fixed-cpu-count to go back to the real topology (the flag is read once per process). The in-game FPS effect of both changes is unmeasured. The topology change can add worker threads to the clients as well; if clients become busier during matches, use the opt-out flag for a controlled comparison. Rollbacks: build/memory-runtime-backups and build/foundation-runtime-backups hold the previous libraries; the pre-change kernel artifact is build/perf-research/impl-cpu/riot-libsystem_kernel.dylib.before.

Optimized libc, libm, libc++ and string-primitive overlays

Four more libraries have optimized overlays. They are built from the configured tree without a full CMake rebuild:

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 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:

python3 build/runtime/tools/install-riot-libs-runtime.py --rollback

--rollback restores every file this installer replaced and removes the ones it created, so the SDK copy is used again. The policy and the rollback are checked offline by python3 tools/riot-libs-installer-offline-check.py, and each artifact is compared with the library it replaces by python3 tools/riot_overlay_verify.py <artifact> <effective> (same install name, versions, dependencies, re-exports, segment protections and exported symbols, no new undefined symbol). The in-game FPS effect of these overlays is unmeasured. The per-call effect of the string primitives is measured on the host (build/riot-platform-memory-bench); see testing.

Metal and Indium overlays

The Metal wrapper and Indium share a private blit interface, so install them as a pair:

python3 build/runtime/tools/build-riot-indium-runtime.py
python3 build/runtime/tools/build-riot-metal-runtime.py
sh build/runtime/tools/install-riot-metal-pair.sh

The installer copies build/riot-Metal and build/riot-libindium.dylib into the guest RuntimeFrameworks/Metal.framework and RuntimeLibraries. It backs up the pair it replaces in build/metal-pair-backups; a repeated install keeps the older backup. sh build/runtime/tools/install-riot-metal-pair.sh --rollback restores that backup. tools/install-riot-gpu-runtime.sh still builds and installs the whole GPU set, including these two. Activation needs the namespace restart below.

Game-process switches

The kernel library decides these once when a process starts, and only for the League game process (.../LeagueofLegends.app/Contents/MacOS/LeagueofLegends). The Riot and League clients keep the server path. Each switch is a host file, so it takes effect at the next game start without reinstalling:

Path Default in the game Host file
Local pthread mutexes (first-fit) on /tmp/rclol-no-local-mutex turns it off
Local condition variables on /tmp/rclol-no-local-condvars turns it off
Local Mach semaphores on /tmp/rclol-no-local-semaphores turns it off
Core isolation of the main thread off /tmp/rclol-core-isolation turns it on (it lowered FPS)
Region-query pacing (50 µs) on /tmp/rclol-region-pace-us overrides the interval
Main-thread RPC reply polling off /tmp/rclol-rpc-spin-us (µs, at most 500) turns it on

The former opt-in files /tmp/rclol-local-mutex, /tmp/rclol-local-condvars and /tmp/rclol-local-semaphores are ignored. Each game start appends its decisions to /tmp/rclol-game-decisions.log, for example local_semaphores=1 and core_isolation=0 errno=-1. -all variants (/tmp/rclol-local-mutex-all and the like) enable a local path in every process; they exist for probe tests and are not for normal play.

An on-disk update is not activation. Existing processes keep their old executable/library mappings, and Darling's active overlay can expose an old library even to a new guest process. After manually exiting the game and both Riot/League clients, restart the entire namespace:

darling shutdown
sh scripts/run.sh

darling shutdown terminates the guest namespace; never run it during a match or while applications have work to save. Reopening only the client is insufficient. scripts/run.sh renders the current launcher templates for the next launch. No FPS/RAM improvement has yet been established for these incremental changes; see status and standalone checks.

For GL resolvers, multi-window presentation and the LaunchServices crash fix, use the full paired installation from the README after exiting clients. It rebuilds the native GL wrapper and installs both GL library copies, as well as the LaunchServices absolute subframework path used by Electron. Copying only the OpenGL framework or only the flat LaunchServices override leaves an old library reachable.

Build/install steps need substantial time and storage. All 29 component patches were checked against their pinned base indexes. The runtime has been exercised on the new RX 6600 PC, but an entirely fresh checkout/build/install remains to be validated on another machine; patch applicability alone does not establish distro/driver support. Use JOBS=2 ./scripts/build.sh if the default nproc parallelism exceeds your available RAM. See troubleshooting before changing compiler flags or prefix ownership.

Committing from a new PC

Building does not require a Git author identity, but committing does. If Git reports Author identity unknown, configure your own name and email in this repository (use your GitHub noreply address if desired):

git config user.name "Your Name"
git config user.email "your-commit-email"

Omitting --global keeps these settings local to this checkout. Commit source patches, tools and docs; keep build/, binaries, backups, captures and account data ignored.