mirror of
https://github.com/lucannez64/darling-lol.git
synced 2026-10-26 19:47:42 +00:00
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]>
This commit is contained in:
1 parent
5609d96f16
commit
4370f375cd
51 files changed
+8817
-498
No files matched your search
@@ -64,6 +64,8 @@ Start with standalone regression checks, then play a match manually. See [testin
|
||||
|
||||
FPS counters, game presentation requests and physical display refresh are different measurements. Record average and p99 frame times, the active scene, graphics settings, charger/power state and whether the game stayed focused. See [current status](docs/STATUS.md).
|
||||
|
||||
On the Ryzen 5 5500 / RX 6600 test PC, ARAM on Hexgate went from 84 to 127 FPS average over 2026-10-13/14, with the slowest 10-second stretches around 110 ([status](docs/STATUS.md#game-fps-2026-10-1314)). The game-process speed switches and their opt-out files are listed in [setup](docs/SETUP.md#game-process-switches), and passive match capture is described in [testing](docs/TESTING.md#passive-match-capture).
|
||||
|
||||
For existing installations, [incremental update instructions](docs/SETUP.md#updating-an-existing-runtime) explain the server and memory installers, user-owned backups and the full Darling restart needed to activate replacements. Perform activation only after manually exiting the game and both clients.
|
||||
|
||||
## Layout
|
||||
|
||||
+65
-1
@@ -32,14 +32,78 @@ These commands assume the pinned, patched source tree and its configured Makefil
|
||||
|
||||
```sh
|
||||
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 memory installer updates the guest `RuntimeLibraries` and per-prefix bootstrap copy of `libsystem_kernel.dylib`, plus `build/riot-libsystem_kernel.dylib` if present. 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.
|
||||
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](TROUBLESHOOTING.md#memory-installer-permission-denied). 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:
|
||||
|
||||
```sh
|
||||
sudo install -m755 build/riot-mldr /usr/local/libexec/darling/usr/libexec/darling/mldr
|
||||
```
|
||||
|
||||
The optimized Foundation overlay is built and installed per user:
|
||||
|
||||
```sh
|
||||
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
|
||||
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:
|
||||
|
||||
```sh
|
||||
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](TESTING.md#optimized-library-overlays-and-string-primitives).
|
||||
|
||||
### Metal and Indium overlays
|
||||
|
||||
The Metal wrapper and Indium share a private blit interface, so install them as a pair:
|
||||
|
||||
```sh
|
||||
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:
|
||||
|
||||
```sh
|
||||
|
||||
+28
-2
@@ -1,7 +1,33 @@
|
||||
# Snapshot status — 2026-10-13
|
||||
# Snapshot status — 2026-10-14
|
||||
|
||||
This repository preserves a development runtime, including historical experiments. All 29 patches apply to their pinned revisions and the exported source matches the development tree for the patched files. A fresh checkout/build/install has not been validated end to end on another machine. The normal workflow uses manual client installation and play, without debugger attachment, client API calls or input automation.
|
||||
|
||||
## Game FPS, 2026-10-13/14
|
||||
|
||||
All matches were ARAM, played manually with unchanged game settings; FPS is the game's own 10-second average (`netstats.csv`). Bilgewater runs a few percent below Hexgate on the same build.
|
||||
|
||||
| Match | Map | Runtime | Avg FPS | Slowest quarter |
|
||||
|---|---|---|---|---|
|
||||
| 10-13 12:51 | Hexgate | before this work | 83.8 | — |
|
||||
| 10-13 13:14 | Bilgewater | before this work | 76.9 | — |
|
||||
| 10-13 16:54 | Hexgate | first kernel batch | 110.7 | — |
|
||||
| 10-13 20:36 | Bilgewater | + `-O2` libc/libc++, AVX2 string primitives, real CPU count | 109.7 | — |
|
||||
| 10-14 13:04 | Hexgate | lock switches off (lost to a reboot) | 119.2 | 104.2 |
|
||||
| 10-14 14:18 | Hexgate | + core isolation (since made opt-in) | 102.9 | 92.0 |
|
||||
| 10-14 14:42 | Bilgewater | local mutexes, condition variables and semaphores | 119.5 | 103.4 |
|
||||
| 10-14 16:26 | Hexgate | + Metal bind deduplication and cheaper blits | 126.7 | 109.6 |
|
||||
|
||||
The 13:04 and 16:26 Hexgate matches were captured with the passive sampler (`tools/riot-match-capture`). Per frame, the game's main thread waited 1.36 ms on darlingserver in the first and 0.10 ms in the second. It blocked 39 times per frame in the first and 14 in the second. darlingserver CPU fell from 82% to 61% of a core. GPU utilization stayed at 70–74%: the main thread is the limit and is now about 90% busy running code. Frame times within each 10-second window vary by about 1–1.4 ms (standard deviation) and never more than 2.7 ms, versus hitch windows of 77–168 ms in the matches of 10-13 morning.
|
||||
|
||||
What changed in the runtime (all in the patches):
|
||||
|
||||
- **Process-local pthread mutexes and condition variables** (`psynch_mutexwait.c`, `bsdthread_register.c`): contended first-fit mutexes and their condition variables sleep on futexes inside the game instead of making two darlingserver round trips. On by default in the League game process only.
|
||||
- **Process-local Mach semaphores** (`mach_traps.c`): semaphores the game creates are counted and waited on in the process. A destroyed or reused slot can no longer satisfy a wait or take another semaphore's signal: the count and the slot generation change in one compare-exchange, and a destroy leaves a negative marker. A forked child resets the table, and every name the table did not create goes to the server. The game's worker threads wake 380 times a second instead of 940. On by default in the game process only. The 2026-10-10 client crash happened while the switch was on in every process; the clients still use the server path. A local wait cannot be interrupted by `thread_abort`, and a semaphore shared with another task is not supported locally.
|
||||
- **Metal render encoder** (`MTLRenderCommandEncoder.mm`): re-binding the identical texture, sampler, buffer and offset, pipeline or depth-stencil state at the same slot is dropped before any work. The comparison key is the Indium object, which the encoder's binding tables keep alive, so a reused wrapper address cannot alias. The depth-stencil state, which Indium does not retain, is retained by the record. `endEncoding` clears the records. Blit encoder calls pass the resources by reference: 5 atomic operations per copy become 1. `-[MTLVertexDescriptor reset]` no longer resets every attribute twice.
|
||||
- **Core isolation** (`mach_traps.c`, opt-in with `/tmp/rclol-core-isolation`): gives the game's main thread its own Linux core-scheduling cookie. In its one match the main thread waited for a CPU 12.6% of the time instead of 1%, and FPS fell from 118 to 103 over the same minutes. It stays in the tree for experiments only.
|
||||
|
||||
Where the main thread's time goes now: about 72% of its user time is in the game binary, mostly in memory-latency-bound code. The game binary runs natively and selects its AVX paths with `cpuid`. Objective-C dispatch, the Metal wrapper, Indium, the allocator and the Vulkan driver take most of the rest. The remaining Darling-side levers are smaller: more of the heap on transparent huge pages (35% of anonymous memory is), and the per-draw Metal/Objective-C path.
|
||||
|
||||
## New PC: verified runtime changes
|
||||
|
||||
The current machine uses a Ryzen 5 5500, RX 6600 and Linux 7.2.9. The source snapshot includes the build corrections needed with its newer Clang/LLVM: x86-64-only OpenDirectory, consistent objc4 debug macros, older Objective-C collection code generation and LLVM 22/23 branch opcode compatibility.
|
||||
@@ -22,7 +48,7 @@ Server-only CPU samples identified worker context switching and receive/message
|
||||
|
||||
The receive queue now retains unused receive slots, transfers real messages into the existing queue and adopts every returned control header before potentially throwing. This preserves received descriptor ownership even if deque allocation fails. Disabled debug logging no longer prepares PID/TID strings. Independent host socket checks reduce warmed empty receive allocations from 32 to zero and single-packet allocations from 64 to two. Normal and address/undefined-behavior sanitizer checks passed, including exception cleanup and descriptor ownership.
|
||||
|
||||
The normal launcher no longer enables a client inspector, starts scheduling/update/link helpers or forces unlimited UI frames. The user installed the new server atomically while the old server remained active. **No post-activation FPS gain has been measured.** Validate through manually played matches after a full Darling restart when the game and clients are closed.
|
||||
The normal launcher no longer enables a client inspector, starts scheduling/update/link helpers or forces unlimited UI frames. The user installed the new server atomically while the old server remained active. Later matches measured the gains of this and the following changes; see [Game FPS, 2026-10-13/14](#game-fps-2026-10-1314).
|
||||
|
||||
## Memory reclamation and activation
|
||||
|
||||
|
||||
+171
-6
@@ -9,11 +9,13 @@ After preparing the pinned source tree:
|
||||
```sh
|
||||
python3 scripts/prepare.py --check-only
|
||||
python3 tools/riot-launcher-offline-check.py
|
||||
python3 tools/riot-memory-installer-offline-check.py
|
||||
python3 tools/riot-foundation-installer-offline-check.py
|
||||
node tools/riot-league-update-check.js
|
||||
node tools/riot-open-url-check.js
|
||||
```
|
||||
|
||||
The patch check uses temporary base indexes and does not reset or modify the source checkout. The launcher check uses stub commands and never starts or contacts Riot or Darling. The JavaScript checks use mock Electron/filesystem/HTTP objects; they do not enable the legacy hooks in a real client. The installer handoff mock covers 28 scenarios, including HTTP 409, interrupted verification and preserving active downloads.
|
||||
The patch check uses temporary base indexes and does not reset or modify the source checkout. The launcher check uses stub commands and never starts or contacts Riot or Darling. The memory-installer check imports the installer and exercises its policy (default source is the prepared artifact; the artifact is never a destination of a different library) in temporary directories. The Foundation-installer check does the same for the overlay installer: it seeds a temporary framework from a stub SDK, replaces `Versions/C/Foundation`, keeps the standard symlinks, stages a backup, treats a re-run as current and refuses a non-Mach-O source. The JavaScript checks use mock Electron/filesystem/HTTP objects; they do not enable the legacy hooks in a real client. The installer handoff mock covers 28 scenarios, including HTTP 409, interrupted verification and preserving active downloads.
|
||||
|
||||
## Standalone guest probes
|
||||
|
||||
@@ -62,16 +64,53 @@ Expected: `warm_empty_allocations=0 single_packet_allocations=2`, correct creden
|
||||
|
||||
The same source passed AddressSanitizer/UndefinedBehaviorSanitizer and leak detection. Allocation counts depend on this fixture/compiler/library; preserve the no-wasted-slot property rather than interpreting them as game performance. See [setup](SETUP.md#updating-an-existing-runtime) for server build/install/activation.
|
||||
|
||||
## Host reply path regression
|
||||
|
||||
The direct-reply checks run entirely on independent host sockets and exercise the real `src/message.cpp` (`MessageQueue::pushAndSend`, `sendMany`, `empty`):
|
||||
|
||||
```sh
|
||||
c++ -std=c++17 -O2 -pthread \
|
||||
-I build/darling-src/src/external/darlingserver/internal-include \
|
||||
tools/riot-host-reply-check.cpp \
|
||||
build/darling-src/src/external/darlingserver/src/message.cpp \
|
||||
-o build/riot-host-reply-check
|
||||
build/riot-host-reply-check
|
||||
```
|
||||
|
||||
Expected: `HOST_REPLY pass=1 direct_send=ok eagain_retry=ok fifo=ok concurrency=ok notify=ok descriptors=ok dead_peer=ok`. It covers: a direct send that drains the queue does not invoke the wakeup callback while a queued message does; EAGAIN leaves the queue intact and the retry path delivers every message in order; four concurrent pushers plus a draining "main loop" preserve per-pusher order without loss or duplication; descriptors are delivered exactly once, closed on the sender side right after the send and released on the receiver side; and a dead peer (ENOENT after unlink, ECONNREFUSED after close) drops only its own message without wedging the queue or leaking descriptors. Compile the same way with `-fsanitize=address,undefined` and `-fsanitize=thread`; the ThreadSanitizer run is the important one.
|
||||
|
||||
A same-process microbenchmark compares the direct path with the old `push` + eventfd + main-loop `sendMany` hand-off:
|
||||
|
||||
```sh
|
||||
c++ -std=c++17 -O2 -pthread \
|
||||
-I build/darling-src/src/external/darlingserver/internal-include \
|
||||
tools/riot-host-reply-bench.cpp \
|
||||
build/darling-src/src/external/darlingserver/src/message.cpp \
|
||||
-o build/riot-host-reply-bench
|
||||
build/riot-host-reply-bench 5000
|
||||
```
|
||||
|
||||
Expected: a `HOST_REPLY_BENCH same_process=1 ...` line; the direct path is a few microseconds faster on average. Both endpoints are threads of one process, so this isolates the hand-off, not real guest scheduling.
|
||||
|
||||
## Memory reclamation regression
|
||||
|
||||
The memory-reclamation check links the actual Darling `sys_madvise` implementation to host syscall hooks and uses only its own 4 MiB anonymous allocation:
|
||||
The memory-reclamation check links the actual Darling `sys_madvise` source to host syscall hooks and a `read_string` stub that selects the host's transparent-huge-page policy, and uses only its own 32 MiB anonymous allocation:
|
||||
|
||||
```sh
|
||||
python3 tools/build-riot-host-madvise-check.py
|
||||
build/logs/madvise-fix/host-madvise-check
|
||||
```
|
||||
|
||||
Expected: `reclaimable_KiB=4096 reuse_reclaimable_KiB=0 contents=preserved`. The old implementation returns success without marking any pages reclaimable. Darwin `MADV_FREE` and `MADV_FREE_REUSABLE` now map to Linux `MADV_FREE`; Darwin `MADV_FREE_REUSE` uses `MADV_POPULATE_WRITE` to cancel lazy reclamation while preserving surviving contents. The reuse operation requires Linux 5.14 or newer; an unsupported or invalid operation returns the translated kernel error rather than reporting false success. Reclamation is lazy and does not promise an immediate RSS decrease when RAM is available.
|
||||
The build script runs the check once per policy. Expected:
|
||||
|
||||
```
|
||||
HOST_MADVISE pass=1 mode=always madvise_calls=6 sub_page_syscalls=0 reclaimable_KiB=32768 reuse_reclaimable_KiB=0 anon_huge_KiB=32768/32768 contents=preserved
|
||||
HOST_MADVISE pass=1 mode=never ... reclaimable_KiB=32768 reuse_reclaimable_KiB=0 ...
|
||||
HOST_MADVISE pass=1 mode=madvise ... reclaimable_KiB=32768 reuse_reclaimable_KiB=0 ...
|
||||
```
|
||||
|
||||
`mode=always` covers the deployment host: sub-huge-page hints reach no `madvise` syscall and do not reduce the mapping's `AnonHugePages` (read from `/proc/self/smaps`), the 2 MiB-aligned region is marked reclaimable, an unaligned large range is rounded inward to the contained whole huge pages, and reuse cancels reclamation with contents preserved. `mode=never`/`mode=madvise` prove the full range is still forwarded when the host does not give this layer's mappings huge pages. The old implementation returned success without marking any pages.
|
||||
|
||||
Darwin `MADV_FREE` and `MADV_FREE_REUSABLE` map to Linux `MADV_FREE` for whole 2 MiB huge pages only; Darwin `MADV_FREE_REUSE` uses `MADV_POPULATE_WRITE` over the full requested range to cancel lazy reclamation while preserving surviving contents. The reuse operation requires Linux 5.14 or newer; an unsupported or invalid operation returns the translated kernel error rather than reporting false success. Reclamation is lazy and does not promise an immediate RSS decrease when RAM is available.
|
||||
|
||||
For the actual guest ABI, `tools/build-riot-madvise-check.py` builds an independent executable. After a full restart has activated the installed library, run it as follows; replace `me` and the checkout path:
|
||||
|
||||
@@ -82,9 +121,135 @@ darling shell env \
|
||||
/Volumes/SystemRoot/path/to/darling-lol/build/logs/madvise-fix/riot-madvise-check
|
||||
```
|
||||
|
||||
Expected: `DARLING_MADVISE pass=1 reclaimable_KiB=4096 reuse_reclaimable_KiB=0 contents=preserved`. To test a candidate before installation, place only its `libsystem_kernel.dylib` in a private directory and prepend its guest-visible path to `DYLD_LIBRARY_PATH`. The candidate passed; the previous installed implementation failed with zero reclaimable pages. The probe uses only its own allocation and Linux memory counters, and never contacts a client/game. It tests free/reuse hints directly, not complete Apple malloc or client behavior under memory pressure.
|
||||
Expected: `DARLING_MADVISE pass=1 reclaimable_KiB=4096 reuse_reclaimable_KiB=0 contents=preserved`. The probe marks a 2 MiB-aligned 4 MiB region inside a larger mapping, because only whole huge pages are forwarded; it uses only its own allocation and Linux memory counters, and never contacts a client/game.
|
||||
|
||||
Install the tested library with `python3 tools/install-riot-memory-runtime.py`. It uses user-owned `build/memory-runtime-backups` instead of the shared `build/backups` directory, which an earlier sudo server install may have made root-owned. It atomically replaces the two per-user kernel-library copies and the prepared runtime artifact, without stopping any process or changing the system SDK. The active Darling overlay can retain the old file even for new guest processes after an update to its backing directory; this was verified by differing host versus namespace checksums. After exiting the game and both clients manually, run `darling shutdown`, then `sh scripts/run.sh` to start a new namespace. Do not restart Darling during a match. Verify reclamation and client memory use after that restart.
|
||||
## Host vDSO clock regression
|
||||
|
||||
The clock check compiles the actual `emulation/src/linux_premigration/ext/clock_gettime.c` against a stub ELF bridge backed by the host's `dlopen`/`dlsym`:
|
||||
|
||||
```sh
|
||||
python3 tools/build-riot-host-vdso-clock-check.py
|
||||
build/logs/vdso-clock/host-vdso-clock-check default
|
||||
build/logs/vdso-clock/host-vdso-clock-check no-symbol
|
||||
build/logs/vdso-clock/host-vdso-clock-check race
|
||||
```
|
||||
|
||||
Expected (costs vary with machine load):
|
||||
|
||||
```
|
||||
HOST_VDSO_CLOCK pass=1 mode=default syscall_ns=528 vdso_ns=24 dlopen_calls=1 dlsym_calls=1 million_reads=1 threads=8 fork=preserved
|
||||
HOST_VDSO_CLOCK pass=1 mode=no-symbol ... (syscall fallback, correct values)
|
||||
HOST_VDSO_CLOCK pass=1 mode=race ... dlopen_calls=1 ...
|
||||
```
|
||||
|
||||
`default` reads the clock before the bridge exists (syscall fallback, not cached), resolves the vDSO entry once the bridge appears, checks a million reads per clock id against the raw syscall and across eight threads, and checks that a forked child inherits the resolved pointer. `no-symbol` proves the fallback when the bridge has no entry. `race` starts eight threads before resolution and requires that only one resolution ran.
|
||||
|
||||
Install the tested library with `python3 tools/install-riot-memory-runtime.py`. Its default source is the optimized relink `build/riot-libsystem_kernel.dylib`; if it is missing, build it first with `python3 build/runtime/tools/build-riot-kqueue-runtime.py`. The installer uses user-owned `build/memory-runtime-backups` instead of the shared `build/backups` directory, which an earlier sudo server install may have made root-owned. It atomically replaces the two per-user kernel-library copies, without stopping any process or changing the system SDK. The prepared artifact is never a destination of a different library: a run with an explicit `--binary` replaces the two prefix copies and leaves `build/riot-libsystem_kernel.dylib` untouched (the check `tools/riot-memory-installer-offline-check.py` covers this). The active Darling overlay can retain the old file even for new guest processes after an update to its backing directory; this was verified by differing host versus namespace checksums. After exiting the game and both clients manually, run `darling shutdown`, then `sh scripts/run.sh` to start a new namespace. Do not restart Darling during a match. Verify reclamation and client memory use after that restart.
|
||||
|
||||
## CPU topology regression
|
||||
|
||||
The hw.* / machdep.cpu.* handlers are checked on the host against the host's real topology. The check links the actual `sysctl_hw.c` and `sysctl_unspec.c` to host shims (real `/sys` reads, forwarded syscalls, sentinel Mach replies) and drives each handler directly plus `sysctlbyname("hw.*")` through the real name-to-oid lookup:
|
||||
|
||||
```sh
|
||||
python3 tools/build-riot-host-sysctl-cpu-check.py
|
||||
```
|
||||
|
||||
Expected on this 6-core/12-thread host:
|
||||
|
||||
```
|
||||
HOST_SYSCTL_CPU pass=1 mode=normal ncpu=12 active=12 logicalcpu=12 logicalcpu_max=12 physicalcpu=6 physicalcpu_max=6 model=MacBookPro16,1
|
||||
HOST_SYSCTL_CPU pass=1 mode=affinity ncpu=12 active=4 logicalcpu=4 logicalcpu_max=12 physicalcpu=4 physicalcpu_max=6 model=MacBookPro16,1
|
||||
HOST_SYSCTL_CPU pass=1 mode=no-detect ncpu=8 active=8 logicalcpu=8 logicalcpu_max=8 physicalcpu=8 physicalcpu_max=8 model=MacBookPro16,1
|
||||
HOST_SYSCTL_CPU pass=1 mode=optout ncpu=8 active=8 logicalcpu=8 logicalcpu_max=8 physicalcpu=8 physicalcpu_max=8 model=MacBookPro16,1
|
||||
```
|
||||
|
||||
`normal` is the deployment case: hw.ncpu/hw.logicalcpu_max/hw.physicalcpu_max are the host's online counts, hw.activecpu/hw.availcpu/hw.logicalcpu/hw.physicalcpu follow this process's affinity mask. `affinity` restricts the check to four CPUs first, so avail/active fall to 4 while the max values stay at 12 (and 6). `no-detect` fails every `/sys` CPU read and `sched_getaffinity`, `optout` sets the flag file (a check-local path compiled into that binary); both must fall back to the old fixed 8s. The emulated `hw.model` must stay `MacBookPro16,1` in every mode. The production opt-out flag is `/tmp/rclol-fixed-cpu-count`; the host check overrides only the path, never touches the real file.
|
||||
|
||||
The commpage fields the same change publishes (`_COMM_PAGE_NCPUS`, `_COMM_PAGE_ACTIVE_CPUS`, `_COMM_PAGE_PHYSICAL_CPUS`, `_COMM_PAGE_LOGICAL_CPUS` and the `kNumCPUs` capability bits) come from mldr. `build/riot-mldr` is the rebuilt loader; activating it means replacing the root-owned SDK copy (`/usr/local/libexec/darling/usr/libexec/darling/mldr`) after exiting clients and shutting down Darling, as in the setup notes.
|
||||
|
||||
## Optimized library overlays and string primitives
|
||||
|
||||
The string primitives the guest resolves to are checked on the host against two copies of the same libplatform sources, a byte-loop reference, and an unmapped page on each side of every buffer:
|
||||
|
||||
```sh
|
||||
python3 tools/build-riot-platform-memory-check.py
|
||||
```
|
||||
|
||||
Expected (five runs: the generic path, the vector path, the vector plus REP path, then the same check built with AddressSanitizer and UndefinedBehaviorSanitizer):
|
||||
|
||||
```
|
||||
PLATFORM_MEMORY_CHECK pass=1 capabilities=0 cases=194680 mismatches=0 page_faults=0 variants=2
|
||||
PLATFORM_MEMORY_CHECK pass=1 capabilities=0x20000000 cases=194680 mismatches=0 page_faults=0 variants=2
|
||||
PLATFORM_MEMORY_CHECK pass=1 capabilities=0x28000000 cases=194680 mismatches=0 page_faults=0 variants=2
|
||||
PLATFORM_MEMORY_CHECK pass=1 capabilities=0 cases=194680 mismatches=0 page_faults=0 variants=2
|
||||
PLATFORM_MEMORY_CHECK pass=1 capabilities=0x28000000 cases=194680 mismatches=0 page_faults=0 variants=2
|
||||
```
|
||||
|
||||
`current` is the code and flags the installed `libsystem_platform.dylib` was built from and `new` is what `tools/build-riot-platform-strings.py` produces; every case runs against both and against the reference, for every size up to 512 with a subset of the 64x64 alignments, for random sizes and alignments, for both overlap directions, and for buffers that only just fit the guarded region. The same script builds `build/riot-platform-memory-bench`, which measures the same functions against the host's glibc for every size from 1 to 256 and at 512 B to 1 MiB (`BENCH size=... op=... current_ns=... new_ns=... glibc_ns=...`). That is a throughput measurement of the primitives, not an FPS measurement.
|
||||
|
||||
Which file the guest actually loads for a library, and how that file was compiled, is listed by:
|
||||
|
||||
```sh
|
||||
python3 tools/riot_overlay_audit.py
|
||||
```
|
||||
|
||||
An overlay is compared with the library it would replace by:
|
||||
|
||||
```sh
|
||||
python3 tools/riot_overlay_verify.py build/riot-libsystem_c.dylib /usr/local/libexec/darling/usr/lib/system/libsystem_c.dylib
|
||||
```
|
||||
|
||||
Expected `OVERLAY_VERIFY pass=1` plus the export counts and the stack-reference share before and after. The check requires the same install name and versions, the same dependent libraries, re-exports and segment protections, the same exported symbols (a weak definition the optimized build drops is reported as a note, because the ABI makes a weak definition optional), no new undefined symbol, and an improved optimization estimate. It never starts a guest process.
|
||||
|
||||
The batch installer's policy and rollback are exercised in temporary directories:
|
||||
|
||||
```sh
|
||||
python3 tools/riot-libs-installer-offline-check.py
|
||||
```
|
||||
|
||||
Expected: `LIBS_INSTALLER_OFFLINE pass=1 overlays=5 backup_before_replace=1 rollback_restores=1 rollback_removes=1 idempotent=1 rejects_non_macho=1`. It imports the installer, never executes it, and never touches the real prefix.
|
||||
|
||||
## Local semaphore model
|
||||
|
||||
```sh
|
||||
sh tools/riot-lsem-model/run.sh
|
||||
sh tools/riot-lsem-model/sensitivity-check.sh
|
||||
```
|
||||
|
||||
`extract-lsem.py` copies the shipped local-semaphore table and trap functions verbatim out of the patched `mach_traps.c`; it refuses to run if the generation-checked token compare-exchange or the negative destroy marker is missing. The model compiles that code with shims for the raw syscalls and server RPCs. It runs create/destroy churn with name reuse on 8 threads, waiter/signaler pairs, timed waits, `signal_all`, destroy while waiting, and the XNU ordering of `wait_signal`. Expected: `LSEM_MODEL checks=686 failed=0 spurious_success=0 lost_wakeup=0 steal=0 hang=0`. The sensitivity check re-breaks the protocol in a copy and must report failures: positive tombstone (`failed=5 spurious_success=1`), missing generation bump (`failed=2 spurious_success=400`). Outputs go to `build/riot-lsem-model`.
|
||||
|
||||
## Metal bind model
|
||||
|
||||
```sh
|
||||
sh tools/riot-metal-bind-model/run.sh
|
||||
```
|
||||
|
||||
The bind-tracking helpers are extracted verbatim from `MTLRenderCommandEncoder.mm`. The model drives two encoder states, one receiving every call and one only the forwarded calls, and requires them to be equal after every call. That covers directed scenarios, among them a freed wrapper whose address is reused by a different resource and depth-stencil retention, plus 119,401 random calls. Expected: `randomized calls=119401 forwarded=110294 dropped=9107 (7.6%)`, `BIND_DEDUP_MODEL failures=0` and `BLIT_CAST_MODEL failures=0`.
|
||||
|
||||
## Passive match capture
|
||||
|
||||
These tools observe a manually played match from the host. They wait for the game process, stop when it exits, and never attach to it, write to it, or read its memory.
|
||||
|
||||
```sh
|
||||
python3 tools/riot-match-capture/sample-match.py # start before the match; writes build/logs/host-sample-<stamp>.jsonl
|
||||
python3 tools/riot-match-capture/sample-report.py build/logs/host-sample-<stamp>.jsonl
|
||||
python3 tools/riot-match-capture/compare-matches.py \
|
||||
"before=build/logs/host-sample-A.jsonl:$HOME/.darling/Applications/League of Legends.app/Contents/LoL/Logs/GameLogs/<A>" \
|
||||
"after=build/logs/host-sample-B.jsonl:$HOME/.darling/Applications/League of Legends.app/Contents/LoL/Logs/GameLogs/<B>"
|
||||
```
|
||||
|
||||
`sample-match.py` reads only `/proc` and `/sys`. Per thread of the game and of darlingserver it records scheduler run, runqueue and wakeup counts every second, and the system call each thread is blocked in (50 Hz for the main thread, 10 Hz for the others). It also records GPU busy percentage, the game's huge-page share every 30 seconds, and whether another of our threads runs on the main thread's SMT sibling. `compare-matches.py` aligns the capture with the game's 10-second `netstats.csv` rows. It prints average FPS, the worst row, the slowest quarter and, per frame, the main thread's CPU time, server waits, futex waits and blocks.
|
||||
|
||||
For an instruction-address profile, which needs `perf_event_paranoid` ≤ 2 (user-space samples only), build the sampler once and run the wrapper instead:
|
||||
|
||||
```sh
|
||||
c++ -O2 -std=c++17 -o build/riot-ip-profiler tools/riot-match-capture/ip-profiler.cpp
|
||||
python3 tools/riot-match-capture/profile-match.py
|
||||
python3 tools/riot-match-capture/profile-report.py build/logs/host-profile-<stamp> 120 4
|
||||
python3 tools/riot-match-capture/profile-symbols.py build/logs/host-profile-<stamp> <main tid> Metal libobjc.A.dylib libindium.dylib
|
||||
```
|
||||
|
||||
The profiler samples each thread's user-space instruction pointer every 1 ms on the main thread and every 4 ms on the others, with no call stacks and no registers. The wrapper records the memory map every 15 seconds so the report can attribute samples to modules. Ask the person playing before profiling their match.
|
||||
|
||||
## Manual game observation
|
||||
|
||||
|
||||
@@ -78,6 +78,12 @@ A separate multi-window path stopped the active async presenter when another dif
|
||||
|
||||
## Lower fight FPS than Windows
|
||||
|
||||
Host observation found substantial Darling CPU/RPC costs and main-thread socket waits, with reported fight FPS of 70–80 and quieter moments at 100–120 on the Ryzen 5 5500 / RX 6600. GPU utilization averaged about 61%; the CPU reached 92–94°C. These observations identify areas to investigate, not the full cause of every slow frame.
|
||||
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.
|
||||
|
||||
The server now retains unused receive buffers and avoids preparing disabled debug strings. The host socket fixture reduces warmed empty receive allocations from 32 to zero and single-packet allocations from 64 to two. Normal launches also stop forcing unlimited UI frames and inspector scheduling hooks. An FPS gain has not yet been measured after activation. Compare manually played matches with the same graphics settings and record CPU/GPU utilization, temperature and memory pressure. Do not use legacy automated practice or profiling cycles.
|
||||
If FPS drops back towards the 10-13 morning numbers (75–85 on Hexgate):
|
||||
|
||||
1. Make sure the current kernel library, Metal and Indium overlays are installed and active: exit the game and both clients, then `darling shutdown` and `sh scripts/run.sh` ([setup](SETUP.md#game-process-switches)).
|
||||
2. Check the game's line in `/tmp/rclol-game-decisions.log`: it should read `local_semaphores=1` and `core_isolation=0`. No `/tmp/rclol-no-local-*` file should exist unless you created it on purpose.
|
||||
3. Capture a match with `tools/riot-match-capture/sample-match.py` and compare it with an earlier one using `compare-matches.py` ([testing](TESTING.md#passive-match-capture)). A server wait above about 0.5 ms per frame means one of the local paths is off. A runqueue wait above a few percent on the main thread means the CPU is oversubscribed, for example by `/tmp/rclol-core-isolation` or background load.
|
||||
|
||||
Compare matches on the same map: Bilgewater runs a few percent below Hexgate.
|
||||
+248
-11
@@ -310,6 +310,95 @@ index 0000000..1741d59
|
||||
+ }
|
||||
+ free(fp);
|
||||
+}
|
||||
diff --git a/duct-tape/src/host.c b/duct-tape/src/host.c
|
||||
index a841dfd..9fe8bb4 100644
|
||||
--- a/duct-tape/src/host.c
|
||||
+++ b/duct-tape/src/host.c
|
||||
@@ -28,6 +28,64 @@ long sysconf(int name);
|
||||
#define _SC_NPROCESSORS_CONF 83
|
||||
#define _SC_NPROCESSORS_ONLN 84
|
||||
|
||||
+// Linux file access for the SMT topology read (manual prototypes, as for sysconf above)
|
||||
+int open(const char* pathname, int flags, ...);
|
||||
+long read(int fd, void* buf, unsigned long count);
|
||||
+int close(int fd);
|
||||
+
|
||||
+// darling-lol: host_info used to report physical_cpu == logical_cpu, which
|
||||
+// tells the guest there is no SMT. Read the sibling count of cpu0 from sysfs
|
||||
+// and divide; on any failure keep the logical count. The per-CPU package/core
|
||||
+// scan in the guest-side sysctl_hw.c is more precise, but host_info has no
|
||||
+// other source for the physical count.
|
||||
+static uint32_t host_smt_siblings(void) {
|
||||
+ static uint32_t cached;
|
||||
+ char buf[64];
|
||||
+ uint32_t count = 0;
|
||||
+ long n;
|
||||
+
|
||||
+ if (cached != 0) {
|
||||
+ return cached;
|
||||
+ }
|
||||
+
|
||||
+ int fd = open("/sys/devices/system/cpu/cpu0/topology/thread_siblings_list", 0 /* O_RDONLY */);
|
||||
+ if (fd < 0) {
|
||||
+ return 1;
|
||||
+ }
|
||||
+ n = read(fd, buf, sizeof(buf) - 1);
|
||||
+ close(fd);
|
||||
+ if (n <= 0) {
|
||||
+ return 1;
|
||||
+ }
|
||||
+ buf[n] = 0;
|
||||
+
|
||||
+ // a list like "0,6" or "0-3": count one CPU per element, ranges as ranges
|
||||
+ const char* s = buf;
|
||||
+ while (*s >= '0' && *s <= '9') {
|
||||
+ unsigned long first = 0, last;
|
||||
+ while (*s >= '0' && *s <= '9') {
|
||||
+ first = first * 10 + (unsigned long)(*s++ - '0');
|
||||
+ }
|
||||
+ last = first;
|
||||
+ if (*s == '-') {
|
||||
+ ++s;
|
||||
+ last = 0;
|
||||
+ while (*s >= '0' && *s <= '9') {
|
||||
+ last = last * 10 + (unsigned long)(*s++ - '0');
|
||||
+ }
|
||||
+ }
|
||||
+ if (last >= first && last - first < 4096) {
|
||||
+ count += (uint32_t)(last - first + 1);
|
||||
+ }
|
||||
+ if (*s == ',') {
|
||||
+ ++s;
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
+ cached = (count != 0) ? count : 1;
|
||||
+ return cached;
|
||||
+}
|
||||
+
|
||||
static void cache_sysinfo(void* context) {
|
||||
struct sysinfo* cached_sysinfo = context;
|
||||
|
||||
@@ -73,11 +131,16 @@ kern_return_t host_info(host_t host, host_flavor_t flavor, host_info_t info, mac
|
||||
|
||||
// if there's room for the modern structure, fill in some additional info
|
||||
if (*count >= HOST_BASIC_INFO_COUNT) {
|
||||
- // TODO: properly differentiate physical vs. logical cores
|
||||
+ uint32_t siblings = host_smt_siblings();
|
||||
dtape_stub_safe("modern HOST_BASIC_INFO");
|
||||
basic_info->cpu_threadtype = CPU_THREADTYPE_NONE;
|
||||
- basic_info->physical_cpu = basic_info->avail_cpus;
|
||||
- basic_info->physical_cpu_max = basic_info->max_cpus;
|
||||
+ // darling-lol: physical cores, not logical CPUs (the guest
|
||||
+ // sizes worker pools off physical_cpu and libmalloc derives
|
||||
+ // its SMT sharing from the commpage's physical/logical pair).
|
||||
+ basic_info->physical_cpu = (siblings != 0 && basic_info->avail_cpus % siblings == 0)
|
||||
+ ? basic_info->avail_cpus / siblings : basic_info->avail_cpus;
|
||||
+ basic_info->physical_cpu_max = (siblings != 0 && basic_info->max_cpus % siblings == 0)
|
||||
+ ? basic_info->max_cpus / siblings : basic_info->max_cpus;
|
||||
basic_info->logical_cpu = basic_info->avail_cpus;
|
||||
basic_info->logical_cpu_max = basic_info->max_cpus;
|
||||
|
||||
diff --git a/duct-tape/src/kqchan.c b/duct-tape/src/kqchan.c
|
||||
index 458e659..2e33f20 100644
|
||||
--- a/duct-tape/src/kqchan.c
|
||||
@@ -1273,7 +1362,7 @@ index d46bdb4..9a58727 100644
|
||||
};
|
||||
};
|
||||
diff --git a/internal-include/darlingserver/message.hpp b/internal-include/darlingserver/message.hpp
|
||||
index 708f3f3..e68cd28 100644
|
||||
index 708f3f3..fd46ab8 100644
|
||||
--- a/internal-include/darlingserver/message.hpp
|
||||
+++ b/internal-include/darlingserver/message.hpp
|
||||
@@ -28,6 +28,7 @@
|
||||
@@ -1296,7 +1385,7 @@ index 708f3f3..e68cd28 100644
|
||||
/**
|
||||
* Default-initializes a Message.
|
||||
*
|
||||
@@ -161,6 +167,8 @@ namespace DarlingServer {
|
||||
@@ -161,13 +167,30 @@ namespace DarlingServer {
|
||||
class MessageQueue {
|
||||
private:
|
||||
std::deque<Message> _messages;
|
||||
@@ -1305,6 +1394,28 @@ index 708f3f3..e68cd28 100644
|
||||
mutable std::mutex _lock;
|
||||
std::function<void()> _messageArrivalNotificationCallback = nullptr;
|
||||
|
||||
+ void _notifyMessageArrival();
|
||||
+
|
||||
public:
|
||||
void setMessageArrivalNotificationCallback(std::function<void()> messageArrivalNotificationCallback);
|
||||
|
||||
void push(Message&& message);
|
||||
+
|
||||
+ /**
|
||||
+ * Pushes a message and immediately tries to send as much of the queue as
|
||||
+ * possible through the given socket from the calling thread.
|
||||
+ *
|
||||
+ * This avoids the event loop hop for messages that can be sent right away.
|
||||
+ * Sending always starts at the head of the queue, so a direct send can
|
||||
+ * never overtake messages queued by an earlier EAGAIN. When the socket
|
||||
+ * would block, the message-arrival notification callback fires (once) so
|
||||
+ * the receiving loop retries once the socket is writable again.
|
||||
+ */
|
||||
+ void pushAndSend(Message&& message, int socket);
|
||||
+
|
||||
std::optional<Message> pop();
|
||||
|
||||
bool sendMany(int socket);
|
||||
diff --git a/internal-include/darlingserver/process.hpp b/internal-include/darlingserver/process.hpp
|
||||
index c97be30..31772dc 100644
|
||||
--- a/internal-include/darlingserver/process.hpp
|
||||
@@ -1323,6 +1434,20 @@ index c97be30..31772dc 100644
|
||||
bool _startSuspended = false;
|
||||
bool _pendingReplacement = false;
|
||||
std::unordered_map<uintptr_t, std::shared_ptr<Kqchan>> _kqchannels;
|
||||
diff --git a/internal-include/darlingserver/server.hpp b/internal-include/darlingserver/server.hpp
|
||||
index b960f17..5f0e621 100644
|
||||
--- a/internal-include/darlingserver/server.hpp
|
||||
+++ b/internal-include/darlingserver/server.hpp
|
||||
@@ -46,6 +46,9 @@ namespace DarlingServer {
|
||||
WorkQueue<std::shared_ptr<Thread>> _workQueue;
|
||||
bool _canRead = false;
|
||||
bool _canWrite = true;
|
||||
+ /* darling-lol: producers send their own replies (see sendMessage) unless
|
||||
+ * DSERVER_DIRECT_REPLY=0 restored the old outbox wakeup hand-off. */
|
||||
+ bool _directReply = true;
|
||||
int _wakeupFD;
|
||||
int _timerFD;
|
||||
uint64_t _currentTimerDeadline = 0;
|
||||
diff --git a/scripts/generate-rpc-wrappers.py b/scripts/generate-rpc-wrappers.py
|
||||
index e35221c..1068426 100755
|
||||
--- a/scripts/generate-rpc-wrappers.py
|
||||
@@ -1871,7 +1996,7 @@ index 7b8000f..dbaf832 100644
|
||||
}
|
||||
|
||||
diff --git a/src/message.cpp b/src/message.cpp
|
||||
index 414c1d6..06b3f67 100644
|
||||
index 414c1d6..a7f5a94 100644
|
||||
--- a/src/message.cpp
|
||||
+++ b/src/message.cpp
|
||||
@@ -24,6 +24,25 @@
|
||||
@@ -1942,7 +2067,7 @@ index 414c1d6..06b3f67 100644
|
||||
memcpy(CMSG_DATA(credHeader), &creds, sizeof(struct ucred));
|
||||
};
|
||||
|
||||
@@ -496,13 +509,47 @@ std::optional<DarlingServer::Message> DarlingServer::MessageQueue::pop() {
|
||||
@@ -496,13 +509,52 @@ std::optional<DarlingServer::Message> DarlingServer::MessageQueue::pop() {
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1959,6 +2084,11 @@ index 414c1d6..06b3f67 100644
|
||||
+ if (*(const int*) data != 33) {
|
||||
+ return;
|
||||
+ }
|
||||
+ /* darling-lol: replies from workers and receives on the main loop can now
|
||||
+ * reach this logging helper concurrently; serialize the lazy open and the
|
||||
+ * shared counter. */
|
||||
+ static std::mutex m58akLogLock;
|
||||
+ std::scoped_lock m58akLock(m58akLogLock);
|
||||
+ static FILE* f = nullptr;
|
||||
+ static unsigned long n = 0;
|
||||
+ if (!f) {
|
||||
@@ -1991,7 +2121,16 @@ index 414c1d6..06b3f67 100644
|
||||
while (ret >= 0) {
|
||||
len = 0;
|
||||
for (size_t i = 0; i < _messages.size() && i < sizeof(mmsgs) / sizeof(*mmsgs); ++i) {
|
||||
@@ -549,6 +596,7 @@ bool DarlingServer::MessageQueue::sendMany(int socket) {
|
||||
@@ -538,7 +590,7 @@ bool DarlingServer::MessageQueue::sendMany(int socket) {
|
||||
break;
|
||||
} else if (errno == EINTR) {
|
||||
ret = 0;
|
||||
- } else if (errno == EPIPE || errno == ECONNREFUSED) {
|
||||
+ } else if (errno == EPIPE || errno == ECONNREFUSED || errno == ENOENT) {
|
||||
// this means that peer of the first message we tried to send has died.
|
||||
// we'll just drop that message and try the others.
|
||||
_messages.pop_front();
|
||||
@@ -549,6 +601,7 @@ bool DarlingServer::MessageQueue::sendMany(int socket) {
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < ret; ++i) {
|
||||
@@ -1999,7 +2138,7 @@ index 414c1d6..06b3f67 100644
|
||||
auto cb = _messages.front().sendNotificationCallback();
|
||||
_messages.pop_front();
|
||||
if (cb) {
|
||||
@@ -567,10 +615,11 @@ bool DarlingServer::MessageQueue::receiveMany(int socket) {
|
||||
@@ -567,10 +620,11 @@ bool DarlingServer::MessageQueue::receiveMany(int socket) {
|
||||
int ret = 0;
|
||||
|
||||
while (ret >= 0) {
|
||||
@@ -2015,7 +2154,7 @@ index 414c1d6..06b3f67 100644
|
||||
mmsgs[i].msg_len = 0;
|
||||
}
|
||||
|
||||
@@ -587,11 +636,25 @@ bool DarlingServer::MessageQueue::receiveMany(int socket) {
|
||||
@@ -587,11 +641,25 @@ bool DarlingServer::MessageQueue::receiveMany(int socket) {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2046,6 +2185,50 @@ index 414c1d6..06b3f67 100644
|
||||
}
|
||||
}
|
||||
|
||||
@@ -615,3 +683,43 @@ bool DarlingServer::MessageQueue::empty() const {
|
||||
std::unique_lock lock(_lock);
|
||||
return _messages.empty();
|
||||
};
|
||||
+
|
||||
+void DarlingServer::MessageQueue::_notifyMessageArrival() {
|
||||
+ std::function<void()> callback;
|
||||
+
|
||||
+ {
|
||||
+ std::scoped_lock lock(_lock);
|
||||
+ if (_messages.empty()) {
|
||||
+ return;
|
||||
+ }
|
||||
+ callback = _messageArrivalNotificationCallback;
|
||||
+ }
|
||||
+
|
||||
+ if (callback) {
|
||||
+ callback();
|
||||
+ }
|
||||
+};
|
||||
+
|
||||
+void DarlingServer::MessageQueue::pushAndSend(Message&& message, int socket) {
|
||||
+ {
|
||||
+ std::scoped_lock lock(_lock);
|
||||
+ _messages.push_back(std::move(message));
|
||||
+ }
|
||||
+
|
||||
+ bool canSendMore = true;
|
||||
+
|
||||
+ try {
|
||||
+ // send from this thread, but only by draining the queue in order, so that
|
||||
+ // a direct send can never overtake messages queued by an earlier EAGAIN
|
||||
+ canSendMore = sendMany(socket);
|
||||
+ } catch (...) {
|
||||
+ // the messages remain queued; the receiving loop retries and reports the error
|
||||
+ _notifyMessageArrival();
|
||||
+ throw;
|
||||
+ }
|
||||
+
|
||||
+ if (!canSendMore) {
|
||||
+ // the socket is full; the receiving loop has to retry once it is writable
|
||||
+ _notifyMessageArrival();
|
||||
+ }
|
||||
+};
|
||||
diff --git a/src/process.cpp b/src/process.cpp
|
||||
index 9cd7b81..180f372 100644
|
||||
--- a/src/process.cpp
|
||||
@@ -2329,7 +2512,7 @@ index 9cd7b81..180f372 100644
|
||||
continue;
|
||||
}
|
||||
diff --git a/src/server.cpp b/src/server.cpp
|
||||
index e502e28..e225510 100644
|
||||
index e502e28..8d1cf2b 100644
|
||||
--- a/src/server.cpp
|
||||
+++ b/src/server.cpp
|
||||
@@ -28,6 +28,8 @@
|
||||
@@ -2482,7 +2665,21 @@ index e502e28..e225510 100644
|
||||
.get_load_info = dtape_hook_get_load_info,
|
||||
|
||||
.thread_suspend = dtape_hook_thread_suspend,
|
||||
@@ -540,6 +634,7 @@ void DarlingServer::Server::start() {
|
||||
@@ -445,6 +539,13 @@ DarlingServer::Server::Server(std::string prefix):
|
||||
{
|
||||
sharedInstancePointer = this;
|
||||
|
||||
+ /* darling-lol: producers send their own replies by default; DSERVER_DIRECT_REPLY=0
|
||||
+ * restores the old path (every reply wakes the main loop out of epoll_wait). */
|
||||
+ _directReply = []() {
|
||||
+ const char* value = std::getenv("DSERVER_DIRECT_REPLY");
|
||||
+ return !(value && value[0] == '0' && value[1] == '\0');
|
||||
+ }();
|
||||
+
|
||||
// remove the old socket (if it exists)
|
||||
unlink(_socketPath.c_str());
|
||||
|
||||
@@ -540,6 +641,7 @@ void DarlingServer::Server::start() {
|
||||
// TODO: this could be done concurrently
|
||||
auto call = DarlingServer::Call::callFromMessage(std::move(*msg));
|
||||
if (call) {
|
||||
@@ -2490,7 +2687,25 @@ index e502e28..e225510 100644
|
||||
_workQueue.push(call->thread());
|
||||
}
|
||||
}
|
||||
@@ -604,6 +699,25 @@ void DarlingServer::Server::start() {
|
||||
@@ -549,13 +651,15 @@ void DarlingServer::Server::start() {
|
||||
eventfd_t value;
|
||||
eventfd_read(_wakeupFD, &value);
|
||||
|
||||
- if (_canWrite) {
|
||||
+ if (_canWrite || !_outbox.empty()) {
|
||||
_canWrite = _outbox.sendMany(_listenerSocket);
|
||||
}
|
||||
|
||||
struct epoll_event settings;
|
||||
settings.data.ptr = &_wakeupFD;
|
||||
- settings.events = (_canWrite) ? (EPOLLIN | EPOLLONESHOT) : 0;
|
||||
+ // keep the wakeup armed while replies may still be waiting for the socket
|
||||
+ // to become writable again, so a producer's failed direct send is retried
|
||||
+ settings.events = (_canWrite || !_outbox.empty()) ? (EPOLLIN | EPOLLONESHOT) : 0;
|
||||
|
||||
if (epoll_ctl(_epollFD, EPOLL_CTL_MOD, _wakeupFD, &settings) < 0) {
|
||||
throw std::system_error(errno, std::generic_category(), "Failed to modify eventfd in epoll context");
|
||||
@@ -604,6 +708,25 @@ void DarlingServer::Server::start() {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -2516,7 +2731,7 @@ index e502e28..e225510 100644
|
||||
// we're done handling the timerfd;
|
||||
// we don't need to lock anymore (and the following call might need to arm the timer again)
|
||||
lock.unlock();
|
||||
@@ -681,6 +795,7 @@ std::string DarlingServer::Server::prefix() const {
|
||||
@@ -681,6 +804,7 @@ std::string DarlingServer::Server::prefix() const {
|
||||
};
|
||||
|
||||
void DarlingServer::Server::_worker(std::shared_ptr<Thread> thread) {
|
||||
@@ -2524,3 +2739,25 @@ index e502e28..e225510 100644
|
||||
thread->doWork();
|
||||
};
|
||||
|
||||
@@ -815,5 +939,20 @@ std::shared_ptr<DarlingServer::FD> DarlingServer::Monitor::fd() const {
|
||||
};
|
||||
|
||||
void DarlingServer::Server::sendMessage(Message&& message) {
|
||||
- _outbox.push(std::move(message));
|
||||
+ if (!_directReply) {
|
||||
+ _outbox.push(std::move(message));
|
||||
+ return;
|
||||
+ }
|
||||
+
|
||||
+ /* darling-lol: the thread that produced the message usually sends it itself,
|
||||
+ * instead of waking the main loop for a second pass through its event loop.
|
||||
+ * pushAndSend only ever sends from the head of the outbox, so ordering is the
|
||||
+ * same as the main loop's, and it wakes the main loop when the socket is full. */
|
||||
+ try {
|
||||
+ _outbox.pushAndSend(std::move(message), _listenerSocket);
|
||||
+ } catch (const std::system_error&) {
|
||||
+ /* pushAndSend has already left the message queued and woken the main loop,
|
||||
+ * where unexpected send errors have always been reported; a worker
|
||||
+ * microthread must not throw. */
|
||||
+ }
|
||||
};
|
||||
+455
-9
@@ -1,8 +1,8 @@
|
||||
diff --git a/CMakeLists.txt b/CMakeLists.txt
|
||||
index 54d2843..b6ec62e 100644
|
||||
index 54d2843..cb87d13 100644
|
||||
--- a/CMakeLists.txt
|
||||
+++ b/CMakeLists.txt
|
||||
@@ -127,3 +127,12 @@ if (BUILD_TARGET_64BIT)
|
||||
@@ -127,3 +127,27 @@ if (BUILD_TARGET_64BIT)
|
||||
endif (BUILD_TARGET_64BIT)
|
||||
|
||||
install(TARGETS platform DESTINATION libexec/darling/usr/lib/system)
|
||||
@@ -11,26 +11,316 @@ index 54d2843..b6ec62e 100644
|
||||
+# otherwise unoptimized development build. Disable builtin recognition so
|
||||
+# optimizing their loops cannot introduce recursive libc calls.
|
||||
+set_source_files_properties(
|
||||
+ src/string/generic/bzero.c
|
||||
+ src/string/generic/ffsll.c
|
||||
+ src/string/generic/flsll.c
|
||||
+ src/string/generic/memccpy.c
|
||||
+ src/string/generic/memchr.c
|
||||
+ src/string/generic/memcmp.c
|
||||
+ src/string/generic/memmove.c
|
||||
+ src/string/generic/memset_pattern.c
|
||||
+ src/string/generic/bzero.c
|
||||
+ src/string/generic/strchr.c
|
||||
+ src/string/generic/strcmp.c
|
||||
+ src/string/generic/strcpy.c
|
||||
+ src/string/generic/strlcat.c
|
||||
+ src/string/generic/strlcpy.c
|
||||
+ src/string/generic/strlen.c
|
||||
+ src/string/generic/strncmp.c
|
||||
+ src/string/generic/strncpy.c
|
||||
+ src/string/generic/strnlen.c
|
||||
+ src/string/generic/strstr.c
|
||||
+ PROPERTIES COMPILE_OPTIONS "-O2;-fno-builtin")
|
||||
diff --git a/private/platform/string_avx2.h b/private/platform/string_avx2.h
|
||||
new file mode 100644
|
||||
index 0000000..32cdf76
|
||||
--- /dev/null
|
||||
+++ b/private/platform/string_avx2.h
|
||||
@@ -0,0 +1,142 @@
|
||||
+/*
|
||||
+ * AVX2 primitives for the x86_64 string routines.
|
||||
+ *
|
||||
+ * Every _PLATFORM_OPTIMIZED_* selector is 0, so the guest's memcpy, memmove,
|
||||
+ * memset, bzero, memcmp and memchr all land in the generic loops next to this
|
||||
+ * header. Those loops move at most one word per iteration and have no wide
|
||||
+ * path at all; the routines below add one, selected at run time from the
|
||||
+ * commpage capability word (the same word OSAtomicFifo.c reads).
|
||||
+ *
|
||||
+ * mldr maps the commpage and writes the capability word before it loads dyld
|
||||
+ * or any other library (startup/mldr/mldr.c:commpage_setup), and a word that
|
||||
+ * has not been written yet - it is zero - selects the generic loop, so the
|
||||
+ * primitives stay usable on an early or unusual path.
|
||||
+ *
|
||||
+ * Every routine below keeps the exact semantics of the routine it
|
||||
+ * accelerates: it reads and writes [dst, dst+n) and [src, src+n) only, nothing
|
||||
+ * outside them, including the last short copy, so no page boundary is ever
|
||||
+ * crossed. No routine calls out of this library, so all of them stay
|
||||
+ * async-signal safe.
|
||||
+ */
|
||||
+
|
||||
+#ifndef _PLATFORM_STRING_AVX2_H_
|
||||
+#define _PLATFORM_STRING_AVX2_H_
|
||||
+
|
||||
+#include <platform/string.h>
|
||||
+
|
||||
+#if defined(__x86_64__)
|
||||
+
|
||||
+#include <stdint.h>
|
||||
+#include <immintrin.h>
|
||||
+#include <i386/cpu_capabilities.h>
|
||||
+
|
||||
+void *_platform_memmove_avx2(void *dst, const void *src, size_t n);
|
||||
+void *_platform_memset_avx2(void *b, int c, size_t n);
|
||||
+int _platform_memcmp_avx2(const void *s1, const void *s2, size_t n);
|
||||
+void *_platform_memchr_avx2(const void *s, int c, size_t n);
|
||||
+
|
||||
+/* The width all of these move, and the number of bytes one iteration of the
|
||||
+ wide loops moves. */
|
||||
+#define PLATFORM_MEM_VECTOR 32
|
||||
+#define PLATFORM_MEM_STEP 64
|
||||
+
|
||||
+/*
|
||||
+ * The capability word is read once and cached. Two threads can store the same
|
||||
+ * value at the same time, which is harmless; a zero word is re-read, so a
|
||||
+ * process that starts using a primitive before the word is filled in keeps
|
||||
+ * falling back until it is.
|
||||
+ */
|
||||
+static inline uint64_t
|
||||
+platform_cpu_capabilities(void)
|
||||
+{
|
||||
+ static volatile uint64_t capabilities;
|
||||
+
|
||||
+ uint64_t caps = capabilities;
|
||||
+
|
||||
+ if (__builtin_expect(caps == 0, 0)) {
|
||||
+ caps = *(const volatile uint64_t *)(uintptr_t)_COMM_PAGE_CPU_CAPABILITIES64;
|
||||
+ capabilities = caps;
|
||||
+ }
|
||||
+ return (caps);
|
||||
+}
|
||||
+
|
||||
+static inline int
|
||||
+platform_has_avx2(void)
|
||||
+{
|
||||
+ return ((platform_cpu_capabilities() & kHasAVX2_0) != 0);
|
||||
+}
|
||||
+
|
||||
+/*
|
||||
+ * Copies of fewer than 32 bytes as 8, 4, 2 and 1 byte moves. Every load is
|
||||
+ * issued before the stores that could overwrite its bytes, so head and tail
|
||||
+ * may overlap each other and the copy is correct in either direction; all
|
||||
+ * loads and stores stay inside [source, source+length).
|
||||
+ */
|
||||
+typedef unsigned long long __attribute__((aligned(1), may_alias)) platform_u64u;
|
||||
+typedef unsigned int __attribute__((aligned(1), may_alias)) platform_u32u;
|
||||
+typedef unsigned short __attribute__((aligned(1), may_alias)) platform_u16u;
|
||||
+
|
||||
+static inline void
|
||||
+platform_copy_short(unsigned char *destination, const unsigned char *source,
|
||||
+ size_t length)
|
||||
+{
|
||||
+ if (length >= 16) {
|
||||
+ const unsigned long long first = *(const platform_u64u *)source;
|
||||
+ const unsigned long long second = *(const platform_u64u *)(source + 8);
|
||||
+ const unsigned long long low = *(const platform_u64u *)(source + length - 16);
|
||||
+ const unsigned long long high = *(const platform_u64u *)(source + length - 8);
|
||||
+ *(platform_u64u *)destination = first;
|
||||
+ *(platform_u64u *)(destination + 8) = second;
|
||||
+ *(platform_u64u *)(destination + length - 16) = low;
|
||||
+ *(platform_u64u *)(destination + length - 8) = high;
|
||||
+ } else if (length >= 8) {
|
||||
+ const unsigned long long first = *(const platform_u64u *)source;
|
||||
+ const unsigned long long last = *(const platform_u64u *)(source + length - 8);
|
||||
+ *(platform_u64u *)destination = first;
|
||||
+ *(platform_u64u *)(destination + length - 8) = last;
|
||||
+ } else if (length >= 4) {
|
||||
+ const unsigned first = *(const platform_u32u *)source;
|
||||
+ const unsigned last = *(const platform_u32u *)(source + length - 4);
|
||||
+ *(platform_u32u *)destination = first;
|
||||
+ *(platform_u32u *)(destination + length - 4) = last;
|
||||
+ } else if (length >= 2) {
|
||||
+ const unsigned short first = *(const platform_u16u *)source;
|
||||
+ const unsigned short last = *(const platform_u16u *)(source + length - 2);
|
||||
+ *(platform_u16u *)destination = first;
|
||||
+ *(platform_u16u *)(destination + length - 2) = last;
|
||||
+ } else if (length != 0) {
|
||||
+ destination[0] = source[0];
|
||||
+ }
|
||||
+}
|
||||
+
|
||||
+/*
|
||||
+ * Fills of fewer than 32 bytes with 8, 4, 2 and 1 byte stores. A fill may
|
||||
+ * write its last bytes twice, so the overlapping store needs no extra care.
|
||||
+ */
|
||||
+static inline void
|
||||
+platform_fill_short(unsigned char *destination, unsigned char value, size_t length)
|
||||
+{
|
||||
+ const unsigned long long wide = 0x0101010101010101ull * value;
|
||||
+
|
||||
+ if (length >= 16) {
|
||||
+ *(platform_u64u *)destination = wide;
|
||||
+ *(platform_u64u *)(destination + 8) = wide;
|
||||
+ *(platform_u64u *)(destination + length - 16) = wide;
|
||||
+ *(platform_u64u *)(destination + length - 8) = wide;
|
||||
+ } else if (length >= 8) {
|
||||
+ *(platform_u64u *)destination = wide;
|
||||
+ *(platform_u64u *)(destination + length - 8) = wide;
|
||||
+ } else if (length >= 4) {
|
||||
+ *(platform_u32u *)destination = (unsigned)(wide & 0xffffffffu);
|
||||
+ *(platform_u32u *)(destination + length - 4) = (unsigned)(wide & 0xffffffffu);
|
||||
+ } else if (length >= 2) {
|
||||
+ *(platform_u16u *)destination = (unsigned short)(wide & 0xffffu);
|
||||
+ *(platform_u16u *)(destination + length - 2) = (unsigned short)(wide & 0xffffu);
|
||||
+ } else if (length != 0) {
|
||||
+ destination[0] = value;
|
||||
+ }
|
||||
+}
|
||||
+
|
||||
+#endif /* __x86_64__ */
|
||||
+
|
||||
+#endif /* _PLATFORM_STRING_AVX2_H_ */
|
||||
diff --git a/src/string/generic/bzero.c b/src/string/generic/bzero.c
|
||||
index 4559dde..b059fb4 100644
|
||||
--- a/src/string/generic/bzero.c
|
||||
+++ b/src/string/generic/bzero.c
|
||||
@@ -23,6 +23,10 @@
|
||||
|
||||
#include <platform/string.h>
|
||||
|
||||
+#if defined(__x86_64__)
|
||||
+#include <platform/string_avx2.h>
|
||||
+#endif
|
||||
+
|
||||
#if !VARIANT_STATIC
|
||||
// to satisfy compiler-generated memset inside libplatform (e.g. makecontext)
|
||||
__attribute__((visibility("hidden")))
|
||||
@@ -35,10 +39,51 @@ memset(void *b, int c, size_t len)
|
||||
|
||||
#if !_PLATFORM_OPTIMIZED_MEMSET
|
||||
|
||||
+#if defined(__x86_64__)
|
||||
+/*
|
||||
+ * A fill has no overlap to preserve, so the vector and string-engine paths
|
||||
+ * below write 32 bytes at a time and let the last store overlap the ones
|
||||
+ * before it. Only [b, b+len) is written.
|
||||
+ */
|
||||
+__attribute__((target("avx2"), visibility("hidden")))
|
||||
+void *
|
||||
+_platform_memset_avx2(void *b, int c, size_t len)
|
||||
+{
|
||||
+ const __m256i pattern = _mm256_set1_epi8((char)c);
|
||||
+ unsigned char *destination = (unsigned char *)b;
|
||||
+ size_t filled = 0;
|
||||
+
|
||||
+ while (len - filled >= PLATFORM_MEM_STEP) {
|
||||
+ _mm256_storeu_si256((__m256i *)(void *)(destination + filled), pattern);
|
||||
+ _mm256_storeu_si256((__m256i *)(void *)(destination + filled + PLATFORM_MEM_VECTOR), pattern);
|
||||
+ filled += PLATFORM_MEM_STEP;
|
||||
+ }
|
||||
+ while (len - filled >= PLATFORM_MEM_VECTOR) {
|
||||
+ _mm256_storeu_si256((__m256i *)(void *)(destination + filled), pattern);
|
||||
+ filled += PLATFORM_MEM_VECTOR;
|
||||
+ }
|
||||
+ if (filled != len)
|
||||
+ _mm256_storeu_si256((__m256i *)(void *)(destination + len - PLATFORM_MEM_VECTOR), pattern);
|
||||
+ _mm256_zeroupper();
|
||||
+ return (b);
|
||||
+}
|
||||
+#endif /* __x86_64__ */
|
||||
+
|
||||
void *
|
||||
_platform_memset(void *b, int c, size_t len) {
|
||||
unsigned char pattern[4];
|
||||
|
||||
+#if defined(__x86_64__)
|
||||
+ if (platform_has_avx2()) {
|
||||
+ if (len >= PLATFORM_MEM_VECTOR)
|
||||
+ return _platform_memset_avx2(b, c, len);
|
||||
+ /* The doubling fill below costs two memmove calls per call; a
|
||||
+ short fill writes the bytes directly instead. */
|
||||
+ platform_fill_short(b, (unsigned char)c, len);
|
||||
+ return b;
|
||||
+ }
|
||||
+#endif
|
||||
+
|
||||
pattern[0] = (unsigned char)c;
|
||||
pattern[1] = (unsigned char)c;
|
||||
pattern[2] = (unsigned char)c;
|
||||
diff --git a/src/string/generic/memchr.c b/src/string/generic/memchr.c
|
||||
index 3e1dc30..937e7d1 100644
|
||||
--- a/src/string/generic/memchr.c
|
||||
+++ b/src/string/generic/memchr.c
|
||||
@@ -36,12 +36,57 @@
|
||||
|
||||
#include <stdlib.h>
|
||||
|
||||
+#if defined(__x86_64__)
|
||||
+#include <platform/string_avx2.h>
|
||||
+
|
||||
+/*
|
||||
+ * 32 bytes at a time, then the last possibly overlapping 32 bytes; a position
|
||||
+ * inside a block is the first match overall because every block below it was
|
||||
+ * scanned first. Only [s, s+n) is read.
|
||||
+ */
|
||||
+__attribute__((target("avx2"), visibility("hidden")))
|
||||
+void *
|
||||
+_platform_memchr_avx2(const void *s, int c, size_t n)
|
||||
+{
|
||||
+ const unsigned char *p = s;
|
||||
+ const __m256i needle = _mm256_set1_epi8((char)c);
|
||||
+
|
||||
+ while (n >= PLATFORM_MEM_VECTOR) {
|
||||
+ const __m256i block = _mm256_loadu_si256((const __m256i *)(const void *)p);
|
||||
+ const unsigned equal = (unsigned)_mm256_movemask_epi8(_mm256_cmpeq_epi8(block, needle));
|
||||
+
|
||||
+ if (equal != 0) {
|
||||
+ _mm256_zeroupper();
|
||||
+ return ((void *)(p + (unsigned)__builtin_ctz(equal)));
|
||||
+ }
|
||||
+ p += PLATFORM_MEM_VECTOR;
|
||||
+ n -= PLATFORM_MEM_VECTOR;
|
||||
+ }
|
||||
+ if (n != 0) {
|
||||
+ const unsigned char *tail = p + n - PLATFORM_MEM_VECTOR;
|
||||
+ const __m256i block = _mm256_loadu_si256((const __m256i *)(const void *)tail);
|
||||
+ const unsigned equal = (unsigned)_mm256_movemask_epi8(_mm256_cmpeq_epi8(block, needle));
|
||||
+
|
||||
+ _mm256_zeroupper();
|
||||
+ if (equal != 0)
|
||||
+ return ((void *)(tail + (unsigned)__builtin_ctz(equal)));
|
||||
+ return (NULL);
|
||||
+ }
|
||||
+ _mm256_zeroupper();
|
||||
+ return (NULL);
|
||||
+}
|
||||
+#endif
|
||||
+
|
||||
void *
|
||||
_platform_memchr(const void *s, int c, size_t n)
|
||||
{
|
||||
if (n != 0) {
|
||||
const unsigned char *p = s;
|
||||
|
||||
+#if defined(__x86_64__)
|
||||
+ if (n >= PLATFORM_MEM_VECTOR && platform_has_avx2())
|
||||
+ return (_platform_memchr_avx2(s, c, n));
|
||||
+#endif
|
||||
do {
|
||||
if (*p++ == (unsigned char)c)
|
||||
return ((void *)(p - 1));
|
||||
diff --git a/src/string/generic/memcmp.c b/src/string/generic/memcmp.c
|
||||
index fd4311d..34e52aa 100644
|
||||
index fd4311d..ee89f67 100644
|
||||
--- a/src/string/generic/memcmp.c
|
||||
+++ b/src/string/generic/memcmp.c
|
||||
@@ -34,6 +34,79 @@
|
||||
@@ -34,6 +34,122 @@
|
||||
|
||||
#if !_PLATFORM_OPTIMIZED_MEMCMP
|
||||
|
||||
+#if defined(__x86_64__)
|
||||
+#include <emmintrin.h>
|
||||
+#include <platform/string_avx2.h>
|
||||
+
|
||||
+// The byte loop below was 12% of the League main thread (the game compares buffers every frame, most of them
|
||||
+// shorter than 16 bytes). Blocks are compared with SSE2 (always present on x86_64) and the remainder with
|
||||
+// overlapping unaligned loads; no read goes past either buffer.
|
||||
+typedef unsigned long long __attribute__((aligned(1), may_alias)) platform_u64u;
|
||||
+typedef unsigned int __attribute__((aligned(1), may_alias)) platform_u32u;
|
||||
+
|
||||
+static inline int
|
||||
+platform_memcmp16(const unsigned char *p1, const unsigned char *p2)
|
||||
@@ -46,11 +336,56 @@ index fd4311d..34e52aa 100644
|
||||
+ return (0);
|
||||
+}
|
||||
+
|
||||
+__attribute__((target("avx2")))
|
||||
+static inline int
|
||||
+platform_memcmp32(const unsigned char *p1, const unsigned char *p2)
|
||||
+{
|
||||
+ const __m256i a = _mm256_loadu_si256((const __m256i *)(const void *)p1);
|
||||
+ const __m256i b = _mm256_loadu_si256((const __m256i *)(const void *)p2);
|
||||
+ const unsigned equal = (unsigned)_mm256_movemask_epi8(_mm256_cmpeq_epi8(a, b));
|
||||
+
|
||||
+ if (equal != 0xffffffffu) {
|
||||
+ const unsigned i = (unsigned)__builtin_ctz(~equal);
|
||||
+ return (p1[i] - p2[i]);
|
||||
+ }
|
||||
+ return (0);
|
||||
+}
|
||||
+
|
||||
+/*
|
||||
+ * 32 bytes at a time, then the last possibly overlapping 32 bytes. Only
|
||||
+ * [s1, s1+n) and [s2, s2+n) are read, and a block is only reached when every
|
||||
+ * byte below it compared equal, so the returned difference is the first one.
|
||||
+ */
|
||||
+__attribute__((target("avx2"), visibility("hidden")))
|
||||
+int
|
||||
+_platform_memcmp_avx2(const void *s1, const void *s2, size_t n)
|
||||
+{
|
||||
+ const unsigned char *p1 = s1, *p2 = s2;
|
||||
+ int result;
|
||||
+
|
||||
+ while (n >= PLATFORM_MEM_VECTOR) {
|
||||
+ if ((result = platform_memcmp32(p1, p2)) != 0) {
|
||||
+ _mm256_zeroupper();
|
||||
+ return (result);
|
||||
+ }
|
||||
+ p1 += PLATFORM_MEM_VECTOR; p2 += PLATFORM_MEM_VECTOR;
|
||||
+ n -= PLATFORM_MEM_VECTOR;
|
||||
+ }
|
||||
+ if (n != 0)
|
||||
+ result = platform_memcmp32(p1 + n - PLATFORM_MEM_VECTOR, p2 + n - PLATFORM_MEM_VECTOR);
|
||||
+ else
|
||||
+ result = 0;
|
||||
+ _mm256_zeroupper();
|
||||
+ return (result);
|
||||
+}
|
||||
+
|
||||
+int
|
||||
+_platform_memcmp(const void *s1, const void *s2, size_t n)
|
||||
+{
|
||||
+ const unsigned char *p1 = s1, *p2 = s2;
|
||||
+
|
||||
+ if (n >= PLATFORM_MEM_VECTOR && platform_has_avx2())
|
||||
+ return (_platform_memcmp_avx2(s1, s2, n));
|
||||
+ if (n >= 16) {
|
||||
+ int r;
|
||||
+
|
||||
@@ -99,7 +434,7 @@ index fd4311d..34e52aa 100644
|
||||
int
|
||||
_platform_memcmp(const void *s1, const void *s2, size_t n)
|
||||
{
|
||||
@@ -48,6 +121,8 @@ _platform_memcmp(const void *s1, const void *s2, size_t n)
|
||||
@@ -48,6 +164,8 @@ _platform_memcmp(const void *s1, const void *s2, size_t n)
|
||||
return (0);
|
||||
}
|
||||
|
||||
@@ -108,6 +443,117 @@ index fd4311d..34e52aa 100644
|
||||
#if VARIANT_STATIC
|
||||
int
|
||||
memcmp(const void *s1, const void *s2, size_t n)
|
||||
diff --git a/src/string/generic/memmove.c b/src/string/generic/memmove.c
|
||||
index e11f68c..573d4b5 100644
|
||||
--- a/src/string/generic/memmove.c
|
||||
+++ b/src/string/generic/memmove.c
|
||||
@@ -34,6 +34,10 @@
|
||||
|
||||
#if !_PLATFORM_OPTIMIZED_MEMMOVE
|
||||
|
||||
+#if defined(__x86_64__)
|
||||
+#include <platform/string_avx2.h>
|
||||
+#endif
|
||||
+
|
||||
#include <sys/types.h>
|
||||
|
||||
/*
|
||||
@@ -45,6 +49,83 @@ typedef long word; /* "word" used for optimal copy speed */
|
||||
#define wsize sizeof(word)
|
||||
#define wmask (wsize - 1)
|
||||
|
||||
+#if defined(__x86_64__)
|
||||
+
|
||||
+/*
|
||||
+ * A forward copy moves low to high and reads each 32-byte block before it
|
||||
+ * writes it, so an overlapping copy stays correct: the block written at
|
||||
+ * offset i ends below the first source byte not read yet. The block that does
|
||||
+ * not fill a vector is finished with the 8, 4, 2 and 1 byte moves of
|
||||
+ * platform_copy_short, which read every byte before they write any of it.
|
||||
+ */
|
||||
+__attribute__((target("avx2"), visibility("hidden")))
|
||||
+static void
|
||||
+platform_copy_forward_avx2(unsigned char *destination, const unsigned char *source, size_t length)
|
||||
+{
|
||||
+ size_t copied = 0;
|
||||
+
|
||||
+ while (length - copied >= PLATFORM_MEM_STEP) {
|
||||
+ const __m256i first = _mm256_loadu_si256((const __m256i *)(const void *)(source + copied));
|
||||
+ const __m256i second = _mm256_loadu_si256((const __m256i *)(const void *)(source + copied + PLATFORM_MEM_VECTOR));
|
||||
+
|
||||
+ _mm256_storeu_si256((__m256i *)(void *)(destination + copied), first);
|
||||
+ _mm256_storeu_si256((__m256i *)(void *)(destination + copied + PLATFORM_MEM_VECTOR), second);
|
||||
+ copied += PLATFORM_MEM_STEP;
|
||||
+ }
|
||||
+ while (length - copied >= PLATFORM_MEM_VECTOR) {
|
||||
+ const __m256i block = _mm256_loadu_si256((const __m256i *)(const void *)(source + copied));
|
||||
+
|
||||
+ _mm256_storeu_si256((__m256i *)(void *)(destination + copied), block);
|
||||
+ copied += PLATFORM_MEM_VECTOR;
|
||||
+ }
|
||||
+ platform_copy_short(destination + copied, source + copied, length - copied);
|
||||
+}
|
||||
+
|
||||
+/* The same, high to low, for a destination above the source. */
|
||||
+__attribute__((target("avx2"), visibility("hidden")))
|
||||
+static void
|
||||
+platform_copy_backward_avx2(unsigned char *destination, const unsigned char *source, size_t length)
|
||||
+{
|
||||
+ size_t copied = 0;
|
||||
+
|
||||
+ while (length - copied >= PLATFORM_MEM_STEP) {
|
||||
+ const __m256i first = _mm256_loadu_si256((const __m256i *)(const void *)(source + length - copied - PLATFORM_MEM_STEP));
|
||||
+ const __m256i second = _mm256_loadu_si256((const __m256i *)(const void *)(source + length - copied - PLATFORM_MEM_VECTOR));
|
||||
+
|
||||
+ copied += PLATFORM_MEM_STEP;
|
||||
+ _mm256_storeu_si256((__m256i *)(void *)(destination + length - copied), first);
|
||||
+ _mm256_storeu_si256((__m256i *)(void *)(destination + length - copied + PLATFORM_MEM_VECTOR), second);
|
||||
+ }
|
||||
+ while (length - copied >= PLATFORM_MEM_VECTOR) {
|
||||
+ __m256i block;
|
||||
+
|
||||
+ copied += PLATFORM_MEM_VECTOR;
|
||||
+ block = _mm256_loadu_si256((const __m256i *)(const void *)(source + length - copied));
|
||||
+ _mm256_storeu_si256((__m256i *)(void *)(destination + length - copied), block);
|
||||
+ }
|
||||
+ platform_copy_short(destination, source, length - copied);
|
||||
+}
|
||||
+
|
||||
+__attribute__((target("avx2"), visibility("hidden")))
|
||||
+void *
|
||||
+_platform_memmove_avx2(void *dst, const void *src, size_t length)
|
||||
+{
|
||||
+ unsigned char *destination = (unsigned char *)dst;
|
||||
+ const unsigned char *source = (const unsigned char *)src;
|
||||
+ const unsigned long destinationAddress = (unsigned long)destination;
|
||||
+ const unsigned long sourceAddress = (unsigned long)source;
|
||||
+
|
||||
+ if (destinationAddress < sourceAddress) {
|
||||
+ platform_copy_forward_avx2(destination, source, length);
|
||||
+ } else if (destinationAddress > sourceAddress) {
|
||||
+ platform_copy_backward_avx2(destination, source, length);
|
||||
+ }
|
||||
+ _mm256_zeroupper();
|
||||
+ return (dst);
|
||||
+}
|
||||
+
|
||||
+#endif /* __x86_64__ */
|
||||
+
|
||||
/*
|
||||
* Copy a block of memory, handling overlap.
|
||||
* This is the routine that actually implements
|
||||
@@ -58,6 +139,11 @@ _platform_memmove(void *dst0, const void *src0, size_t length)
|
||||
const char *src = src0;
|
||||
size_t t;
|
||||
|
||||
+#if defined(__x86_64__)
|
||||
+ if (length >= PLATFORM_MEM_VECTOR && platform_has_avx2())
|
||||
+ return (_platform_memmove_avx2(dst0, src0, length));
|
||||
+#endif
|
||||
+
|
||||
if (length == 0 || dst == src) /* nothing to do */
|
||||
goto done;
|
||||
|
||||
diff --git a/src/string/generic/memset_pattern.c b/src/string/generic/memset_pattern.c
|
||||
index 94aa085..0966fb7 100644
|
||||
--- a/src/string/generic/memset_pattern.c
|
||||
|
||||
@@ -87,30 +87,47 @@ index 900d03a..ae5eff1 100644
|
||||
};
|
||||
|
||||
diff --git a/private-include/indium/blit-command-encoder.private.hpp b/private-include/indium/blit-command-encoder.private.hpp
|
||||
index d6e71b2..fc19a42 100644
|
||||
index d6e71b2..ae31d39 100644
|
||||
--- a/private-include/indium/blit-command-encoder.private.hpp
|
||||
+++ b/private-include/indium/blit-command-encoder.private.hpp
|
||||
@@ -19,6 +19,11 @@ namespace Indium {
|
||||
@@ -19,21 +19,28 @@ namespace Indium {
|
||||
std::shared_ptr<PrivateDevice> _privateDevice;
|
||||
BlitPassDescriptor _descriptor;
|
||||
|
||||
- void copy(std::shared_ptr<Texture> source, size_t sourceSlice, size_t sourceLevel, Origin sourceOrigin, std::shared_ptr<Texture> destination, size_t destinationSlice, size_t destinationLevel, Origin destinationOrigin, size_t sliceCount, size_t levelCount, Size size);
|
||||
+ // destination textures that need presentation synchronization (drawables)
|
||||
+ std::vector<std::shared_ptr<Texture>> _readWriteTextures;
|
||||
+
|
||||
+ void noteDestination(const std::shared_ptr<Texture>& destination);
|
||||
+
|
||||
void copy(std::shared_ptr<Texture> source, size_t sourceSlice, size_t sourceLevel, Origin sourceOrigin, std::shared_ptr<Texture> destination, size_t destinationSlice, size_t destinationLevel, Origin destinationOrigin, size_t sliceCount, size_t levelCount, Size size);
|
||||
+ void copy(const std::shared_ptr<Texture>& source, size_t sourceSlice, size_t sourceLevel, Origin sourceOrigin, const std::shared_ptr<Texture>& destination, size_t destinationSlice, size_t destinationLevel, Origin destinationOrigin, size_t sliceCount, size_t levelCount, Size size);
|
||||
|
||||
public:
|
||||
@@ -26,6 +31,8 @@ namespace Indium {
|
||||
PrivateBlitCommandEncoder(std::shared_ptr<PrivateCommandBuffer> commandBuffer);
|
||||
PrivateBlitCommandEncoder(std::shared_ptr<PrivateCommandBuffer> commandBuffer, const BlitPassDescriptor& descriptor);
|
||||
~PrivateBlitCommandEncoder();
|
||||
|
||||
- virtual void copy(std::shared_ptr<Buffer> source, size_t sourceOffset, std::shared_ptr<Buffer> destination, size_t destinationOffset, size_t size) override;
|
||||
- virtual void copy(std::shared_ptr<Buffer> source, size_t sourceOffset, size_t sourceBytesPerRow, size_t sourceBytesPerImage, Size sourceSize, std::shared_ptr<Texture> destination, size_t destinationSlice, size_t destinationLevel, Origin destinationOrigin, BlitOption options = BlitOption::None) override;
|
||||
- virtual void copy(std::shared_ptr<Texture> source, size_t sourceSlice, size_t sourceLevel, Origin sourceOrigin, Size sourceSize, std::shared_ptr<Buffer> destination, size_t destinationOffset, size_t destinationBytesPerRow, size_t destinationBytesPerImage, BlitOption options = BlitOption::None) override;
|
||||
- virtual void copy(std::shared_ptr<Texture> source, std::shared_ptr<Texture> destination) override;
|
||||
- virtual void copy(std::shared_ptr<Texture> source, size_t sourceSlice, size_t sourceLevel, std::shared_ptr<Texture> destination, size_t destinationSlice, size_t destinationLevel, size_t sliceCount, size_t levelCount) override;
|
||||
- virtual void copy(std::shared_ptr<Texture> source, size_t sourceSlice, size_t sourceLevel, Origin sourceOrigin, Size sourceSize, std::shared_ptr<Texture> destination, size_t destinationSlice, size_t destinationLevel, Origin destinationOrigin) override;
|
||||
- virtual void fillBuffer(std::shared_ptr<Buffer> buffer, Range<size_t> range, uint8_t value) override;
|
||||
- virtual void generateMipmapsForTexture(std::shared_ptr<Texture> texture) override;
|
||||
+ const std::vector<std::shared_ptr<Texture>>& readWriteTextures() const { return _readWriteTextures; };
|
||||
+
|
||||
virtual void copy(std::shared_ptr<Buffer> source, size_t sourceOffset, std::shared_ptr<Buffer> destination, size_t destinationOffset, size_t size) override;
|
||||
virtual void copy(std::shared_ptr<Buffer> source, size_t sourceOffset, size_t sourceBytesPerRow, size_t sourceBytesPerImage, Size sourceSize, std::shared_ptr<Texture> destination, size_t destinationSlice, size_t destinationLevel, Origin destinationOrigin, BlitOption options = BlitOption::None) override;
|
||||
virtual void copy(std::shared_ptr<Texture> source, size_t sourceSlice, size_t sourceLevel, Origin sourceOrigin, Size sourceSize, std::shared_ptr<Buffer> destination, size_t destinationOffset, size_t destinationBytesPerRow, size_t destinationBytesPerImage, BlitOption options = BlitOption::None) override;
|
||||
+ virtual void copy(const std::shared_ptr<Buffer>& source, size_t sourceOffset, const std::shared_ptr<Buffer>& destination, size_t destinationOffset, size_t size) override;
|
||||
+ virtual void copy(const std::shared_ptr<Buffer>& source, size_t sourceOffset, size_t sourceBytesPerRow, size_t sourceBytesPerImage, Size sourceSize, const std::shared_ptr<Texture>& destination, size_t destinationSlice, size_t destinationLevel, Origin destinationOrigin, BlitOption options = BlitOption::None) override;
|
||||
+ virtual void copy(const std::shared_ptr<Texture>& source, size_t sourceSlice, size_t sourceLevel, Origin sourceOrigin, Size sourceSize, const std::shared_ptr<Buffer>& destination, size_t destinationOffset, size_t destinationBytesPerRow, size_t destinationBytesPerImage, BlitOption options = BlitOption::None) override;
|
||||
+ virtual void copy(const std::shared_ptr<Texture>& source, const std::shared_ptr<Texture>& destination) override;
|
||||
+ virtual void copy(const std::shared_ptr<Texture>& source, size_t sourceSlice, size_t sourceLevel, const std::shared_ptr<Texture>& destination, size_t destinationSlice, size_t destinationLevel, size_t sliceCount, size_t levelCount) override;
|
||||
+ virtual void copy(const std::shared_ptr<Texture>& source, size_t sourceSlice, size_t sourceLevel, Origin sourceOrigin, Size sourceSize, const std::shared_ptr<Texture>& destination, size_t destinationSlice, size_t destinationLevel, Origin destinationOrigin) override;
|
||||
+ virtual void fillBuffer(const std::shared_ptr<Buffer>& buffer, Range<size_t> range, uint8_t value) override;
|
||||
+ virtual void generateMipmapsForTexture(const std::shared_ptr<Texture>& texture) override;
|
||||
|
||||
virtual void endEncoding() override;
|
||||
};
|
||||
diff --git a/private-include/indium/buffer.private.hpp b/private-include/indium/buffer.private.hpp
|
||||
index a348b2b..9ad8975 100644
|
||||
--- a/private-include/indium/buffer.private.hpp
|
||||
@@ -1433,7 +1450,7 @@ index 8d00405..6a5fde8 100644
|
||||
// TODO
|
||||
abort();
|
||||
diff --git a/src/indium/blit-command-encoder.cpp b/src/indium/blit-command-encoder.cpp
|
||||
index e58f0ba..3191023 100644
|
||||
index e58f0ba..616d177 100644
|
||||
--- a/src/indium/blit-command-encoder.cpp
|
||||
+++ b/src/indium/blit-command-encoder.cpp
|
||||
@@ -3,6 +3,7 @@
|
||||
@@ -1444,7 +1461,7 @@ index e58f0ba..3191023 100644
|
||||
|
||||
Indium::BlitCommandEncoder::~BlitCommandEncoder() {};
|
||||
|
||||
@@ -17,6 +18,19 @@ Indium::PrivateBlitCommandEncoder::PrivateBlitCommandEncoder(std::shared_ptr<Pri
|
||||
@@ -17,14 +18,27 @@ Indium::PrivateBlitCommandEncoder::PrivateBlitCommandEncoder(std::shared_ptr<Pri
|
||||
_descriptor(descriptor)
|
||||
{};
|
||||
|
||||
@@ -1464,15 +1481,32 @@ index e58f0ba..3191023 100644
|
||||
Indium::PrivateBlitCommandEncoder::~PrivateBlitCommandEncoder() {
|
||||
// nothing for now
|
||||
};
|
||||
@@ -38,6 +52,7 @@ void Indium::PrivateBlitCommandEncoder::copy(std::shared_ptr<Buffer> source, siz
|
||||
|
||||
-void Indium::PrivateBlitCommandEncoder::copy(std::shared_ptr<Buffer> source, size_t sourceOffset, std::shared_ptr<Buffer> destination, size_t destinationOffset, size_t size) {
|
||||
+void Indium::PrivateBlitCommandEncoder::copy(const std::shared_ptr<Buffer>& source, size_t sourceOffset, const std::shared_ptr<Buffer>& destination, size_t destinationOffset, size_t size) {
|
||||
auto cmdbuf = _privateCommandBuffer.lock();
|
||||
- auto privateSource = std::dynamic_pointer_cast<PrivateBuffer>(source);
|
||||
- auto privateDest = std::dynamic_pointer_cast<PrivateBuffer>(destination);
|
||||
+ auto* privateSource = dynamic_cast<PrivateBuffer*>(source.get());
|
||||
+ auto* privateDest = dynamic_cast<PrivateBuffer*>(destination.get());
|
||||
|
||||
VkBufferCopy info {};
|
||||
|
||||
@@ -37,18 +51,18 @@ void Indium::PrivateBlitCommandEncoder::copy(std::shared_ptr<Buffer> source, siz
|
||||
DynamicVK::vkCmdCopyBuffer(cmdbuf->commandBuffer(), privateSource->buffer(), privateDest->buffer(), 1, &info);
|
||||
};
|
||||
|
||||
void Indium::PrivateBlitCommandEncoder::copy(std::shared_ptr<Buffer> source, size_t sourceOffset, size_t sourceBytesPerRow, size_t sourceBytesPerImage, Size sourceSize, std::shared_ptr<Texture> destination, size_t destinationSlice, size_t destinationLevel, Origin destinationOrigin, BlitOption options) {
|
||||
-void Indium::PrivateBlitCommandEncoder::copy(std::shared_ptr<Buffer> source, size_t sourceOffset, size_t sourceBytesPerRow, size_t sourceBytesPerImage, Size sourceSize, std::shared_ptr<Texture> destination, size_t destinationSlice, size_t destinationLevel, Origin destinationOrigin, BlitOption options) {
|
||||
+void Indium::PrivateBlitCommandEncoder::copy(const std::shared_ptr<Buffer>& source, size_t sourceOffset, size_t sourceBytesPerRow, size_t sourceBytesPerImage, Size sourceSize, const std::shared_ptr<Texture>& destination, size_t destinationSlice, size_t destinationLevel, Origin destinationOrigin, BlitOption options) {
|
||||
+ noteDestination(destination);
|
||||
auto cmdbuf = _privateCommandBuffer.lock();
|
||||
auto privateSource = std::dynamic_pointer_cast<PrivateBuffer>(source);
|
||||
auto privateDest = std::dynamic_pointer_cast<PrivateTexture>(destination);
|
||||
@@ -47,8 +62,7 @@ void Indium::PrivateBlitCommandEncoder::copy(std::shared_ptr<Buffer> source, siz
|
||||
- auto privateSource = std::dynamic_pointer_cast<PrivateBuffer>(source);
|
||||
- auto privateDest = std::dynamic_pointer_cast<PrivateTexture>(destination);
|
||||
+ auto* privateSource = dynamic_cast<PrivateBuffer*>(source.get());
|
||||
+ auto* privateDest = dynamic_cast<PrivateTexture*>(destination.get());
|
||||
|
||||
if (options != BlitOption::None) {
|
||||
throw std::runtime_error("TODO: support blit options");
|
||||
}
|
||||
|
||||
auto aspect = pixelFormatToVkImageAspectFlags(privateDest->pixelFormat());
|
||||
@@ -1495,7 +1529,20 @@ index e58f0ba..3191023 100644
|
||||
copyInfo.imageSubresource.aspectMask = aspect;
|
||||
copyInfo.imageSubresource.mipLevel = destinationLevel;
|
||||
copyInfo.imageSubresource.baseArrayLayer = destinationSlice;
|
||||
@@ -105,8 +119,7 @@ void Indium::PrivateBlitCommandEncoder::copy(std::shared_ptr<Texture> source, si
|
||||
@@ -95,18 +109,17 @@ void Indium::PrivateBlitCommandEncoder::copy(std::shared_ptr<Buffer> source, siz
|
||||
DynamicVK::vkCmdPipelineBarrier(cmdbuf->commandBuffer(), VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 1, &barrier);
|
||||
};
|
||||
|
||||
-void Indium::PrivateBlitCommandEncoder::copy(std::shared_ptr<Texture> source, size_t sourceSlice, size_t sourceLevel, Origin sourceOrigin, Size sourceSize, std::shared_ptr<Buffer> destination, size_t destinationOffset, size_t destinationBytesPerRow, size_t destinationBytesPerImage, BlitOption options) {
|
||||
+void Indium::PrivateBlitCommandEncoder::copy(const std::shared_ptr<Texture>& source, size_t sourceSlice, size_t sourceLevel, Origin sourceOrigin, Size sourceSize, const std::shared_ptr<Buffer>& destination, size_t destinationOffset, size_t destinationBytesPerRow, size_t destinationBytesPerImage, BlitOption options) {
|
||||
auto cmdbuf = _privateCommandBuffer.lock();
|
||||
- auto privateSource = std::dynamic_pointer_cast<PrivateTexture>(source);
|
||||
- auto privateDest = std::dynamic_pointer_cast<PrivateBuffer>(destination);
|
||||
+ auto* privateSource = dynamic_cast<PrivateTexture*>(source.get());
|
||||
+ auto* privateDest = dynamic_cast<PrivateBuffer*>(destination.get());
|
||||
|
||||
if (options != BlitOption::None) {
|
||||
throw std::runtime_error("TODO: support blit options");
|
||||
}
|
||||
|
||||
auto aspect = pixelFormatToVkImageAspectFlags(privateSource->pixelFormat());
|
||||
@@ -1518,22 +1565,70 @@ index e58f0ba..3191023 100644
|
||||
copyInfo.imageSubresource.aspectMask = aspect;
|
||||
copyInfo.imageSubresource.mipLevel = sourceLevel;
|
||||
copyInfo.imageSubresource.baseArrayLayer = sourceSlice;
|
||||
@@ -154,6 +167,7 @@ void Indium::PrivateBlitCommandEncoder::copy(std::shared_ptr<Texture> source, si
|
||||
@@ -153,10 +166,11 @@ void Indium::PrivateBlitCommandEncoder::copy(std::shared_ptr<Texture> source, si
|
||||
DynamicVK::vkCmdPipelineBarrier(cmdbuf->commandBuffer(), VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 1, &barrier);
|
||||
};
|
||||
|
||||
void Indium::PrivateBlitCommandEncoder::copy(std::shared_ptr<Texture> source, size_t sourceSlice, size_t sourceLevel, Origin sourceOrigin, std::shared_ptr<Texture> destination, size_t destinationSlice, size_t destinationLevel, Origin destinationOrigin, size_t sliceCount, size_t levelCount, Size size) {
|
||||
-void Indium::PrivateBlitCommandEncoder::copy(std::shared_ptr<Texture> source, size_t sourceSlice, size_t sourceLevel, Origin sourceOrigin, std::shared_ptr<Texture> destination, size_t destinationSlice, size_t destinationLevel, Origin destinationOrigin, size_t sliceCount, size_t levelCount, Size size) {
|
||||
+void Indium::PrivateBlitCommandEncoder::copy(const std::shared_ptr<Texture>& source, size_t sourceSlice, size_t sourceLevel, Origin sourceOrigin, const std::shared_ptr<Texture>& destination, size_t destinationSlice, size_t destinationLevel, Origin destinationOrigin, size_t sliceCount, size_t levelCount, Size size) {
|
||||
+ noteDestination(destination);
|
||||
auto cmdbuf = _privateCommandBuffer.lock();
|
||||
auto privateSource = std::dynamic_pointer_cast<PrivateTexture>(source);
|
||||
auto privateDest = std::dynamic_pointer_cast<PrivateTexture>(destination);
|
||||
@@ -315,6 +329,7 @@ void Indium::PrivateBlitCommandEncoder::fillBuffer(std::shared_ptr<Buffer> buffe
|
||||
- auto privateSource = std::dynamic_pointer_cast<PrivateTexture>(source);
|
||||
- auto privateDest = std::dynamic_pointer_cast<PrivateTexture>(destination);
|
||||
+ auto* privateSource = dynamic_cast<PrivateTexture*>(source.get());
|
||||
+ auto* privateDest = dynamic_cast<PrivateTexture*>(destination.get());
|
||||
|
||||
auto aspect = pixelFormatToVkImageAspectFlags(privateSource->pixelFormat());
|
||||
|
||||
@@ -231,10 +245,10 @@ void Indium::PrivateBlitCommandEncoder::copy(std::shared_ptr<Texture> source, si
|
||||
DynamicVK::vkCmdPipelineBarrier(cmdbuf->commandBuffer(), VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, sizeof(barriers) / sizeof(*barriers), barriers);
|
||||
};
|
||||
|
||||
void Indium::PrivateBlitCommandEncoder::generateMipmapsForTexture(std::shared_ptr<Texture> texture) {
|
||||
-void Indium::PrivateBlitCommandEncoder::copy(std::shared_ptr<Texture> source, std::shared_ptr<Texture> destination) {
|
||||
+void Indium::PrivateBlitCommandEncoder::copy(const std::shared_ptr<Texture>& source, const std::shared_ptr<Texture>& destination) {
|
||||
auto cmdbuf = _privateCommandBuffer.lock();
|
||||
- auto privateSource = std::dynamic_pointer_cast<PrivateTexture>(source);
|
||||
- auto privateDest = std::dynamic_pointer_cast<PrivateTexture>(destination);
|
||||
+ auto* privateSource = dynamic_cast<PrivateTexture*>(source.get());
|
||||
+ auto* privateDest = dynamic_cast<PrivateTexture*>(destination.get());
|
||||
size_t sourceMipLevel = SIZE_MAX;
|
||||
size_t destMipLevel = SIZE_MAX;
|
||||
|
||||
@@ -282,18 +296,18 @@ void Indium::PrivateBlitCommandEncoder::copy(std::shared_ptr<Texture> source, st
|
||||
copy(source, 0, sourceMipLevel, Origin { 0, 0, 0 }, destination, 0, destMipLevel, Origin { 0, 0, 0 }, sliceCount, mipCount, Size { source->width() / divisor, source->height() / divisor, source->depth() / divisor });
|
||||
};
|
||||
|
||||
-void Indium::PrivateBlitCommandEncoder::copy(std::shared_ptr<Texture> source, size_t sourceSlice, size_t sourceLevel, std::shared_ptr<Texture> destination, size_t destinationSlice, size_t destinationLevel, size_t sliceCount, size_t levelCount) {
|
||||
+void Indium::PrivateBlitCommandEncoder::copy(const std::shared_ptr<Texture>& source, size_t sourceSlice, size_t sourceLevel, const std::shared_ptr<Texture>& destination, size_t destinationSlice, size_t destinationLevel, size_t sliceCount, size_t levelCount) {
|
||||
size_t divisor = static_cast<size_t>(1) << sourceLevel;
|
||||
copy(source, sourceSlice, sourceLevel, Origin { 0, 0, 0 }, destination, destinationSlice, destinationLevel, Origin { 0, 0, 0 }, sliceCount, levelCount, Size { source->width() / divisor, source->height() / divisor, source->depth() / divisor });
|
||||
};
|
||||
|
||||
-void Indium::PrivateBlitCommandEncoder::copy(std::shared_ptr<Texture> source, size_t sourceSlice, size_t sourceLevel, Origin sourceOrigin, Size sourceSize, std::shared_ptr<Texture> destination, size_t destinationSlice, size_t destinationLevel, Origin destinationOrigin) {
|
||||
+void Indium::PrivateBlitCommandEncoder::copy(const std::shared_ptr<Texture>& source, size_t sourceSlice, size_t sourceLevel, Origin sourceOrigin, Size sourceSize, const std::shared_ptr<Texture>& destination, size_t destinationSlice, size_t destinationLevel, Origin destinationOrigin) {
|
||||
copy(source, sourceSlice, sourceLevel, sourceOrigin, destination, destinationSlice, destinationLevel, destinationOrigin, 1, 1, sourceSize);
|
||||
};
|
||||
|
||||
-void Indium::PrivateBlitCommandEncoder::fillBuffer(std::shared_ptr<Buffer> buffer, Range<size_t> range, uint8_t value) {
|
||||
+void Indium::PrivateBlitCommandEncoder::fillBuffer(const std::shared_ptr<Buffer>& buffer, Range<size_t> range, uint8_t value) {
|
||||
auto cmdbuf = _privateCommandBuffer.lock();
|
||||
- auto privateBuffer = std::dynamic_pointer_cast<PrivateBuffer>(buffer);
|
||||
+ auto* privateBuffer = dynamic_cast<PrivateBuffer*>(buffer.get());
|
||||
uint32_t value32 =
|
||||
((uint32_t)value << 24) |
|
||||
((uint32_t)value << 16) |
|
||||
@@ -314,8 +328,9 @@ void Indium::PrivateBlitCommandEncoder::fillBuffer(std::shared_ptr<Buffer> buffe
|
||||
DynamicVK::vkCmdFillBuffer(cmdbuf->commandBuffer(), privateBuffer->buffer(), range.start, range.length, value32);
|
||||
};
|
||||
|
||||
-void Indium::PrivateBlitCommandEncoder::generateMipmapsForTexture(std::shared_ptr<Texture> texture) {
|
||||
- auto privateTexture = std::dynamic_pointer_cast<PrivateTexture>(texture);
|
||||
+void Indium::PrivateBlitCommandEncoder::generateMipmapsForTexture(const std::shared_ptr<Texture>& texture) {
|
||||
+ noteDestination(texture);
|
||||
auto privateTexture = std::dynamic_pointer_cast<PrivateTexture>(texture);
|
||||
+ auto* privateTexture = dynamic_cast<PrivateTexture*>(texture.get());
|
||||
auto cmdbuf = _privateCommandBuffer.lock();
|
||||
|
||||
auto aspect = pixelFormatToVkImageAspectFlags(privateTexture->pixelFormat());
|
||||
@@ -439,5 +454,5 @@ void Indium::PrivateBlitCommandEncoder::generateMipmapsForTexture(std::shared_pt
|
||||
};
|
||||
|
||||
@@ -7289,3 +7384,29 @@ new file mode 100644
|
||||
+ }
|
||||
+ }
|
||||
+}
|
||||
diff --git a/include/indium/blit-command-encoder.hpp b/include/indium/blit-command-encoder.hpp
|
||||
index b000d25..98baa9b 100644
|
||||
--- a/include/indium/blit-command-encoder.hpp
|
||||
+++ b/include/indium/blit-command-encoder.hpp
|
||||
@@ -28,13 +28,13 @@ namespace Indium {
|
||||
public:
|
||||
virtual ~BlitCommandEncoder() = 0;
|
||||
|
||||
- virtual void copy(std::shared_ptr<Buffer> source, size_t sourceOffset, std::shared_ptr<Buffer> destination, size_t destinationOffset, size_t size) = 0;
|
||||
- virtual void copy(std::shared_ptr<Buffer> source, size_t sourceOffset, size_t sourceBytesPerRow, size_t sourceBytesPerImage, Size sourceSize, std::shared_ptr<Texture> destination, size_t destinationSlice, size_t destinationLevel, Origin destinationOrigin, BlitOption options = BlitOption::None) = 0;
|
||||
- virtual void copy(std::shared_ptr<Texture> source, size_t sourceSlice, size_t sourceLevel, Origin sourceOrigin, Size sourceSize, std::shared_ptr<Buffer> destination, size_t destinationOffset, size_t destinationBytesPerRow, size_t destinationBytesPerImage, BlitOption options = BlitOption::None) = 0;
|
||||
- virtual void copy(std::shared_ptr<Texture> source, std::shared_ptr<Texture> destination) = 0;
|
||||
- virtual void copy(std::shared_ptr<Texture> source, size_t sourceSlice, size_t sourceLevel, std::shared_ptr<Texture> destination, size_t destinationSlice, size_t destinationLevel, size_t sliceCount, size_t levelCount) = 0;
|
||||
- virtual void copy(std::shared_ptr<Texture> source, size_t sourceSlice, size_t sourceLevel, Origin sourceOrigin, Size sourceSize, std::shared_ptr<Texture> destination, size_t destinationSlice, size_t destinationLevel, Origin destinationOrigin) = 0;
|
||||
- virtual void fillBuffer(std::shared_ptr<Buffer> buffer, Range<size_t> range, uint8_t value) = 0;
|
||||
- virtual void generateMipmapsForTexture(std::shared_ptr<Texture> texture) = 0;
|
||||
+ virtual void copy(const std::shared_ptr<Buffer>& source, size_t sourceOffset, const std::shared_ptr<Buffer>& destination, size_t destinationOffset, size_t size) = 0;
|
||||
+ virtual void copy(const std::shared_ptr<Buffer>& source, size_t sourceOffset, size_t sourceBytesPerRow, size_t sourceBytesPerImage, Size sourceSize, const std::shared_ptr<Texture>& destination, size_t destinationSlice, size_t destinationLevel, Origin destinationOrigin, BlitOption options = BlitOption::None) = 0;
|
||||
+ virtual void copy(const std::shared_ptr<Texture>& source, size_t sourceSlice, size_t sourceLevel, Origin sourceOrigin, Size sourceSize, const std::shared_ptr<Buffer>& destination, size_t destinationOffset, size_t destinationBytesPerRow, size_t destinationBytesPerImage, BlitOption options = BlitOption::None) = 0;
|
||||
+ virtual void copy(const std::shared_ptr<Texture>& source, const std::shared_ptr<Texture>& destination) = 0;
|
||||
+ virtual void copy(const std::shared_ptr<Texture>& source, size_t sourceSlice, size_t sourceLevel, const std::shared_ptr<Texture>& destination, size_t destinationSlice, size_t destinationLevel, size_t sliceCount, size_t levelCount) = 0;
|
||||
+ virtual void copy(const std::shared_ptr<Texture>& source, size_t sourceSlice, size_t sourceLevel, Origin sourceOrigin, Size sourceSize, const std::shared_ptr<Texture>& destination, size_t destinationSlice, size_t destinationLevel, Origin destinationOrigin) = 0;
|
||||
+ virtual void fillBuffer(const std::shared_ptr<Buffer>& buffer, Range<size_t> range, uint8_t value) = 0;
|
||||
+ virtual void generateMipmapsForTexture(const std::shared_ptr<Texture>& texture) = 0;
|
||||
};
|
||||
};
|
||||
+338
-96
@@ -1489,7 +1489,7 @@ index 0feb46e..e0c0e93 100644
|
||||
return nil;
|
||||
}
|
||||
diff --git a/src/Metal/MTLRenderCommandEncoder.mm b/src/Metal/MTLRenderCommandEncoder.mm
|
||||
index e931b90..e13dc0e 100644
|
||||
index e931b90..b2fe0a5 100644
|
||||
--- a/src/Metal/MTLRenderCommandEncoder.mm
|
||||
+++ b/src/Metal/MTLRenderCommandEncoder.mm
|
||||
@@ -1,3 +1,9 @@
|
||||
@@ -1502,7 +1502,7 @@ index e931b90..e13dc0e 100644
|
||||
// SPDX-FileCopyrightText: 2026 Darling Developers
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
@@ -9,8 +15,268 @@
|
||||
@@ -9,8 +15,408 @@
|
||||
#import <Metal/MTLRenderPipelineInternal.h>
|
||||
#import <Metal/MTLTypesInternal.h>
|
||||
|
||||
@@ -1607,7 +1607,7 @@ index e931b90..e13dc0e 100644
|
||||
+ }
|
||||
+ fclose(log);
|
||||
+}
|
||||
|
||||
+
|
||||
+static void MTLDebugLogDraw(const char* kind, unsigned long count, unsigned long instances, unsigned long indexType, long baseVertex, id<MTLBuffer> indexBuffer, NSUInteger indexOffset)
|
||||
+{
|
||||
+ if (!MTLDebugEnabled()) {
|
||||
@@ -1748,6 +1748,146 @@ index e931b90..e13dc0e 100644
|
||||
+}
|
||||
+#endif
|
||||
+
|
||||
+#if DARLING_METAL_ENABLED
|
||||
+// Metal's render encoders start with every slot unbound, and a bind only replaces the value its slot
|
||||
+// holds, so re-binding the identical value at the same slot cannot change what later draws see: the
|
||||
+// call can be dropped before any of its work (the resource's shared_ptr copy and its atomic refcount,
|
||||
+// the Indium op, and the first touch of both objects). These records are what the setters compare
|
||||
+// against.
|
||||
+//
|
||||
+// A record holds the *Indium* object's address (`shared_ptr::get()`, see MTLBindKey), never the
|
||||
+// Objective-C wrapper's address. The wrapper is not a stable identity: the game may release it while
|
||||
+// the Indium encoder still keeps the resource it wrapped alive, the allocator can then hand the same
|
||||
+// address to a wrapper around a different resource (transient per-draw buffers do exactly that), and a
|
||||
+// comparison on the wrapper's address would drop that new resource's bind -- every later draw would
|
||||
+// keep using the old one (the ABA problem). The Indium encoder holds each resource it was given until
|
||||
+// the encoder ends -- `FunctionResources::textures/buffers/samplers` store the shared_ptr the setter
|
||||
+// received (command-encoder.private.hpp), and the pipeline is held by `_privatePSO`/`_pipelineOwners`
|
||||
+// (render-command-encoder.cpp), on both the deferred and the direct encoder path -- so an Indium
|
||||
+// address cannot be reused while it is recorded. The depth-stencil state is the one kind Indium does
|
||||
+// not keep (its setter reads the descriptor and issues the Vulkan commands), so its record retains
|
||||
+// one itself (`_depthStencilOwner`).
|
||||
+//
|
||||
+// `valid` marks the slots a value was recorded for: a slot that was never bound is always forwarded,
|
||||
+// and setVertexBytes/setFragmentBytes (inline data in the same slot) just clears the mark.
|
||||
+struct MTLBindObjectSlots {
|
||||
+ const void* keys[31] = {};
|
||||
+ uint32_t valid = 0;
|
||||
+};
|
||||
+
|
||||
+struct MTLBindBufferSlots {
|
||||
+ const void* keys[31] = {};
|
||||
+ NSUInteger offsets[31] = {};
|
||||
+ uint32_t valid = 0;
|
||||
+};
|
||||
+
|
||||
+// The key a bind is recorded and compared under: the Indium object's address. Reading it out of the
|
||||
+// accessor's const shared_ptr reference is one load of the wrapper's payload -- no copy, no atomic.
|
||||
+template<class T>
|
||||
+static inline const void* MTLBindKey(const std::shared_ptr<T>& object)
|
||||
+{
|
||||
+ return object.get();
|
||||
+}
|
||||
+
|
||||
+// true: the slot already holds this value, so the caller can drop the call; false: forward it, and the
|
||||
+// value is recorded so the next identical call can be dropped.
|
||||
+static bool MTLBindObjectSlot(MTLBindObjectSlots& slots, NSUInteger index, const void* key)
|
||||
+{
|
||||
+ if (index >= 31) {
|
||||
+ return false;
|
||||
+ }
|
||||
+ const uint32_t bit = 1u << index;
|
||||
+ if ((slots.valid & bit) && slots.keys[index] == key) {
|
||||
+ return true;
|
||||
+ }
|
||||
+ slots.keys[index] = key;
|
||||
+ slots.valid |= bit;
|
||||
+ return false;
|
||||
+}
|
||||
+
|
||||
+static bool MTLBindBufferSlot(MTLBindBufferSlots& slots, NSUInteger index, const void* key, NSUInteger offset)
|
||||
+{
|
||||
+ if (index >= 31) {
|
||||
+ return false;
|
||||
+ }
|
||||
+ const uint32_t bit = 1u << index;
|
||||
+ if ((slots.valid & bit) && slots.keys[index] == key && slots.offsets[index] == offset) {
|
||||
+ return true;
|
||||
+ }
|
||||
+ slots.keys[index] = key;
|
||||
+ slots.offsets[index] = offset;
|
||||
+ slots.valid |= bit;
|
||||
+ return false;
|
||||
+}
|
||||
+
|
||||
+// The offset belongs to the buffer's slot state, so an unchanged one is dropped the same way.
|
||||
+static bool MTLBindBufferOffset(MTLBindBufferSlots& slots, NSUInteger index, NSUInteger offset)
|
||||
+{
|
||||
+ if (index >= 31 || !(slots.valid & (1u << index))) {
|
||||
+ return false;
|
||||
+ }
|
||||
+ if (slots.offsets[index] == offset) {
|
||||
+ return true;
|
||||
+ }
|
||||
+ slots.offsets[index] = offset;
|
||||
+ return false;
|
||||
+}
|
||||
+
|
||||
+static void MTLBindForgetBufferSlot(MTLBindBufferSlots& slots, NSUInteger index)
|
||||
+{
|
||||
+ if (index < 31) {
|
||||
+ slots.valid &= ~(1u << index);
|
||||
+ }
|
||||
+}
|
||||
+
|
||||
+// endEncoding: nothing may be bound after it, and the Indium encoder releases the resources it held,
|
||||
+// so no record -- and no address a record kept meaningful -- survives the pass.
|
||||
+static void MTLBindForgetAll(MTLBindObjectSlots& slots)
|
||||
+{
|
||||
+ slots = MTLBindObjectSlots {};
|
||||
+}
|
||||
+
|
||||
+static void MTLBindForgetAll(MTLBindBufferSlots& slots)
|
||||
+{
|
||||
+ slots = MTLBindBufferSlots {};
|
||||
+}
|
||||
+
|
||||
+// Diagnostic (guest flag /tmp/metal-debug-binds, or the full trace flag): count the dropped re-binds.
|
||||
+// The setters test MTLBindSkipsEnabled() inline first, so a dropped call pays one load and a branch
|
||||
+// rather than the counter's call frame; MTLBindSkipCount then appends the totals to the trace log on
|
||||
+// the same 3 s interval as the other counters.
|
||||
+static bool MTLBindSkipsEnabled()
|
||||
+{
|
||||
+ static const bool enabled = MTLDebugEnabled() || access("/tmp/metal-debug-binds", F_OK) == 0;
|
||||
+ return enabled;
|
||||
+}
|
||||
+
|
||||
+static void MTLBindSkipCount(const char* what)
|
||||
+{
|
||||
+ static const bool enabled = MTLDebugEnabled() || access("/tmp/metal-debug-binds", F_OK) == 0;
|
||||
+ if (!enabled) {
|
||||
+ return;
|
||||
+ }
|
||||
+ static std::mutex lock;
|
||||
+ static std::map<std::string, unsigned long> counts;
|
||||
+ static time_t last = 0;
|
||||
+ std::lock_guard<std::mutex> guard(lock);
|
||||
+ ++counts[what];
|
||||
+ time_t now = time(nullptr);
|
||||
+ if (now - last >= 3) {
|
||||
+ last = now;
|
||||
+ if (FILE* log = fopen("/tmp/metal-trace.log", "a")) {
|
||||
+ fprintf(log, "bind-skips@%ld:", (long) now);
|
||||
+ for (auto& count: counts) {
|
||||
+ fprintf(log, " %s=%lu", count.first.c_str(), count.second);
|
||||
+ }
|
||||
+ fprintf(log, "\n");
|
||||
+ fclose(log);
|
||||
+ }
|
||||
+ }
|
||||
+}
|
||||
+#endif
|
||||
|
||||
+#if DARLING_METAL_ENABLED
|
||||
+// Live experiments (guest flag files, re-read every few hundred state changes): /tmp/metal-flag-nocull, -flipwind, -nodepth
|
||||
+static bool MTLLiveFlag(int which)
|
||||
@@ -1771,7 +1911,7 @@ index e931b90..e13dc0e 100644
|
||||
@implementation MTLRenderPassAttachmentDescriptor
|
||||
|
||||
#if DARLING_METAL_ENABLED
|
||||
@@ -96,11 +362,11 @@ MTL_UNSUPPORTED_CLASS
|
||||
@@ -96,11 +502,11 @@ MTL_UNSUPPORTED_CLASS
|
||||
- (Indium::RenderPassAttachmentDescriptor)asIndiumDescriptor
|
||||
{
|
||||
return Indium::RenderPassAttachmentDescriptor {
|
||||
@@ -1785,7 +1925,7 @@ index e931b90..e13dc0e 100644
|
||||
_resolveLevel,
|
||||
_resolveSlice,
|
||||
_resolveDepthPlane,
|
||||
@@ -367,7 +633,11 @@ MTL_UNSUPPORTED_CLASS
|
||||
@@ -367,7 +773,11 @@ MTL_UNSUPPORTED_CLASS
|
||||
std::vector<Indium::RenderPassColorAttachmentDescriptor> result;
|
||||
|
||||
for (const auto& [key, value]: tmp2) {
|
||||
@@ -1798,7 +1938,7 @@ index e931b90..e13dc0e 100644
|
||||
}
|
||||
|
||||
return result;
|
||||
@@ -404,6 +674,10 @@ MTL_UNSUPPORTED_CLASS
|
||||
@@ -404,6 +814,10 @@ MTL_UNSUPPORTED_CLASS
|
||||
- (void)_doInit
|
||||
{
|
||||
_colorAttachments = [MTLRenderPassColorAttachmentDescriptorArray new];
|
||||
@@ -1809,7 +1949,7 @@ index e931b90..e13dc0e 100644
|
||||
}
|
||||
|
||||
- (void)_doDealloc
|
||||
@@ -502,9 +776,9 @@ MTL_UNSUPPORTED_CLASS
|
||||
@@ -502,9 +916,9 @@ MTL_UNSUPPORTED_CLASS
|
||||
{
|
||||
return Indium::RenderPassDescriptor {
|
||||
[_colorAttachments asContiguousIndiumDescriptors],
|
||||
@@ -1822,7 +1962,30 @@ index e931b90..e13dc0e 100644
|
||||
_renderTargetArrayLength,
|
||||
_imageblockSampleLength,
|
||||
_threadgroupMemoryLength,
|
||||
@@ -538,6 +812,7 @@ MTL_UNSUPPORTED_CLASS
|
||||
@@ -527,6 +941,22 @@ MTL_UNSUPPORTED_CLASS
|
||||
|
||||
#if DARLING_METAL_ENABLED
|
||||
|
||||
+{
|
||||
+ // The bindings this encoder has been given, so an identical re-bind can be dropped (see the
|
||||
+ // tracking helpers above). Every encoder object is new and starts with every slot unbound, and
|
||||
+ // nothing outside this class changes an encoder's bindings, so the records are per-encoder state.
|
||||
+ // `_depthStencilOwner` is the strong reference the depth-stencil record needs (see the helpers).
|
||||
+ MTLBindObjectSlots _vertexTextures;
|
||||
+ MTLBindObjectSlots _fragmentTextures;
|
||||
+ MTLBindObjectSlots _vertexSamplers;
|
||||
+ MTLBindObjectSlots _fragmentSamplers;
|
||||
+ MTLBindObjectSlots _pipelineState;
|
||||
+ MTLBindObjectSlots _depthStencilState;
|
||||
+ MTLBindBufferSlots _vertexBuffers;
|
||||
+ MTLBindBufferSlots _fragmentBuffers;
|
||||
+ std::shared_ptr<Indium::DepthStencilState> _depthStencilOwner;
|
||||
+}
|
||||
+
|
||||
@synthesize device = _device;
|
||||
@synthesize encoder = _encoder;
|
||||
@synthesize label = _label;
|
||||
@@ -538,6 +968,7 @@ MTL_UNSUPPORTED_CLASS
|
||||
if (self != nil) {
|
||||
_encoder = encoder;
|
||||
_device = [device retain];
|
||||
@@ -1830,30 +1993,43 @@ index e931b90..e13dc0e 100644
|
||||
}
|
||||
return self;
|
||||
}
|
||||
@@ -553,6 +828,34 @@ MTL_UNSUPPORTED_CLASS
|
||||
@@ -553,8 +984,60 @@ MTL_UNSUPPORTED_CLASS
|
||||
// overridden methods
|
||||
//
|
||||
|
||||
+- (void)setVertexTexture:(id<MTLTexture>)texture atIndex:(NSUInteger)index {
|
||||
+ _encoder->setVertexTexture(MTLTextureGetIndium(texture), index);
|
||||
+ const std::shared_ptr<Indium::Texture>& indiumTexture = MTLTextureGetIndium(texture);
|
||||
+ if (MTLBindObjectSlot(_vertexTextures, index, MTLBindKey(indiumTexture))) { if (MTLBindSkipsEnabled()) { MTLBindSkipCount("bindSkipVertexTexture"); } return; }
|
||||
+ _encoder->setVertexTexture(indiumTexture, index);
|
||||
+}
|
||||
+- (void)setFragmentTexture:(id<MTLTexture>)texture atIndex:(NSUInteger)index {
|
||||
+ const std::shared_ptr<Indium::Texture>& indiumTexture = MTLTextureGetIndium(texture);
|
||||
+ if (MTLBindObjectSlot(_fragmentTextures, index, MTLBindKey(indiumTexture))) { if (MTLBindSkipsEnabled()) { MTLBindSkipCount("bindSkipFragmentTexture"); } return; }
|
||||
+ if (MTLDrawTracking() && index < 31) debugDrawState.fragmentTextures[index] = texture;
|
||||
+ _encoder->setFragmentTexture(MTLTextureGetIndium(texture), index);
|
||||
+ _encoder->setFragmentTexture(indiumTexture, index);
|
||||
+}
|
||||
+- (void)setVertexSamplerState:(id<MTLSamplerState>)sampler atIndex:(NSUInteger)index {
|
||||
+ _encoder->setVertexSamplerState(MTLSamplerGetIndium(sampler), index);
|
||||
+ const std::shared_ptr<Indium::SamplerState>& indiumSampler = MTLSamplerGetIndium(sampler);
|
||||
+ if (MTLBindObjectSlot(_vertexSamplers, index, MTLBindKey(indiumSampler))) { if (MTLBindSkipsEnabled()) { MTLBindSkipCount("bindSkipVertexSamplerState"); } return; }
|
||||
+ _encoder->setVertexSamplerState(indiumSampler, index);
|
||||
+}
|
||||
+- (void)setFragmentSamplerState:(id<MTLSamplerState>)sampler atIndex:(NSUInteger)index {
|
||||
+ _encoder->setFragmentSamplerState(MTLSamplerGetIndium(sampler), index);
|
||||
+ const std::shared_ptr<Indium::SamplerState>& indiumSampler = MTLSamplerGetIndium(sampler);
|
||||
+ if (MTLBindObjectSlot(_fragmentSamplers, index, MTLBindKey(indiumSampler))) { if (MTLBindSkipsEnabled()) { MTLBindSkipCount("bindSkipFragmentSamplerState"); } return; }
|
||||
+ _encoder->setFragmentSamplerState(indiumSampler, index);
|
||||
+}
|
||||
+- (void)setDepthBias:(float)depthBias slopeScale:(float)slopeScale clamp:(float)clamp {
|
||||
+ _encoder->setDepthBias(depthBias, slopeScale, clamp);
|
||||
+}
|
||||
+- (void)setDepthStencilState:(id<MTLDepthStencilState>)state {
|
||||
+ const std::shared_ptr<Indium::DepthStencilState>& indiumState = MTLDepthStencilGetIndium(state);
|
||||
+ // Indium's setter does not keep the state (it reads the descriptor and issues the Vulkan commands),
|
||||
+ // so this record holds the reference that keeps the recorded address from being reused instead.
|
||||
+ if (MTLBindObjectSlot(_depthStencilState, 0, MTLBindKey(indiumState))) { if (MTLBindSkipsEnabled()) { MTLBindSkipCount("bindSkipDepthStencilState"); } return; }
|
||||
+ _depthStencilOwner = indiumState;
|
||||
+ if (MTLDrawTracking()) debugDrawState.depthStencil = state;
|
||||
+ if (MTLLiveFlag(2)) return;
|
||||
+ _encoder->setDepthStencilState(MTLDepthStencilGetIndium(state));
|
||||
+ _encoder->setDepthStencilState(indiumState);
|
||||
+}
|
||||
+- (void)setStencilReferenceValue:(uint32_t)value {
|
||||
+ _encoder->setStencilReferenceValue(value);
|
||||
@@ -1864,15 +2040,31 @@ index e931b90..e13dc0e 100644
|
||||
+
|
||||
- (void)endEncoding
|
||||
{
|
||||
+ // Nothing may be bound after this, and the Indium encoder releases the resources it held, so the
|
||||
+ // records must not survive the pass (a later bind must never be compared against them again).
|
||||
+ MTLBindForgetAll(_vertexTextures);
|
||||
+ MTLBindForgetAll(_fragmentTextures);
|
||||
+ MTLBindForgetAll(_vertexSamplers);
|
||||
+ MTLBindForgetAll(_fragmentSamplers);
|
||||
+ MTLBindForgetAll(_pipelineState);
|
||||
+ MTLBindForgetAll(_depthStencilState);
|
||||
+ MTLBindForgetAll(_vertexBuffers);
|
||||
+ MTLBindForgetAll(_fragmentBuffers);
|
||||
+ _depthStencilOwner.reset();
|
||||
_encoder->endEncoding();
|
||||
@@ -564,26 +867,34 @@ MTL_UNSUPPORTED_CLASS
|
||||
}
|
||||
|
||||
@@ -564,26 +1047,37 @@ MTL_UNSUPPORTED_CLASS
|
||||
|
||||
- (void)setRenderPipelineState: (id<MTLRenderPipelineState>)pipelineState
|
||||
{
|
||||
- _encoder->setRenderPipelineState(((MTLRenderPipelineStateInternal*)pipelineState).state);
|
||||
+ const std::shared_ptr<Indium::RenderPipelineState>& indiumPipelineState = MTLRenderPipelineGetIndium(pipelineState);
|
||||
+ // dropped re-binds are not counted as "setPipeline" in the diagnostics, they are not calls
|
||||
+ if (MTLBindObjectSlot(_pipelineState, 0, MTLBindKey(indiumPipelineState))) { if (MTLBindSkipsEnabled()) { MTLBindSkipCount("bindSkipPipelineState"); } return; }
|
||||
+ if (MTLDrawTracking()) debugDrawState.pipeline = pipelineState;
|
||||
+ MTLTraceCount(pipelineState ? "setPipeline" : "setPipelineNil");
|
||||
+ _encoder->setRenderPipelineState(MTLRenderPipelineGetIndium(pipelineState));
|
||||
+ _encoder->setRenderPipelineState(indiumPipelineState);
|
||||
}
|
||||
|
||||
- (void)setTriangleFillMode: (MTLTriangleFillMode)fillMode
|
||||
@@ -1901,7 +2093,7 @@ index e931b90..e13dc0e 100644
|
||||
_encoder->setViewport(MTLViewportToIndium(viewport));
|
||||
}
|
||||
|
||||
@@ -599,6 +910,8 @@ MTL_UNSUPPORTED_CLASS
|
||||
@@ -599,6 +1093,8 @@ MTL_UNSUPPORTED_CLASS
|
||||
|
||||
- (void)setScissorRect: (MTLScissorRect)rect
|
||||
{
|
||||
@@ -1910,11 +2102,13 @@ index e931b90..e13dc0e 100644
|
||||
_encoder->setScissorRect(MTLScissorRectToIndium(rect));
|
||||
}
|
||||
|
||||
@@ -616,7 +929,25 @@ MTL_UNSUPPORTED_CLASS
|
||||
@@ -616,7 +1112,27 @@ MTL_UNSUPPORTED_CLASS
|
||||
offset: (NSUInteger)offset
|
||||
atIndex: (NSUInteger)index
|
||||
{
|
||||
- _encoder->setVertexBuffer(((MTLBufferInternal*)buffer).buffer, offset, index);
|
||||
+ const std::shared_ptr<Indium::Buffer>& indiumBuffer = MTLBufferGetIndium(buffer);
|
||||
+ if (MTLBindBufferSlot(_vertexBuffers, index, MTLBindKey(indiumBuffer), offset)) { if (MTLBindSkipsEnabled()) { MTLBindSkipCount("bindSkipVertexBuffer"); } return; }
|
||||
+ if (MTLDebugEnabled()) {
|
||||
+ static unsigned long calls = 0;
|
||||
+ static unsigned long unusual = 0;
|
||||
@@ -1933,11 +2127,11 @@ index e931b90..e13dc0e 100644
|
||||
+ }
|
||||
+ }
|
||||
+ if (MTLDrawTracking() && index < 31) { debugDrawState.vertexBuffers[index] = buffer; debugDrawState.vertexOffsets[index] = offset; debugDrawState.vertexBytesLength[index] = 0; }
|
||||
+ _encoder->setVertexBuffer(MTLBufferGetIndium(buffer), offset, index);
|
||||
+ _encoder->setVertexBuffer(indiumBuffer, offset, index);
|
||||
}
|
||||
|
||||
- (void)setVertexBuffers: (const id<MTLBuffer>*)buffers
|
||||
@@ -626,7 +957,14 @@ MTL_UNSUPPORTED_CLASS
|
||||
@@ -626,7 +1142,17 @@ MTL_UNSUPPORTED_CLASS
|
||||
std::vector<std::shared_ptr<Indium::Buffer>> tmp;
|
||||
std::vector<size_t> tmp2(offsets, offsets + range.length);
|
||||
for (size_t i = 0; i < range.length; ++i) {
|
||||
@@ -1950,13 +2144,17 @@ index e931b90..e13dc0e 100644
|
||||
+ debugDrawState.vertexOffsets[range.location + i] = offsets[i];
|
||||
+ debugDrawState.vertexBytesLength[range.location + i] = 0;
|
||||
+ }
|
||||
+ }
|
||||
+ for (NSUInteger i = 0; i < range.length; ++i) {
|
||||
+ MTLBindBufferSlot(_vertexBuffers, range.location + i, MTLBindKey(tmp[i]), offsets[i]);
|
||||
}
|
||||
_encoder->setVertexBuffers(tmp, tmp2, NSRangeToIndium(range));
|
||||
}
|
||||
@@ -634,6 +972,16 @@ MTL_UNSUPPORTED_CLASS
|
||||
@@ -634,6 +1160,17 @@ MTL_UNSUPPORTED_CLASS
|
||||
- (void)setVertexBufferOffset: (NSUInteger)offset
|
||||
atIndex: (NSUInteger)index
|
||||
{
|
||||
+ if (MTLBindBufferOffset(_vertexBuffers, index, offset)) { if (MTLBindSkipsEnabled()) { MTLBindSkipCount("bindSkipVertexBufferOffset"); } return; }
|
||||
+ if (MTLDebugEnabled()) {
|
||||
+ static unsigned long calls = 0;
|
||||
+ if (++calls <= 120) {
|
||||
@@ -1970,25 +2168,28 @@ index e931b90..e13dc0e 100644
|
||||
_encoder->setVertexBufferOffset(offset, index);
|
||||
}
|
||||
|
||||
@@ -641,6 +989,7 @@ MTL_UNSUPPORTED_CLASS
|
||||
@@ -641,6 +1178,8 @@ MTL_UNSUPPORTED_CLASS
|
||||
length: (NSUInteger)length
|
||||
atIndex: (NSUInteger)index
|
||||
{
|
||||
+ MTLBindForgetBufferSlot(_vertexBuffers, index);
|
||||
+ if (MTLDrawTracking() && index < 31) { debugDrawState.vertexBuffers[index] = nil; debugDrawState.vertexBytesLength[index] = length; memcpy(debugDrawState.vertexBytes[index], bytes, std::min<NSUInteger>(64, length)); }
|
||||
_encoder->setVertexBytes(bytes, length, index);
|
||||
}
|
||||
|
||||
@@ -648,7 +997,8 @@ MTL_UNSUPPORTED_CLASS
|
||||
@@ -648,7 +1187,10 @@ MTL_UNSUPPORTED_CLASS
|
||||
offset: (NSUInteger)offset
|
||||
atIndex: (NSUInteger)index
|
||||
{
|
||||
- _encoder->setFragmentBuffer(((MTLBufferInternal*)buffer).buffer, offset, index);
|
||||
+ const std::shared_ptr<Indium::Buffer>& indiumBuffer = MTLBufferGetIndium(buffer);
|
||||
+ if (MTLBindBufferSlot(_fragmentBuffers, index, MTLBindKey(indiumBuffer), offset)) { if (MTLBindSkipsEnabled()) { MTLBindSkipCount("bindSkipFragmentBuffer"); } return; }
|
||||
+ if (MTLDrawTracking() && index < 31) { debugDrawState.fragmentBuffers[index] = buffer; debugDrawState.fragmentOffsets[index] = offset; debugDrawState.fragmentBytesLength[index] = 0; }
|
||||
+ _encoder->setFragmentBuffer(MTLBufferGetIndium(buffer), offset, index);
|
||||
+ _encoder->setFragmentBuffer(indiumBuffer, offset, index);
|
||||
}
|
||||
|
||||
- (void)setFragmentBuffers: (const id<MTLBuffer>*)buffers
|
||||
@@ -658,7 +1008,14 @@ MTL_UNSUPPORTED_CLASS
|
||||
@@ -658,7 +1200,17 @@ MTL_UNSUPPORTED_CLASS
|
||||
std::vector<std::shared_ptr<Indium::Buffer>> tmp;
|
||||
std::vector<size_t> tmp2(offsets, offsets + range.length);
|
||||
for (size_t i = 0; i < range.length; ++i) {
|
||||
@@ -2001,26 +2202,31 @@ index e931b90..e13dc0e 100644
|
||||
+ debugDrawState.fragmentOffsets[range.location + i] = offsets[i];
|
||||
+ debugDrawState.fragmentBytesLength[range.location + i] = 0;
|
||||
+ }
|
||||
+ }
|
||||
+ for (NSUInteger i = 0; i < range.length; ++i) {
|
||||
+ MTLBindBufferSlot(_fragmentBuffers, range.location + i, MTLBindKey(tmp[i]), offsets[i]);
|
||||
}
|
||||
_encoder->setFragmentBuffers(tmp, tmp2, NSRangeToIndium(range));
|
||||
}
|
||||
@@ -666,6 +1023,7 @@ MTL_UNSUPPORTED_CLASS
|
||||
@@ -666,6 +1218,8 @@ MTL_UNSUPPORTED_CLASS
|
||||
- (void)setFragmentBufferOffset: (NSUInteger)offset
|
||||
atIndex: (NSUInteger)index
|
||||
{
|
||||
+ if (MTLBindBufferOffset(_fragmentBuffers, index, offset)) { if (MTLBindSkipsEnabled()) { MTLBindSkipCount("bindSkipFragmentBufferOffset"); } return; }
|
||||
+ if (MTLDrawTracking() && index < 31) debugDrawState.fragmentOffsets[index] = offset;
|
||||
_encoder->setFragmentBufferOffset(offset, index);
|
||||
}
|
||||
|
||||
@@ -673,6 +1031,7 @@ MTL_UNSUPPORTED_CLASS
|
||||
@@ -673,6 +1227,8 @@ MTL_UNSUPPORTED_CLASS
|
||||
length: (NSUInteger)length
|
||||
atIndex: (NSUInteger)index
|
||||
{
|
||||
+ MTLBindForgetBufferSlot(_fragmentBuffers, index);
|
||||
+ if (MTLDrawTracking() && index < 31) { debugDrawState.fragmentBuffers[index] = nil; debugDrawState.fragmentBytesLength[index] = length; memcpy(debugDrawState.fragmentBytes[index], bytes, std::min<NSUInteger>(64, length)); }
|
||||
_encoder->setFragmentBytes(bytes, length, index);
|
||||
}
|
||||
|
||||
@@ -682,6 +1041,9 @@ MTL_UNSUPPORTED_CLASS
|
||||
@@ -682,6 +1238,9 @@ MTL_UNSUPPORTED_CLASS
|
||||
instanceCount: (NSUInteger)instanceCount
|
||||
baseInstance: (NSUInteger)baseInstance
|
||||
{
|
||||
@@ -2030,7 +2236,7 @@ index e931b90..e13dc0e 100644
|
||||
_encoder->drawPrimitives(static_cast<Indium::PrimitiveType>(primitiveType), vertexStart, vertexCount, instanceCount, baseInstance);
|
||||
}
|
||||
|
||||
@@ -690,6 +1052,9 @@ MTL_UNSUPPORTED_CLASS
|
||||
@@ -690,6 +1249,9 @@ MTL_UNSUPPORTED_CLASS
|
||||
vertexCount: (NSUInteger)vertexCount
|
||||
instanceCount: (NSUInteger)instanceCount
|
||||
{
|
||||
@@ -2040,7 +2246,7 @@ index e931b90..e13dc0e 100644
|
||||
_encoder->drawPrimitives(static_cast<Indium::PrimitiveType>(primitiveType), vertexStart, vertexCount, instanceCount);
|
||||
}
|
||||
|
||||
@@ -697,6 +1062,9 @@ MTL_UNSUPPORTED_CLASS
|
||||
@@ -697,6 +1259,9 @@ MTL_UNSUPPORTED_CLASS
|
||||
vertexStart: (NSUInteger)vertexStart
|
||||
vertexCount: (NSUInteger)vertexCount
|
||||
{
|
||||
@@ -2050,7 +2256,7 @@ index e931b90..e13dc0e 100644
|
||||
_encoder->drawPrimitives(static_cast<Indium::PrimitiveType>(primitiveType), vertexStart, vertexCount);
|
||||
}
|
||||
|
||||
@@ -709,7 +1077,10 @@ MTL_UNSUPPORTED_CLASS
|
||||
@@ -709,7 +1274,10 @@ MTL_UNSUPPORTED_CLASS
|
||||
baseVertex: (NSInteger)baseVertex
|
||||
baseInstance: (NSUInteger)baseInstance
|
||||
{
|
||||
@@ -2062,7 +2268,7 @@ index e931b90..e13dc0e 100644
|
||||
}
|
||||
|
||||
- (void)drawIndexedPrimitives: (MTLPrimitiveType)primitiveType
|
||||
@@ -719,7 +1090,10 @@ MTL_UNSUPPORTED_CLASS
|
||||
@@ -719,7 +1287,10 @@ MTL_UNSUPPORTED_CLASS
|
||||
indexBufferOffset: (NSUInteger)indexBufferOffset
|
||||
instanceCount: (NSUInteger)instanceCount
|
||||
{
|
||||
@@ -2074,7 +2280,7 @@ index e931b90..e13dc0e 100644
|
||||
}
|
||||
|
||||
- (void)drawIndexedPrimitives: (MTLPrimitiveType)primitiveType
|
||||
@@ -728,7 +1102,10 @@ MTL_UNSUPPORTED_CLASS
|
||||
@@ -728,7 +1299,10 @@ MTL_UNSUPPORTED_CLASS
|
||||
indexBuffer: (id<MTLBuffer>)indexBuffer
|
||||
indexBufferOffset: (NSUInteger)indexBufferOffset
|
||||
{
|
||||
@@ -2433,10 +2639,10 @@ index 37cccb3..79b2350 100644
|
||||
-
|
||||
@end
|
||||
diff --git a/src/Metal/MTLVertexDescriptor.mm b/src/Metal/MTLVertexDescriptor.mm
|
||||
index cde968d..fc7d99d 100644
|
||||
index cde968d..a3819d0 100644
|
||||
--- a/src/Metal/MTLVertexDescriptor.mm
|
||||
+++ b/src/Metal/MTLVertexDescriptor.mm
|
||||
@@ -1,24 +1,168 @@
|
||||
@@ -1,24 +1,205 @@
|
||||
// SPDX-FileCopyrightText: 2026 Darling Developers
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
@@ -2444,6 +2650,11 @@ index cde968d..fc7d99d 100644
|
||||
-#import <Foundation/NSMethodSignature.h>
|
||||
-#import <Foundation/NSInvocation.h>
|
||||
-
|
||||
-@implementation MTLVertexDescriptor
|
||||
-
|
||||
-#if DARLING_METAL_ENABLED
|
||||
-
|
||||
-- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
|
||||
+#import <Metal/MTLVertexDescriptorInternal.h>
|
||||
+#import <objc/runtime.h>
|
||||
+// The bridge owns these concrete descriptor implementations. Keep their
|
||||
@@ -2470,21 +2681,77 @@ index cde968d..fc7d99d 100644
|
||||
+- (instancetype)init { self = [super init]; if (self) { _stepFunction = MTLVertexStepFunctionPerVertex; _stepRate = 1; } return self; }
|
||||
+- (id)copyWithZone:(NSZone*)zone { MTLVertexBufferLayoutDescriptor* copy = [[[self class] allocWithZone:zone] init]; copy.stride = _stride; copy.stepFunction = _stepFunction; copy.stepRate = _stepRate; return copy; }
|
||||
+@end
|
||||
+@interface MTLVertexBufferLayoutDescriptorArray (Existing)
|
||||
+- (MTLVertexBufferLayoutDescriptor*)existingObjectAtIndex:(NSUInteger)index;
|
||||
+- (void)resetAll;
|
||||
+@end
|
||||
+@interface MTLVertexAttributeDescriptorArray (Existing)
|
||||
+- (MTLVertexAttributeDescriptor*)existingObjectAtIndex:(NSUInteger)index;
|
||||
+- (void)resetAll;
|
||||
+@end
|
||||
+// The arrays used to be NSMutableDictionaries keyed by boxed NSNumbers, and converting a descriptor created all 31 entries;
|
||||
+// under a game that builds vertex descriptors constantly that was ~30% of the render thread. Fixed storage, entries created lazily.
|
||||
+@implementation MTLVertexBufferLayoutDescriptorArray {
|
||||
+// The array storage is visible within this file (same pattern as the item classes above) so that -[MTLVertexDescriptor reset]
|
||||
+// can run the in-place reset without a message send per array: the game resets a vertex descriptor for every draw. Both arrays
|
||||
+// are only ever created by -init/-reset/-copyWithZone: below, and the two properties are readonly, so the storage a descriptor
|
||||
+// holds is always the concrete class declared here.
|
||||
+@interface MTLVertexBufferLayoutDescriptorArray () {
|
||||
+@public
|
||||
+ MTLVertexBufferLayoutDescriptor* _items[31];
|
||||
+ uint32_t _present; // bit i: _items[i] exists
|
||||
+ uint32_t _concrete; // bit i: _items[i] is exactly MTLVertexBufferLayoutDescriptor (ivars can be written directly)
|
||||
+}
|
||||
+- (MTLVertexBufferLayoutDescriptor*)existingObjectAtIndex:(NSUInteger)index;
|
||||
+- (void)resetAll;
|
||||
+@end
|
||||
+@interface MTLVertexAttributeDescriptorArray () {
|
||||
+@public
|
||||
+ MTLVertexAttributeDescriptor* _items[31];
|
||||
+ uint32_t _present; // bit i: _items[i] exists
|
||||
+ uint32_t _concrete; // bit i: _items[i] is exactly MTLVertexAttributeDescriptor (ivars can be written directly)
|
||||
+}
|
||||
+- (MTLVertexAttributeDescriptor*)existingObjectAtIndex:(NSUInteger)index;
|
||||
+- (void)resetAll;
|
||||
+@end
|
||||
+// A present-but-non-concrete entry is a caller-installed subclass instance (see
|
||||
+// -setObject:atIndexedSubscript:): it keeps its own property setters. Kept out of
|
||||
+// line so the common all-concrete loop does not carry the selector references and
|
||||
+// the message-send stub in its preheader.
|
||||
+__attribute__((noinline)) static void MTLVertexBufferLayoutSetDefaults(MTLVertexBufferLayoutDescriptor* item)
|
||||
{
|
||||
- return [NSMethodSignature signatureWithObjCTypes: "v@:"];
|
||||
+ item.stride = 0; item.stepFunction = MTLVertexStepFunctionPerVertex; item.stepRate = 1;
|
||||
}
|
||||
-
|
||||
-- (void)forwardInvocation:(NSInvocation *)anInvocation
|
||||
+__attribute__((noinline)) static void MTLVertexAttributeSetDefaults(MTLVertexAttributeDescriptor* item)
|
||||
{
|
||||
- NSLog(@"Stub called: %@ in %@", NSStringFromSelector([anInvocation selector]), [self class]);
|
||||
+ item.format = MTLVertexFormatInvalid; item.offset = 0; item.bufferIndex = 0;
|
||||
+}
|
||||
+// Reset every entry the array has handed out, in place: an object a caller still
|
||||
+// holds must read back the defaults. Exact-class entries are written through
|
||||
+// their ivars; the present-but-non-concrete mask is taken once so the loop does
|
||||
+// not reload it per entry.
|
||||
+static void MTLVertexBufferLayoutArrayResetItems(MTLVertexBufferLayoutDescriptorArray* array)
|
||||
+{
|
||||
+ const uint32_t exotic = array->_present & ~array->_concrete;
|
||||
+ for (uint32_t bits = array->_present; bits; bits &= bits - 1) {
|
||||
+ const int i = __builtin_ctz(bits);
|
||||
+ MTLVertexBufferLayoutDescriptor* item = array->_items[i];
|
||||
+ if (exotic & (1u << i)) {
|
||||
+ MTLVertexBufferLayoutSetDefaults(item);
|
||||
+ } else {
|
||||
+ item->_stride = 0; item->_stepFunction = MTLVertexStepFunctionPerVertex; item->_stepRate = 1;
|
||||
+ }
|
||||
+ }
|
||||
+}
|
||||
+static void MTLVertexAttributeArrayResetItems(MTLVertexAttributeDescriptorArray* array)
|
||||
+{
|
||||
+ const uint32_t exotic = array->_present & ~array->_concrete;
|
||||
+ for (uint32_t bits = array->_present; bits; bits &= bits - 1) {
|
||||
+ const int i = __builtin_ctz(bits);
|
||||
+ MTLVertexAttributeDescriptor* item = array->_items[i];
|
||||
+ if (exotic & (1u << i)) {
|
||||
+ MTLVertexAttributeSetDefaults(item);
|
||||
+ } else {
|
||||
+ item->_format = MTLVertexFormatInvalid; item->_offset = 0; item->_bufferIndex = 0;
|
||||
+ }
|
||||
+ }
|
||||
+}
|
||||
+// The arrays used to be NSMutableDictionaries keyed by boxed NSNumbers, and converting a descriptor created all 31 entries;
|
||||
+// under a game that builds vertex descriptors constantly that was ~30% of the render thread. Fixed storage, entries created lazily.
|
||||
+@implementation MTLVertexBufferLayoutDescriptorArray
|
||||
+- (void)dealloc { for (int i = 0; i < 31; ++i) [_items[i] release]; [super dealloc]; }
|
||||
+- (MTLVertexBufferLayoutDescriptor*)objectAtIndexedSubscript:(NSUInteger)index {
|
||||
+ if (index >= 31) [NSException raise:NSRangeException format:@"Vertex descriptor index %lu exceeds 30", (unsigned long)index];
|
||||
@@ -2493,17 +2760,7 @@ index cde968d..fc7d99d 100644
|
||||
+}
|
||||
+- (MTLVertexBufferLayoutDescriptor*)existingObjectAtIndex:(NSUInteger)index { return index < 31 ? _items[index] : nil; }
|
||||
+// reset in place: the game calls -[MTLVertexDescriptor reset] for every draw and then refills it, so keep the objects
|
||||
+- (void)resetAll {
|
||||
+ for (uint32_t bits = _present; bits; bits &= bits - 1) {
|
||||
+ const int i = __builtin_ctz(bits);
|
||||
+ MTLVertexBufferLayoutDescriptor* item = _items[i];
|
||||
+ if (_concrete & (1u << i)) {
|
||||
+ item->_stride = 0; item->_stepFunction = MTLVertexStepFunctionPerVertex; item->_stepRate = 1;
|
||||
+ } else {
|
||||
+ item.stride = 0; item.stepFunction = MTLVertexStepFunctionPerVertex; item.stepRate = 1;
|
||||
+ }
|
||||
+ }
|
||||
+}
|
||||
+- (void)resetAll { MTLVertexBufferLayoutArrayResetItems(self); }
|
||||
+- (void)setObject:(MTLVertexBufferLayoutDescriptor*)descriptor atIndexedSubscript:(NSUInteger)index {
|
||||
+ if (index >= 31) [NSException raise:NSRangeException format:@"Vertex descriptor index %lu exceeds 30", (unsigned long)index];
|
||||
+ id old = _items[index];
|
||||
@@ -2527,11 +2784,7 @@ index cde968d..fc7d99d 100644
|
||||
+@synthesize bufferIndex = _bufferIndex;
|
||||
+- (id)copyWithZone:(NSZone*)zone { MTLVertexAttributeDescriptor* copy = [[[self class] allocWithZone:zone] init]; copy.format = _format; copy.offset = _offset; copy.bufferIndex = _bufferIndex; return copy; }
|
||||
+@end
|
||||
+@implementation MTLVertexAttributeDescriptorArray {
|
||||
+ MTLVertexAttributeDescriptor* _items[31];
|
||||
+ uint32_t _present; // bit i: _items[i] exists
|
||||
+ uint32_t _concrete; // bit i: _items[i] is exactly MTLVertexAttributeDescriptor (ivars can be written directly)
|
||||
+}
|
||||
+@implementation MTLVertexAttributeDescriptorArray
|
||||
+- (void)dealloc { for (int i = 0; i < 31; ++i) [_items[i] release]; [super dealloc]; }
|
||||
+- (MTLVertexAttributeDescriptor*)objectAtIndexedSubscript:(NSUInteger)index {
|
||||
+ if (index >= 31) [NSException raise:NSRangeException format:@"Vertex descriptor index %lu exceeds 30", (unsigned long)index];
|
||||
@@ -2539,17 +2792,7 @@ index cde968d..fc7d99d 100644
|
||||
+ return _items[index];
|
||||
+}
|
||||
+- (MTLVertexAttributeDescriptor*)existingObjectAtIndex:(NSUInteger)index { return index < 31 ? _items[index] : nil; }
|
||||
+- (void)resetAll {
|
||||
+ for (uint32_t bits = _present; bits; bits &= bits - 1) {
|
||||
+ const int i = __builtin_ctz(bits);
|
||||
+ MTLVertexAttributeDescriptor* item = _items[i];
|
||||
+ if (_concrete & (1u << i)) {
|
||||
+ item->_format = MTLVertexFormatInvalid; item->_offset = 0; item->_bufferIndex = 0;
|
||||
+ } else {
|
||||
+ item.format = MTLVertexFormatInvalid; item.offset = 0; item.bufferIndex = 0;
|
||||
+ }
|
||||
+ }
|
||||
+}
|
||||
+- (void)resetAll { MTLVertexAttributeArrayResetItems(self); }
|
||||
+- (void)setObject:(MTLVertexAttributeDescriptor*)descriptor atIndexedSubscript:(NSUInteger)index {
|
||||
+ if (index >= 31) [NSException raise:NSRangeException format:@"Vertex descriptor index %lu exceeds 30", (unsigned long)index];
|
||||
+ id old = _items[index];
|
||||
@@ -2567,28 +2810,27 @@ index cde968d..fc7d99d 100644
|
||||
+ return copy;
|
||||
+}
|
||||
+@end
|
||||
@implementation MTLVertexDescriptor
|
||||
-
|
||||
-#if DARLING_METAL_ENABLED
|
||||
-
|
||||
-- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
|
||||
-{
|
||||
- return [NSMethodSignature signatureWithObjCTypes: "v@:"];
|
||||
+@implementation MTLVertexDescriptor
|
||||
+@synthesize layouts = _layouts;
|
||||
+@synthesize attributes = _attributes;
|
||||
++ (instancetype)vertexDescriptor { return [[[self alloc] init] autorelease]; }
|
||||
+- (instancetype)init { self = [super init]; if (self) [self reset]; return self; }
|
||||
+- (void)dealloc { [_layouts release]; [_attributes release]; [super dealloc]; }
|
||||
+- (void)reset {
|
||||
+ if (_layouts && _attributes) { [_layouts resetAll]; [_attributes resetAll]; return; }
|
||||
+ if (_layouts && _attributes) {
|
||||
+ // Direct call instead of [_layouts resetAll]/[_attributes resetAll]: the game
|
||||
+ // resets a vertex descriptor for every draw. Both arrays are only ever created
|
||||
+ // by -init/-reset/-copyWithZone: in this file and the two properties are
|
||||
+ // readonly, so their storage is the concrete class. The per-entry subclass
|
||||
+ // case below keeps using the property setters through their own message sends.
|
||||
+ MTLVertexBufferLayoutArrayResetItems(_layouts);
|
||||
+ MTLVertexAttributeArrayResetItems(_attributes);
|
||||
+ return;
|
||||
+ }
|
||||
+ [_layouts release]; [_attributes release];
|
||||
+ _layouts = [MTLVertexBufferLayoutDescriptorArray new];
|
||||
+ _attributes = [MTLVertexAttributeDescriptorArray new];
|
||||
}
|
||||
-
|
||||
-- (void)forwardInvocation:(NSInvocation *)anInvocation
|
||||
-{
|
||||
- NSLog(@"Stub called: %@ in %@", NSStringFromSelector([anInvocation selector]), [self class]);
|
||||
+}
|
||||
+- (id)copyWithZone:(NSZone*)zone {
|
||||
+ MTLVertexDescriptor* copy = [[[self class] allocWithZone:zone] init];
|
||||
+ [copy->_layouts release]; [copy->_attributes release];
|
||||
@@ -3079,43 +3321,43 @@ new file mode 100644
|
||||
+- (void)endEncoding { _encoder->endEncoding(); }
|
||||
+- (void)copyFromBuffer:(id<MTLBuffer>)source sourceOffset:(NSUInteger)sourceOffset toBuffer:(id<MTLBuffer>)destination destinationOffset:(NSUInteger)destinationOffset size:(NSUInteger)size {
|
||||
+ MTLTraceCount("blitBufferToBuffer");
|
||||
+ _encoder->copy(((MTLBufferInternal*)source).buffer, sourceOffset, ((MTLBufferInternal*)destination).buffer, destinationOffset, size);
|
||||
+ _encoder->copy(MTLBufferGetIndium(source), sourceOffset, MTLBufferGetIndium(destination), destinationOffset, size);
|
||||
+}
|
||||
+- (void)copyFromBuffer:(id<MTLBuffer>)source sourceOffset:(NSUInteger)sourceOffset sourceBytesPerRow:(NSUInteger)sourceBytesPerRow sourceBytesPerImage:(NSUInteger)sourceBytesPerImage sourceSize:(MTLSize)sourceSize toTexture:(id<MTLTexture>)destination destinationSlice:(NSUInteger)destinationSlice destinationLevel:(NSUInteger)destinationLevel destinationOrigin:(MTLOrigin)destinationOrigin {
|
||||
+ MTLTraceCount("blitBufferToTexture");
|
||||
+ MTLDebugUpload(source, sourceOffset, destination, destinationLevel, sourceSize);
|
||||
+ _encoder->copy(((MTLBufferInternal*)source).buffer, sourceOffset, sourceBytesPerRow, sourceBytesPerImage, MTLSizeToIndium(sourceSize), ((MTLTextureInternal*)destination).texture, destinationSlice, destinationLevel, (Indium::Origin {destinationOrigin.x, destinationOrigin.y, destinationOrigin.z}));
|
||||
+ _encoder->copy(MTLBufferGetIndium(source), sourceOffset, sourceBytesPerRow, sourceBytesPerImage, MTLSizeToIndium(sourceSize), MTLTextureGetIndium(destination), destinationSlice, destinationLevel, (Indium::Origin {destinationOrigin.x, destinationOrigin.y, destinationOrigin.z}));
|
||||
+}
|
||||
+- (void)copyFromTexture:(id<MTLTexture>)source sourceSlice:(NSUInteger)sourceSlice sourceLevel:(NSUInteger)sourceLevel sourceOrigin:(MTLOrigin)sourceOrigin sourceSize:(MTLSize)sourceSize toBuffer:(id<MTLBuffer>)destination destinationOffset:(NSUInteger)destinationOffset destinationBytesPerRow:(NSUInteger)destinationBytesPerRow destinationBytesPerImage:(NSUInteger)destinationBytesPerImage {
|
||||
+ MTLTraceCount("blitTextureToBuffer");
|
||||
+ _encoder->copy(((MTLTextureInternal*)source).texture, sourceSlice, sourceLevel, (Indium::Origin {sourceOrigin.x, sourceOrigin.y, sourceOrigin.z}), MTLSizeToIndium(sourceSize), ((MTLBufferInternal*)destination).buffer, destinationOffset, destinationBytesPerRow, destinationBytesPerImage);
|
||||
+ _encoder->copy(MTLTextureGetIndium(source), sourceSlice, sourceLevel, (Indium::Origin {sourceOrigin.x, sourceOrigin.y, sourceOrigin.z}), MTLSizeToIndium(sourceSize), MTLBufferGetIndium(destination), destinationOffset, destinationBytesPerRow, destinationBytesPerImage);
|
||||
+}
|
||||
+- (void)copyFromTexture:(id<MTLTexture>)source toTexture:(id<MTLTexture>)destination {
|
||||
+ MTLTraceCount("blitTextureToTexture");
|
||||
+ _encoder->copy(((MTLTextureInternal*)source).texture, ((MTLTextureInternal*)destination).texture);
|
||||
+ _encoder->copy(MTLTextureGetIndium(source), MTLTextureGetIndium(destination));
|
||||
+}
|
||||
+- (void)copyFromTexture:(id<MTLTexture>)source sourceSlice:(NSUInteger)sourceSlice sourceLevel:(NSUInteger)sourceLevel toTexture:(id<MTLTexture>)destination destinationSlice:(NSUInteger)destinationSlice destinationLevel:(NSUInteger)destinationLevel sliceCount:(NSUInteger)sliceCount levelCount:(NSUInteger)levelCount {
|
||||
+ MTLTraceCount("blitTextureToTexture");
|
||||
+ _encoder->copy(((MTLTextureInternal*)source).texture, sourceSlice, sourceLevel, ((MTLTextureInternal*)destination).texture, destinationSlice, destinationLevel, sliceCount, levelCount);
|
||||
+ _encoder->copy(MTLTextureGetIndium(source), sourceSlice, sourceLevel, MTLTextureGetIndium(destination), destinationSlice, destinationLevel, sliceCount, levelCount);
|
||||
+}
|
||||
+- (void)copyFromTexture:(id<MTLTexture>)source sourceSlice:(NSUInteger)sourceSlice sourceLevel:(NSUInteger)sourceLevel sourceOrigin:(MTLOrigin)sourceOrigin sourceSize:(MTLSize)sourceSize toTexture:(id<MTLTexture>)destination destinationSlice:(NSUInteger)destinationSlice destinationLevel:(NSUInteger)destinationLevel destinationOrigin:(MTLOrigin)destinationOrigin {
|
||||
+ MTLTraceCount("blitTextureToTexture");
|
||||
+ _encoder->copy(((MTLTextureInternal*)source).texture, sourceSlice, sourceLevel, (Indium::Origin {sourceOrigin.x, sourceOrigin.y, sourceOrigin.z}), MTLSizeToIndium(sourceSize), ((MTLTextureInternal*)destination).texture, destinationSlice, destinationLevel, (Indium::Origin {destinationOrigin.x, destinationOrigin.y, destinationOrigin.z}));
|
||||
+ _encoder->copy(MTLTextureGetIndium(source), sourceSlice, sourceLevel, (Indium::Origin {sourceOrigin.x, sourceOrigin.y, sourceOrigin.z}), MTLSizeToIndium(sourceSize), MTLTextureGetIndium(destination), destinationSlice, destinationLevel, (Indium::Origin {destinationOrigin.x, destinationOrigin.y, destinationOrigin.z}));
|
||||
+}
|
||||
+- (void)fillBuffer:(id<MTLBuffer>)buffer range:(NSRange)range value:(uint8_t)value {
|
||||
+ _encoder->fillBuffer(((MTLBufferInternal*)buffer).buffer, NSRangeToIndium(range), value);
|
||||
+ _encoder->fillBuffer(MTLBufferGetIndium(buffer), NSRangeToIndium(range), value);
|
||||
+}
|
||||
+- (void)generateMipmapsForTexture:(id<MTLTexture>)texture {
|
||||
+ MTLTraceCount("generateMipmaps");
|
||||
+ _encoder->generateMipmapsForTexture(((MTLTextureInternal*)texture).texture);
|
||||
+ _encoder->generateMipmapsForTexture(MTLTextureGetIndium(texture));
|
||||
+}
|
||||
+- (void)copyFromBuffer:(id<MTLBuffer>)source sourceOffset:(NSUInteger)sourceOffset sourceBytesPerRow:(NSUInteger)sourceBytesPerRow sourceBytesPerImage:(NSUInteger)sourceBytesPerImage sourceSize:(MTLSize)sourceSize toTexture:(id<MTLTexture>)destination destinationSlice:(NSUInteger)destinationSlice destinationLevel:(NSUInteger)destinationLevel destinationOrigin:(MTLOrigin)destinationOrigin options:(MTLBlitOption)options {
|
||||
+ MTLTraceCount("blitBufferToTexture");
|
||||
+ _encoder->copy(((MTLBufferInternal*)source).buffer, sourceOffset, sourceBytesPerRow, sourceBytesPerImage, MTLSizeToIndium(sourceSize), ((MTLTextureInternal*)destination).texture, destinationSlice, destinationLevel, (Indium::Origin {destinationOrigin.x, destinationOrigin.y, destinationOrigin.z}), static_cast<Indium::BlitOption>(options));
|
||||
+ _encoder->copy(MTLBufferGetIndium(source), sourceOffset, sourceBytesPerRow, sourceBytesPerImage, MTLSizeToIndium(sourceSize), MTLTextureGetIndium(destination), destinationSlice, destinationLevel, (Indium::Origin {destinationOrigin.x, destinationOrigin.y, destinationOrigin.z}), static_cast<Indium::BlitOption>(options));
|
||||
+}
|
||||
+- (void)copyFromTexture:(id<MTLTexture>)source sourceSlice:(NSUInteger)sourceSlice sourceLevel:(NSUInteger)sourceLevel sourceOrigin:(MTLOrigin)sourceOrigin sourceSize:(MTLSize)sourceSize toBuffer:(id<MTLBuffer>)destination destinationOffset:(NSUInteger)destinationOffset destinationBytesPerRow:(NSUInteger)destinationBytesPerRow destinationBytesPerImage:(NSUInteger)destinationBytesPerImage options:(MTLBlitOption)options {
|
||||
+ MTLTraceCount("blitTextureToBuffer");
|
||||
+ _encoder->copy(((MTLTextureInternal*)source).texture, sourceSlice, sourceLevel, (Indium::Origin {sourceOrigin.x, sourceOrigin.y, sourceOrigin.z}), MTLSizeToIndium(sourceSize), ((MTLBufferInternal*)destination).buffer, destinationOffset, destinationBytesPerRow, destinationBytesPerImage, static_cast<Indium::BlitOption>(options));
|
||||
+ _encoder->copy(MTLTextureGetIndium(source), sourceSlice, sourceLevel, (Indium::Origin {sourceOrigin.x, sourceOrigin.y, sourceOrigin.z}), MTLSizeToIndium(sourceSize), MTLBufferGetIndium(destination), destinationOffset, destinationBytesPerRow, destinationBytesPerImage, static_cast<Indium::BlitOption>(options));
|
||||
+}
|
||||
+#else
|
||||
+@dynamic device, label;
|
||||
|
||||
+1361
-282
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,68 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Rebuild libc++ and libc++abi with -O2.
|
||||
|
||||
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,
|
||||
iostream, locale, shared_ptr/atomic refcounts, mutex, chrono, new/delete) and
|
||||
13 from libc++abi.dylib, and neither has an overlay today.
|
||||
|
||||
This is tools/build-riot-cxx-optimized.py without -DNDEBUG: the configured
|
||||
defines and includes are kept exactly, only the optimization level changes.
|
||||
The earlier builder also compiles libc++ at -O2, but with assertions disabled,
|
||||
which changes what the library checks at run time; the batch installer uses
|
||||
this one so the shipped library differs from the configured build in code
|
||||
generation only. Outputs build/riot-libc++.1.dylib and build/riot-libc++abi.dylib.
|
||||
"""
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from pathlib import Path
|
||||
import re
|
||||
import shlex
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||
import riot_build_gate
|
||||
|
||||
root = Path(__file__).resolve().parent.parent
|
||||
resource_dir = subprocess.check_output(["clang", "-print-resource-dir"], text=True).strip()
|
||||
|
||||
|
||||
def current_clang(values):
|
||||
return [re.sub(r"/usr/lib/clang/\d+/include", resource_dir + "/include", value) for value in values]
|
||||
|
||||
|
||||
def build(library, object_dir, link_dir, output):
|
||||
folder = root / "build/darling-src/build/src/external" / library
|
||||
source = root / "build/darling-src/src/external" / library
|
||||
flags = {}
|
||||
for line in (folder / ("CMakeFiles/%s/flags.make" % object_dir)).read_text().splitlines():
|
||||
if line.startswith("CXX_") and " = " in line:
|
||||
key, value = line.split(" = ", 1)
|
||||
flags[key] = current_clang(shlex.split(value))
|
||||
link = shlex.split((folder / ("CMakeFiles/%s/link.txt" % link_dir)).read_text())
|
||||
jobs = []
|
||||
for index, argument in enumerate(link):
|
||||
if argument.startswith("CMakeFiles/%s/" % object_dir) and argument.endswith(".o"):
|
||||
relative = argument[len("CMakeFiles/%s/" % object_dir):-2]
|
||||
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)
|
||||
|
||||
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)],
|
||||
check=True, capture_output=True)
|
||||
|
||||
riot_build_gate.wait_until_free()
|
||||
with ThreadPoolExecutor(max_workers=6) 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)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
build("libcxxabi", "cxxabi_obj.dir", "cxxabi.dir", "riot-libc++abi.dylib")
|
||||
build("libcxx", "cxx_obj.dir", "cxx.dir", "riot-libc++.1.dylib")
|
||||
@@ -0,0 +1,74 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Build an optimized Foundation runtime, retaining the configured build.
|
||||
|
||||
The framework the guest loads is /System/Library/Frameworks/Foundation.framework
|
||||
(the SDK's, or this prefix's copy once installed), and the configured tree has
|
||||
no Foundation overlay in RuntimeFrameworks yet. Recompile every configured
|
||||
Foundation object at -O2 (the same treatment CoreFoundation/AppKit already
|
||||
have) and relink with the configured link line, without -DNDEBUG: the
|
||||
assertion macros in this code carry side effects in places.
|
||||
"""
|
||||
from pathlib import Path
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
import re
|
||||
import shlex
|
||||
import subprocess
|
||||
|
||||
root = Path(__file__).resolve().parent.parent
|
||||
folder = root / 'build/darling-src/build/src/external/foundation'
|
||||
source = root / 'build/darling-src/src/external/foundation'
|
||||
|
||||
flags = {}
|
||||
custom = {}
|
||||
for line in (folder / 'CMakeFiles/Foundation_obj.dir/flags.make').read_text().splitlines():
|
||||
if line.startswith(('C_', 'ASM_')) and ' = ' in line:
|
||||
key, value = line.split(' = ', 1)
|
||||
flags[key] = shlex.split(value)
|
||||
elif line.startswith('# Custom flags: ') and '_FLAGS = ' in line:
|
||||
key, value = line.removeprefix('# Custom flags: ').split('_FLAGS = ', 1)
|
||||
custom[Path(key).name.removesuffix('.o')] = shlex.split(value)
|
||||
|
||||
# The saved CMake flags name the clang resource directory of the compiler that
|
||||
# configured the tree. The system compiler has since moved (22 -> 23), which
|
||||
# makes <stdbool.h> disappear: follow the installed clang instead.
|
||||
resource_dir = subprocess.check_output(['clang', '-print-resource-dir'], text=True).strip()
|
||||
|
||||
|
||||
def current_clang(values):
|
||||
return [re.sub(r'/usr/lib/clang/\d+/include', resource_dir + '/include', value) for value in values]
|
||||
|
||||
|
||||
flags = {key: current_clang(values) for key, values in flags.items()}
|
||||
custom = {key: current_clang(values) for key, values in custom.items()}
|
||||
|
||||
# The configured link line compiles Foundation without ARC and without
|
||||
# -DNDEBUG; keep every define/include/flag exactly as in flags.make and only
|
||||
# append the optimization flags.
|
||||
extras = ['-O2', '-fno-strict-aliasing']
|
||||
|
||||
link = shlex.split((folder / 'CMakeFiles/Foundation_x86_64.dir/link.txt').read_text())
|
||||
objects = []
|
||||
for index, arg in enumerate(link):
|
||||
if arg.startswith('CMakeFiles/Foundation_obj.dir/') and arg.endswith('.o'):
|
||||
relative = arg.split('CMakeFiles/Foundation_obj.dir/', 1)[1][:-2]
|
||||
name = Path(relative).name
|
||||
obj = root / ('build/riot-foundation-' + name + '.o')
|
||||
objects.append((relative, obj, custom.get(name, [])))
|
||||
link[index] = str(obj)
|
||||
assert objects, 'Configured Foundation objects missing'
|
||||
|
||||
|
||||
def compile_object(item):
|
||||
relative, obj, extra_flags = item
|
||||
kind = 'ASM' if relative.endswith('.S') else 'C'
|
||||
subprocess.run(['clang', *flags[kind + '_DEFINES'], *flags[kind + '_INCLUDES'],
|
||||
*flags[kind + '_FLAGS'], *extras, *extra_flags, '-c',
|
||||
str(source / relative), '-o', str(obj)], check=True)
|
||||
|
||||
|
||||
with ThreadPoolExecutor(max_workers=6) as pool:
|
||||
list(pool.map(compile_object, objects))
|
||||
|
||||
link[link.index('-o') + 1] = str(root / 'build/riot-Foundation')
|
||||
subprocess.run(link, cwd=folder, check=True)
|
||||
print('Built', root / 'build/riot-Foundation', 'from', len(objects), 'objects')
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Build the host-only check against Darling's actual madvise implementation."""
|
||||
"""Build and run the host-only check against Darling's actual madvise translation."""
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
|
||||
@@ -14,6 +14,9 @@ if alias.resolve() != headers.resolve():
|
||||
raise RuntimeError("Unexpected host-test header alias")
|
||||
source = project / "build/darling-src/src/external/xnu/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/mman/madvise.c"
|
||||
binary = folder / "host-madvise-check"
|
||||
subprocess.run(["cc", "-std=c11", "-O2", "-I", str(folder / "include"),
|
||||
subprocess.run(["cc", "-std=gnu11", "-O2", "-I", str(folder / "include"),
|
||||
str(project / "tools/riot-host-madvise-check.c"), str(source), "-o", str(binary)], check=True)
|
||||
print("Built", binary)
|
||||
# One process per host THP policy; the code reads the policy once per process.
|
||||
for mode in ("always", "never", "madvise"):
|
||||
subprocess.run([str(binary), mode], check=True)
|
||||
@@ -0,0 +1,117 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Build and run the host-only CPU-topology sysctl check.
|
||||
|
||||
Compiles Darling's actual sysctl_hw.c and sysctl_unspec.c against host shims
|
||||
(host /sys reads, host syscalls, sentinel Mach replies). Nothing but the
|
||||
check's own temporary flag file is written; no prefix, guest or process is
|
||||
touched.
|
||||
"""
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
|
||||
project = Path(__file__).resolve().parent.parent
|
||||
folder = project / "build/logs/sysctl-cpu"
|
||||
emulation = project / "build/darling-src/src/external/xnu/darling/src/libsystem_kernel/emulation"
|
||||
headers = emulation / "include"
|
||||
alias = folder / "include/darling/emulation"
|
||||
alias.parent.mkdir(parents=True, exist_ok=True)
|
||||
if not alias.exists():
|
||||
alias.symlink_to(headers, target_is_directory=True)
|
||||
if alias.resolve() != headers.resolve():
|
||||
raise RuntimeError("Unexpected host-test header alias")
|
||||
|
||||
# Minimal host shims for the guest-only headers the sources include.
|
||||
(folder / "include/sys").mkdir(parents=True, exist_ok=True)
|
||||
(folder / "include/mach").mkdir(parents=True, exist_ok=True)
|
||||
(folder / "include/sys/sysctl.h").write_text("""\
|
||||
/* Host shim: the parts of the guest <sys/sysctl.h> the checked sources use. */
|
||||
#ifndef HOST_SHIM_SYS_SYSCTL_H
|
||||
#define HOST_SHIM_SYS_SYSCTL_H
|
||||
#include <stddef.h>
|
||||
#define CTLTYPE_NODE 1
|
||||
#define CTLTYPE_INT 2
|
||||
#define CTLTYPE_STRING 3
|
||||
#define CTLTYPE_QUAD 4
|
||||
#define CTLTYPE_OPAQUE 5
|
||||
#define CTLTYPE_STRUCT 6
|
||||
#define CTL_HW 6
|
||||
#define HW_MACHINE 1
|
||||
#define HW_MODEL 2
|
||||
#define HW_NCPU 3
|
||||
#define HW_PAGESIZE 7
|
||||
#define HW_MEMSIZE 24
|
||||
#define HW_AVAILCPU 25
|
||||
#endif
|
||||
""")
|
||||
(folder / "include/mach/machine.h").write_text("""\
|
||||
/* Host shim: CPU id types for the checked sources. */
|
||||
#ifndef HOST_SHIM_MACH_MACHINE_H
|
||||
#define HOST_SHIM_MACH_MACHINE_H
|
||||
typedef int cpu_type_t;
|
||||
typedef int cpu_subtype_t;
|
||||
typedef int cpu_threadtype_t;
|
||||
#endif
|
||||
""")
|
||||
(folder / "include/mach/mach_init.h").write_text("""\
|
||||
/* Host shim: Mach init types and calls for the checked sources. */
|
||||
#ifndef HOST_SHIM_MACH_MACH_INIT_H
|
||||
#define HOST_SHIM_MACH_MACH_INIT_H
|
||||
#include <stdint.h>
|
||||
typedef int kern_return_t;
|
||||
typedef unsigned int mach_port_t;
|
||||
typedef unsigned int mach_port_name_t;
|
||||
typedef mach_port_t ipc_space_t;
|
||||
mach_port_t mach_host_self(void);
|
||||
mach_port_t mach_task_self(void);
|
||||
#endif
|
||||
""")
|
||||
(folder / "include/mach/host_info.h").write_text("""\
|
||||
/* Host shim: HOST_BASIC_INFO as the checked source reads it. */
|
||||
#ifndef HOST_SHIM_MACH_HOST_INFO_H
|
||||
#define HOST_SHIM_MACH_HOST_INFO_H
|
||||
#include <stdint.h>
|
||||
#include <mach/machine.h>
|
||||
typedef int integer_t;
|
||||
typedef unsigned int mach_msg_type_number_t;
|
||||
typedef integer_t* host_info_t;
|
||||
typedef int host_flavor_t;
|
||||
#define HOST_BASIC_INFO 1
|
||||
#define HOST_BASIC_INFO_COUNT 8
|
||||
struct host_basic_info {
|
||||
integer_t avail_cpus;
|
||||
integer_t max_cpus;
|
||||
cpu_type_t cpu_type;
|
||||
cpu_subtype_t cpu_subtype;
|
||||
cpu_threadtype_t cpu_threadtype;
|
||||
integer_t physical_cpu;
|
||||
integer_t physical_cpu_max;
|
||||
integer_t logical_cpu;
|
||||
integer_t logical_cpu_max;
|
||||
uint64_t max_mem;
|
||||
};
|
||||
#endif
|
||||
""")
|
||||
|
||||
flag = "/tmp/rclol-fixed-cpu-count.host-check"
|
||||
binary = folder / "host-sysctl-cpu-check"
|
||||
sources = [
|
||||
project / "tools/riot-host-sysctl-cpu-check.c",
|
||||
emulation / "src/xnu_syscall/bsd/helper/misc/sysctl_hw.c",
|
||||
emulation / "src/xnu_syscall/bsd/helper/misc/sysctl_unspec.c",
|
||||
]
|
||||
subprocess.run(["cc", "-std=gnu11", "-O2", "-Wall", "-Wno-unused-function",
|
||||
"-I", str(folder / "include"),
|
||||
'-DDARLING_CPU_FIXED_FLAG="' + flag + '"',
|
||||
*[str(source) for source in sources], "-o", str(binary)], check=True)
|
||||
print("Built", binary)
|
||||
|
||||
# Each mode is its own process: the topology probe caches once per process.
|
||||
subprocess.run([str(binary)], check=True)
|
||||
subprocess.run([str(binary), "affinity"], check=True)
|
||||
subprocess.run([str(binary), "no-detect"], check=True)
|
||||
Path(flag).unlink(missing_ok=True)
|
||||
Path(flag).write_text("")
|
||||
try:
|
||||
subprocess.run([str(binary), "optout"], check=True)
|
||||
finally:
|
||||
Path(flag).unlink(missing_ok=True)
|
||||
@@ -0,0 +1,25 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Build the host-only vDSO clock check against Darling's actual clock source."""
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
|
||||
project = Path(__file__).resolve().parent.parent
|
||||
folder = project / "build/logs/vdso-clock"
|
||||
headers = project / "build/darling-src/src/external/xnu/darling/src/libsystem_kernel/emulation/include"
|
||||
sdk = project / "build/darling-src/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include"
|
||||
alias = folder / "include/darling/emulation"
|
||||
alias.parent.mkdir(parents=True, exist_ok=True)
|
||||
if not alias.exists():
|
||||
alias.symlink_to(headers, target_is_directory=True)
|
||||
if alias.resolve() != headers.resolve():
|
||||
raise RuntimeError("Unexpected host-test header alias")
|
||||
# The real source includes <elfcalls.h>; copy just that one SDK header instead of
|
||||
# adding the whole macOS SDK to the host include path.
|
||||
bridge = folder / "include/elfcalls.h"
|
||||
bridge.write_bytes((sdk / "elfcalls.h").read_bytes())
|
||||
source = project / "build/darling-src/src/external/xnu/darling/src/libsystem_kernel/emulation/src/linux_premigration/ext/clock_gettime.c"
|
||||
binary = folder / "host-vdso-clock-check"
|
||||
subprocess.run(["cc", "-std=gnu11", "-O2", "-pthread", "-I", str(folder / "include"),
|
||||
str(project / "tools/riot-host-vdso-clock-check.c"), str(source),
|
||||
"-ldl", "-o", str(binary)], check=True)
|
||||
print("Built", binary)
|
||||
@@ -29,31 +29,56 @@ 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.
|
||||
# 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
|
||||
# repository; the sources are old BSD/xnu C.
|
||||
repair_flags={**emulation_flags,'C_FLAGS':[*emulation_flags['C_FLAGS'],'-O2','-fno-strict-aliasing']}
|
||||
syscall_repair_flags={**syscall_flags,'C_FLAGS':[*syscall_flags['C_FLAGS'],'-O2','-fno-strict-aliasing']}
|
||||
repairs=[(source,kqueue_flags,project/'build/darling-src/src/external/libkqueue') for source in kqueue_sources]
|
||||
repairs += [('src/xnu_syscall/bsd/bsd_syscall_table.c',emulation_flags,project/'build/darling-src/src/external/xnu/darling/src/libsystem_kernel/emulation')]
|
||||
repairs += [('src/common/base.c',emulation_flags,project/'build/darling-src/src/external/xnu/darling/src/libsystem_kernel/emulation')]
|
||||
repairs += [('src/xnu_syscall/bsd/bsd_syscall_table.c',repair_flags,project/'build/darling-src/src/external/xnu/darling/src/libsystem_kernel/emulation')]
|
||||
repairs += [('src/common/base.c',repair_flags,project/'build/darling-src/src/external/xnu/darling/src/libsystem_kernel/emulation')]
|
||||
# Avoid a redundant absolute-time read when the commpage timestamp is unpublished.
|
||||
repairs += [('wrappers/__commpage_gettimeofday.c',syscall_flags,project/'build/darling-src/src/external/xnu/libsyscall')]
|
||||
repairs += [('src/xnu_syscall/bsd/impl/'+name,emulation_flags,
|
||||
repairs += [('wrappers/__commpage_gettimeofday.c',syscall_repair_flags,project/'build/darling-src/src/external/xnu/libsyscall')]
|
||||
repairs += [('src/xnu_syscall/bsd/impl/'+name,repair_flags,
|
||||
project/'build/darling-src/src/external/xnu/darling/src/libsystem_kernel/emulation')
|
||||
for name in ('bsdthread/bsdthread_register.c','bsdthread/pthread_canceled.c','kqueue/kevent_qos.c','bsdthread/workq_kernreturn.c','psynch/psynch_cvwait.c','psynch/psynch_mutexwait.c','psynch/psynch_mutexdrop.c','psynch/ulock_wait.c',
|
||||
# select busy-loop trace (opt-in) and the recvfrom flag conversion
|
||||
'select/select.c','network/recvfrom.c','network/recvmsg.c','unistd/read.c','unistd/close.c','process/fork.c')]
|
||||
for name in ('bsdthread/pthread_canceled.c','kqueue/kevent_qos.c','bsdthread/workq_kernreturn.c','psynch/psynch_cvwait.c','psynch/psynch_mutexwait.c','psynch/psynch_mutexdrop.c','psynch/ulock_wait.c',
|
||||
# the remaining psynch/ulock traps stay RPCs but are still per-wait
|
||||
'psynch/psynch_cvbroad.c','psynch/psynch_cvclrprepost.c','psynch/psynch_cvsignal.c',
|
||||
'psynch/psynch_rw_rdlock.c','psynch/psynch_rw_unlock.c','psynch/psynch_rw_wrlock.c','psynch/ulock_wake.c',
|
||||
# select busy-loop trace (opt-in) and the network flag conversions
|
||||
'select/select.c','network/recvfrom.c','network/recvmsg.c','network/sendto.c','network/sendmsg.c','unistd/read.c','unistd/close.c','process/fork.c',
|
||||
# the allocator's reclamation hint
|
||||
'mman/madvise.c')]
|
||||
# RPC guards use the full native signal mask. Rebuild both users of the
|
||||
# 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']}
|
||||
# The fault reporter also writes /tmp/lolsig.log (return addresses and module map)
|
||||
# bsdthread_register.c keeps emulation_flags (-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)]
|
||||
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)]
|
||||
repairs += [('src/xnu_syscall/bsd/impl/time/gettimeofday.c',rpc_flags,emulation_root)]
|
||||
repairs += [('src/other/mach/lkm.c',rpc_flags,emulation_root)]
|
||||
# sysctl_kern.c takes the emulated OS identity from per-source definitions (emulation/CMakeLists.txt)
|
||||
identity=['-DEMULATED_SYSNAME="Darwin"','-DEMULATED_RELEASE="23.6.0"',
|
||||
'-DEMULATED_VERSION="Darwin Kernel Version 23.6.0: Mon Jul 29 21:14:30 PDT 2024; root:xnu-10063.141.2~1/RELEASE_X86_64"',
|
||||
'-DEMULATED_OSVERSION="23H124"','-DEMULATED_OSPRODUCTVERSION="14.7"']
|
||||
repairs += [('src/xnu_syscall/bsd/helper/misc/sysctl_kern.c',{**emulation_flags,'C_FLAGS':[*emulation_flags['C_FLAGS'],*identity]},emulation_root)]
|
||||
# The hw.*/machdep.cpu.* sysctls are not on the per-call path: recompile them
|
||||
# with the configured flags only because these sources changed (the same way
|
||||
# bsdthread_register.c keeps -O0). sysctl_hw.c now reports the host's real CPU
|
||||
# topology; sysctl_machdep.c shares its probe for core_count/thread_count.
|
||||
repairs += [('src/xnu_syscall/bsd/helper/misc/sysctl_hw.c',emulation_flags,emulation_root)]
|
||||
repairs += [('src/xnu_syscall/bsd/helper/misc/sysctl_machdep.c',emulation_flags,emulation_root)]
|
||||
repairs += [('src/linux_premigration/resources/dserver-rpc-defs.c',rpc_flags,emulation_root)]
|
||||
forced_rpc_flags={**rpc_flags,'C_FLAGS':[*rpc_flags['C_FLAGS'],'-include',str(emulation_root/'include/linux_premigration/resources/dserver-rpc-defs.h')]}
|
||||
repairs += [('src/rpc.c',forced_rpc_flags,build/'src/external/darlingserver')]
|
||||
|
||||
@@ -0,0 +1,167 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Rebuild libsystem_c (libc) with -O2.
|
||||
|
||||
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
|
||||
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
|
||||
link line. -fno-strict-aliasing matches what the existing optimized rebuilds
|
||||
use for this ten-year-old BSD C.
|
||||
|
||||
Files whose correctness depends on frame layout or on running single-threaded
|
||||
in a forked child keep their configured flags and are listed in EXCLUDED below.
|
||||
No -DNDEBUG, no -ffast-math, no LTO: only the optimization level changes.
|
||||
Output: build/riot-libsystem_c.dylib.
|
||||
"""
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from pathlib import Path
|
||||
import re
|
||||
import shlex
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||
import riot_build_gate
|
||||
|
||||
root = Path(__file__).resolve().parent.parent
|
||||
folder = root / "build/darling-src/build/src/external/libc"
|
||||
source_root = root / "build/darling-src/src/external/libc"
|
||||
resource_dir = subprocess.check_output(["clang", "-print-resource-dir"], text=True).strip()
|
||||
|
||||
# Kept at their configured flags:
|
||||
EXCLUDED = {
|
||||
# fork/vfork paths run in a forked child before anything else is safe
|
||||
"sys/fork.c",
|
||||
"sys/_libc_fork_child.c",
|
||||
# the stack guard handler must not rely on the frame it reports
|
||||
"sys/OpenBSD/stack_protector.c",
|
||||
# walks frame pointers to build a backtrace
|
||||
"gen/thread_stack_pcs.c",
|
||||
# __builtin_return_address in a diagnostic; nothing to gain here
|
||||
"os/assumes.c",
|
||||
}
|
||||
|
||||
|
||||
def current_clang(values):
|
||||
return [re.sub(r"/usr/lib/clang/\d+/include", resource_dir + "/include", value) for value in values]
|
||||
|
||||
|
||||
def variables_of(target_folder):
|
||||
variables = {}
|
||||
for line in (target_folder / "flags.make").read_text().splitlines():
|
||||
if line.startswith("C_") and " = " in line:
|
||||
key, value = line.split(" = ", 1)
|
||||
variables["$(" + key + ")"] = current_clang(shlex.split(value))
|
||||
return variables
|
||||
|
||||
|
||||
def split_entry(entry):
|
||||
"""'db/CMakeFiles/libc-db.dir/btree/bt_close.c.o' -> (head, target, relative)."""
|
||||
head, tail = entry.split("CMakeFiles/", 1)
|
||||
target, relative = tail.split(".dir/", 1)
|
||||
return head, target, relative[:-2]
|
||||
|
||||
|
||||
def source_of(head, relative):
|
||||
path = source_root / (head + relative)
|
||||
if not path.is_file():
|
||||
# CMake writes an out-of-directory source as a "__" component
|
||||
path = (source_root / (head + relative).replace("__", "..")).resolve()
|
||||
return path
|
||||
|
||||
|
||||
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."""
|
||||
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
|
||||
command = shlex.split(line.split("&&", 1)[1])
|
||||
expanded = []
|
||||
for token in command:
|
||||
if token in variables_of(target_folder):
|
||||
expanded += variables_of(target_folder)[token]
|
||||
else:
|
||||
expanded.append(token)
|
||||
cleaned, skip = [], False
|
||||
for token in expanded:
|
||||
if skip:
|
||||
skip = False
|
||||
continue
|
||||
if token in ("-MD", "-MT", "-MF"):
|
||||
skip = token != "-MD"
|
||||
continue
|
||||
if token == "-o":
|
||||
cleaned.append(token)
|
||||
cleaned.append(object_path)
|
||||
skip = True
|
||||
continue
|
||||
cleaned.append(token)
|
||||
if "-O2" not in cleaned:
|
||||
cleaned.append("-O2")
|
||||
cleaned.append("-fno-strict-aliasing")
|
||||
return cleaned
|
||||
return None
|
||||
|
||||
|
||||
def build():
|
||||
link = shlex.split((folder / "CMakeFiles/system_c.dir/link.txt").read_text())
|
||||
rsp_index = next(index for index, argument in enumerate(link) if argument.startswith("@"))
|
||||
# The response file is a whitespace-separated list; CMake leaves the
|
||||
# objects of other directories unquoted, so split like the linker does.
|
||||
entries = shlex.split((folder / link[rsp_index][1:]).read_text())
|
||||
jobs, objects = [], []
|
||||
for entry in entries:
|
||||
head, target, relative = split_entry(entry)
|
||||
source = source_of(head, relative)
|
||||
target_folder = folder / head / "CMakeFiles" / (target + ".dir")
|
||||
try:
|
||||
source_key = str(source.relative_to(source_root))
|
||||
except ValueError:
|
||||
source_key = relative
|
||||
if "libkqueue" in head:
|
||||
# These objects belong to the libkqueue target that the kernel
|
||||
# library also links; the kernel component owns them, so they keep
|
||||
# the configured flags here.
|
||||
objects.append(entry)
|
||||
continue
|
||||
if source_key in EXCLUDED or source.suffix != ".c" or not source.is_file():
|
||||
objects.append(entry)
|
||||
continue
|
||||
obj = root / ("build/riot-libc-%s.o" % (head + target + "-" + relative).replace("/", "_"))
|
||||
command = compile_command(target_folder, source, str(obj))
|
||||
if command is None:
|
||||
print("no compile command for", entry, "- keeping the configured object")
|
||||
objects.append(entry)
|
||||
continue
|
||||
jobs.append((target_folder, command, obj))
|
||||
objects.append(str(obj))
|
||||
print("recompiling", len(jobs), "of", len(entries), "objects at -O2", flush=True)
|
||||
|
||||
def compile_one(job):
|
||||
working_directory, command, obj = job
|
||||
result = subprocess.run(command, cwd=working_directory, capture_output=True, text=True)
|
||||
if result.returncode != 0:
|
||||
print("failed:", " ".join(command[-6:]))
|
||||
print(result.stderr[-2000:])
|
||||
raise SystemExit(1)
|
||||
if not obj.is_file():
|
||||
raise SystemExit("no object produced for " + str(obj))
|
||||
|
||||
riot_build_gate.wait_until_free()
|
||||
with ThreadPoolExecutor(max_workers=6) as pool:
|
||||
list(pool.map(compile_one, jobs))
|
||||
|
||||
rsp = root / "build/riot-libc-objects.rsp"
|
||||
rsp.write_text(" ".join('"%s"' % object for object in objects) + "\n")
|
||||
link[rsp_index] = "@" + str(rsp)
|
||||
link[link.index("-o") + 1] = str(root / "build/riot-libsystem_c.dylib")
|
||||
subprocess.run(link, cwd=folder, check=True)
|
||||
print("built build/riot-libsystem_c.dylib with", len(jobs), "objects at -O2", flush=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
build()
|
||||
Executable
+15
@@ -0,0 +1,15 @@
|
||||
#!/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
|
||||
# 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
|
||||
# measuring. Install the artifacts with
|
||||
# python3 build/runtime/tools/install-riot-libs-runtime.py.
|
||||
set -eu
|
||||
project_dir=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
|
||||
cd "$project_dir"
|
||||
python3 tools/build-riot-libc-runtime.py
|
||||
python3 tools/build-riot-cxx-runtime.py
|
||||
python3 tools/build-riot-platform-strings.py
|
||||
@@ -0,0 +1,83 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Build and run the host-only checks for the libplatform string primitives.
|
||||
|
||||
Two copies of the same libplatform sources are compiled with host clang:
|
||||
|
||||
current - the flags the installed libsystem_platform.dylib was built with:
|
||||
memmove.c, bzero.c and memset_pattern.c at -O2 -fno-builtin, every
|
||||
other string source at the library default (-O0);
|
||||
new - the flags tools/build-riot-platform-strings.py uses for the
|
||||
overlay: -O2 -fno-builtin -fno-strict-aliasing for every string
|
||||
source.
|
||||
|
||||
The check binary links both copies and compares them against a byte-loop
|
||||
reference with guard pages on both sides of every buffer, for the generic path
|
||||
and for the vector and REP paths; the bench binary measures both copies and the
|
||||
host's glibc. Nothing here touches a Darling process, prefix or guest file.
|
||||
"""
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
PLATFORM = ROOT / "build/darling-src/src/external/libplatform"
|
||||
# configure.py renders the project root; the rendered copy runs from
|
||||
# build/runtime/tools/, where the shim directory does not exist.
|
||||
SHIM_TOKEN = "@PROJECT_ROOT@/tools/riot-platform-memory-shim"
|
||||
SHIM = ROOT / "tools/riot-platform-memory-shim" if SHIM_TOKEN.startswith("@") else Path(SHIM_TOKEN)
|
||||
SOURCES = ["memmove.c", "bzero.c", "memset_pattern.c", "memcmp.c", "memchr.c",
|
||||
"strlen.c", "strcmp.c"]
|
||||
# The three files the configured build already compiles optimized.
|
||||
OPTIMIZED_IN_CURRENT = {"memmove.c", "bzero.c", "memset_pattern.c"}
|
||||
RENAMES = ["_platform_memmove", "_platform_memmove_avx2", "_platform_memset",
|
||||
"_platform_memset_avx2", "_platform_bzero", "_platform_memset_pattern4",
|
||||
"_platform_memset_pattern8", "_platform_memset_pattern16",
|
||||
"_platform_memcmp", "_platform_memcmp_avx2", "_platform_memchr",
|
||||
"_platform_memchr_avx2", "_platform_strlen", "_platform_strcmp", "memset"]
|
||||
|
||||
|
||||
def flags(variant, source):
|
||||
common = ["-w", "-fno-builtin", "-DLONG_BIT=64", "-include", str(SHIM / "force.h"),
|
||||
"-I", str(SHIM), "-I", str(PLATFORM / "private"),
|
||||
"-I", str(PLATFORM / "include"), "-I", str(PLATFORM / "internal")]
|
||||
common += ["-D%s=%s" % (name, variant + (name if name.startswith("_") else "_" + name))
|
||||
for name in RENAMES]
|
||||
if variant == "current":
|
||||
optimization = ["-O2"] if source in OPTIMIZED_IN_CURRENT else ["-O0"]
|
||||
else:
|
||||
optimization = ["-O2", "-fno-strict-aliasing"]
|
||||
return common + optimization
|
||||
|
||||
|
||||
def objects(folder, variant, extra):
|
||||
built = []
|
||||
for source in SOURCES:
|
||||
obj = folder / ("%s-%s.o" % (variant, source.replace("/", "_")))
|
||||
subprocess.run(["clang", *flags(variant, source), *extra,
|
||||
"-c", str(PLATFORM / "src/string/generic" / source), "-o", str(obj)],
|
||||
check=True)
|
||||
built.append(str(obj))
|
||||
return built
|
||||
|
||||
|
||||
def build(program, source, extra=(), run=()):
|
||||
folder = ROOT / "build"
|
||||
binary = folder / program
|
||||
subprocess.run(["clang", "-O2", "-w", *extra, str(ROOT / "tools" / source),
|
||||
*objects(folder, "current", extra), *objects(folder, "new", extra),
|
||||
"-o", str(binary)], check=True)
|
||||
print("Built", binary)
|
||||
for arguments in run:
|
||||
subprocess.run([str(binary), *arguments], check=True)
|
||||
|
||||
|
||||
def main():
|
||||
build("riot-platform-memory-check", "riot-platform-memory-check.c",
|
||||
run=[("--no-avx2",), ("--avx2",), ("--avx2-erms",)])
|
||||
build("riot-platform-memory-check-asan", "riot-platform-memory-check.c",
|
||||
extra=["-fsanitize=address,undefined", "-fno-sanitize-recover=all"],
|
||||
run=[("--no-avx2",), ("--avx2-erms",)])
|
||||
build("riot-platform-memory-bench", "riot-platform-memory-bench.c", run=[()])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,7 +1,17 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Optimize configured string helpers, preserving every other platform object."""
|
||||
"""Optimize configured string helpers, preserving every other platform object.
|
||||
|
||||
Every src/string/generic source is recompiled at -O2 -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
|
||||
sources and are selected from the commpage at run time, so this builder only
|
||||
has to change the optimization level.
|
||||
"""
|
||||
from pathlib import Path
|
||||
import shlex,subprocess,shutil
|
||||
import shlex,subprocess,shutil,sys
|
||||
sys.path.insert(0,str(Path(__file__).resolve().parent))
|
||||
import riot_build_gate
|
||||
root=Path(__file__).resolve().parent.parent
|
||||
folder=root/'build/darling-src/build/src/external/libplatform'
|
||||
flags={}
|
||||
@@ -11,12 +21,16 @@ for line in (folder/'CMakeFiles/platform_obj.dir/flags.make').read_text().splitl
|
||||
inc=subprocess.check_output(['clang','-print-resource-dir'],text=True).strip()+'/include'
|
||||
flags['C_INCLUDES']=[inc if x.startswith('/usr/lib/clang/') and x.endswith('/include') else x for x in flags['C_INCLUDES']]
|
||||
link=shlex.split((folder/'CMakeFiles/platform.dir/link.txt').read_text())
|
||||
recompiled=0
|
||||
riot_build_gate.wait_until_free()
|
||||
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)
|
||||
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')
|
||||
p=root/'build/m810-platform-runtime';p.mkdir(exist_ok=True);shutil.copy2(root/'build/riot-libsystem_platform.dylib',p/'libsystem_platform.dylib')
|
||||
@@ -9,7 +9,7 @@ for line in (build/'CMakeFiles/darlingserver.dir/flags.make').read_text().splitl
|
||||
if line.startswith('CXX_') and ' = ' in line:
|
||||
k,v=line.split(' = ',1);flags[k]=shlex.split(v)
|
||||
link=shlex.split((build/'CMakeFiles/darlingserver.dir/link.txt').read_text())
|
||||
for name in ['call','message','darlingserver','process']:
|
||||
for name in ['call','message','darlingserver','process','server']:
|
||||
obj=project/f'build/riot-server-{name}.o'
|
||||
subprocess.run(['clang++',*flags['CXX_DEFINES'],*flags['CXX_INCLUDES'],*flags['CXX_FLAGS'],'-c',str(project/f'build/darling-src/src/external/darlingserver/src/{name}.cpp'),'-o',str(obj)],check=True)
|
||||
link[link.index(f'CMakeFiles/darlingserver.dir/src/{name}.cpp.o')]=str(obj)
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Install the optimized Foundation overlay atomically, without restarting clients.
|
||||
|
||||
The guest resolves frameworks through DYLD_FRAMEWORK_PATH=/Users/<user>/RuntimeFrameworks,
|
||||
so Foundation is added there the same way CoreFoundation/AppKit already are:
|
||||
copy the SDK framework once, then replace Versions/C/Foundation with the built
|
||||
binary. The kernel library from the same batch is installed by
|
||||
tools/install-riot-memory-runtime.py (its default source is the rebuilt
|
||||
build/riot-libsystem_kernel.dylib); this installer does not touch it.
|
||||
"""
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import stat
|
||||
import tempfile
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
DEFAULT_ARTIFACT = ROOT / "build/riot-Foundation"
|
||||
|
||||
|
||||
def digest(path):
|
||||
with path.open("rb") as input:
|
||||
return hashlib.file_digest(input, "sha256").hexdigest()
|
||||
|
||||
|
||||
def copy_framework(sdk_framework, framework):
|
||||
"""Create the overlay framework from the SDK layout (Versions/C + links)."""
|
||||
framework.parent.mkdir(parents=True, exist_ok=True)
|
||||
staged = framework.parent / (framework.name + ".staged")
|
||||
if staged.exists():
|
||||
shutil.rmtree(staged)
|
||||
shutil.copytree(sdk_framework, staged, symlinks=True)
|
||||
os.rename(staged, framework)
|
||||
|
||||
|
||||
def install(artifact, sdk_framework, framework, backup_folder):
|
||||
with artifact.open("rb") as input:
|
||||
if input.read(4) != b"\xcf\xfa\xed\xfe":
|
||||
raise ValueError("Expected the tested 64-bit Mach-O Foundation library")
|
||||
expected = digest(artifact)
|
||||
records = []
|
||||
|
||||
if not framework.exists():
|
||||
# No overlay yet: seed it from the SDK, then replace the binary.
|
||||
copy_framework(sdk_framework, framework)
|
||||
records.append({"path": str(framework), "created_from": str(sdk_framework)})
|
||||
|
||||
binary = framework / "Versions/C/Foundation"
|
||||
if binary.is_symlink() or not binary.is_file():
|
||||
raise ValueError("Expected a regular installed Foundation binary: " + str(binary))
|
||||
|
||||
metadata = binary.stat()
|
||||
old_hash = digest(binary)
|
||||
if old_hash == expected:
|
||||
records.append({"path": str(binary), "already_current": True})
|
||||
return {"installed": True, "sha256": expected, "libraries": records,
|
||||
"restart_performed": False,
|
||||
"activation": "Exit the game and clients, then restart Darling; an active overlay may still expose the old library."}
|
||||
|
||||
backup_folder.mkdir(parents=True, exist_ok=True)
|
||||
backup = backup_folder / ("foundation-" + old_hash)
|
||||
if backup.exists():
|
||||
if digest(backup) != old_hash:
|
||||
raise ValueError("The existing backup has unexpected contents")
|
||||
else:
|
||||
shutil.copy2(binary, backup)
|
||||
|
||||
fd, temporary = tempfile.mkstemp(prefix=".Foundation-staged-", dir=binary.parent)
|
||||
staged = Path(temporary)
|
||||
try:
|
||||
with os.fdopen(fd, "wb") as output, artifact.open("rb") as input:
|
||||
shutil.copyfileobj(input, output)
|
||||
output.flush()
|
||||
os.fchmod(output.fileno(), stat.S_IMODE(metadata.st_mode))
|
||||
os.fchown(output.fileno(), metadata.st_uid, metadata.st_gid)
|
||||
os.fsync(output.fileno())
|
||||
if digest(staged) != expected:
|
||||
raise ValueError("The staged library does not match the build")
|
||||
os.replace(staged, binary)
|
||||
directory = os.open(binary.parent, os.O_DIRECTORY)
|
||||
try:
|
||||
os.fsync(directory)
|
||||
finally:
|
||||
os.close(directory)
|
||||
finally:
|
||||
if staged.exists():
|
||||
staged.unlink()
|
||||
|
||||
if digest(binary) != expected:
|
||||
raise ValueError("Installed library verification failed")
|
||||
records.append({"path": str(binary), "backup": str(backup), "previous_sha256": old_hash})
|
||||
return {"installed": True, "sha256": expected, "libraries": records,
|
||||
"restart_performed": False,
|
||||
"activation": "Exit the game and clients, then restart Darling; an active overlay may still expose the old library."}
|
||||
|
||||
|
||||
def parse_arguments(argv=None):
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--binary", type=Path, default=DEFAULT_ARTIFACT)
|
||||
parser.add_argument("--backup-dir", type=Path, default=ROOT / "build/foundation-runtime-backups")
|
||||
return parser, parser.parse_args(argv)
|
||||
|
||||
|
||||
def main():
|
||||
parser, args = parse_arguments()
|
||||
if not args.binary.is_file():
|
||||
parser.exit(1, "The library to install does not exist: " + str(args.binary) + "; build it first: "
|
||||
"python3 build/runtime/tools/build-riot-foundation-runtime.py\n")
|
||||
config = json.loads((ROOT / "build/config.json").read_text())
|
||||
prefix = Path(config["prefix"])
|
||||
guest_home = config["guest_home"].lstrip("/")
|
||||
framework = prefix / guest_home / "RuntimeFrameworks/Foundation.framework"
|
||||
sdk_framework = Path(config["sdk"]) / "System/Library/Frameworks/Foundation.framework"
|
||||
if not sdk_framework.exists():
|
||||
parser.exit(1, "The SDK framework does not exist: " + str(sdk_framework) + "\n")
|
||||
try:
|
||||
result = install(args.binary, sdk_framework, framework, args.backup_dir)
|
||||
except PermissionError as error:
|
||||
parser.exit(1, "Cannot write " + str(error.filename) + "; choose a writable --backup-dir or run the installer with sudo.\n")
|
||||
result["binary"] = str(args.binary)
|
||||
result["framework"] = str(framework)
|
||||
print(json.dumps(result))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,213 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Install the optimized libc, libc++ and libsystem_platform overlays.
|
||||
|
||||
The guest finds a library by leaf name in DYLD_LIBRARY_PATH before its install
|
||||
path (dyld3/PathOverrides.cpp:forEachPathVariant), so a file in the per-user
|
||||
RuntimeLibraries directory replaces the copy in the prefix and in the installed
|
||||
SDK without touching either. The overlays installed here are exactly the
|
||||
artifacts built by tools/build-riot-*.py; each is staged next to its
|
||||
destination, verified against the built file, and renamed over the old one
|
||||
after the old one is backed up. --rollback restores every file this installer
|
||||
replaced and removes the ones it created.
|
||||
|
||||
This installer was checked offline (tools/riot-libs-installer-offline-check.py)
|
||||
and is never run against the real prefix by the build. Nothing is activated
|
||||
until the guest processes are restarted.
|
||||
"""
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import stat
|
||||
import tempfile
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
|
||||
# (built artifact, file name in the guest's RuntimeLibraries directory). The
|
||||
# artifacts are the ones tools/build-riot-libs-runtime.sh builds.
|
||||
# libsystem_m is deliberately absent: its C sources are built at -O0 upstream and an
|
||||
# -O2 rebuild made the Riot client UI render a black window (2026-10-13).
|
||||
LIBRARIES = [
|
||||
("build/riot-libsystem_c.dylib", "libsystem_c.dylib"),
|
||||
("build/riot-libc++.1.dylib", "libc++.1.dylib"),
|
||||
("build/riot-libc++abi.dylib", "libc++abi.dylib"),
|
||||
("build/riot-libsystem_platform.dylib", "libsystem_platform.dylib"),
|
||||
]
|
||||
|
||||
|
||||
def digest(path):
|
||||
with path.open("rb") as input:
|
||||
return hashlib.file_digest(input, "sha256").hexdigest()
|
||||
|
||||
|
||||
def destinations(prefix, guest_home):
|
||||
folder = Path(prefix) / guest_home.lstrip("/") / "RuntimeLibraries"
|
||||
return [(ROOT / artifact, folder / name) for artifact, name in LIBRARIES]
|
||||
|
||||
|
||||
def existing_records(backup_folder):
|
||||
"""What the previous installation recorded, so a second run keeps the
|
||||
state from before the first one instead of overwriting it."""
|
||||
manifest = backup_folder / "manifest.json"
|
||||
if not manifest.is_file():
|
||||
return {}
|
||||
return {record["path"]: record for record in json.loads(manifest.read_text())["libraries"]}
|
||||
|
||||
|
||||
def install(libraries, backup_folder):
|
||||
previous_records = existing_records(backup_folder)
|
||||
expected = {}
|
||||
for artifact, destination in libraries:
|
||||
if not artifact.is_file():
|
||||
raise ValueError("The artifact does not exist: " + str(artifact))
|
||||
with artifact.open("rb") as input:
|
||||
if input.read(4) not in (b"\xcf\xfa\xed\xfe", b"\xce\xfa\xed\xfe"):
|
||||
raise ValueError("Expected a 64-bit Mach-O library: " + str(artifact))
|
||||
expected[destination] = digest(artifact)
|
||||
records, staged = [], []
|
||||
try:
|
||||
for index, (artifact, destination) in enumerate(libraries):
|
||||
earlier = previous_records.get(str(destination))
|
||||
current = digest(destination) if destination.is_file() else None
|
||||
if current == expected[destination]:
|
||||
# keep the state from before the first installation, if any
|
||||
records.append({"path": str(destination), "already_current": True,
|
||||
"existed": earlier["existed"] if earlier else destination.is_file(),
|
||||
"previous_sha256": earlier["previous_sha256"] if earlier else current,
|
||||
"backup": earlier.get("backup") if earlier else None,
|
||||
"sha256": expected[destination]})
|
||||
continue
|
||||
existed = earlier["existed"] if earlier else destination.is_file()
|
||||
previous = earlier["previous_sha256"] if earlier else current
|
||||
backup = Path(earlier["backup"]) if earlier and earlier.get("backup") else None
|
||||
if backup is None and current is not None:
|
||||
backup_folder.mkdir(parents=True, exist_ok=True)
|
||||
backup = backup_folder / ("%d-%s.dylib" % (index, current))
|
||||
if backup.exists():
|
||||
if digest(backup) != current:
|
||||
raise ValueError("The existing backup has unexpected contents: " + str(backup))
|
||||
else:
|
||||
shutil.copy2(destination, backup)
|
||||
metadata = destination.stat() if destination.is_file() else None
|
||||
descriptor, temporary = tempfile.mkstemp(prefix="." + destination.name + "-staged-",
|
||||
dir=destination.parent)
|
||||
staged.append((Path(temporary), destination))
|
||||
with os.fdopen(descriptor, "wb") as output, artifact.open("rb") as input:
|
||||
shutil.copyfileobj(input, output)
|
||||
output.flush()
|
||||
if metadata is not None:
|
||||
os.fchmod(output.fileno(), stat.S_IMODE(metadata.st_mode))
|
||||
os.fchown(output.fileno(), metadata.st_uid, metadata.st_gid)
|
||||
else:
|
||||
os.fchmod(output.fileno(), 0o755)
|
||||
os.fsync(output.fileno())
|
||||
if digest(Path(temporary)) != expected[destination]:
|
||||
raise ValueError("The staged library does not match the build: " + str(artifact))
|
||||
records.append({"path": str(destination), "backup": str(backup) if backup else None,
|
||||
"existed": existed, "sha256": expected[destination],
|
||||
"previous_sha256": previous})
|
||||
for temporary, destination in staged:
|
||||
os.replace(temporary, destination)
|
||||
directory = os.open(destination.parent, os.O_DIRECTORY)
|
||||
try:
|
||||
os.fsync(directory)
|
||||
finally:
|
||||
os.close(directory)
|
||||
for record in records:
|
||||
path = Path(record["path"])
|
||||
if not path.is_file() or digest(path) != record["sha256"]:
|
||||
raise ValueError("Installed library verification failed: " + str(path))
|
||||
backup_folder.mkdir(parents=True, exist_ok=True)
|
||||
manifest = backup_folder / "manifest.json"
|
||||
manifest.write_text(json.dumps({"libraries": records}, indent=1) + "\n")
|
||||
finally:
|
||||
for temporary, _ in staged:
|
||||
if temporary.exists():
|
||||
temporary.unlink()
|
||||
return {"installed": True, "libraries": records, "restart_performed": False,
|
||||
"manifest": str(backup_folder / "manifest.json"),
|
||||
"activation": "Exit the game and the clients, then start them again; a running guest keeps the library it already loaded."}
|
||||
|
||||
|
||||
def rollback(backup_folder):
|
||||
manifest = backup_folder / "manifest.json"
|
||||
if not manifest.is_file():
|
||||
raise ValueError("No installation manifest in " + str(backup_folder))
|
||||
records = json.loads(manifest.read_text())["libraries"]
|
||||
restored, removed = [], []
|
||||
for record in reversed(records):
|
||||
destination = Path(record["path"])
|
||||
if record.get("previous_sha256") is None:
|
||||
# the installer created this file; removing it restores the SDK copy
|
||||
if destination.is_file() and digest(destination) == record["sha256"]:
|
||||
destination.unlink()
|
||||
removed.append(str(destination))
|
||||
else:
|
||||
backup = Path(record["backup"])
|
||||
if not backup.is_file():
|
||||
raise ValueError("The backup is missing: " + str(backup))
|
||||
if digest(backup) != record["previous_sha256"]:
|
||||
raise ValueError("The backup has unexpected contents: " + str(backup))
|
||||
descriptor, temporary = tempfile.mkstemp(prefix="." + destination.name + "-restore-",
|
||||
dir=destination.parent)
|
||||
with os.fdopen(descriptor, "wb") as output, backup.open("rb") as input:
|
||||
shutil.copyfileobj(input, output)
|
||||
output.flush()
|
||||
os.fchmod(output.fileno(), 0o755)
|
||||
os.fsync(output.fileno())
|
||||
os.replace(temporary, destination)
|
||||
restored.append(str(destination))
|
||||
return {"rolled_back": True, "restored": restored, "removed": removed,
|
||||
"activation": "Exit the game and the clients, then start them again."}
|
||||
|
||||
|
||||
def parse_arguments(argv=None):
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--prefix", type=Path, default=None,
|
||||
help="guest prefix (default: build/config.json)")
|
||||
parser.add_argument("--guest-home", default=None)
|
||||
parser.add_argument("--backup-dir", type=Path, default=ROOT / "build/libs-runtime-backups")
|
||||
parser.add_argument("--only", default=None,
|
||||
help="comma-separated library names to install instead of all of them "
|
||||
"(to isolate a faulty overlay; run --rollback before changing the set)")
|
||||
parser.add_argument("--rollback", action="store_true",
|
||||
help="restore every file this installer replaced and remove the ones it created")
|
||||
return parser, parser.parse_args(argv)
|
||||
|
||||
|
||||
def main(argv=None):
|
||||
parser, arguments = parse_arguments(argv)
|
||||
if arguments.rollback:
|
||||
try:
|
||||
print(json.dumps(rollback(arguments.backup_dir)))
|
||||
except (ValueError, OSError) as error:
|
||||
parser.exit(1, str(error) + "\n")
|
||||
return
|
||||
config = json.loads((ROOT / "build/config.json").read_text())
|
||||
prefix = arguments.prefix or config["prefix"]
|
||||
guest_home = arguments.guest_home or config["guest_home"]
|
||||
libraries = destinations(prefix, guest_home)
|
||||
if arguments.only:
|
||||
wanted = set(arguments.only.split(","))
|
||||
unknown = wanted - {name for _, name in LIBRARIES}
|
||||
if unknown:
|
||||
parser.exit(1, "Unknown library: " + ", ".join(sorted(unknown)) + "\n")
|
||||
libraries = [(artifact, destination) for artifact, destination in libraries
|
||||
if destination.name in wanted]
|
||||
for artifact, _ in libraries:
|
||||
if not artifact.is_file():
|
||||
parser.exit(1, "The library to install does not exist: " + str(artifact)
|
||||
+ "; build it first: sh build/runtime/tools/build-riot-libs-runtime.sh\n")
|
||||
try:
|
||||
result = install(libraries, arguments.backup_dir)
|
||||
except PermissionError as error:
|
||||
parser.exit(1, "Cannot write " + str(error.filename) + "; choose a writable --backup-dir or run "
|
||||
"the installer with sudo.\n")
|
||||
result["prefix"] = str(prefix)
|
||||
print(json.dumps(result))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -11,6 +11,9 @@ import stat
|
||||
import tempfile
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
# The optimized relink from tools/build-riot-kqueue-runtime.py is the library the
|
||||
# client runtime is supposed to run; the plain CMake build (no -O flag) is not.
|
||||
DEFAULT_ARTIFACT = ROOT / "build/riot-libsystem_kernel.dylib"
|
||||
|
||||
|
||||
def digest(path):
|
||||
@@ -73,24 +76,54 @@ def install(binary, destinations, backup_folder):
|
||||
"activation": "Exit the game and clients, then restart Darling; an active overlay may still expose the old library."}
|
||||
|
||||
|
||||
def main():
|
||||
def prefix_destinations(config, binary, artifact):
|
||||
"""Destination list for an installation, and a note about the artifact.
|
||||
|
||||
The prefix copies may be replaced from any tested source, but the prepared
|
||||
artifact is never a destination of a different library: a run that does not
|
||||
install the artifact itself leaves it untouched. That is what keeps the
|
||||
optimized relink from being silently replaced by the plain CMake build.
|
||||
"""
|
||||
destinations = [
|
||||
Path(config["prefix"]) / config["guest_home"].lstrip("/") / "RuntimeLibraries/libsystem_kernel.dylib",
|
||||
# Keep the per-prefix bootstrap copy consistent; leave the system SDK alone.
|
||||
Path(config["prefix"]) / "usr/lib/system/libsystem_kernel.dylib",
|
||||
]
|
||||
if not artifact.exists():
|
||||
return destinations, "the prepared artifact " + str(artifact) + " does not exist yet"
|
||||
if Path(binary).resolve() != artifact.resolve():
|
||||
return destinations, ("refusing to overwrite " + str(artifact)
|
||||
+ " with a different library; rebuild it with "
|
||||
"build/runtime/tools/build-riot-kqueue-runtime.py")
|
||||
destinations.append(artifact)
|
||||
return destinations, None
|
||||
|
||||
|
||||
def parse_arguments(argv=None):
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--binary", type=Path, default=ROOT / "build/darling-src/build/src/external/xnu/darling/src/libsystem_kernel/libsystem_kernel.dylib")
|
||||
parser.add_argument("--binary", type=Path, default=DEFAULT_ARTIFACT)
|
||||
parser.add_argument("--artifact", type=Path, default=DEFAULT_ARTIFACT)
|
||||
# A previous sudo server install can make build/backups root-owned.
|
||||
parser.add_argument("--backup-dir", type=Path, default=ROOT / "build/memory-runtime-backups")
|
||||
args = parser.parse_args()
|
||||
return parser, parser.parse_args(argv)
|
||||
|
||||
|
||||
def main():
|
||||
parser, args = parse_arguments()
|
||||
if not args.binary.is_file():
|
||||
parser.exit(1, "The library to install does not exist: " + str(args.binary) + "; build the "
|
||||
"optimized relink first: python3 build/runtime/tools/build-riot-kqueue-runtime.py\n")
|
||||
config = json.loads((ROOT / "build/config.json").read_text())
|
||||
prefix = Path(config["prefix"])
|
||||
destinations = [prefix / config["guest_home"].lstrip("/") / "RuntimeLibraries/libsystem_kernel.dylib"]
|
||||
# Keep the per-prefix bootstrap copy consistent; leave the system SDK alone.
|
||||
destinations.append(prefix / "usr/lib/system/libsystem_kernel.dylib")
|
||||
artifact = ROOT / "build/riot-libsystem_kernel.dylib"
|
||||
if artifact.exists():
|
||||
destinations.append(artifact)
|
||||
destinations, note = prefix_destinations(config, args.binary, args.artifact)
|
||||
if note:
|
||||
print("note:", note)
|
||||
try:
|
||||
result = install(args.binary, destinations, args.backup_dir)
|
||||
except PermissionError as error:
|
||||
parser.exit(1, "Cannot write " + str(error.filename) + "; choose a writable --backup-dir or run the installer with sudo.\n")
|
||||
result["artifact"] = str(args.artifact)
|
||||
result["artifact_replaced"] = any(Path(record["path"]).resolve() == args.artifact.resolve()
|
||||
for record in result["libraries"] if not record.get("already_current"))
|
||||
print(json.dumps(result))
|
||||
|
||||
|
||||
|
||||
Executable
+28
@@ -0,0 +1,28 @@
|
||||
#!/bin/sh
|
||||
# Install the Metal wrapper and Indium overlays built by build-riot-metal-runtime.py / build-riot-indium-runtime.py
|
||||
# as a pair (the Indium blit interface they share changed), with a backup of the files they replace.
|
||||
# --rollback puts the backed-up pair back. Neither stops a process: exit the game and both clients, then
|
||||
# `darling shutdown` and `sh scripts/run.sh` to activate.
|
||||
set -eu
|
||||
project_dir=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
|
||||
guest_home=${RIOT_GUEST_HOME:-$HOME/.darling@GUEST_HOME@}
|
||||
metal="$guest_home/RuntimeFrameworks/Metal.framework/Versions/A/Metal"
|
||||
indium="$guest_home/RuntimeLibraries/libindium.dylib"
|
||||
backups="$project_dir/build/metal-pair-backups"
|
||||
if [ "${1:-}" = "--rollback" ]; then
|
||||
source_metal="$backups/Metal"
|
||||
source_indium="$backups/libindium.dylib"
|
||||
else
|
||||
source_metal="$project_dir/build/riot-Metal"
|
||||
source_indium="$project_dir/build/riot-libindium.dylib"
|
||||
mkdir -p "$backups"
|
||||
# a repeated install must not replace the backup with the pair it installed last time
|
||||
cmp -s "$metal" "$source_metal" || cp -p "$metal" "$backups/Metal"
|
||||
cmp -s "$indium" "$source_indium" || cp -p "$indium" "$backups/libindium.dylib"
|
||||
fi
|
||||
for file in "$source_metal" "$source_indium"; do
|
||||
[ -f "$file" ] || { echo "missing $file" >&2; exit 1; }
|
||||
done
|
||||
install -m755 "$source_indium" "$indium.next" && mv "$indium.next" "$indium"
|
||||
install -m755 "$source_metal" "$metal.next" && mv "$metal.next" "$metal"
|
||||
sha256sum "$indium" "$metal"
|
||||
@@ -0,0 +1,83 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Check the Foundation-overlay installer policy in temporary directories.
|
||||
|
||||
The installer is imported, never executed against a real prefix: temporary SDK,
|
||||
prefix and backup directories only. Properties under test: the overlay is
|
||||
seeded from the SDK framework, the built binary replaces Versions/C/Foundation
|
||||
with a backup of the previous one, re-running is a no-op, and a non-Mach-O
|
||||
source is refused.
|
||||
"""
|
||||
import importlib.util
|
||||
import os
|
||||
from pathlib import Path
|
||||
import tempfile
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
|
||||
|
||||
def load_installer():
|
||||
path = ROOT / "tools/install-riot-foundation-runtime.py"
|
||||
spec = importlib.util.spec_from_file_location("riot_foundation_installer", path)
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
def write_library(path, fill):
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_bytes(b"\xcf\xfa\xed\xfe" + bytes([fill]) * 4096)
|
||||
|
||||
|
||||
def check():
|
||||
installer = load_installer()
|
||||
_, defaults = installer.parse_arguments([])
|
||||
assert defaults.binary == installer.DEFAULT_ARTIFACT, "default source is not the built artifact"
|
||||
assert defaults.backup_dir == ROOT / "build/foundation-runtime-backups", "default backup directory changed"
|
||||
|
||||
with tempfile.TemporaryDirectory(prefix="riot-foundation-offline-") as directory:
|
||||
folder = Path(directory)
|
||||
sdk_framework = folder / "sdk/System/Library/Frameworks/Foundation.framework"
|
||||
old_binary = sdk_framework / "Versions/C/Foundation"
|
||||
write_library(old_binary, 0x22)
|
||||
os.symlink("C", sdk_framework / "Versions/Current")
|
||||
os.symlink("Versions/Current/Foundation", sdk_framework / "Foundation")
|
||||
|
||||
framework = folder / "prefix/Users/offline-check/RuntimeFrameworks/Foundation.framework"
|
||||
backups = folder / "backups"
|
||||
artifact = folder / "riot-Foundation"
|
||||
write_library(artifact, 0x11)
|
||||
artifact_hash = installer.digest(artifact)
|
||||
old_hash = installer.digest(old_binary)
|
||||
|
||||
# First run: seeds the overlay from the SDK and replaces the binary.
|
||||
result = installer.install(artifact, sdk_framework, framework, backups)
|
||||
assert result["installed"], "first installation failed"
|
||||
installed = framework / "Versions/C/Foundation"
|
||||
assert installer.digest(installed) == artifact_hash, "built library not installed"
|
||||
assert (framework / "Foundation").is_symlink() and os.readlink(framework / "Foundation") == "Versions/Current/Foundation", "outer symlink not preserved"
|
||||
assert (framework / "Versions/Current").is_symlink(), "Current symlink not preserved"
|
||||
assert any(installer.digest(path) == old_hash for path in backups.iterdir()), "no backup of the SDK binary staged"
|
||||
assert not result["restart_performed"], "installer claims a restart"
|
||||
|
||||
# Second run: already current, no new backup directory entries.
|
||||
before = sorted(path.name for path in backups.iterdir())
|
||||
result = installer.install(artifact, sdk_framework, framework, backups)
|
||||
assert any(record.get("already_current") for record in result["libraries"]), "re-run not detected as current"
|
||||
assert sorted(path.name for path in backups.iterdir()) == before, "re-run changed the backups"
|
||||
|
||||
# A non-Mach-O source is refused before anything is touched.
|
||||
bad = folder / "not-a-library"
|
||||
bad.write_bytes(b"not a Mach-O")
|
||||
try:
|
||||
installer.install(bad, sdk_framework, framework, backups)
|
||||
except ValueError as error:
|
||||
assert "Mach-O" in str(error), "unexpected rejection reason"
|
||||
else:
|
||||
raise AssertionError("non-Mach-O source accepted")
|
||||
assert installer.digest(installed) == artifact_hash, "refused run changed the installed library"
|
||||
|
||||
print("FOUNDATION_INSTALLER_OFFLINE pass=1 default_source=artifact overlay_seeded=1 symlinks_preserved=1 backup_staged=1 rerun_noop=1 bad_source_refused=1")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
check()
|
||||
+173
-31
@@ -1,29 +1,78 @@
|
||||
// Host-only check for the darling-lol THP-aware MADV_FREE translation.
|
||||
//
|
||||
// The real Darling sys_madvise source is linked against host syscall hooks and
|
||||
// a read_string() stub that selects the host's transparent-huge-page policy
|
||||
// ("code path selector"). All memory belongs to this standalone check; no
|
||||
// client, server or Darling namespace is contacted.
|
||||
//
|
||||
// Modes (argv[1]):
|
||||
// (none) the deployment host: THP "always"
|
||||
// never THP disabled on the host
|
||||
// madvise THP only for mappings that ask for it (this layer never does)
|
||||
#define _GNU_SOURCE
|
||||
#include <sys/mman.h>
|
||||
#include <sys/syscall.h>
|
||||
#include <unistd.h>
|
||||
#include <errno.h>
|
||||
#include <stdbool.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
// Links the actual Darling sys_madvise source against host syscall hooks.
|
||||
// All memory belongs to this standalone check; no client/server is contacted.
|
||||
long sys_madvise(void* address, unsigned long size, int advice);
|
||||
|
||||
#define HUGE_PAGE_SIZE (2ul * 1024 * 1024)
|
||||
|
||||
// --- host THP policy as the emulation reads it through read_string() ---
|
||||
static const char* thpEnabled = "[always] madvise never\n";
|
||||
static const char* thpPmdSize = "2097152\n";
|
||||
|
||||
bool read_string(const char* path, char* dst, int maxlen) {
|
||||
const char* value = NULL;
|
||||
if (strstr(path, "hpage_pmd_size")) value = thpPmdSize;
|
||||
else if (strstr(path, "enabled")) value = thpEnabled;
|
||||
if (!value) return false;
|
||||
snprintf(dst, maxlen, "%s", value);
|
||||
return true;
|
||||
}
|
||||
|
||||
// --- syscall hook ---
|
||||
static int lastAdvice = -1, conversions = 0;
|
||||
static unsigned long lastAddress = 0, lastLength = 0, madviseCalls = 0;
|
||||
long __lolsys_syscall(long a, long b, long c, long d, long e, long f, int nr) {
|
||||
if (nr == SYS_madvise) lastAdvice = (int)c;
|
||||
if (nr == SYS_madvise) {
|
||||
++madviseCalls;
|
||||
lastAdvice = (int)c;
|
||||
lastAddress = (unsigned long)a;
|
||||
lastLength = (unsigned long)b;
|
||||
}
|
||||
long result = syscall(nr, a, b, c, d, e, f);
|
||||
return result == -1 ? -errno : result;
|
||||
}
|
||||
|
||||
int errno_linux_to_bsd(int error) {
|
||||
// EINVAL and ENOMEM have the same numeric values on these two systems.
|
||||
++conversions;
|
||||
return error;
|
||||
}
|
||||
|
||||
// The emulation's own parser (src/common/simple.c) links into the library; the
|
||||
// host check only needs the decimal form it uses for hpage_pmd_size.
|
||||
unsigned long long __simple_atoi(const char* str, const char** endp) {
|
||||
unsigned long long value = 0;
|
||||
while (*str >= '0' && *str <= '9') {
|
||||
value = value * 10 + (unsigned long long)(*str - '0');
|
||||
++str;
|
||||
}
|
||||
if (endp) *endp = str;
|
||||
return value;
|
||||
}
|
||||
|
||||
static void require(int condition, const char* message) {
|
||||
if (!condition) { fprintf(stderr, "HOST_MADVISE pass=0 error=%s\n", message); exit(1); }
|
||||
}
|
||||
|
||||
static unsigned long lazyFreeKB(void) {
|
||||
FILE* file = fopen("/proc/self/smaps_rollup", "r");
|
||||
require(file != NULL, "smaps_rollup");
|
||||
@@ -31,35 +80,128 @@ static unsigned long lazyFreeKB(void) {
|
||||
while (fgets(line, sizeof(line), file)) if (sscanf(line, "LazyFree: %lu kB", &value) == 1) break;
|
||||
fclose(file); return value;
|
||||
}
|
||||
int main(int argc, char** argv) {
|
||||
const int oldImplementation = argc > 1;
|
||||
const size_t size = 4 * 1024 * 1024;
|
||||
unsigned char* memory = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
|
||||
require(memory != MAP_FAILED, "anonymous mapping");
|
||||
memset(memory, 0x5a, size);
|
||||
unsigned long before = lazyFreeKB();
|
||||
require(sys_madvise(memory, size, 7) == 0, "Darwin free reusable");
|
||||
unsigned long marked = lazyFreeKB();
|
||||
if (!oldImplementation) {
|
||||
require(lastAdvice == 8, "Linux free translation");
|
||||
require(marked >= before + size / 1024, "pages not marked reclaimable");
|
||||
} else require(marked == before && lastAdvice == -1, "baseline behavior changed");
|
||||
require(sys_madvise(memory, size, 8) == 0, "Darwin reuse");
|
||||
unsigned long reused = lazyFreeKB();
|
||||
if (!oldImplementation) require(lastAdvice == 23 && reused == before, "reuse did not cancel lazy reclaim");
|
||||
for (size_t i = 0; i < size; ++i) require(memory[i] == 0x5a, "reuse changed surviving contents");
|
||||
memset(memory, 0x37, size);
|
||||
require(sys_madvise(memory, size, 5) == 0, "Darwin free");
|
||||
if (!oldImplementation) require(lastAdvice == 8 && lazyFreeKB() >= before + size / 1024, "plain free not reclaimable");
|
||||
require(sys_madvise(memory, size, 8) == 0, "second reuse");
|
||||
for (size_t i = 0; i < size; ++i) require(memory[i] == 0x37, "second reuse corrupted contents");
|
||||
require(sys_madvise(memory, size, 0) == 0 && lastAdvice == 0, "normal hint passthrough");
|
||||
if (!oldImplementation) {
|
||||
require(sys_madvise(memory + 1, size - 1, 7) == -EINVAL, "unaligned free error");
|
||||
require(sys_madvise(memory + 1, size - 1, 8) == -EINVAL, "unaligned reuse error");
|
||||
require(conversions == 2, "error translation hook");
|
||||
|
||||
// AnonHugePages of the single VMA that contains the given address.
|
||||
static unsigned long mappingAnonHugeKB(void* address, int* found) {
|
||||
FILE* file = fopen("/proc/self/smaps", "r");
|
||||
require(file != NULL, "smaps");
|
||||
unsigned long start, end, value = 0, result = 0;
|
||||
int inside = 0;
|
||||
*found = 0;
|
||||
char line[256];
|
||||
while (fgets(line, sizeof(line), file)) {
|
||||
if (sscanf(line, "%lx-%lx", &start, &end) == 2) {
|
||||
inside = (unsigned long)address >= start && (unsigned long)address < end;
|
||||
if (inside) { *found = 1; result = 0; }
|
||||
continue;
|
||||
}
|
||||
if (inside && sscanf(line, "AnonHugePages: %lu kB", &value) == 1) result = value;
|
||||
}
|
||||
require(munmap(memory, size) == 0, "unmap");
|
||||
printf("HOST_MADVISE pass=1 reclaimable_KiB=%lu reuse_reclaimable_KiB=%lu contents=preserved errors=ok\n", marked - before, reused - before);
|
||||
fclose(file);
|
||||
return result;
|
||||
}
|
||||
|
||||
static char policy[64];
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
const char* mode = argc > 1 ? argv[1] : "always";
|
||||
if (strcmp(mode, "never") == 0) thpEnabled = "always madvise [never]\n";
|
||||
else if (strcmp(mode, "madvise") == 0) thpEnabled = "always [madvise] never\n";
|
||||
|
||||
// The real host policy decides whether the no-split property can be
|
||||
// observed at all.
|
||||
FILE* hostPolicy = fopen("/sys/kernel/mm/transparent_hugepage/enabled", "r");
|
||||
if (hostPolicy) { fgets(policy, sizeof(policy), hostPolicy); fclose(hostPolicy); }
|
||||
|
||||
// A 2 MiB-aligned 32 MiB region inside a larger mapping. A 32 MiB region is
|
||||
// the size at which this host reliably backs the touched pages with 2 MiB
|
||||
// huge pages, which is what the sub-huge-page case observes.
|
||||
const size_t size = 32 * 1024 * 1024;
|
||||
unsigned char* mapping = mmap(NULL, size + HUGE_PAGE_SIZE, PROT_READ | PROT_WRITE,
|
||||
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
|
||||
require(mapping != MAP_FAILED, "anonymous mapping");
|
||||
unsigned char* memory = (unsigned char*)(((unsigned long)mapping + HUGE_PAGE_SIZE - 1)
|
||||
& ~(HUGE_PAGE_SIZE - 1));
|
||||
// Ask the host for huge pages explicitly so the sub-huge-page case has some
|
||||
// to observe; this is the check's own mapping, not the guest path.
|
||||
madvise(mapping, size + HUGE_PAGE_SIZE, MADV_HUGEPAGE);
|
||||
memset(memory, 0x5a, size);
|
||||
|
||||
int found = 0;
|
||||
unsigned long hugeBefore = 0, hugeAfter = 0, subPageCalls = 0;
|
||||
unsigned long marked = 0, before = 0, reused = 0;
|
||||
|
||||
if (strcmp(mode, "always") == 0) {
|
||||
// A sub-huge-page hint must not reach Linux and must not split the
|
||||
// region's huge pages.
|
||||
hugeBefore = mappingAnonHugeKB(memory, &found);
|
||||
require(found, "own mapping missing from smaps");
|
||||
require(!strstr(policy, "[always]") || hugeBefore > 0,
|
||||
"host THP always but the region has no huge pages");
|
||||
unsigned long callsBefore = madviseCalls;
|
||||
require(sys_madvise(memory + 4096, 4096, 7) == 0, "sub-page free hint failed");
|
||||
require(sys_madvise(memory, 4096, 5) == 0, "sub-page plain free hint failed");
|
||||
subPageCalls = madviseCalls - callsBefore;
|
||||
require(subPageCalls == 0, "sub-page hint reached Linux");
|
||||
int foundAfter = 0;
|
||||
hugeAfter = mappingAnonHugeKB(memory, &foundAfter);
|
||||
require(foundAfter && hugeAfter == hugeBefore, "sub-page hint split a huge page");
|
||||
|
||||
// The aligned region is marked reclaimable as whole huge pages and
|
||||
// cancelled on reuse with the contents preserved.
|
||||
before = lazyFreeKB();
|
||||
callsBefore = madviseCalls;
|
||||
require(sys_madvise(memory, size, 7) == 0, "Darwin free reusable");
|
||||
require(madviseCalls == callsBefore + 1 && lastAdvice == 8, "aligned region translation");
|
||||
require(lastAddress == (unsigned long)memory && lastLength == size, "aligned region changed");
|
||||
marked = lazyFreeKB();
|
||||
require(marked >= before + size / 1024, "aligned pages not marked reclaimable");
|
||||
require(sys_madvise(memory, size, 8) == 0, "Darwin reuse");
|
||||
require(lastAdvice == 23 && lastAddress == (unsigned long)memory && lastLength == size, "reuse translation");
|
||||
reused = lazyFreeKB();
|
||||
require(reused <= before, "reuse did not cancel lazy reclaim");
|
||||
for (size_t i = 0; i < size; ++i) require(memory[i] == 0x5a, "reuse changed surviving contents");
|
||||
|
||||
// An unaligned large range only affects the whole huge pages inside it.
|
||||
memset(memory, 0x37, size);
|
||||
callsBefore = madviseCalls;
|
||||
require(sys_madvise(memory + HUGE_PAGE_SIZE / 2, size, 7) == 0, "unaligned free");
|
||||
require(madviseCalls == callsBefore + 1, "unaligned range not forwarded");
|
||||
require(lastAddress == (unsigned long)memory + HUGE_PAGE_SIZE && lastLength == size - HUGE_PAGE_SIZE,
|
||||
"unaligned range did not round inward to the contained huge pages");
|
||||
require(sys_madvise(memory, size, 8) == 0, "reuse after unaligned hint");
|
||||
|
||||
// A range that contains no whole huge page is a no-op, not an error.
|
||||
callsBefore = madviseCalls;
|
||||
require(sys_madvise(memory + HUGE_PAGE_SIZE - 4096, 8192, 7) == 0, "hint without contained huge page");
|
||||
require(madviseCalls == callsBefore, "hint with no contained huge page reached Linux");
|
||||
} else {
|
||||
// With THP "never" (or "madvise") the full range is forwarded as before.
|
||||
unsigned long callsBefore = madviseCalls;
|
||||
require(sys_madvise(memory + 4096, 4096, 7) == 0, "sub-page free hint failed");
|
||||
require(madviseCalls == callsBefore + 1 && lastAdvice == 8, "disabled THP did not forward the hint");
|
||||
require(lastAddress == (unsigned long)memory + 4096 && lastLength == 4096, "disabled THP changed the range");
|
||||
before = lazyFreeKB();
|
||||
require(sys_madvise(memory, size, 7) == 0, "Darwin free reusable");
|
||||
marked = lazyFreeKB();
|
||||
require(marked >= before + size / 1024, "full range not marked reclaimable");
|
||||
require(sys_madvise(memory, size, 8) == 0, "Darwin reuse");
|
||||
require(lastAdvice == 23 && lastLength == size, "reuse translation");
|
||||
reused = lazyFreeKB();
|
||||
require(reused <= before, "reuse did not cancel lazy reclaim");
|
||||
for (size_t i = 0; i < size; ++i) require(memory[i] == 0x5a, "reuse changed surviving contents");
|
||||
}
|
||||
|
||||
// Other advice values keep the existing translation and error path.
|
||||
unsigned long callsBefore = madviseCalls;
|
||||
require(sys_madvise(memory, size, 0) == 0 && lastAdvice == 0 && madviseCalls == callsBefore + 1,
|
||||
"normal hint passthrough");
|
||||
require(sys_madvise(memory + 1, size - 1, 8) == -EINVAL, "unaligned reuse error");
|
||||
require(conversions >= 1, "error translation hook");
|
||||
|
||||
require(munmap(mapping, size + HUGE_PAGE_SIZE) == 0, "unmap");
|
||||
printf("HOST_MADVISE pass=1 mode=%s madvise_calls=%lu sub_page_syscalls=%lu reclaimable_KiB=%lu "
|
||||
"reuse_reclaimable_KiB=%lu anon_huge_KiB=%lu/%lu contents=preserved\n",
|
||||
mode, madviseCalls, subPageCalls, marked - before, reused - before, hugeBefore, hugeAfter);
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
// Host-side microbenchmark of the reply path with the real MessageQueue:
|
||||
// direct - MessageQueue::pushAndSend, the producer sends its own reply
|
||||
// loop - MessageQueue::push + eventfd wakeup + a separate "main loop" thread
|
||||
// calls MessageQueue::sendMany
|
||||
// Both endpoints (guest, worker) and, for `loop`, the main loop are threads of
|
||||
// this one process, so this measures the hand-off difference, not a real guest.
|
||||
//
|
||||
// Standalone host sockets only. Never connects to Darling, Riot or the game.
|
||||
#include <darlingserver/message.hpp>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/eventfd.h>
|
||||
#include <poll.h>
|
||||
#include <unistd.h>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <cstdlib>
|
||||
#include <cstdint>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <thread>
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <algorithm>
|
||||
|
||||
using DarlingServer::Message;
|
||||
using DarlingServer::MessageQueue;
|
||||
|
||||
static constexpr uint64_t stopTag = 0xdead;
|
||||
static constexpr size_t requestSize = 48, replySize = 24;
|
||||
|
||||
static uint64_t nowNs() {
|
||||
struct timespec ts;
|
||||
clock_gettime(CLOCK_MONOTONIC, &ts);
|
||||
return static_cast<uint64_t>(ts.tv_sec) * 1000000000ull + ts.tv_nsec;
|
||||
}
|
||||
|
||||
struct Result { double average = 0, p50 = 0, p99 = 0, maximum = 0; };
|
||||
|
||||
static Result summarize(std::vector<double>& samples) {
|
||||
std::sort(samples.begin(), samples.end());
|
||||
Result result;
|
||||
double sum = 0;
|
||||
for (double sample : samples) sum += sample;
|
||||
result.average = sum / samples.size();
|
||||
result.p50 = samples[samples.size() / 2];
|
||||
result.p99 = samples[(samples.size() * 99) / 100];
|
||||
result.maximum = samples.back();
|
||||
return result;
|
||||
}
|
||||
|
||||
static Result run(bool direct, unsigned iterations) {
|
||||
int request[2] = { -1, -1 }, reply[2] = { -1, -1 };
|
||||
if (socketpair(AF_UNIX, SOCK_DGRAM | SOCK_CLOEXEC, 0, request) != 0) throw std::runtime_error("request socketpair");
|
||||
if (socketpair(AF_UNIX, SOCK_DGRAM | SOCK_CLOEXEC, 0, reply) != 0) throw std::runtime_error("reply socketpair");
|
||||
|
||||
MessageQueue queue;
|
||||
int wakeup = -1;
|
||||
if (!direct) {
|
||||
wakeup = eventfd(0, EFD_CLOEXEC | EFD_NONBLOCK);
|
||||
if (wakeup < 0) throw std::runtime_error("eventfd");
|
||||
queue.setMessageArrivalNotificationCallback([&] { eventfd_write(wakeup, 1); });
|
||||
}
|
||||
|
||||
std::atomic<bool> stopping{false};
|
||||
std::thread worker([&] {
|
||||
// blocks until the guest sends a request
|
||||
while (true) {
|
||||
Message requestMessage(requestSize, 0);
|
||||
struct msghdr header{};
|
||||
struct iovec iov{ requestMessage.data().data(), requestMessage.data().size() };
|
||||
header.msg_iov = &iov;
|
||||
header.msg_iovlen = 1;
|
||||
ssize_t received = recvmsg(request[0], &header, 0);
|
||||
if (received < 0) throw std::runtime_error("worker recvmsg");
|
||||
uint64_t tag = 0;
|
||||
memcpy(&tag, requestMessage.data().data(), sizeof(tag));
|
||||
if (tag == stopTag) return;
|
||||
|
||||
Message replyMessage(replySize, 0);
|
||||
memcpy(replyMessage.data().data(), &tag, sizeof(tag));
|
||||
if (direct) {
|
||||
queue.pushAndSend(std::move(replyMessage), reply[0]);
|
||||
} else {
|
||||
queue.push(std::move(replyMessage));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
std::thread mainLoop;
|
||||
if (!direct) {
|
||||
mainLoop = std::thread([&] {
|
||||
while (!stopping.load()) {
|
||||
struct pollfd pollFD{ wakeup, POLLIN, 0 };
|
||||
if (poll(&pollFD, 1, 20) <= 0) continue;
|
||||
eventfd_t value;
|
||||
eventfd_read(wakeup, &value);
|
||||
queue.sendMany(reply[0]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
auto cycle = [&]() {
|
||||
uint64_t tag = 1;
|
||||
if (send(request[1], &tag, sizeof(tag), 0) != static_cast<ssize_t>(sizeof(tag))) throw std::runtime_error("guest send");
|
||||
char replyBuffer[replySize];
|
||||
if (recv(reply[1], replyBuffer, sizeof(replyBuffer), 0) != static_cast<ssize_t>(sizeof(replyBuffer))) throw std::runtime_error("guest recv");
|
||||
};
|
||||
|
||||
for (unsigned i = 0; i < 200; ++i) cycle(); // warm up
|
||||
|
||||
std::vector<double> samples;
|
||||
samples.reserve(iterations);
|
||||
for (unsigned i = 0; i < iterations; ++i) {
|
||||
uint64_t start = nowNs();
|
||||
cycle();
|
||||
samples.push_back(nowNs() - start);
|
||||
}
|
||||
|
||||
uint64_t tag = stopTag;
|
||||
if (send(request[1], &tag, sizeof(tag), 0) != static_cast<ssize_t>(sizeof(tag))) throw std::runtime_error("guest stop");
|
||||
worker.join();
|
||||
stopping.store(true);
|
||||
if (mainLoop.joinable()) {
|
||||
// wake the main loop for its final pass, then wait for it to notice
|
||||
eventfd_write(wakeup, 1);
|
||||
mainLoop.join();
|
||||
}
|
||||
while (!queue.empty()) queue.sendMany(reply[0]);
|
||||
|
||||
close(request[0]); close(request[1]); close(reply[0]); close(reply[1]);
|
||||
if (wakeup >= 0) close(wakeup);
|
||||
return summarize(samples);
|
||||
}
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
try {
|
||||
unsigned iterations = (argc > 1) ? static_cast<unsigned>(strtoul(argv[1], nullptr, 10)) : 2000;
|
||||
auto loop = run(false, iterations);
|
||||
auto direct = run(true, iterations);
|
||||
std::printf("HOST_REPLY_BENCH same_process=1 n=%u loop_avg=%.2fus loop_p50=%.2fus loop_p99=%.2fus loop_max=%.2fus direct_avg=%.2fus direct_p50=%.2fus direct_p99=%.2fus direct_max=%.2fus\n",
|
||||
iterations, loop.average / 1000.0, loop.p50 / 1000.0, loop.p99 / 1000.0, loop.maximum / 1000.0,
|
||||
direct.average / 1000.0, direct.p50 / 1000.0, direct.p99 / 1000.0, direct.maximum / 1000.0);
|
||||
return 0;
|
||||
} catch (const std::exception& error) {
|
||||
std::fprintf(stderr, "HOST_REPLY_BENCH pass=0 error=%s\n", error.what());
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,340 @@
|
||||
// Host-side checks for the reply path (MessageQueue::pushAndSend): direct sends
|
||||
// from the producing thread, FIFO order under concurrency and EAGAIN, wakeup
|
||||
// notification rules, descriptor ownership and dead-peer handling.
|
||||
//
|
||||
// Standalone host sockets only. Never connects to Darling, Riot or the game.
|
||||
#include <darlingserver/message.hpp>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/un.h>
|
||||
#include <unistd.h>
|
||||
#include <fcntl.h>
|
||||
#include <dirent.h>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <stdexcept>
|
||||
#include <system_error>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <vector>
|
||||
#include <algorithm>
|
||||
|
||||
using DarlingServer::Address;
|
||||
using DarlingServer::Message;
|
||||
using DarlingServer::MessageQueue;
|
||||
|
||||
static void require(bool pass, const char* what) {
|
||||
if (!pass) throw std::runtime_error(what);
|
||||
}
|
||||
|
||||
static unsigned openFDs() {
|
||||
DIR* directory = opendir("/proc/self/fd");
|
||||
require(directory, "fd directory");
|
||||
unsigned n = 0;
|
||||
while (auto entry = readdir(directory)) if (entry->d_name[0] != '.') ++n;
|
||||
closedir(directory);
|
||||
return n;
|
||||
}
|
||||
|
||||
// A guest-side socket with its own bound address, like a guest RPC endpoint.
|
||||
struct Peer {
|
||||
int fd = -1;
|
||||
std::string path;
|
||||
explicit Peer(unsigned index) {
|
||||
fd = socket(AF_UNIX, SOCK_DGRAM | SOCK_CLOEXEC | SOCK_NONBLOCK, 0);
|
||||
require(fd >= 0, "peer socket");
|
||||
path = "/tmp/opencode/riot-host-reply-check-" + std::to_string(getpid()) + "-" + std::to_string(index);
|
||||
unlink(path.c_str());
|
||||
sockaddr_un address{};
|
||||
address.sun_family = AF_UNIX;
|
||||
std::strncpy(address.sun_path, path.c_str(), sizeof(address.sun_path) - 1);
|
||||
require(bind(fd, reinterpret_cast<sockaddr*>(&address), sizeof(address)) == 0, "peer bind");
|
||||
}
|
||||
~Peer() {
|
||||
if (fd >= 0) close(fd);
|
||||
if (!path.empty()) unlink(path.c_str());
|
||||
}
|
||||
sockaddr_un address() const {
|
||||
sockaddr_un address{};
|
||||
address.sun_family = AF_UNIX;
|
||||
std::strncpy(address.sun_path, path.c_str(), sizeof(address.sun_path) - 1);
|
||||
return address;
|
||||
}
|
||||
};
|
||||
|
||||
// The server's listener socket; a small send buffer forces EAGAIN quickly.
|
||||
static int serverSocket(int sendBuffer = 0) {
|
||||
int fd = socket(AF_UNIX, SOCK_DGRAM | SOCK_CLOEXEC | SOCK_NONBLOCK, 0);
|
||||
require(fd >= 0, "server socket");
|
||||
if (sendBuffer) {
|
||||
require(setsockopt(fd, SOL_SOCKET, SO_SNDBUF, &sendBuffer, sizeof(sendBuffer)) == 0, "send buffer");
|
||||
}
|
||||
return fd;
|
||||
}
|
||||
|
||||
static Message packetFor(const Peer& peer, uint64_t tag, size_t size = 64, unsigned descriptors = 0) {
|
||||
Message message(size, descriptors);
|
||||
std::memcpy(message.data().data(), &tag, sizeof(tag));
|
||||
message.setAddress(Address(peer.address()));
|
||||
return message;
|
||||
}
|
||||
|
||||
struct Incoming {
|
||||
bool valid = false;
|
||||
uint64_t tag = 0;
|
||||
size_t size = 0;
|
||||
std::vector<int> fds;
|
||||
};
|
||||
|
||||
static Incoming receive(int fd) {
|
||||
Incoming incoming;
|
||||
unsigned char buffer[512];
|
||||
unsigned char control[CMSG_SPACE(sizeof(int) * 8)];
|
||||
struct iovec iov{ buffer, sizeof(buffer) };
|
||||
struct msghdr header{};
|
||||
header.msg_iov = &iov;
|
||||
header.msg_iovlen = 1;
|
||||
header.msg_control = control;
|
||||
header.msg_controllen = sizeof(control);
|
||||
ssize_t received = recvmsg(fd, &header, 0);
|
||||
if (received < 0) {
|
||||
if (errno == EAGAIN || errno == EWOULDBLOCK) return incoming;
|
||||
throw std::system_error(errno, std::generic_category(), "recvmsg");
|
||||
}
|
||||
incoming.valid = true;
|
||||
incoming.size = static_cast<size_t>(received);
|
||||
if (received >= static_cast<ssize_t>(sizeof(incoming.tag))) std::memcpy(&incoming.tag, buffer, sizeof(incoming.tag));
|
||||
for (auto cmsg = CMSG_FIRSTHDR(&header); cmsg; cmsg = CMSG_NXTHDR(&header, cmsg)) {
|
||||
if (cmsg->cmsg_level == SOL_SOCKET && cmsg->cmsg_type == SCM_RIGHTS) {
|
||||
size_t count = (cmsg->cmsg_len - CMSG_LEN(0)) / sizeof(int);
|
||||
for (size_t i = 0; i < count; ++i) {
|
||||
int descriptor = -1;
|
||||
std::memcpy(&descriptor, CMSG_DATA(cmsg) + (sizeof(int) * i), sizeof(int));
|
||||
incoming.fds.push_back(descriptor);
|
||||
}
|
||||
}
|
||||
}
|
||||
return incoming;
|
||||
}
|
||||
|
||||
// Reads everything currently queued on the peer, in arrival order.
|
||||
static void receiveAll(int fd, std::vector<uint64_t>& tags, std::vector<std::vector<int>>& descriptors) {
|
||||
while (true) {
|
||||
auto incoming = receive(fd);
|
||||
if (!incoming.valid) return;
|
||||
tags.push_back(incoming.tag);
|
||||
descriptors.push_back(std::move(incoming.fds));
|
||||
}
|
||||
}
|
||||
|
||||
// The main loop's job: send what the queue can send, then let the peer consume it.
|
||||
static void drainQueue(MessageQueue& queue, int serverFD, int peerFD, std::vector<uint64_t>& tags, std::vector<std::vector<int>>& descriptors, size_t expected) {
|
||||
for (unsigned spin = 0; tags.size() < expected; ++spin) {
|
||||
require(spin < 20000, "drain made no progress");
|
||||
queue.sendMany(serverFD);
|
||||
receiveAll(peerFD, tags, descriptors);
|
||||
if (tags.size() < expected) std::this_thread::sleep_for(std::chrono::microseconds(50));
|
||||
}
|
||||
}
|
||||
|
||||
static void checkWakeupRules() {
|
||||
int server = serverSocket(4096);
|
||||
Peer peer(0);
|
||||
MessageQueue queue;
|
||||
unsigned callbacks = 0;
|
||||
queue.setMessageArrivalNotificationCallback([&] {
|
||||
++callbacks;
|
||||
require(!queue.empty(), "callback fired on an empty queue");
|
||||
});
|
||||
|
||||
// A direct send that drains the queue must not wake the main loop.
|
||||
queue.pushAndSend(packetFor(peer, 1000), server);
|
||||
require(callbacks == 0, "drained direct send notified");
|
||||
require(queue.empty(), "drained direct send left messages");
|
||||
auto first = receive(peer.fd);
|
||||
require(first.valid && first.tag == 1000, "direct send did not arrive");
|
||||
|
||||
// The legacy push path still notifies unconditionally.
|
||||
queue.push(packetFor(peer, 1001));
|
||||
require(callbacks == 1, "push did not notify");
|
||||
require(!queue.empty(), "push did not queue");
|
||||
|
||||
// Under EAGAIN the queue remains and the main loop is notified.
|
||||
std::vector<uint64_t> tags;
|
||||
std::vector<std::vector<int>> descriptors;
|
||||
for (uint64_t i = 0; i < 64; ++i) queue.pushAndSend(packetFor(peer, 2000 + i), server);
|
||||
require(callbacks >= 2, "EAGAIN did not notify");
|
||||
require(!queue.empty(), "EAGAIN did not leave messages queued");
|
||||
unsigned afterEagain = callbacks;
|
||||
|
||||
drainQueue(queue, server, peer.fd, tags, descriptors, 64 + 1);
|
||||
require(queue.empty(), "drain left messages");
|
||||
require(tags.size() == 65, "wrong message count");
|
||||
require(tags[0] == 1001, "queued message lost");
|
||||
for (size_t i = 0; i < 64; ++i) require(tags[i + 1] == 2000 + i, "EAGAIN retry reordered or lost a message");
|
||||
require(callbacks == afterEagain, "drain notified without a push");
|
||||
|
||||
close(server);
|
||||
}
|
||||
|
||||
static void checkConcurrentPushers() {
|
||||
int server = serverSocket(4096); // small buffer: mix direct sends and the retry path
|
||||
Peer peer(1);
|
||||
MessageQueue queue;
|
||||
std::atomic<unsigned> callbacks{0};
|
||||
|
||||
constexpr unsigned pusherCount = 4;
|
||||
constexpr unsigned perPusher = 2000;
|
||||
constexpr size_t total = pusherCount * perPusher;
|
||||
std::atomic<unsigned> ready{0};
|
||||
std::vector<std::thread> pushers;
|
||||
// Under concurrency the queue may be drained between the notification and this
|
||||
// callback, so only its firing is observable here (like the server's eventfd).
|
||||
queue.setMessageArrivalNotificationCallback([&] { ++callbacks; });
|
||||
for (unsigned p = 0; p < pusherCount; ++p) {
|
||||
pushers.emplace_back([&, p] {
|
||||
++ready;
|
||||
for (uint64_t i = 0; i < perPusher; ++i) {
|
||||
queue.pushAndSend(packetFor(peer, (static_cast<uint64_t>(p) << 32) | i), server);
|
||||
}
|
||||
});
|
||||
}
|
||||
while (ready.load() != pusherCount) std::this_thread::yield();
|
||||
|
||||
// The main loop's role: send whatever the pushers had to leave behind.
|
||||
std::vector<unsigned> nextSequence(pusherCount, 0);
|
||||
size_t received = 0;
|
||||
for (unsigned spin = 0; received < total; ++spin) {
|
||||
require(spin < 400000, "concurrent drain made no progress");
|
||||
queue.sendMany(server);
|
||||
while (true) {
|
||||
auto incoming = receive(peer.fd);
|
||||
if (!incoming.valid) break;
|
||||
unsigned p = incoming.tag >> 32;
|
||||
unsigned sequence = incoming.tag & 0xffffffff;
|
||||
require(p < pusherCount, "unknown pusher");
|
||||
require(sequence == nextSequence[p], "message reordered for one pusher");
|
||||
++nextSequence[p];
|
||||
++received;
|
||||
}
|
||||
if (received < total) std::this_thread::sleep_for(std::chrono::microseconds(50));
|
||||
}
|
||||
|
||||
for (auto& pusher : pushers) pusher.join();
|
||||
require(received == total, "lost or duplicated messages");
|
||||
for (unsigned p = 0; p < pusherCount; ++p) require(nextSequence[p] == perPusher, "incomplete pusher sequence");
|
||||
|
||||
close(server);
|
||||
}
|
||||
|
||||
static void checkDescriptors() {
|
||||
int server = serverSocket(65536);
|
||||
Peer peer(2);
|
||||
MessageQueue queue;
|
||||
unsigned baseline = openFDs();
|
||||
|
||||
for (uint64_t i = 0; i < 8; ++i) {
|
||||
Message message = packetFor(peer, 3000 + i, 64, 2);
|
||||
for (unsigned d = 0; d < 2; ++d) {
|
||||
int pipeFDs[2];
|
||||
require(pipe(pipeFDs) == 0, "pipe");
|
||||
unsigned char byte = static_cast<unsigned char>(i * 2 + d);
|
||||
require(write(pipeFDs[1], &byte, 1) == 1, "pipe write");
|
||||
close(pipeFDs[1]);
|
||||
message.pushDescriptor(pipeFDs[0]);
|
||||
}
|
||||
queue.pushAndSend(std::move(message), server);
|
||||
// the queue drops its message (and its descriptors) right after the send
|
||||
require(openFDs() == baseline, "descriptor not closed on the sender");
|
||||
}
|
||||
|
||||
std::vector<uint64_t> tags;
|
||||
std::vector<std::vector<int>> descriptors;
|
||||
receiveAll(peer.fd, tags, descriptors);
|
||||
require(tags.size() == 8, "wrong descriptor message count");
|
||||
for (size_t i = 0; i < tags.size(); ++i) {
|
||||
require(tags[i] == 3000 + i, "descriptor message order");
|
||||
require(descriptors[i].size() == 2, "descriptor count");
|
||||
for (unsigned d = 0; d < 2; ++d) {
|
||||
unsigned char byte = 0;
|
||||
require(read(descriptors[i][d], &byte, 1) == 1, "descriptor read");
|
||||
require(byte == static_cast<unsigned char>(i * 2 + d), "descriptor payload");
|
||||
close(descriptors[i][d]);
|
||||
}
|
||||
}
|
||||
require(openFDs() == baseline, "received descriptors leaked");
|
||||
|
||||
close(server);
|
||||
}
|
||||
|
||||
static void checkDeadPeer() {
|
||||
int server = serverSocket(0);
|
||||
MessageQueue queue;
|
||||
Peer live(3);
|
||||
unsigned baseline = openFDs();
|
||||
|
||||
// A peer that unbound its address makes the kernel report ENOENT.
|
||||
{
|
||||
Peer dead(4);
|
||||
sockaddr_un address = dead.address();
|
||||
close(dead.fd);
|
||||
dead.fd = -1;
|
||||
unlink(dead.path.c_str());
|
||||
Message message(64, 0);
|
||||
message.setAddress(Address(address));
|
||||
queue.pushAndSend(std::move(message), server); // must not throw
|
||||
}
|
||||
|
||||
// A peer that closed its (still-bound) address reports ECONNREFUSED.
|
||||
{
|
||||
Peer dead(5);
|
||||
sockaddr_un address = dead.address();
|
||||
Message message(64, 0);
|
||||
message.setAddress(Address(address));
|
||||
close(dead.fd);
|
||||
dead.fd = -1;
|
||||
queue.pushAndSend(std::move(message), server); // must not throw
|
||||
}
|
||||
|
||||
require(queue.empty(), "dead peer wedged the queue");
|
||||
require(openFDs() == baseline, "dead peer leaked descriptors");
|
||||
|
||||
// Two dropped messages in front of a live one must not block or reorder it.
|
||||
sockaddr_un deadAddress{};
|
||||
{
|
||||
Peer dead(6);
|
||||
deadAddress = dead.address();
|
||||
close(dead.fd);
|
||||
dead.fd = -1;
|
||||
}
|
||||
for (unsigned i = 0; i < 2; ++i) {
|
||||
Message message(64, 0);
|
||||
message.setAddress(Address(deadAddress));
|
||||
queue.pushAndSend(std::move(message), server);
|
||||
}
|
||||
require(queue.empty(), "dead peer messages remained queued");
|
||||
queue.pushAndSend(packetFor(live, 4000), server);
|
||||
|
||||
std::vector<uint64_t> tags;
|
||||
std::vector<std::vector<int>> descriptors;
|
||||
receiveAll(live.fd, tags, descriptors);
|
||||
require(tags.size() == 1 && tags[0] == 4000, "live message after dead peers");
|
||||
|
||||
close(server);
|
||||
}
|
||||
|
||||
int main() {
|
||||
try {
|
||||
checkWakeupRules();
|
||||
checkConcurrentPushers();
|
||||
checkDescriptors();
|
||||
checkDeadPeer();
|
||||
std::printf("HOST_REPLY pass=1 direct_send=ok eagain_retry=ok fifo=ok concurrency=ok notify=ok descriptors=ok dead_peer=ok\n");
|
||||
return 0;
|
||||
} catch (const std::exception& error) {
|
||||
std::fprintf(stderr, "HOST_REPLY pass=0 error=%s\n", error.what());
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,386 @@
|
||||
// Host-only check for darling-lol's real CPU topology reporting in
|
||||
// emulation/src/xnu_syscall/bsd/helper/misc/sysctl_hw.c (+ the shared
|
||||
// common/cpu_topology.h probe it drives).
|
||||
//
|
||||
// The real sysctl_hw.c and sysctl_unspec.c sources are linked against host
|
||||
// shims: read_string() reads the host's actual /sys files, __lolsys_syscall()
|
||||
// forwards to the host (and can fail sched_getaffinity on request), and the
|
||||
// Mach calls return sentinel values that must never reach the results. The
|
||||
// host's own topology is computed independently of the code under test.
|
||||
//
|
||||
// Modes (argv[1]):
|
||||
// (none) the host as it is
|
||||
// affinity this process is restricted to four online CPUs
|
||||
// no-detect /sys CPU files and sched_getaffinity both fail -> fixed 8s
|
||||
// optout DARLING_CPU_FIXED_FLAG exists -> the old fixed 8s
|
||||
#define _GNU_SOURCE
|
||||
#include <errno.h>
|
||||
#include <sched.h>
|
||||
#include <stdarg.h>
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/syscall.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <mach/host_info.h>
|
||||
#include <mach/mach_init.h>
|
||||
|
||||
#include <darling/emulation/xnu_syscall/bsd/helper/misc/sysctl_hw.h>
|
||||
#include <darling/emulation/xnu_syscall/bsd/helper/misc/sysctl_proc.h>
|
||||
#include <darling/emulation/xnu_syscall/bsd/helper/misc/sysctl_unspec.h>
|
||||
#include <darling/emulation/linux_premigration/ext/sys/utsname.h>
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Shims for the compiled emulation sources
|
||||
|
||||
static int failDetection = 0; // read_string()/sched_getaffinity() fail on /sys CPU files
|
||||
|
||||
bool read_string(const char* path, char* dst, int maxlen) {
|
||||
if (failDetection && strncmp(path, "/sys/devices/system/cpu", 23) == 0)
|
||||
return false;
|
||||
FILE* file = fopen(path, "r");
|
||||
if (file == NULL)
|
||||
return false;
|
||||
int n = (int)fread(dst, 1, (size_t)(maxlen - 1), file);
|
||||
fclose(file);
|
||||
if (n <= 0)
|
||||
return false;
|
||||
dst[n] = '\0';
|
||||
return true;
|
||||
}
|
||||
|
||||
long __lolsys_syscall(long a1, long a2, long a3, long a4, long a5, long a6, int nr) {
|
||||
if (failDetection && nr == SYS_sched_getaffinity)
|
||||
return -EINVAL;
|
||||
long result = syscall(nr, a1, a2, a3, a4, a5, a6);
|
||||
return result == -1 ? -errno : result;
|
||||
}
|
||||
|
||||
unsigned long long __simple_atoi(const char* str, const char** endp) {
|
||||
unsigned long long value = 0;
|
||||
while (*str >= '0' && *str <= '9')
|
||||
value = value * 10 + (unsigned long long)(*str++ - '0');
|
||||
if (endp)
|
||||
*endp = str;
|
||||
return value;
|
||||
}
|
||||
|
||||
int __simple_sprintf(char* buffer, const char* format, ...) {
|
||||
va_list args;
|
||||
va_start(args, format);
|
||||
int ret = vsnprintf(buffer, 4096, format, args);
|
||||
va_end(args);
|
||||
return ret;
|
||||
}
|
||||
|
||||
void copyout_string(const char* str, char* out, unsigned long* out_len) {
|
||||
unsigned long need = strlen(str) + 1;
|
||||
if (out == NULL) {
|
||||
*out_len = need;
|
||||
return;
|
||||
}
|
||||
if (*out_len < need)
|
||||
need = *out_len;
|
||||
memcpy(out, str, need);
|
||||
out[need - 1] = '\0';
|
||||
*out_len = need;
|
||||
}
|
||||
|
||||
// glibc has no strlcpy; sysctl_unspec.c declares it.
|
||||
size_t strlcpy(char* dst, const char* src, size_t size) {
|
||||
size_t len = strlen(src);
|
||||
if (size != 0) {
|
||||
size_t copy = len < size - 1 ? len : size - 1;
|
||||
memcpy(dst, src, copy);
|
||||
dst[copy] = '\0';
|
||||
}
|
||||
return len;
|
||||
}
|
||||
|
||||
struct linux_utsname* need_uname(void) {
|
||||
static struct linux_utsname name = { .sysname = "Darwin", .machine = "x86_64" };
|
||||
return &name;
|
||||
}
|
||||
|
||||
// The Mach layer: sentinel values that must be overwritten by the topology
|
||||
// probe. The test fails if any of them survives into an answer.
|
||||
mach_port_t mach_host_self(void) { return 1; }
|
||||
mach_port_t mach_task_self(void) { return 1; }
|
||||
kern_return_t mach_port_deallocate(ipc_space_t task, mach_port_name_t name) { (void)task; (void)name; return 0; }
|
||||
kern_return_t host_info(mach_port_name_t host, int itype, void* hinfo, mach_msg_type_number_t* count) {
|
||||
(void)host; (void)itype;
|
||||
struct host_basic_info* reply = hinfo;
|
||||
*reply = (struct host_basic_info) { 0 };
|
||||
reply->avail_cpus = 99;
|
||||
reply->max_cpus = 99;
|
||||
reply->physical_cpu = 99;
|
||||
reply->physical_cpu_max = 99;
|
||||
reply->logical_cpu = 99;
|
||||
reply->logical_cpu_max = 99;
|
||||
reply->cpu_type = 7;
|
||||
*count = 8; // the legacy reply size
|
||||
return 0;
|
||||
}
|
||||
|
||||
// A minimal sysctl tree whose "hw" node is the real one; sysctl_unspec.c
|
||||
// walks it for sysctlbyname (name -> oid).
|
||||
static const struct known_sysctl root_children[] = {
|
||||
{ .oid = 6 /* CTL_HW */, .type = CTLTYPE_NODE, .exttype = "", .name = "hw", .subctls = sysctls_hw },
|
||||
{ .oid = -1 }
|
||||
};
|
||||
const struct known_sysctl sysctls_root = {
|
||||
.oid = 0, .type = CTLTYPE_NODE, .exttype = "", .name = "root", .subctls = root_children
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Independent host topology (does not use the code under test)
|
||||
|
||||
static unsigned int count_list(const char* list) {
|
||||
unsigned int count = 0;
|
||||
const char* p = list;
|
||||
for (;;) {
|
||||
unsigned long long first = 0, last;
|
||||
const char* start;
|
||||
while (*p == ',' || *p == ' ' || *p == '\t' || *p == '\n')
|
||||
++p;
|
||||
start = p;
|
||||
while (*p >= '0' && *p <= '9')
|
||||
first = first * 10 + (unsigned int)(*p++ - '0');
|
||||
if (p == start)
|
||||
break;
|
||||
last = first;
|
||||
if (*p == '-') {
|
||||
++p;
|
||||
last = 0;
|
||||
while (*p >= '0' && *p <= '9')
|
||||
last = last * 10 + (unsigned int)(*p++ - '0');
|
||||
}
|
||||
if (last >= first && last - first < 4096)
|
||||
count += (unsigned int)(last - first + 1);
|
||||
if (*p != ',' && *p != ' ' && *p != '\t' && *p != '\n')
|
||||
break;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
static unsigned int file_count(const char* path) {
|
||||
FILE* file = fopen(path, "r");
|
||||
if (file == NULL)
|
||||
return 0;
|
||||
char buf[256];
|
||||
if (fgets(buf, sizeof(buf), file) == NULL) {
|
||||
fclose(file);
|
||||
return 0;
|
||||
}
|
||||
fclose(file);
|
||||
return count_list(buf);
|
||||
}
|
||||
|
||||
static unsigned int file_uint(const char* path, int* ok) {
|
||||
FILE* file = fopen(path, "r");
|
||||
if (file == NULL) {
|
||||
*ok = 0;
|
||||
return 0;
|
||||
}
|
||||
char buf[64];
|
||||
if (fgets(buf, sizeof(buf), file) == NULL) {
|
||||
fclose(file);
|
||||
*ok = 0;
|
||||
return 0;
|
||||
}
|
||||
fclose(file);
|
||||
*ok = 1;
|
||||
return (unsigned int)strtoul(buf, NULL, 10);
|
||||
}
|
||||
|
||||
// distinct (package, core) pairs among [0, online), optionally masked
|
||||
static unsigned int host_cores(const unsigned char* mask, int maskBytes, unsigned int online) {
|
||||
unsigned long long pairs[256];
|
||||
unsigned int count = 0;
|
||||
if (online > 256)
|
||||
return 0;
|
||||
for (unsigned int cpu = 0; cpu < online; ++cpu) {
|
||||
char path[112];
|
||||
int ok;
|
||||
if (mask != NULL && (mask[cpu >> 3] & (1u << (cpu & 7))) == 0)
|
||||
continue;
|
||||
snprintf(path, sizeof(path), "/sys/devices/system/cpu/cpu%u/topology/physical_package_id", cpu);
|
||||
unsigned int package = file_uint(path, &ok);
|
||||
if (!ok)
|
||||
return 0;
|
||||
snprintf(path, sizeof(path), "/sys/devices/system/cpu/cpu%u/topology/core_id", cpu);
|
||||
unsigned int core = file_uint(path, &ok);
|
||||
if (!ok)
|
||||
return 0;
|
||||
unsigned long long pair = ((unsigned long long)package << 32) | core;
|
||||
int seen = 0;
|
||||
for (unsigned int i = 0; i < count; ++i)
|
||||
if (pairs[i] == pair)
|
||||
seen = 1;
|
||||
if (!seen)
|
||||
pairs[count++] = pair;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
struct expect {
|
||||
unsigned int ncpu, active, logical, logical_max, physical, physical_max;
|
||||
};
|
||||
|
||||
static void host_expectations(struct expect* e) {
|
||||
unsigned int online = file_count("/sys/devices/system/cpu/online");
|
||||
if (online == 0)
|
||||
online = file_count("/sys/devices/system/cpu/present");
|
||||
|
||||
cpu_set_t set;
|
||||
CPU_ZERO(&set);
|
||||
unsigned int active = online;
|
||||
if (sched_getaffinity(0, sizeof(set), &set) == 0)
|
||||
active = (unsigned int)CPU_COUNT(&set);
|
||||
|
||||
e->ncpu = online;
|
||||
e->active = active;
|
||||
e->logical = active;
|
||||
e->logical_max = online;
|
||||
e->physical_max = host_cores(NULL, 0, online);
|
||||
if (e->physical_max == 0)
|
||||
e->physical_max = online;
|
||||
e->physical = host_cores((const unsigned char*)&set, (int)sizeof(set), online);
|
||||
if (e->physical == 0)
|
||||
e->physical = e->physical_max;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Driving the real handlers
|
||||
|
||||
static const struct known_sysctl* find_by_name(const char* name) {
|
||||
for (const struct known_sysctl* entry = sysctls_hw; entry->oid != -1; ++entry) {
|
||||
if (strcmp(entry->name, name) == 0)
|
||||
return entry;
|
||||
}
|
||||
fprintf(stderr, "HOST_SYSCTL_CPU pass=0 error=%s missing from sysctls_hw\n", name);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
static int int_value(const struct known_sysctl* entry) {
|
||||
unsigned long len = 0;
|
||||
long rc = entry->handler(NULL, 0, NULL, &len, NULL, 0);
|
||||
if (rc != 0 || len != sizeof(int)) {
|
||||
fprintf(stderr, "HOST_SYSCTL_CPU pass=0 error=%s size rc=%ld len=%lu\n", entry->name, rc, len);
|
||||
exit(1);
|
||||
}
|
||||
int value = 0;
|
||||
len = sizeof(value);
|
||||
rc = entry->handler(NULL, 0, &value, &len, NULL, 0);
|
||||
if (rc != 0) {
|
||||
fprintf(stderr, "HOST_SYSCTL_CPU pass=0 error=%s call rc=%ld\n", entry->name, rc);
|
||||
exit(1);
|
||||
}
|
||||
if (value == 99) {
|
||||
fprintf(stderr, "HOST_SYSCTL_CPU pass=0 error=%s leaked the Mach sentinel\n", entry->name);
|
||||
exit(1);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
static void require_value(const struct known_sysctl* entry, int expected) {
|
||||
int value = int_value(entry);
|
||||
if (value != expected) {
|
||||
fprintf(stderr, "HOST_SYSCTL_CPU pass=0 error=%s value=%d expected=%d\n", entry->name, value, expected);
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// sysctlbyname("hw.<name>") through the real name_to_oid handler, then the
|
||||
// handler selected by the returned OID (what the dispatcher does).
|
||||
static void require_byname(const char* full_name, int expected) {
|
||||
const struct known_sysctl* unspec = NULL;
|
||||
for (const struct known_sysctl* entry = sysctls_unspec; entry->oid != -1; ++entry) {
|
||||
if (strcmp(entry->name, "name_to_oid") == 0)
|
||||
unspec = entry;
|
||||
}
|
||||
if (unspec == NULL) {
|
||||
fprintf(stderr, "HOST_SYSCTL_CPU pass=0 error=name_to_oid missing\n");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
int oid[4] = { 0 };
|
||||
unsigned long len = sizeof(oid);
|
||||
long rc = unspec->handler(NULL, 0, oid, &len, (void*)full_name, strlen(full_name));
|
||||
if (rc != 0 || len != 2 * sizeof(int) || oid[0] != 6) {
|
||||
fprintf(stderr, "HOST_SYSCTL_CPU pass=0 error=name_to_oid %s rc=%ld len=%lu oid=%d,%d\n",
|
||||
full_name, rc, len, oid[0], oid[1]);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
const struct known_sysctl* entry = NULL;
|
||||
for (const struct known_sysctl* probe = sysctls_hw; probe->oid != -1; ++probe) {
|
||||
if (probe->oid == oid[1]) {
|
||||
entry = probe;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (entry == NULL) {
|
||||
fprintf(stderr, "HOST_SYSCTL_CPU pass=0 error=name_to_oid %s unknown oid %d\n", full_name, oid[1]);
|
||||
exit(1);
|
||||
}
|
||||
require_value(entry, expected);
|
||||
}
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
const char* mode = argc > 1 ? argv[1] : "normal";
|
||||
|
||||
if (strcmp(mode, "affinity") == 0) {
|
||||
// Restrict this process to the first four online CPUs.
|
||||
unsigned int online = file_count("/sys/devices/system/cpu/online");
|
||||
if (online < 4) {
|
||||
fprintf(stderr, "HOST_SYSCTL_CPU pass=0 error=need four online CPUs (have %u)\n", online);
|
||||
return 1;
|
||||
}
|
||||
cpu_set_t set;
|
||||
CPU_ZERO(&set);
|
||||
for (unsigned int cpu = 0; cpu < 4; ++cpu)
|
||||
CPU_SET(cpu, &set);
|
||||
if (sched_setaffinity(0, sizeof(set), &set) != 0) {
|
||||
fprintf(stderr, "HOST_SYSCTL_CPU pass=0 error=cannot restrict affinity\n");
|
||||
return 1;
|
||||
}
|
||||
} else if (strcmp(mode, "no-detect") == 0) {
|
||||
failDetection = 1;
|
||||
}
|
||||
|
||||
struct expect e;
|
||||
if (strcmp(mode, "no-detect") == 0 || strcmp(mode, "optout") == 0)
|
||||
e = (struct expect) { 8, 8, 8, 8, 8, 8 };
|
||||
else
|
||||
host_expectations(&e);
|
||||
|
||||
require_value(find_by_name("ncpu"), (int)e.ncpu);
|
||||
require_value(find_by_name("activecpu"), (int)e.active);
|
||||
require_value(find_by_name("availcpu"), (int)e.active);
|
||||
require_value(find_by_name("logicalcpu"), (int)e.logical);
|
||||
require_value(find_by_name("logicalcpu_max"), (int)e.logical_max);
|
||||
require_value(find_by_name("physicalcpu"), (int)e.physical);
|
||||
require_value(find_by_name("physicalcpu_max"), (int)e.physical_max);
|
||||
|
||||
require_byname("hw.activecpu", (int)e.active);
|
||||
require_byname("hw.ncpu", (int)e.ncpu);
|
||||
require_byname("hw.logicalcpu_max", (int)e.logical_max);
|
||||
|
||||
const struct known_sysctl* model = find_by_name("model");
|
||||
char model_buf[64] = "";
|
||||
unsigned long model_len = sizeof(model_buf);
|
||||
if (model->handler(NULL, 0, model_buf, &model_len, NULL, 0) != 0 ||
|
||||
strcmp(model_buf, "MacBookPro16,1") != 0) {
|
||||
fprintf(stderr, "HOST_SYSCTL_CPU pass=0 error=model changed to '%s'\n", model_buf);
|
||||
return 1;
|
||||
}
|
||||
|
||||
printf("HOST_SYSCTL_CPU pass=1 mode=%s ncpu=%d active=%d logicalcpu=%d logicalcpu_max=%d physicalcpu=%d physicalcpu_max=%d model=%s\n",
|
||||
mode, (int)e.ncpu, (int)e.active, (int)e.logical, (int)e.logical_max,
|
||||
(int)e.physical, (int)e.physical_max, model_buf);
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,257 @@
|
||||
// Host-only check for the darling-lol vDSO clock path in
|
||||
// emulation/src/linux_premigration/ext/clock_gettime.c.
|
||||
//
|
||||
// The real clock source is compiled against a stub ELF bridge backed by the
|
||||
// host's dlopen/dlsym, so the resolver, the fallback and the steady-state call
|
||||
// all run for real. Only this process's own memory is touched; no guest, client
|
||||
// or Darling namespace is involved.
|
||||
//
|
||||
// Modes (argv[1]):
|
||||
// (none) null bridge first, then a working bridge
|
||||
// no-symbol the bridge exists but has no __vdso_clock_gettime entry
|
||||
// race many threads resolve and read the clock at the same time
|
||||
#define _GNU_SOURCE
|
||||
#include <dlfcn.h>
|
||||
#include <errno.h>
|
||||
#include <pthread.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/syscall.h>
|
||||
#include <sys/wait.h>
|
||||
#include <time.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <elfcalls.h>
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Syscall shim for the compiled emulation source (LINUX_SYSCALL -> __lolsys_syscall).
|
||||
|
||||
static unsigned long clockSyscalls = 0; // __NR_clock_gettime through the shim
|
||||
|
||||
long __lolsys_syscall(long a1, long a2, long a3, long a4, long a5, long a6, int nr) {
|
||||
if (nr == SYS_clock_gettime)
|
||||
++clockSyscalls;
|
||||
if (nr == SYS_access && a1 && strcmp((const char*)a1, "/tmp/rclol-fast-clock") == 0)
|
||||
return -ENOENT; // keep the opt-in TSC path out of the test
|
||||
long result = syscall(nr, a1, a2, a3, a4, a5, a6);
|
||||
return result == -1 ? -errno : result;
|
||||
}
|
||||
|
||||
void cerror(int error) {
|
||||
errno = error;
|
||||
}
|
||||
|
||||
int errno_linux_to_bsd(int error) {
|
||||
return error;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Stub ELF bridge that mirrors mldr's elfcalls: dlopen is the host loader.
|
||||
|
||||
static int dlopenCalls = 0, dlsymCalls = 0;
|
||||
static int bridgeHasSymbol = 1;
|
||||
|
||||
static void* bridgeDlopen(const char* name) {
|
||||
++dlopenCalls;
|
||||
return dlopen(name, RTLD_LAZY);
|
||||
}
|
||||
|
||||
static void* bridgeDlsym(void* library, const char* name) {
|
||||
++dlsymCalls;
|
||||
if (!bridgeHasSymbol)
|
||||
return 0;
|
||||
return dlsym(library, name);
|
||||
}
|
||||
|
||||
static struct elf_calls bridge;
|
||||
static struct elf_calls* hostElfcalls = 0; // what the compiled source sees as _elfcalls
|
||||
|
||||
// The compiled clock source refers to these.
|
||||
extern int __linux_clock_gettime(int clockid, struct timespec* res);
|
||||
extern int __darling_vdso_clock_gettime(int clockid, struct timespec* res);
|
||||
extern void __darling_vdso_clock_init(void);
|
||||
struct elf_calls* _elfcalls = 0;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static void fail(const char* message) {
|
||||
fprintf(stderr, "HOST_VDSO_CLOCK pass=0 error=%s (errno=%d)\n", message, errno);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
static void require(int condition, const char* message) {
|
||||
if (!condition)
|
||||
fail(message);
|
||||
}
|
||||
|
||||
static uint64_t rawNsec(int clockid) {
|
||||
struct timespec ts;
|
||||
long result = syscall(SYS_clock_gettime, clockid, &ts);
|
||||
require(result == 0, "raw syscall");
|
||||
return (uint64_t)ts.tv_sec * 1000000000ull + (uint64_t)ts.tv_nsec;
|
||||
}
|
||||
|
||||
static uint64_t nsec(const struct timespec* ts) {
|
||||
return (uint64_t)ts->tv_sec * 1000000000ull + (uint64_t)ts->tv_nsec;
|
||||
}
|
||||
|
||||
static struct timespec sink;
|
||||
|
||||
struct threadArgs {
|
||||
int rounds;
|
||||
int errors;
|
||||
int backwards;
|
||||
};
|
||||
|
||||
static void* threadBody(void* raw) {
|
||||
struct threadArgs* args = (struct threadArgs*)raw;
|
||||
uint64_t last = 0;
|
||||
for (int i = 0; i < args->rounds; ++i) {
|
||||
struct timespec ts;
|
||||
if (__linux_clock_gettime(CLOCK_MONOTONIC, &ts) != 0) {
|
||||
++args->errors;
|
||||
continue;
|
||||
}
|
||||
uint64_t current = nsec(&ts);
|
||||
if (current < last)
|
||||
++args->backwards;
|
||||
last = current;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static double nsPerCall(void (*body)(void), int rounds) {
|
||||
uint64_t begin = rawNsec(CLOCK_MONOTONIC);
|
||||
for (int i = 0; i < rounds; ++i)
|
||||
body();
|
||||
uint64_t finish = rawNsec(CLOCK_MONOTONIC);
|
||||
return (double)(finish - begin) / rounds;
|
||||
}
|
||||
|
||||
static void bodySyscall(void) {
|
||||
syscall(SYS_clock_gettime, CLOCK_MONOTONIC, &sink);
|
||||
}
|
||||
|
||||
static void bodyWrapper(void) {
|
||||
__linux_clock_gettime(CLOCK_MONOTONIC, &sink);
|
||||
}
|
||||
|
||||
static void runThreads(int count, int rounds) {
|
||||
pthread_t threads[8];
|
||||
struct threadArgs args[8];
|
||||
require(count <= 8, "thread count");
|
||||
for (int i = 0; i < count; ++i) {
|
||||
args[i].rounds = rounds;
|
||||
args[i].errors = 0;
|
||||
args[i].backwards = 0;
|
||||
require(pthread_create(&threads[i], 0, threadBody, &args[i]) == 0, "pthread_create");
|
||||
}
|
||||
for (int i = 0; i < count; ++i)
|
||||
pthread_join(threads[i], 0);
|
||||
for (int i = 0; i < count; ++i) {
|
||||
require(args[i].errors == 0, "threaded clock read failed");
|
||||
require(args[i].backwards == 0, "threaded clock read went backwards");
|
||||
}
|
||||
}
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
const char* mode = argc > 1 ? argv[1] : "default";
|
||||
int race = strcmp(mode, "race") == 0;
|
||||
int noSymbol = strcmp(mode, "no-symbol") == 0;
|
||||
bridge.dlopen = bridgeDlopen;
|
||||
bridge.dlsym = bridgeDlsym;
|
||||
bridge.dlclose = 0;
|
||||
struct timespec ts;
|
||||
|
||||
if (race) {
|
||||
// Resolution has not run yet: several threads resolve and read at once.
|
||||
_elfcalls = &bridge;
|
||||
runThreads(8, 20000);
|
||||
require(dlopenCalls >= 1, "race did not resolve");
|
||||
int resolvedDlopenCalls = dlopenCalls;
|
||||
unsigned long resolvedSyscalls = clockSyscalls;
|
||||
require(__darling_vdso_clock_gettime(CLOCK_MONOTONIC, &ts) == 0, "race did not produce a usable entry");
|
||||
require(dlopenCalls == resolvedDlopenCalls, "resolution ran again after the race");
|
||||
require(clockSyscalls == resolvedSyscalls, "resolved entry still used the syscall");
|
||||
} else {
|
||||
// A clock read before the bridge is set must fall back to the syscall
|
||||
// and must not cache "unavailable".
|
||||
unsigned long before = clockSyscalls;
|
||||
require(__linux_clock_gettime(CLOCK_MONOTONIC, &ts) == 0, "null-bridge fallback");
|
||||
require(clockSyscalls == before + 1, "null bridge did not use the syscall");
|
||||
uint64_t value = nsec(&ts), reference = rawNsec(CLOCK_MONOTONIC);
|
||||
require(value <= reference && reference - value < 50000000ull, "fallback value disagrees with the syscall");
|
||||
|
||||
// The bridge appears: the next call resolves the vDSO entry and uses it.
|
||||
bridgeHasSymbol = !noSymbol;
|
||||
_elfcalls = &bridge;
|
||||
unsigned long syscallsBeforeInit = clockSyscalls;
|
||||
__darling_vdso_clock_init();
|
||||
require(dlopenCalls >= 1, "eager init did not resolve");
|
||||
require(clockSyscalls == syscallsBeforeInit, "eager init made a clock syscall");
|
||||
int resolvedDlopenCalls = dlopenCalls;
|
||||
|
||||
if (noSymbol) {
|
||||
require(__darling_vdso_clock_gettime(CLOCK_MONOTONIC, &ts) != 0, "missing symbol reported success");
|
||||
before = clockSyscalls;
|
||||
require(__linux_clock_gettime(CLOCK_MONOTONIC, &ts) == 0, "no-symbol fallback");
|
||||
require(clockSyscalls == before + 1, "no-symbol path did not use the syscall");
|
||||
} else {
|
||||
require(__darling_vdso_clock_gettime(CLOCK_MONOTONIC, &ts) == 0, "resolved vDSO entry was not used");
|
||||
before = clockSyscalls;
|
||||
require(__linux_clock_gettime(CLOCK_MONOTONIC, &ts) == 0, "vDSO read");
|
||||
require(clockSyscalls == before, "vDSO read still used the syscall");
|
||||
value = nsec(&ts);
|
||||
reference = rawNsec(CLOCK_MONOTONIC);
|
||||
require(value <= reference && reference - value < 50000000ull, "vDSO value disagrees with the syscall");
|
||||
|
||||
// Values agree with the syscall across clock ids and the monotonic
|
||||
// clock is monotonic across a million reads.
|
||||
for (int clockid = 0; clockid <= 1; ++clockid) {
|
||||
uint64_t last = 0;
|
||||
for (int i = 0; i < 1000000; ++i) {
|
||||
require(__linux_clock_gettime(clockid, &ts) == 0, "million reads");
|
||||
uint64_t current = nsec(&ts);
|
||||
if (clockid == 1)
|
||||
require(current >= last, "monotonic clock went backwards");
|
||||
last = current;
|
||||
}
|
||||
reference = rawNsec(clockid);
|
||||
require(last <= reference && reference - last < 100000000ull, "clock drifted from the syscall");
|
||||
}
|
||||
|
||||
// Several threads reading at once stay monotonic within each thread.
|
||||
runThreads(8, 20000);
|
||||
require(dlopenCalls == resolvedDlopenCalls, "resolution ran more than once");
|
||||
|
||||
// After fork the child inherits the resolved pointer with no new
|
||||
// resolution; the vDSO mapping is the same in both processes.
|
||||
unsigned long baselineSyscalls = clockSyscalls;
|
||||
pid_t child = fork();
|
||||
require(child >= 0, "fork");
|
||||
if (child == 0) {
|
||||
struct timespec childTs;
|
||||
int ok = __linux_clock_gettime(CLOCK_MONOTONIC, &childTs) == 0
|
||||
&& clockSyscalls == baselineSyscalls && dlopenCalls == resolvedDlopenCalls;
|
||||
_exit(ok ? 0 : 1);
|
||||
}
|
||||
int status = 0;
|
||||
require(waitpid(child, &status, 0) == child && WIFEXITED(status) && WEXITSTATUS(status) == 0,
|
||||
"forked child re-resolved or failed");
|
||||
}
|
||||
}
|
||||
|
||||
// Cost comparison: a raw syscall against the wrapper's steady-state path.
|
||||
double syscallCost = nsPerCall(bodySyscall, 200000);
|
||||
double wrapperCost = nsPerCall(bodyWrapper, 200000);
|
||||
if (!noSymbol) {
|
||||
require(wrapperCost < syscallCost / 2, "vDSO path is not cheaper than the syscall");
|
||||
}
|
||||
printf("HOST_VDSO_CLOCK pass=1 mode=%s syscall_ns=%.0f vdso_ns=%.0f dlopen_calls=%d dlsym_calls=%d "
|
||||
"million_reads=%d threads=%d fork=preserved\n",
|
||||
mode, syscallCost, wrapperCost, dlopenCalls, dlsymCalls,
|
||||
(race || noSymbol) ? 0 : 1, noSymbol ? 0 : 8);
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Check the batch library installer and its rollback in temporary directories.
|
||||
|
||||
The installer is imported, never executed: no prefix, backup directory or
|
||||
running process is touched. What is checked is the policy and the mechanics -
|
||||
the overlays go to the per-user RuntimeLibraries directory, an existing
|
||||
file is backed up before it is replaced, a file the installer created is
|
||||
removed again by --rollback, a replaced file is restored byte for byte, the
|
||||
staged file is verified against the built artifact, and a file that is not a
|
||||
Mach-O library is refused.
|
||||
"""
|
||||
import importlib.util
|
||||
from pathlib import Path
|
||||
import tempfile
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
|
||||
|
||||
def load_installer():
|
||||
path = ROOT / "tools/install-riot-libs-runtime.py"
|
||||
spec = importlib.util.spec_from_file_location("riot_libs_installer", path)
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
def write_library(path, fill):
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_bytes(b"\xcf\xfa\xed\xfe" + bytes([fill]) * 4096)
|
||||
|
||||
|
||||
def check():
|
||||
installer = load_installer()
|
||||
names = [name for _, name in installer.LIBRARIES]
|
||||
assert names == ["libsystem_c.dylib", "libc++.1.dylib",
|
||||
"libc++abi.dylib", "libsystem_platform.dylib"], "unexpected overlay set"
|
||||
with tempfile.TemporaryDirectory(prefix="riot-libs-offline-") as directory:
|
||||
folder = Path(directory)
|
||||
prefix = folder / "prefix"
|
||||
guest_home = "/Users/offline-check"
|
||||
folder_ = prefix / "Users/offline-check/RuntimeLibraries"
|
||||
built = folder / "built"
|
||||
backups = folder / "backups"
|
||||
libraries = []
|
||||
for index, name in enumerate(names):
|
||||
artifact = built / name
|
||||
write_library(artifact, 0x10 + index)
|
||||
libraries.append((artifact, folder_ / name))
|
||||
# the platform library already exists, the others do not
|
||||
write_library(folder_ / "libsystem_platform.dylib", 0x99)
|
||||
previous_platform = installer.digest(folder_ / "libsystem_platform.dylib")
|
||||
|
||||
result = installer.install(libraries, backups)
|
||||
assert result["installed"], "installation failed"
|
||||
for artifact, destination in libraries:
|
||||
assert installer.digest(destination) == installer.digest(artifact), "artifact not installed"
|
||||
assert any(record.get("existed") for record in result["libraries"]), "existing file not recorded"
|
||||
assert any(not record.get("existed") for record in result["libraries"]), "created file not recorded"
|
||||
assert any(installer.digest(path) == previous_platform for path in backups.iterdir()), "no backup staged"
|
||||
assert (backups / "manifest.json").is_file(), "no manifest written"
|
||||
|
||||
# installing again changes nothing and reports every file current
|
||||
again = installer.install(libraries, backups)
|
||||
assert all(record.get("already_current") for record in again["libraries"]), "second run not idempotent"
|
||||
|
||||
# rollback restores the replaced file and removes the created ones
|
||||
rolled = installer.rollback(backups)
|
||||
assert installer.digest(folder_ / "libsystem_platform.dylib") == previous_platform, "replaced file not restored"
|
||||
for name in names:
|
||||
if name == "libsystem_platform.dylib":
|
||||
continue
|
||||
assert not (folder_ / name).exists(), "created file not removed: " + name
|
||||
assert str(folder_ / "libsystem_c.dylib") in rolled["removed"], "removal not reported"
|
||||
|
||||
# a file that is not a Mach-O library is refused
|
||||
broken = folder / "not-a-library.dylib"
|
||||
broken.write_bytes(b"not a mach-o")
|
||||
try:
|
||||
installer.install([(broken, folder_ / "broken.dylib")], backups)
|
||||
raise AssertionError("a non-Mach-O file was accepted")
|
||||
except ValueError as error:
|
||||
assert "Mach-O" in str(error), "unexpected refusal: " + str(error)
|
||||
|
||||
# destinations() puts every overlay in the per-user RuntimeLibraries
|
||||
resolved = installer.destinations(str(prefix), guest_home)
|
||||
assert [str(destination) for _, destination in resolved] == [
|
||||
str(folder_ / name) for name in names], "unexpected destinations"
|
||||
|
||||
# the command-line entry point parses its arguments and installs and rolls
|
||||
# back into a temporary prefix (uses the built artifacts when they exist)
|
||||
if all(artifact.is_file() for artifact, _ in resolved):
|
||||
import contextlib, io
|
||||
cli_prefix = folder / "cli-prefix"
|
||||
cli = ["--prefix", str(cli_prefix), "--guest-home", guest_home,
|
||||
"--backup-dir", str(folder / "cli-backups")]
|
||||
(cli_prefix / "Users/offline-check/RuntimeLibraries").mkdir(parents=True)
|
||||
with contextlib.redirect_stdout(io.StringIO()):
|
||||
installer.main(cli)
|
||||
for name in names:
|
||||
assert (cli_prefix / "Users/offline-check/RuntimeLibraries" / name).is_file(), "entry point did not install " + name
|
||||
with contextlib.redirect_stdout(io.StringIO()):
|
||||
installer.main(cli + ["--rollback"])
|
||||
assert not (cli_prefix / "Users/offline-check/RuntimeLibraries/libsystem_c.dylib").exists(), "entry point did not roll back"
|
||||
print("LIBS_INSTALLER_OFFLINE pass=1 overlays=%d backup_before_replace=1 rollback_restores=1 "
|
||||
"rollback_removes=1 idempotent=1 rejects_non_macho=1" % len(names))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
check()
|
||||
Executable
+125
@@ -0,0 +1,125 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Copy the shipped process-local Mach semaphore code out of mach_traps.c into lsem-shipped.inc.
|
||||
|
||||
The host model (lsem-model.c) compiles this file instead of a hand-written copy, so the model always exercises the
|
||||
code that ships: the (name, generation, value) table, the trap entry points and the create/destroy wrappers are
|
||||
extracted verbatim. Only the platform edges (raw syscalls, darlingserver RPCs, logging) are shimmed by the model.
|
||||
|
||||
The extraction is deliberately strict: every function must be found exactly once and the token take must still be the
|
||||
generation-checked compare-exchange on the value+generation word. If somebody changes the shipped protocol, this
|
||||
fails instead of silently modelling something else.
|
||||
|
||||
Usage: python3 extract-lsem.py [path/to/mach_traps.c] (writes build/riot-lsem-model/lsem-shipped.inc; run.sh does all steps)
|
||||
"""
|
||||
from pathlib import Path
|
||||
import re
|
||||
import sys
|
||||
|
||||
HERE = Path(__file__).resolve().parent
|
||||
ROOT = HERE.parent.parent
|
||||
OUT_DIR = ROOT / 'build/riot-lsem-model'
|
||||
DEFAULT_SRC = (ROOT / 'build/darling-src/src/external/xnu/darling/src/libsystem_kernel/emulation/'
|
||||
'src/xnu_syscall/mach/impl/mach_traps.c')
|
||||
|
||||
BLOCK_START = '#define LSEM_SLOTS'
|
||||
BLOCK_END = 'static volatile int lsem_lock;'
|
||||
|
||||
# In dependency order (the trap entry points are declared in the model's shim section, so post_signal may call one).
|
||||
FUNCTIONS = [
|
||||
'lsem_log',
|
||||
'lsem_decide',
|
||||
'lsem_hash',
|
||||
'lsem_capture',
|
||||
'lsem_find',
|
||||
'lsem_table_lock',
|
||||
'lsem_table_unlock',
|
||||
'lsem_insert',
|
||||
'lsem_remove',
|
||||
'lsem_signal',
|
||||
'lsem_signal_all',
|
||||
'lsem_signal_thread',
|
||||
'lsem_post_signal',
|
||||
'lsem_wait',
|
||||
'semaphore_create',
|
||||
'semaphore_destroy',
|
||||
'__darling_local_semaphores_after_fork',
|
||||
'semaphore_signal_trap_impl',
|
||||
'semaphore_signal_all_trap_impl',
|
||||
'semaphore_signal_thread_trap_impl',
|
||||
'semaphore_wait_trap_impl',
|
||||
'semaphore_wait_signal_trap_impl',
|
||||
'semaphore_timedwait_trap_impl',
|
||||
'semaphore_timedwait_signal_trap_impl',
|
||||
]
|
||||
|
||||
# Assertions on the extracted text: the model must not model a protocol the shipped code no longer uses.
|
||||
REQUIRED = [
|
||||
'compare_exchange_n(&s->pv.pair', # the generation-checked token take
|
||||
'old - 1', # ... that decrements the value
|
||||
'(unsigned int) (old >> 32) != gen', # ... after checking the generation it loaded
|
||||
'LSEM_FUTEX_PRIVATE', # every futex is process-private
|
||||
'LSEM_TOMB_VALUE', # the destroy tombstone is a negative value ...
|
||||
'(int) (unsigned int) old < 0', # ... that no operation can take for a live count
|
||||
'__darling_local_semaphores_after_fork',
|
||||
# the destroy itself must write that value, not a literal (a positive tombstone is the in-flight bug)
|
||||
'(((old >> 32) + 1) << 32) | (unsigned int) LSEM_TOMB_VALUE',
|
||||
]
|
||||
# A positive tombstone literal anywhere is the bug this task fixed: fail loudly instead of modelling it.
|
||||
FORBIDDEN = ['0x40000000']
|
||||
|
||||
|
||||
def extract_block(lines):
|
||||
start = end = None
|
||||
for i, line in enumerate(lines):
|
||||
if line.startswith(BLOCK_START) and start is None:
|
||||
start = i
|
||||
if line.strip() == BLOCK_END:
|
||||
end = i
|
||||
if start is None or end is None or end <= start:
|
||||
sys.exit('extract-lsem: the LSEM define/table block was not found')
|
||||
return lines[start:end + 1]
|
||||
|
||||
|
||||
def extract_function(lines, name):
|
||||
pattern = re.compile(r'^[ \t]*(?:static[ \t]+)?(?:inline[ \t]+)?[A-Za-z_][A-Za-z0-9_ \t\*]*\b'
|
||||
+ re.escape(name) + r'[ \t]*\(')
|
||||
hits = []
|
||||
for i, line in enumerate(lines):
|
||||
if pattern.match(line) and ';' not in line:
|
||||
for j in range(i, min(i + 400, len(lines))):
|
||||
if lines[j].rstrip('\n') == '}':
|
||||
hits.append((i, j))
|
||||
break
|
||||
if len(hits) != 1:
|
||||
sys.exit(f'extract-lsem: expected exactly one definition of {name}, found {hits}')
|
||||
i, j = hits[0]
|
||||
first = i
|
||||
while first > 0 and (lines[first - 1].strip() == 'VISIBLE'
|
||||
or lines[first - 1].startswith('__attribute__')):
|
||||
first -= 1
|
||||
return lines[first:j + 1]
|
||||
|
||||
|
||||
def main():
|
||||
src = Path(sys.argv[1]) if len(sys.argv) > 1 else DEFAULT_SRC
|
||||
lines = src.read_text().splitlines(keepends=True)
|
||||
chunks = [['/* generated by extract-lsem.py from ' + str(src) + ' -- do not edit */\n\n'],
|
||||
extract_block(lines)]
|
||||
for name in FUNCTIONS:
|
||||
chunks.append(['\n'])
|
||||
chunks.append(extract_function(lines, name))
|
||||
text = ''.join(''.join(chunk) for chunk in chunks)
|
||||
for needle in REQUIRED:
|
||||
if needle not in text:
|
||||
sys.exit(f'extract-lsem: the shipped code no longer contains {needle!r}: review the model')
|
||||
for needle in FORBIDDEN:
|
||||
if needle in text:
|
||||
sys.exit(f'extract-lsem: the shipped code contains {needle!r}, which is the fixed bug: review the model')
|
||||
OUT_DIR.mkdir(parents=True, exist_ok=True)
|
||||
out = OUT_DIR / 'lsem-shipped.inc'
|
||||
out.write_text(text)
|
||||
print(f'extract-lsem: wrote {out} ({len(text.splitlines())} lines, {len(FUNCTIONS)} functions)')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,981 @@
|
||||
/* Host model of the shipped process-local Mach semaphore code in
|
||||
* darling/src/libsystem_kernel/emulation/src/xnu_syscall/mach/impl/mach_traps.c
|
||||
*
|
||||
* The model does not re-implement the algorithm: extract-lsem.py copies the `lsem` definitions, the trap entry points
|
||||
* and the create/destroy wrappers verbatim out of the real source into lsem-shipped.inc, which is #included below.
|
||||
* Only the platform edges (raw syscalls, darlingserver RPCs, the league-game check, logging) are replaced by the
|
||||
* shims in this file.
|
||||
*
|
||||
* Build:
|
||||
* python3 extract-lsem.py
|
||||
* cc -O2 -pthread -Wall -o lsem-model lsem-model.c
|
||||
* Run ./lsem-model; the exit status is non-zero if any check failed or any counter below is non-zero. The last line
|
||||
* is the count the fix is judged by: spurious_success / lost_wakeup / steal / hang must all be zero.
|
||||
*
|
||||
* What it checks (see the local semaphore section of docs/STATUS.md):
|
||||
* counting, signal_all, signal_thread, timedwait arithmetic, destroy waking waiters
|
||||
* bug A: find/destroy race, slot reuse under the same name, token stealing by a stale waiter (deterministic and
|
||||
* multithreaded)
|
||||
* bug B: fork (the child must go to the server for inherited names and work locally for its own)
|
||||
* conservative fallback: unknown names, other tasks, failed creates, the switch off
|
||||
* combined wait_signal/timedwait_signal: XNU registration order, both local, one local, both server
|
||||
* stress: stable names with a produced/consumed ledger, waiter+signaler pairs, create/destroy churn with name
|
||||
* reuse, destroy-while-waiting rounds; a 60 s watchdog reports a hang
|
||||
*/
|
||||
#define _GNU_SOURCE
|
||||
#include <errno.h>
|
||||
#include <pthread.h>
|
||||
#include <stdarg.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/syscall.h>
|
||||
#include <sys/types.h>
|
||||
#include <sys/wait.h>
|
||||
#include <time.h>
|
||||
#include <unistd.h>
|
||||
|
||||
/* ------------------------------------------------------------------------------ machine-independent shims ---- */
|
||||
|
||||
typedef int kern_return_t;
|
||||
typedef unsigned int mach_port_name_t;
|
||||
typedef int clock_res_t;
|
||||
|
||||
#define KERN_SUCCESS 0
|
||||
#define KERN_FAILURE 5
|
||||
#define KERN_ABORTED 14
|
||||
#define KERN_INVALID_VALUE 18
|
||||
#define KERN_INVALID_NAME 15
|
||||
#define KERN_TERMINATED 37
|
||||
#define KERN_NOT_WAITING 48
|
||||
#define KERN_OPERATION_TIMED_OUT 49
|
||||
#define LINUX_EINTR 4
|
||||
#define VISIBLE
|
||||
|
||||
#define __NR_read 0
|
||||
#define __NR_write 1
|
||||
#define __NR_open 2
|
||||
#define __NR_close 3
|
||||
#define __NR_access 21
|
||||
#define __NR_getpid 39
|
||||
#define __NR_futex 202
|
||||
#define __NR_clock_gettime 228
|
||||
|
||||
#define MODEL_SELF_TASK 7u
|
||||
/* What the "server" stubs answer for calls that would really go to darlingserver: a positive code no local path can
|
||||
* return, so the tests see exactly which trap fell back. */
|
||||
#define MODEL_SERVER 0x7f00
|
||||
#define MODEL_LOG_FD 42
|
||||
|
||||
static int model_switch_enabled = 1; /* the /tmp/rclol-local-semaphores switch the game process would see */
|
||||
static int model_create_fail; /* make __darling_rpc_semaphore_create fail */
|
||||
static int model_rpc_calls[7]; /* signal, signal_all, wait, wait_signal, timedwait, timedwait_signal, create */
|
||||
static int model_rpc_wait_code = MODEL_SERVER;
|
||||
static int model_rpc_signal_code = KERN_SUCCESS;
|
||||
static int model_unimplemented;
|
||||
static mach_port_name_t model_next_port_name = 0x1000;
|
||||
static mach_port_name_t model_forced_name; /* the name the next create must return (port-name reuse) */
|
||||
static char model_log[4096];
|
||||
static size_t model_log_used;
|
||||
|
||||
static int model_access(const char* path)
|
||||
{
|
||||
if (!model_switch_enabled)
|
||||
return -1;
|
||||
if (!strcmp(path, "/tmp/rclol-local-semaphores-all") || !strcmp(path, "/tmp/rclol-local-semaphores"))
|
||||
return 0;
|
||||
return -1;
|
||||
}
|
||||
|
||||
static long model_syscall(long nr, ...)
|
||||
{
|
||||
va_list ap;
|
||||
long r = -1;
|
||||
va_start(ap, nr);
|
||||
switch (nr) {
|
||||
case __NR_futex: {
|
||||
int* addr = va_arg(ap, int*);
|
||||
int op = va_arg(ap, int);
|
||||
int val = va_arg(ap, int);
|
||||
void* ts = va_arg(ap, void*);
|
||||
(void) va_arg(ap, unsigned long);
|
||||
(void) va_arg(ap, int);
|
||||
r = (long) syscall(SYS_futex, addr, op, val, ts, NULL, 0);
|
||||
break;
|
||||
}
|
||||
case __NR_clock_gettime: {
|
||||
int clk = va_arg(ap, int);
|
||||
struct timespec* ts = va_arg(ap, struct timespec*);
|
||||
r = clock_gettime(clk, ts);
|
||||
break;
|
||||
}
|
||||
case __NR_access:
|
||||
r = model_access(va_arg(ap, const char*));
|
||||
(void) va_arg(ap, int);
|
||||
break;
|
||||
case __NR_getpid:
|
||||
r = getpid();
|
||||
break;
|
||||
case __NR_open: {
|
||||
const char* path = va_arg(ap, const char*);
|
||||
(void) va_arg(ap, int);
|
||||
(void) va_arg(ap, int);
|
||||
r = strstr(path, "rclol-game-decisions.log") ? MODEL_LOG_FD : -1;
|
||||
break;
|
||||
}
|
||||
case __NR_write: {
|
||||
long fd = va_arg(ap, long);
|
||||
const char* buf = va_arg(ap, const char*);
|
||||
int len = va_arg(ap, int);
|
||||
if (fd == MODEL_LOG_FD && len > 0) {
|
||||
size_t room = sizeof(model_log) - 1 - model_log_used;
|
||||
if ((size_t) len > room)
|
||||
len = (int) room;
|
||||
memcpy(model_log + model_log_used, buf, (size_t) len);
|
||||
model_log_used += (size_t) len;
|
||||
model_log[model_log_used] = 0;
|
||||
}
|
||||
r = len;
|
||||
break;
|
||||
}
|
||||
case __NR_close:
|
||||
(void) va_arg(ap, long);
|
||||
r = 0;
|
||||
break;
|
||||
default:
|
||||
fprintf(stderr, "model_syscall: unhandled syscall %ld\n", nr);
|
||||
abort();
|
||||
}
|
||||
va_end(ap);
|
||||
return r;
|
||||
}
|
||||
#define LINUX_SYSCALL(nr, ...) model_syscall((nr), ##__VA_ARGS__)
|
||||
|
||||
static int model_sprintf(char* buf, const char* fmt, ...)
|
||||
{
|
||||
va_list ap;
|
||||
int n;
|
||||
va_start(ap, fmt);
|
||||
n = vsnprintf(buf, 4096, fmt, ap);
|
||||
va_end(ap);
|
||||
return n;
|
||||
}
|
||||
static void model_printf(const char* fmt, ...)
|
||||
{
|
||||
va_list ap;
|
||||
va_start(ap, fmt);
|
||||
vfprintf(stderr, fmt, ap);
|
||||
va_end(ap);
|
||||
}
|
||||
#define __simple_sprintf model_sprintf
|
||||
#define __simple_printf model_printf
|
||||
#define UNIMPLEMENTED_TRAP() do { ++model_unimplemented; } while (0)
|
||||
|
||||
static int darling_is_league_game(void)
|
||||
{
|
||||
return model_switch_enabled;
|
||||
}
|
||||
|
||||
static mach_port_name_t mach_task_self(void)
|
||||
{
|
||||
return MODEL_SELF_TASK;
|
||||
}
|
||||
|
||||
static void dserver_retry_sleep(int attempt)
|
||||
{
|
||||
(void) attempt;
|
||||
}
|
||||
|
||||
static int slowwait_enabled(void)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
static unsigned long long slowwait_now(void)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
static void slowwait_log(const char* what, unsigned int name, unsigned long long start, void** frames, int option, int extra)
|
||||
{
|
||||
(void) what; (void) name; (void) start; (void) frames; (void) option; (void) extra;
|
||||
}
|
||||
|
||||
/* The darlingserver calls the local table has no entry for. A server-side wait would block, which is not modelled;
|
||||
* the code the stub returns is what the trap returns, and the tests use MODEL_SERVER to see the fallback. */
|
||||
static kern_return_t dserver_rpc_semaphore_signal(uint32_t name)
|
||||
{
|
||||
(void) name; ++model_rpc_calls[0]; return model_rpc_signal_code;
|
||||
}
|
||||
static kern_return_t dserver_rpc_semaphore_signal_all(uint32_t name)
|
||||
{
|
||||
(void) name; ++model_rpc_calls[1]; return model_rpc_signal_code;
|
||||
}
|
||||
static kern_return_t dserver_rpc_semaphore_wait(uint32_t name)
|
||||
{
|
||||
(void) name; ++model_rpc_calls[2]; return model_rpc_wait_code;
|
||||
}
|
||||
static kern_return_t dserver_rpc_semaphore_wait_signal(uint32_t wait_name, uint32_t signal_name)
|
||||
{
|
||||
(void) wait_name; (void) signal_name; ++model_rpc_calls[3]; return model_rpc_wait_code;
|
||||
}
|
||||
static kern_return_t dserver_rpc_semaphore_timedwait(uint32_t wait_name, uint32_t sec, uint32_t nsec)
|
||||
{
|
||||
(void) wait_name; (void) sec; (void) nsec; ++model_rpc_calls[4]; return model_rpc_wait_code;
|
||||
}
|
||||
static kern_return_t dserver_rpc_semaphore_timedwait_signal(uint32_t wait_name, uint32_t signal_name, uint32_t sec, uint32_t nsec)
|
||||
{
|
||||
(void) wait_name; (void) signal_name; (void) sec; (void) nsec; ++model_rpc_calls[5]; return model_rpc_wait_code;
|
||||
}
|
||||
static kern_return_t __darling_rpc_semaphore_create(mach_port_name_t task, mach_port_name_t* semaphore, int policy, int value)
|
||||
{
|
||||
(void) task; (void) policy; (void) value; ++model_rpc_calls[6];
|
||||
if (model_create_fail)
|
||||
return KERN_FAILURE;
|
||||
if (model_forced_name) {
|
||||
*semaphore = model_forced_name; /* port-name reuse, as the server is free to hand a freed name out again */
|
||||
model_forced_name = 0;
|
||||
} else {
|
||||
model_next_port_name += 4;
|
||||
*semaphore = model_next_port_name;
|
||||
}
|
||||
return KERN_SUCCESS;
|
||||
}
|
||||
static kern_return_t __darling_rpc_semaphore_destroy(mach_port_name_t task, mach_port_name_t semaphore)
|
||||
{
|
||||
(void) task; (void) semaphore;
|
||||
return KERN_SUCCESS;
|
||||
}
|
||||
|
||||
/* the trap entry points are defined in the shipped block below; lsem_post_signal calls this one */
|
||||
kern_return_t semaphore_signal_trap_impl(mach_port_name_t signal_name);
|
||||
|
||||
/* ------------------------------------------------------------------------------- the shipped code ---------- */
|
||||
|
||||
#include "lsem-shipped.inc"
|
||||
|
||||
/* ------------------------------------------------------------------------------- test harness -------------- */
|
||||
|
||||
static long checks, failures;
|
||||
static long spurious_success, lost_wakeup, steal, hang;
|
||||
static long local_success, terminated, timed_out, server_fallbacks;
|
||||
static volatile int model_done;
|
||||
|
||||
static void check(const char* what, int ok)
|
||||
{
|
||||
++checks;
|
||||
if (!ok) {
|
||||
++failures;
|
||||
printf(" FAIL %s\n", what);
|
||||
}
|
||||
}
|
||||
|
||||
static void note(const char* name)
|
||||
{
|
||||
printf("[%s]\n", name);
|
||||
}
|
||||
|
||||
/* silent create; the caller checks the result so a failure inside a loop prints once */
|
||||
static mach_port_name_t model_create_named(mach_port_name_t name, int value, int* ok)
|
||||
{
|
||||
mach_port_name_t got = 0;
|
||||
int kr;
|
||||
model_forced_name = name;
|
||||
kr = semaphore_create(MODEL_SELF_TASK, &got, 0, value);
|
||||
if (ok)
|
||||
*ok = (kr == KERN_SUCCESS && (name == 0 || got == name));
|
||||
return got;
|
||||
}
|
||||
|
||||
static mach_port_name_t model_create(int value)
|
||||
{
|
||||
int ok = 0;
|
||||
mach_port_name_t name = model_create_named(0, value, &ok);
|
||||
check("semaphore_create succeeds and returns a name", ok && name != 0 && name != LSEM_TOMB);
|
||||
return name;
|
||||
}
|
||||
|
||||
static void model_destroy(mach_port_name_t name)
|
||||
{
|
||||
semaphore_destroy(MODEL_SELF_TASK, name);
|
||||
}
|
||||
|
||||
static int model_timedwait(mach_port_name_t name, long long ns)
|
||||
{
|
||||
int kr = semaphore_timedwait_trap_impl(name, (unsigned int) (ns / 1000000000ll), (clock_res_t) (ns % 1000000000ll));
|
||||
if (kr == KERN_SUCCESS) ++local_success;
|
||||
else if (kr == KERN_OPERATION_TIMED_OUT) ++timed_out;
|
||||
else if (kr == MODEL_SERVER) ++server_fallbacks;
|
||||
return kr;
|
||||
}
|
||||
|
||||
static void wait_until_waiters(mach_port_name_t name, int n)
|
||||
{
|
||||
for (int i = 0; i < 20000; ++i) {
|
||||
struct lsem* s = lsem_find(name);
|
||||
if (s && __atomic_load_n(&s->waiters, __ATOMIC_SEQ_CST) >= n)
|
||||
return;
|
||||
usleep(200);
|
||||
}
|
||||
++hang;
|
||||
printf(" FAIL waiters of name %u never reached %d\n", name, n);
|
||||
}
|
||||
|
||||
struct waiter_arg {
|
||||
mach_port_name_t name;
|
||||
mach_port_name_t second; /* the signal side of the combined traps */
|
||||
long long timeout_ns; /* -1: plain wait */
|
||||
int result;
|
||||
};
|
||||
|
||||
static void* waiter_thread(void* p)
|
||||
{
|
||||
struct waiter_arg* a = p;
|
||||
if (a->second)
|
||||
a->result = semaphore_wait_signal_trap_impl(a->name, a->second);
|
||||
else if (a->timeout_ns < 0)
|
||||
a->result = semaphore_wait_trap_impl(a->name);
|
||||
else
|
||||
a->result = semaphore_timedwait_trap_impl(a->name, (unsigned int) (a->timeout_ns / 1000000000ll),
|
||||
(clock_res_t) (a->timeout_ns % 1000000000ll));
|
||||
return 0;
|
||||
}
|
||||
|
||||
static long long now_ns(void)
|
||||
{
|
||||
struct timespec ts;
|
||||
clock_gettime(CLOCK_MONOTONIC, &ts);
|
||||
return (long long) ts.tv_sec * 1000000000ll + ts.tv_nsec;
|
||||
}
|
||||
|
||||
static void model_clear_table(void)
|
||||
{
|
||||
for (unsigned int i = 0; i < LSEM_SLOTS; ++i) {
|
||||
unsigned long long old = __atomic_load_n(&lsems[i].pv.pair, __ATOMIC_RELAXED);
|
||||
__atomic_store_n(&lsems[i].pv.pair, (((old >> 32) + 1) << 32) | 0, __ATOMIC_RELEASE);
|
||||
__atomic_store_n(&lsems[i].name, LSEM_TOMB, __ATOMIC_RELEASE);
|
||||
__atomic_store_n(&lsems[i].waiters, 0, __ATOMIC_RELAXED);
|
||||
}
|
||||
__atomic_store_n(&lsem_state, 1, __ATOMIC_RELEASE);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------------------------------- tests -------- */
|
||||
|
||||
/* Runs in a fresh child before this process decided anything: the switch files are read at the first create. */
|
||||
static void test_switch_off_child(void)
|
||||
{
|
||||
pid_t pid = fork();
|
||||
int status = 0;
|
||||
note("switch off (fresh process)");
|
||||
if (pid == 0) {
|
||||
mach_port_name_t n = 0;
|
||||
model_switch_enabled = 0;
|
||||
semaphore_create(MODEL_SELF_TASK, &n, 0, 1);
|
||||
int local = lsem_find(n) != 0;
|
||||
int kr = semaphore_wait_trap_impl(n);
|
||||
_exit(local ? 1 : (kr == MODEL_SERVER ? 0 : 2));
|
||||
}
|
||||
waitpid(pid, &status, 0);
|
||||
check("with the switch off nothing is served locally", WIFEXITED(status) && WEXITSTATUS(status) == 0);
|
||||
}
|
||||
|
||||
static void test_decide_and_log(void)
|
||||
{
|
||||
note("decide/log");
|
||||
check("the switch turns local semaphores on", lsem_decide() == 1);
|
||||
check("the decision is logged for the game process", strstr(model_log, "local_semaphores=1") != 0);
|
||||
{
|
||||
size_t before = model_log_used;
|
||||
check("deciding again returns the same answer", lsem_decide() == 1);
|
||||
check("and does not log a second line", model_log_used == before);
|
||||
}
|
||||
model_clear_table();
|
||||
}
|
||||
|
||||
static void test_counting(void)
|
||||
{
|
||||
mach_port_name_t a = model_create(0);
|
||||
mach_port_name_t b = model_create(2);
|
||||
note("counting");
|
||||
check("empty semaphore times out immediately", model_timedwait(a, 0) == KERN_OPERATION_TIMED_OUT);
|
||||
check("banked signals are consumed one by one", model_timedwait(b, 0) == KERN_SUCCESS
|
||||
&& model_timedwait(b, 0) == KERN_SUCCESS
|
||||
&& model_timedwait(b, 0) == KERN_OPERATION_TIMED_OUT);
|
||||
check("a signal on a local semaphore is local", semaphore_signal_trap_impl(b) == KERN_SUCCESS
|
||||
&& model_timedwait(b, 0) == KERN_SUCCESS);
|
||||
model_destroy(a);
|
||||
model_destroy(b);
|
||||
}
|
||||
|
||||
static void test_signal_all(void)
|
||||
{
|
||||
mach_port_name_t s = model_create(0);
|
||||
pthread_t t[4];
|
||||
struct waiter_arg a[4];
|
||||
note("signal_all");
|
||||
check("signal_all with nobody waiting is a no-op and banks nothing",
|
||||
semaphore_signal_all_trap_impl(s) == KERN_SUCCESS && model_timedwait(s, 0) == KERN_OPERATION_TIMED_OUT);
|
||||
for (int i = 0; i < 4; ++i) {
|
||||
a[i].name = s; a[i].second = 0; a[i].timeout_ns = 2000000000ll; a[i].result = -1;
|
||||
pthread_create(&t[i], 0, waiter_thread, &a[i]);
|
||||
}
|
||||
wait_until_waiters(s, 4);
|
||||
check("signal_all wakes all four waiters", semaphore_signal_all_trap_impl(s) == KERN_SUCCESS);
|
||||
for (int i = 0; i < 4; ++i)
|
||||
pthread_join(t[i], 0);
|
||||
check("every waiter returned SUCCESS", a[0].result == KERN_SUCCESS && a[1].result == KERN_SUCCESS
|
||||
&& a[2].result == KERN_SUCCESS && a[3].result == KERN_SUCCESS);
|
||||
check("signal_all banks nothing for later waiters", model_timedwait(s, 0) == KERN_OPERATION_TIMED_OUT);
|
||||
model_destroy(s);
|
||||
}
|
||||
|
||||
static void test_signal_thread(void)
|
||||
{
|
||||
mach_port_name_t s = model_create(0);
|
||||
pthread_t t;
|
||||
struct waiter_arg a = { .name = s, .second = 0, .timeout_ns = 2000000000ll, .result = -1 };
|
||||
note("signal_thread");
|
||||
check("signal_thread(any) with nobody waiting is KERN_NOT_WAITING", semaphore_signal_thread_trap_impl(s, 0) == KERN_NOT_WAITING);
|
||||
pthread_create(&t, 0, waiter_thread, &a);
|
||||
wait_until_waiters(s, 1);
|
||||
check("signal_thread(any) wakes one waiter", semaphore_signal_thread_trap_impl(s, 0) == KERN_SUCCESS);
|
||||
pthread_join(t, 0);
|
||||
check("the waiter returned SUCCESS", a.result == KERN_SUCCESS);
|
||||
check("signal_thread with a real thread is KERN_NOT_WAITING", semaphore_signal_thread_trap_impl(s, 0x1234) == KERN_NOT_WAITING);
|
||||
check("and banks nothing", model_timedwait(s, 0) == KERN_OPERATION_TIMED_OUT);
|
||||
model_destroy(s);
|
||||
}
|
||||
|
||||
static void test_destroy_wakes(void)
|
||||
{
|
||||
mach_port_name_t n = model_create(0);
|
||||
pthread_t t[3];
|
||||
struct waiter_arg a[3];
|
||||
note("destroy");
|
||||
for (int i = 0; i < 3; ++i) {
|
||||
a[i].name = n; a[i].second = 0; a[i].timeout_ns = 2000000000ll; a[i].result = -1;
|
||||
pthread_create(&t[i], 0, waiter_thread, &a[i]);
|
||||
}
|
||||
wait_until_waiters(n, 3);
|
||||
model_destroy(n);
|
||||
for (int i = 0; i < 3; ++i)
|
||||
pthread_join(t[i], 0);
|
||||
check("all three waiters got KERN_TERMINATED", a[0].result == KERN_TERMINATED && a[1].result == KERN_TERMINATED
|
||||
&& a[2].result == KERN_TERMINATED);
|
||||
check("the destroyed name goes to the server afterwards", semaphore_wait_trap_impl(n) == MODEL_SERVER);
|
||||
}
|
||||
|
||||
static void test_find_destroy_race(void)
|
||||
{
|
||||
mach_port_name_t n = model_create(0);
|
||||
struct lsem* s = lsem_find(n);
|
||||
unsigned int gen = lsem_capture(s);
|
||||
int kr;
|
||||
note("find/destroy race (bug A)");
|
||||
check("the object is found", s != 0 && s->name == n);
|
||||
model_destroy(n);
|
||||
kr = lsem_wait(s, n, gen, 0, 0);
|
||||
check("a wait that captured the object before the destroy is KERN_TERMINATED", kr == KERN_TERMINATED);
|
||||
if (kr == KERN_SUCCESS) ++spurious_success;
|
||||
check("a signal that captured the object before the destroy is KERN_TERMINATED", lsem_signal(s, n, gen) == KERN_TERMINATED);
|
||||
check("signal_all likewise", lsem_signal_all(s, n, gen) == KERN_TERMINATED);
|
||||
check("signal_thread likewise", lsem_signal_thread(s, n, gen, 0) == KERN_TERMINATED);
|
||||
}
|
||||
|
||||
/* A trap that resolves the slot while a destroy is in flight sees the generation and the tombstone value the
|
||||
* destroy wrote (its two stores are the pair and the name, and another CPU can see the pair first). It must report
|
||||
* KERN_TERMINATED, never take the tombstone for a token. Reading the slot right after the destroy is exactly the
|
||||
* state such a trap sees; the check drives the *shipped* destroy, so it cannot pass on a positive tombstone. */
|
||||
static void test_inflight_destroy(void)
|
||||
{
|
||||
mach_port_name_t n = model_create(0);
|
||||
struct lsem* s = lsem_find(n);
|
||||
unsigned long long pair;
|
||||
unsigned int gen;
|
||||
int kr;
|
||||
note("destroy in flight: the tombstone value must never be a token");
|
||||
check("the object is found", s != 0);
|
||||
semaphore_signal_trap_impl(n); /* a real token is banked */
|
||||
model_destroy(n); /* gen+1 and the tombstone value, then the name */
|
||||
pair = __atomic_load_n(&s->pv.pair, __ATOMIC_SEQ_CST);
|
||||
check("the destroy's tombstone value is negative so no token take can accept it",
|
||||
(int) (unsigned int) pair < 0);
|
||||
/* Simulate what a trap that resolved the slot during the destroy's two stores sees: the pair is the destroy's,
|
||||
* the name is still the one its own load returned (before the destroy's name store landed). */
|
||||
__atomic_store_n(&s->name, n, __ATOMIC_RELEASE);
|
||||
gen = lsem_capture(s); /* what that trap would capture */
|
||||
kr = lsem_wait(s, n, gen, 0, 0);
|
||||
check("the wait reports KERN_TERMINATED instead of consuming the tombstone", kr == KERN_TERMINATED);
|
||||
if (kr == KERN_SUCCESS) ++spurious_success;
|
||||
check("a signal on the tombstone value is KERN_TERMINATED", lsem_signal(s, n, gen) == KERN_TERMINATED);
|
||||
check("signal_all likewise", lsem_signal_all(s, n, gen) == KERN_TERMINATED);
|
||||
check("signal_thread likewise", lsem_signal_thread(s, n, gen, 0) == KERN_TERMINATED);
|
||||
model_destroy(n);
|
||||
}
|
||||
|
||||
/* Fill the table except one slot, then create/destroy/re-create the same name: every generation gets the same slot.
|
||||
* A waiter that resolved the previous generation must not consume the new object's token (bug A, second half), and
|
||||
* the tombstone sentinel must never be reported as a success. */
|
||||
#define STEAL_ROUNDS 400
|
||||
static void test_slot_reuse_steal(void)
|
||||
{
|
||||
unsigned int i;
|
||||
const mach_port_name_t name = 0x900000u;
|
||||
long bad = 0, rounds = 0;
|
||||
note("slot reuse / token steal (bug A)");
|
||||
model_clear_table();
|
||||
for (i = 0; i < LSEM_SLOTS - 1; ++i)
|
||||
lsem_insert(0x100000u + i * 8u, 0);
|
||||
check("the table is full except one slot", lsem_find(0x100000u) != 0 && lsem_find(0x100000u + (LSEM_SLOTS - 2) * 8u) != 0);
|
||||
for (int round = 0; round < STEAL_ROUNDS; ++round) {
|
||||
struct lsem* s;
|
||||
unsigned int gen, tomb_gen;
|
||||
int kr, kr2, kr3, ok1 = 0, ok2 = 0;
|
||||
mach_port_name_t created = model_create_named(name, 0, &ok1);
|
||||
if (!ok1) { ++bad; break; }
|
||||
s = lsem_find(created);
|
||||
if (!s) { ++bad; break; }
|
||||
gen = lsem_capture(s);
|
||||
model_destroy(created);
|
||||
tomb_gen = lsem_capture(s); /* the generation the tombstone holds */
|
||||
model_create_named(name, 0, &ok2); /* same slot, new generation, no token */
|
||||
if (!ok2 || lsem_find(name) != s) { ++bad; break; }
|
||||
semaphore_signal_trap_impl(name); /* the new object now has exactly one token */
|
||||
kr = lsem_wait(s, name, gen, 0, 0); /* the stale waiter of the pre-destroy generation */
|
||||
kr3 = lsem_wait(s, name, tomb_gen, 0, 0); /* a trap that saw the destroy's tombstone generation */
|
||||
if (kr == KERN_SUCCESS || kr3 == KERN_SUCCESS) {
|
||||
++spurious_success;
|
||||
++bad;
|
||||
}
|
||||
kr2 = model_timedwait(name, 0); /* the new object's token must still be there */
|
||||
if (kr2 != KERN_SUCCESS)
|
||||
++bad;
|
||||
model_destroy(name);
|
||||
++rounds;
|
||||
}
|
||||
check("400 re-use rounds: no stale success, no stolen token", bad == 0 && rounds == STEAL_ROUNDS);
|
||||
model_clear_table();
|
||||
}
|
||||
|
||||
static void test_fork(void)
|
||||
{
|
||||
pid_t pid;
|
||||
int status = 0;
|
||||
note("fork (bug B)");
|
||||
{
|
||||
mach_port_name_t n = model_create(1);
|
||||
int kr;
|
||||
pid = fork();
|
||||
if (pid == 0) {
|
||||
__darling_local_semaphores_after_fork();
|
||||
/* the inherited name must go to the server now... */
|
||||
int inherited = semaphore_wait_trap_impl(n) == MODEL_SERVER ? 0 : 1;
|
||||
/* ...and the child's own semaphores must work locally */
|
||||
mach_port_name_t own = 0;
|
||||
semaphore_create(MODEL_SELF_TASK, &own, 0, 1);
|
||||
int mine = semaphore_wait_trap_impl(own) == KERN_SUCCESS ? 0 : 1;
|
||||
_exit(inherited | (mine << 1));
|
||||
}
|
||||
waitpid(pid, &status, 0);
|
||||
check("the child sends inherited names to the server and serves its own locally",
|
||||
WIFEXITED(status) && WEXITSTATUS(status) == 0);
|
||||
kr = model_timedwait(n, 0);
|
||||
check("the parent's local entry still has its token", kr == KERN_SUCCESS);
|
||||
model_destroy(n);
|
||||
}
|
||||
{
|
||||
/* the child's own table must really be empty: it may not see the parent's entries at all */
|
||||
mach_port_name_t n = model_create(1);
|
||||
pid = fork();
|
||||
if (pid == 0) {
|
||||
__darling_local_semaphores_after_fork();
|
||||
_exit(lsem_find(n) == 0 ? 0 : 1);
|
||||
}
|
||||
waitpid(pid, &status, 0);
|
||||
check("the child's table has no inherited entry", WIFEXITED(status) && WEXITSTATUS(status) == 0);
|
||||
model_destroy(n);
|
||||
}
|
||||
}
|
||||
|
||||
static void test_fallback(void)
|
||||
{
|
||||
mach_port_name_t unknown = 0x707000u;
|
||||
mach_port_name_t other = 0;
|
||||
int signals_before = model_rpc_calls[0];
|
||||
note("fallback");
|
||||
check("an unknown name goes to the server", semaphore_wait_trap_impl(unknown) == MODEL_SERVER);
|
||||
check("signal too", semaphore_signal_trap_impl(unknown) == KERN_SUCCESS && model_rpc_calls[0] > signals_before);
|
||||
check("signal_all too", semaphore_signal_all_trap_impl(unknown + 4) == KERN_SUCCESS);
|
||||
check("timedwait too", semaphore_timedwait_trap_impl(unknown, 0, 1000000) == MODEL_SERVER);
|
||||
check("wait_signal too", semaphore_wait_signal_trap_impl(unknown, unknown + 1) == MODEL_SERVER);
|
||||
check("timedwait_signal too", semaphore_timedwait_signal_trap_impl(unknown, unknown + 1, 0, 1000000) == MODEL_SERVER);
|
||||
check("signal_thread (no server support) stays unimplemented", semaphore_signal_thread_trap_impl(unknown, 0) == KERN_FAILURE);
|
||||
check("semaphore_create for another task succeeds server-side",
|
||||
semaphore_create(99u, &other, 0, 1) == KERN_SUCCESS && lsem_find(other) == 0);
|
||||
check("its wait goes to the server", semaphore_wait_trap_impl(other) == MODEL_SERVER);
|
||||
model_create_fail = 1;
|
||||
{
|
||||
mach_port_name_t failed = 0;
|
||||
check("a failed create fails", semaphore_create(MODEL_SELF_TASK, &failed, 0, 1) != KERN_SUCCESS);
|
||||
check("and leaves no entry behind", lsem_find(failed) == 0);
|
||||
}
|
||||
model_create_fail = 0;
|
||||
{
|
||||
/* only the signal side is local: it is signalled locally, the wait stays in the server */
|
||||
mach_port_name_t local_signal = model_create(0);
|
||||
int kr = semaphore_wait_signal_trap_impl(unknown, local_signal);
|
||||
check("wait_signal(server wait, local signal) signals locally", kr == MODEL_SERVER
|
||||
&& model_timedwait(local_signal, 0) == KERN_SUCCESS);
|
||||
model_destroy(local_signal);
|
||||
}
|
||||
{
|
||||
/* only the wait side is local: the signal goes to the server, the wait keeps its token */
|
||||
mach_port_name_t local_wait = model_create(1);
|
||||
int before = model_rpc_calls[0];
|
||||
int kr = semaphore_wait_signal_trap_impl(local_wait, unknown);
|
||||
check("wait_signal(local wait, server signal) keeps its local token", kr == KERN_SUCCESS);
|
||||
check("and signalled the server side", model_rpc_calls[0] == before + 1);
|
||||
model_destroy(local_wait);
|
||||
}
|
||||
}
|
||||
|
||||
static void test_combined_local(void)
|
||||
{
|
||||
mach_port_name_t w = model_create(0);
|
||||
mach_port_name_t s = model_create(0);
|
||||
pthread_t t;
|
||||
struct waiter_arg a = { .name = w, .second = s, .timeout_ns = -1, .result = -2 };
|
||||
note("combined traps, both local");
|
||||
pthread_create(&t, 0, waiter_thread, &a);
|
||||
wait_until_waiters(w, 1);
|
||||
check("the wait side registered itself", __atomic_load_n(&lsem_find(w)->waiters, __ATOMIC_SEQ_CST) >= 1);
|
||||
check("and the signal side was signalled", semaphore_timedwait_trap_impl(s, 0, 0) == KERN_SUCCESS);
|
||||
check("the combined wait has not returned yet", __atomic_load_n(&a.result, __ATOMIC_SEQ_CST) == -2);
|
||||
semaphore_signal_trap_impl(w);
|
||||
pthread_join(t, 0);
|
||||
check("the combined wait then returned SUCCESS", a.result == KERN_SUCCESS);
|
||||
model_destroy(w);
|
||||
model_destroy(s);
|
||||
}
|
||||
|
||||
/* XNU registers the wait and only then signals the signal semaphore. Observable: the signal semaphore has a token
|
||||
* while the wait side's registration is already visible. */
|
||||
static void test_combined_order(void)
|
||||
{
|
||||
mach_port_name_t w = model_create(0);
|
||||
mach_port_name_t s = model_create(0);
|
||||
struct lsem* ws = lsem_find(w);
|
||||
struct lsem* ss = lsem_find(s);
|
||||
pthread_t t;
|
||||
struct waiter_arg a = { .name = w, .second = s, .timeout_ns = -1, .result = -2 };
|
||||
int token_before_registration = 0, saw_token = 0;
|
||||
note("combined traps, XNU registration order");
|
||||
pthread_create(&t, 0, waiter_thread, &a);
|
||||
for (int i = 0; i < 20000; ++i) {
|
||||
if (__atomic_load_n(&ss->pv.w.value, __ATOMIC_SEQ_CST) > 0) {
|
||||
saw_token = 1;
|
||||
if (__atomic_load_n(&ws->waiters, __ATOMIC_SEQ_CST) < 1)
|
||||
token_before_registration = 1;
|
||||
break;
|
||||
}
|
||||
if (__atomic_load_n(&a.result, __ATOMIC_SEQ_CST) != -2)
|
||||
break;
|
||||
usleep(50);
|
||||
}
|
||||
semaphore_signal_trap_impl(w);
|
||||
pthread_join(t, 0);
|
||||
check("the signal side saw its token", saw_token);
|
||||
check("the signal was issued only after the wait registered itself", !token_before_registration);
|
||||
check("the combined wait returned SUCCESS", a.result == KERN_SUCCESS);
|
||||
model_destroy(w);
|
||||
model_destroy(s);
|
||||
}
|
||||
|
||||
static void test_timeout(void)
|
||||
{
|
||||
mach_port_name_t n = model_create(0);
|
||||
long long t0, dt;
|
||||
note("timeout arithmetic");
|
||||
t0 = now_ns();
|
||||
check("timedwait(0,0) is a non-blocking poll", model_timedwait(n, 0) == KERN_OPERATION_TIMED_OUT);
|
||||
check("and returns immediately", now_ns() - t0 < 5000000);
|
||||
t0 = now_ns();
|
||||
check("timedwait(5ms) times out", model_timedwait(n, 5000000) == KERN_OPERATION_TIMED_OUT);
|
||||
dt = now_ns() - t0;
|
||||
check("after roughly 5ms", dt >= 4000000 && dt < 500000000);
|
||||
check("nsec >= 1e9 is KERN_INVALID_VALUE", semaphore_timedwait_trap_impl(n, 0, 1000000000) == KERN_INVALID_VALUE);
|
||||
check("negative nsec is KERN_INVALID_VALUE", semaphore_timedwait_trap_impl(n, 0, -1) == KERN_INVALID_VALUE);
|
||||
{
|
||||
pthread_t t;
|
||||
struct waiter_arg a = { .name = n, .second = 0, .timeout_ns = 30000000ll, .result = -1 };
|
||||
pthread_create(&t, 0, waiter_thread, &a);
|
||||
usleep(2000);
|
||||
semaphore_signal_trap_impl(n);
|
||||
pthread_join(t, 0);
|
||||
check("a signal before the deadline wins over the timeout", a.result == KERN_SUCCESS);
|
||||
}
|
||||
model_destroy(n);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------------------- multithreaded stress ------ */
|
||||
|
||||
#define STRESS_THREADS 8
|
||||
#define STRESS_NAMES 24
|
||||
#define STRESS_OPS 10000
|
||||
|
||||
static unsigned int stress_names[STRESS_NAMES];
|
||||
static long produced[STRESS_NAMES], consumed[STRESS_NAMES];
|
||||
|
||||
static void* stress_accounting(void* p)
|
||||
{
|
||||
unsigned int seed = (unsigned int) (uintptr_t) p;
|
||||
for (int i = 0; i < STRESS_OPS; ++i) {
|
||||
unsigned int which = rand_r(&seed) % STRESS_NAMES;
|
||||
switch (rand_r(&seed) % 5) {
|
||||
case 0: case 1:
|
||||
if (semaphore_signal_trap_impl(stress_names[which]) == KERN_SUCCESS)
|
||||
__atomic_add_fetch(&produced[which], 1, __ATOMIC_RELAXED);
|
||||
break;
|
||||
case 2:
|
||||
if (model_timedwait(stress_names[which], 100000) == KERN_SUCCESS)
|
||||
__atomic_add_fetch(&consumed[which], 1, __ATOMIC_RELAXED);
|
||||
break;
|
||||
case 3:
|
||||
/* a wait that only blocks if a signal raced in; the produced/consumed ledger counts it when it
|
||||
* succeeds, so a success without a token is caught below */
|
||||
if (semaphore_timedwait_trap_impl(stress_names[which], 0, 1000) == KERN_SUCCESS)
|
||||
__atomic_add_fetch(&consumed[which], 1, __ATOMIC_RELAXED);
|
||||
break;
|
||||
case 4:
|
||||
if (model_timedwait(stress_names[which], 0) == KERN_SUCCESS)
|
||||
__atomic_add_fetch(&consumed[which], 1, __ATOMIC_RELAXED);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void test_stress_accounting(void)
|
||||
{
|
||||
pthread_t t[STRESS_THREADS];
|
||||
long bad = 0;
|
||||
note("stress: stable names, produced/consumed ledger");
|
||||
for (int i = 0; i < STRESS_NAMES; ++i) {
|
||||
stress_names[i] = 0x200000u + (unsigned) i * 4u;
|
||||
model_create_named(stress_names[i], 0, 0);
|
||||
produced[i] = consumed[i] = 0;
|
||||
}
|
||||
for (int i = 0; i < STRESS_THREADS; ++i)
|
||||
pthread_create(&t[i], 0, stress_accounting, (void*) (uintptr_t) (i + 1));
|
||||
for (int i = 0; i < STRESS_THREADS; ++i)
|
||||
pthread_join(t[i], 0);
|
||||
for (int i = 0; i < STRESS_NAMES; ++i)
|
||||
if (consumed[i] > produced[i])
|
||||
++bad;
|
||||
check("no name consumed a token that was never produced (no spurious success)", bad == 0);
|
||||
if (bad)
|
||||
spurious_success += bad;
|
||||
for (int i = 0; i < STRESS_NAMES; ++i)
|
||||
model_destroy(stress_names[i]);
|
||||
}
|
||||
|
||||
#define WAKE_ROUNDS 300
|
||||
static void test_wake_pairs(void)
|
||||
{
|
||||
mach_port_name_t n = model_create(0);
|
||||
long lost = 0;
|
||||
note("stress: waiter/signaler pairs");
|
||||
for (int i = 0; i < WAKE_ROUNDS; ++i) {
|
||||
pthread_t t;
|
||||
struct waiter_arg a = { .name = n, .second = 0, .timeout_ns = 1000000000ll, .result = -1 };
|
||||
pthread_create(&t, 0, waiter_thread, &a);
|
||||
wait_until_waiters(n, 1);
|
||||
semaphore_signal_trap_impl(n);
|
||||
pthread_join(t, 0);
|
||||
if (a.result != KERN_SUCCESS)
|
||||
++lost;
|
||||
}
|
||||
lost_wakeup += lost;
|
||||
check("no signal was lost", lost == 0);
|
||||
model_destroy(n);
|
||||
}
|
||||
|
||||
/* create/destroy churn with name reuse; hammer threads look for a stale wait that reports success between the
|
||||
* re-create and the first signal. Each churn thread owns two names, and the window flag is per name: inside a
|
||||
* name's window that object's value is zero and nobody signals it, so a successful wait there can only have taken
|
||||
* a token that was never produced (a stale waiter consuming the re-created object's token or the tombstone). */
|
||||
#define CHURN_NAMES 8
|
||||
#define CHURN_THREADS 4
|
||||
#define CHURN_ROUNDS 3000
|
||||
static unsigned int churn_names[CHURN_NAMES];
|
||||
static volatile int churn_window[CHURN_NAMES];
|
||||
|
||||
static void* churn_thread(void* p)
|
||||
{
|
||||
unsigned int index = (unsigned int) (uintptr_t) p; /* 0..CHURN_THREADS-1 */
|
||||
unsigned int seed = index * 2654435761u + 1u;
|
||||
for (int i = 0; i < CHURN_ROUNDS; ++i) {
|
||||
unsigned int which = index * 2u + (unsigned int) (rand_r(&seed) & 1u);
|
||||
mach_port_name_t name = churn_names[which];
|
||||
int ok = 0;
|
||||
__atomic_store_n(&churn_window[which], 1, __ATOMIC_SEQ_CST);
|
||||
model_destroy(name);
|
||||
model_create_named(name, 0, &ok); /* same port name handed out again */
|
||||
for (volatile int spin = 0; spin < 2000; ++spin)
|
||||
;
|
||||
__atomic_store_n(&churn_window[which], 0, __ATOMIC_SEQ_CST);
|
||||
semaphore_signal_trap_impl(name);
|
||||
semaphore_signal_trap_impl(name);
|
||||
(void) model_timedwait(name, 0);
|
||||
(void) semaphore_signal_all_trap_impl(name);
|
||||
/* leave the object empty: the next window assumes no token exists when it opens */
|
||||
while (semaphore_timedwait_trap_impl(name, 0, 0) == KERN_SUCCESS)
|
||||
;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void* churn_hammer(void* p)
|
||||
{
|
||||
unsigned int seed = (unsigned int) (uintptr_t) p;
|
||||
for (int i = 0; i < CHURN_ROUNDS * 4; ++i) {
|
||||
unsigned int which = rand_r(&seed) % CHURN_NAMES;
|
||||
mach_port_name_t name = churn_names[which];
|
||||
struct lsem* s;
|
||||
unsigned int gen;
|
||||
int open_before, open_after;
|
||||
if (!__atomic_load_n(&churn_window[which], __ATOMIC_SEQ_CST))
|
||||
continue;
|
||||
s = lsem_find(name);
|
||||
if (!s)
|
||||
continue;
|
||||
gen = lsem_capture(s);
|
||||
open_before = __atomic_load_n(&churn_window[which], __ATOMIC_SEQ_CST);
|
||||
/* the object may have been destroyed and re-created between the find and the wait; its re-created
|
||||
* generation starts empty and is only signalled after the window closes */
|
||||
if (lsem_wait(s, name, gen, 0, 0) == KERN_SUCCESS) {
|
||||
open_after = __atomic_load_n(&churn_window[which], __ATOMIC_SEQ_CST);
|
||||
if (open_before && open_after)
|
||||
__atomic_add_fetch(&steal, 1, __ATOMIC_RELAXED);
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void test_stress_churn(void)
|
||||
{
|
||||
pthread_t t[CHURN_THREADS], h[3];
|
||||
long steal_before = steal;
|
||||
note("stress: create/destroy churn, name reuse, stale waiters");
|
||||
for (int i = 0; i < CHURN_NAMES; ++i) {
|
||||
churn_names[i] = 0x300000u + (unsigned) i * 4u;
|
||||
model_create_named(churn_names[i], 0, 0);
|
||||
churn_window[i] = 0;
|
||||
}
|
||||
for (int i = 0; i < CHURN_THREADS; ++i)
|
||||
pthread_create(&t[i], 0, churn_thread, (void*) (uintptr_t) i);
|
||||
for (int i = 0; i < 3; ++i)
|
||||
pthread_create(&h[i], 0, churn_hammer, (void*) (uintptr_t) (i + 11));
|
||||
for (int i = 0; i < CHURN_THREADS; ++i)
|
||||
pthread_join(t[i], 0);
|
||||
for (int i = 0; i < 3; ++i)
|
||||
pthread_join(h[i], 0);
|
||||
check("no stale waiter consumed a token of a reused slot", steal == steal_before);
|
||||
for (int i = 0; i < CHURN_NAMES; ++i)
|
||||
model_destroy(churn_names[i]);
|
||||
}
|
||||
|
||||
#define DESTROY_ROUNDS 300
|
||||
static void test_destroy_while_waiting(void)
|
||||
{
|
||||
mach_port_name_t n;
|
||||
long bad = 0, stolen = 0;
|
||||
note("stress: destroy while waiting");
|
||||
for (int round = 0; round < DESTROY_ROUNDS; ++round) {
|
||||
pthread_t t[3];
|
||||
struct waiter_arg a[3];
|
||||
n = model_create(0);
|
||||
for (int i = 0; i < 3; ++i) {
|
||||
a[i].name = n; a[i].second = 0; a[i].timeout_ns = 2000000000ll; a[i].result = -1;
|
||||
pthread_create(&t[i], 0, waiter_thread, &a[i]);
|
||||
}
|
||||
wait_until_waiters(n, 3);
|
||||
model_destroy(n);
|
||||
for (int i = 0; i < 3; ++i)
|
||||
pthread_join(t[i], 0);
|
||||
if (a[0].result != KERN_TERMINATED || a[1].result != KERN_TERMINATED || a[2].result != KERN_TERMINATED)
|
||||
++bad;
|
||||
}
|
||||
check("every waiter of a destroyed semaphore got KERN_TERMINATED", bad == 0);
|
||||
bad = 0;
|
||||
for (int round = 0; round < DESTROY_ROUNDS; ++round) {
|
||||
pthread_t t;
|
||||
struct waiter_arg a = { .name = 0, .second = 0, .timeout_ns = 2000000000ll, .result = -1 };
|
||||
int ok = 0;
|
||||
n = model_create(0);
|
||||
a.name = n;
|
||||
pthread_create(&t, 0, waiter_thread, &a);
|
||||
wait_until_waiters(n, 1);
|
||||
model_destroy(n);
|
||||
model_create_named(n, 0, &ok); /* the same port name again: a different object */
|
||||
if (!ok)
|
||||
++bad;
|
||||
semaphore_signal_trap_impl(n); /* and its own token */
|
||||
pthread_join(t, 0);
|
||||
if (a.result == KERN_SUCCESS)
|
||||
++stolen; /* the old waiter consumed the new object's token */
|
||||
if (model_timedwait(n, 0) != KERN_SUCCESS)
|
||||
++bad; /* or the token is gone */
|
||||
model_destroy(n);
|
||||
}
|
||||
check("a waiter of a destroyed object never takes the re-created object's token", stolen == 0);
|
||||
check("the re-created object kept its token", bad == 0);
|
||||
steal += stolen;
|
||||
}
|
||||
|
||||
static void* watchdog(void* p)
|
||||
{
|
||||
(void) p;
|
||||
for (int i = 0; i < 600; ++i) {
|
||||
if (__atomic_load_n(&model_done, __ATOMIC_ACQUIRE))
|
||||
return 0;
|
||||
usleep(100000);
|
||||
}
|
||||
++hang;
|
||||
printf("LSEM_MODEL HANG: no progress for 60 s\n");
|
||||
_exit(9);
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
pthread_t wd;
|
||||
setvbuf(stdout, NULL, _IONBF, 0);
|
||||
pthread_create(&wd, 0, watchdog, 0);
|
||||
pthread_detach(wd);
|
||||
printf("LSEM_MODEL slots=%u\n", (unsigned) LSEM_SLOTS);
|
||||
test_switch_off_child();
|
||||
test_decide_and_log();
|
||||
test_counting();
|
||||
test_signal_all();
|
||||
test_signal_thread();
|
||||
test_destroy_wakes();
|
||||
test_find_destroy_race();
|
||||
test_inflight_destroy();
|
||||
test_fork();
|
||||
test_timeout();
|
||||
test_combined_local();
|
||||
test_combined_order();
|
||||
test_stress_accounting();
|
||||
test_wake_pairs();
|
||||
test_stress_churn();
|
||||
test_destroy_while_waiting();
|
||||
test_fallback();
|
||||
test_slot_reuse_steal();
|
||||
__atomic_store_n(&model_done, 1, __ATOMIC_RELEASE);
|
||||
printf("LSEM_MODEL checks=%ld failed=%ld spurious_success=%ld lost_wakeup=%ld steal=%ld hang=%ld "
|
||||
"local_success=%ld terminated=%ld timed_out=%ld server=%ld rpc=%d/%d/%d/%d/%d/%d/%d\n",
|
||||
checks, failures, spurious_success, lost_wakeup, steal, hang,
|
||||
local_success, terminated, timed_out, server_fallbacks,
|
||||
model_rpc_calls[0], model_rpc_calls[1], model_rpc_calls[2], model_rpc_calls[3],
|
||||
model_rpc_calls[4], model_rpc_calls[5], model_rpc_calls[6]);
|
||||
return failures != 0 || spurious_success != 0 || lost_wakeup != 0 || steal != 0 || hang != 0;
|
||||
}
|
||||
Executable
+9
@@ -0,0 +1,9 @@
|
||||
#!/bin/sh
|
||||
# Host model of the process-local Mach semaphores: extracts the shipped code from the patched tree, builds it with
|
||||
# shims for the syscalls and server RPCs, and runs the stress scenarios. Linux only; never runs a Mach-O binary.
|
||||
set -e
|
||||
here=$(cd "$(dirname "$0")" && pwd)
|
||||
out=$(cd "$here/../.." && pwd)/build/riot-lsem-model
|
||||
python3 "$here/extract-lsem.py" "$@"
|
||||
cc -O2 -pthread -Wall -I"$out" -o "$out/lsem-model" "$here/lsem-model.c"
|
||||
"$out/lsem-model" | tee "$out/lsem-model-output.txt" | grep -E "FAIL|LSEM_MODEL checks"
|
||||
Executable
+67
@@ -0,0 +1,67 @@
|
||||
#!/bin/sh
|
||||
# Prove the model is not vacuous: re-break the fixed protocol in a copy of the extracted code, build the same model
|
||||
# against it and show that the counters catch the bug. Nothing here touches the tree or the shipped library.
|
||||
# Usage: sh sensitivity-check.sh (after run.sh; writes the outputs to build/riot-lsem-model)
|
||||
set -e
|
||||
here=$(cd "$(dirname "$0")" && pwd)
|
||||
out=$(cd "$here/../.." && pwd)/build/riot-lsem-model
|
||||
tmp=${TMPDIR:-/tmp}/lsem-sensitivity.$$
|
||||
mkdir -p "$tmp"
|
||||
|
||||
make_variant() { # $1 = variant name, $2 = python replacement program
|
||||
mkdir -p "$tmp/$1"
|
||||
cp "$here/lsem-model.c" "$tmp/$1/"
|
||||
python3 - "$out/lsem-shipped.inc" "$tmp/$1/lsem-shipped.inc" <<EOF
|
||||
import sys
|
||||
from pathlib import Path
|
||||
src = Path(sys.argv[1]).read_text()
|
||||
$2
|
||||
Path(sys.argv[2]).write_text(src)
|
||||
EOF
|
||||
cc -O2 -pthread -o "$tmp/$1/lsem-model" "$tmp/$1/lsem-model.c"
|
||||
(cd "$tmp/$1" && ./lsem-model) > "$out/sensitivity-$1-output.txt" 2>&1 || true
|
||||
grep -E "FAIL|LSEM_MODEL checks" "$out/sensitivity-$1-output.txt" || true
|
||||
}
|
||||
|
||||
echo "== broken tombstone: the destroy's wake value is a positive count again and the wait may take it"
|
||||
make_variant broken-tombstone '
|
||||
src = src.replace("(((old >> 32) + 1) << 32) | (unsigned int) LSEM_TOMB_VALUE",
|
||||
"(((old >> 32) + 1) << 32) | 0x40000000u")
|
||||
src = src.replace(""" if (v < 0) {
|
||||
/* the destroy tombstone: a trap that resolved the slot while the destroy was in flight must
|
||||
* report the death, never take the tombstone for a token */
|
||||
(void) lsem_post_signal(post);
|
||||
return KERN_TERMINATED;
|
||||
}
|
||||
""", "")
|
||||
'
|
||||
|
||||
echo "== broken reuse: no new generation when a slot is reassigned and a value-only (32-bit) token CAS"
|
||||
make_variant broken-reuse '
|
||||
src = src.replace("unsigned long long fresh = (((old >> 32) + 1) << 32)", "unsigned long long fresh = ((old >> 32) << 32)")
|
||||
src = src.replace(""" if ((unsigned int) __atomic_load_n(&s->pv.w.gen, __ATOMIC_ACQUIRE) != gen ||
|
||||
__atomic_load_n(&s->name, __ATOMIC_ACQUIRE) != name) {
|
||||
(void) lsem_post_signal(post); /* XNU signals the signal semaphore even when the wait fails */""",
|
||||
""" if ((unsigned int) __atomic_load_n(&s->pv.w.gen, __ATOMIC_ACQUIRE) != gen) {
|
||||
(void) lsem_post_signal(post); /* XNU signals the signal semaphore even when the wait fails */""")
|
||||
src = src.replace(""" unsigned long long old = __atomic_load_n(&s->pv.pair, __ATOMIC_SEQ_CST);
|
||||
if ((unsigned int) (old >> 32) != gen)
|
||||
continue; /* the slot changed while we looked at it: the top of the loop reports it */
|
||||
int v = (int) (unsigned int) old;
|
||||
if (v < 0) {
|
||||
/* the destroy tombstone: a trap that resolved the slot while the destroy was in flight must
|
||||
* report the death, never take the tombstone for a token */
|
||||
(void) lsem_post_signal(post);
|
||||
return KERN_TERMINATED;
|
||||
}
|
||||
if (v > 0) {
|
||||
/* take the token only if value and generation still belong to this object: the compare-exchange
|
||||
* is on the whole pair, so a reused slot cannot hand its token to this waiter */
|
||||
if (__atomic_compare_exchange_n(&s->pv.pair, &old, old - 1, 0, __ATOMIC_SEQ_CST, __ATOMIC_SEQ_CST)) {""",
|
||||
""" int v = (int) (unsigned int) __atomic_load_n(&s->pv.pair, __ATOMIC_SEQ_CST);
|
||||
if (v > 0) {
|
||||
if (__atomic_compare_exchange_n((int*) &s->pv.w.value, &v, v - 1, 0, __ATOMIC_SEQ_CST, __ATOMIC_SEQ_CST)) {""")
|
||||
'
|
||||
|
||||
rm -rf "$tmp"
|
||||
echo "== done; outputs are $out/sensitivity-broken-*-output.txt"
|
||||
@@ -1,10 +1,16 @@
|
||||
#include <sys/mman.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
// A standalone Darling process. Reads only its own Linux memory counters.
|
||||
//
|
||||
// The free hint is only forwarded for whole 2 MiB huge pages (see
|
||||
// emulation/src/xnu_syscall/bsd/impl/mman/madvise.c), so the probe marks a
|
||||
// 2 MiB-aligned region inside a larger mapping; an unaligned 4 MiB region would
|
||||
// still be marked for the contained huge pages, but not for all of it.
|
||||
extern int __linux_syscall(int number, ...);
|
||||
#define HUGE_PAGE_SIZE (2ul * 1024 * 1024)
|
||||
|
||||
static unsigned long lazy_free_kib(void) {
|
||||
char text[4096];
|
||||
int fd = __linux_syscall(257, -100, "/proc/self/smaps_rollup", 0, 0);
|
||||
@@ -17,10 +23,14 @@ static unsigned long lazy_free_kib(void) {
|
||||
unsigned long value = 0;
|
||||
return field && sscanf(field, "LazyFree: %lu kB", &value) == 1 ? value : (unsigned long)-1;
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
const size_t size = 4 * 1024 * 1024;
|
||||
unsigned char* memory = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANON, -1, 0);
|
||||
if (memory == MAP_FAILED) return 1;
|
||||
unsigned char* mapping = mmap(NULL, size + HUGE_PAGE_SIZE, PROT_READ | PROT_WRITE,
|
||||
MAP_PRIVATE | MAP_ANON, -1, 0);
|
||||
if (mapping == MAP_FAILED) return 1;
|
||||
unsigned char* memory = (unsigned char*)(((unsigned long)mapping + HUGE_PAGE_SIZE - 1)
|
||||
& ~(HUGE_PAGE_SIZE - 1));
|
||||
memset(memory, 0x5a, size);
|
||||
unsigned long before = lazy_free_kib();
|
||||
if (before == (unsigned long)-1 || madvise(memory, size, 7) != 0) return 2;
|
||||
@@ -29,8 +39,9 @@ int main(void) {
|
||||
unsigned long reused = lazy_free_kib();
|
||||
int contents = 1;
|
||||
for (size_t i = 0; i < size; ++i) if (memory[i] != 0x5a) { contents = 0; break; }
|
||||
munmap(memory, size);
|
||||
int pass = freed >= before + size / 1024 && reused <= before && contents;
|
||||
printf("DARLING_MADVISE pass=%d reclaimable_KiB=%lu reuse_reclaimable_KiB=%lu contents=%s\n", pass, freed - before, reused - before, contents ? "preserved" : "changed");
|
||||
printf("DARLING_MADVISE pass=%d reclaimable_KiB=%lu reuse_reclaimable_KiB=%lu contents=%s\n",
|
||||
pass, freed - before, reused - before, contents ? "preserved" : "changed");
|
||||
munmap(mapping, size + HUGE_PAGE_SIZE);
|
||||
return !pass;
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Check the memory-runtime installer policy in temporary directories.
|
||||
|
||||
The installer is imported, never executed: no prefix, backup directory or
|
||||
running process is touched. The two properties under test are that the default
|
||||
source is the prepared optimized artifact and that the artifact is never a
|
||||
destination of a different library (the plain CMake build).
|
||||
"""
|
||||
import importlib.util
|
||||
from pathlib import Path
|
||||
import tempfile
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
|
||||
|
||||
def load_installer():
|
||||
path = ROOT / "tools/install-riot-memory-runtime.py"
|
||||
spec = importlib.util.spec_from_file_location("riot_memory_installer", path)
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
def write_library(path, fill):
|
||||
path.write_bytes(b"\xcf\xfa\xed\xfe" + bytes([fill]) * 4096)
|
||||
|
||||
|
||||
def check():
|
||||
installer = load_installer()
|
||||
_, defaults = installer.parse_arguments([])
|
||||
assert defaults.binary == installer.DEFAULT_ARTIFACT, "default source is not the prepared artifact"
|
||||
assert defaults.artifact == installer.DEFAULT_ARTIFACT, "default artifact changed"
|
||||
with tempfile.TemporaryDirectory(prefix="riot-memory-offline-") as directory:
|
||||
folder = Path(directory)
|
||||
prefix = folder / "prefix"
|
||||
runtime = prefix / "Users/offline-check/RuntimeLibraries/libsystem_kernel.dylib"
|
||||
bootstrap = prefix / "usr/lib/system/libsystem_kernel.dylib"
|
||||
runtime.parent.mkdir(parents=True)
|
||||
bootstrap.parent.mkdir(parents=True)
|
||||
config = {"prefix": str(prefix), "guest_home": "/Users/offline-check"}
|
||||
artifact = folder / "riot-libsystem_kernel.dylib"
|
||||
backups = folder / "backups"
|
||||
write_library(artifact, 0x11)
|
||||
write_library(runtime, 0x22)
|
||||
write_library(bootstrap, 0x22)
|
||||
artifact_hash = installer.digest(artifact)
|
||||
old_hash = installer.digest(runtime)
|
||||
|
||||
# The default run installs the prepared artifact into the prefix and
|
||||
# leaves the artifact itself current.
|
||||
destinations, note = installer.prefix_destinations(config, artifact, artifact)
|
||||
assert note is None, "default artifacts report a conflict"
|
||||
assert artifact in destinations, "artifact missing from its own installation"
|
||||
result = installer.install(artifact, destinations, backups)
|
||||
assert result["installed"], "default installation failed"
|
||||
assert installer.digest(runtime) == artifact_hash and installer.digest(bootstrap) == artifact_hash, "artifact not installed"
|
||||
assert installer.digest(artifact) == artifact_hash, "artifact changed by itself"
|
||||
assert any(record.get("already_current") for record in result["libraries"]), "artifact not detected as current"
|
||||
assert any(installer.digest(path) == old_hash for path in backups.iterdir()), "no backup staged"
|
||||
|
||||
# An explicitly different source reaches the prefix copies only: the
|
||||
# artifact must not become a destination of that library.
|
||||
other = folder / "plain-cmake.dylib"
|
||||
write_library(other, 0x33)
|
||||
destinations, note = installer.prefix_destinations(config, other, artifact)
|
||||
assert artifact not in destinations, "artifact is a destination of a different source"
|
||||
assert note and "refusing to overwrite" in note, "no warning about the protected artifact"
|
||||
result = installer.install(other, destinations, backups)
|
||||
assert result["installed"], "explicit installation failed"
|
||||
other_hash = installer.digest(other)
|
||||
assert installer.digest(runtime) == other_hash and installer.digest(bootstrap) == other_hash, "prefix copy not updated"
|
||||
assert installer.digest(artifact) == artifact_hash, "artifact overwritten by a different source"
|
||||
assert not result.get("artifact_replaced"), "installer reports the protected artifact as replaced"
|
||||
assert any(installer.digest(path) == artifact_hash for path in backups.iterdir()), "backup of the replaced prefix copies missing"
|
||||
|
||||
# A missing artifact is reported, not created.
|
||||
missing = folder / "not-built-yet.dylib"
|
||||
destinations, note = installer.prefix_destinations(config, other, missing)
|
||||
assert artifact not in destinations and "does not exist" in note, "missing artifact not reported"
|
||||
print("MEMORY_INSTALLER_OFFLINE pass=1 default_source=artifact artifact_protected=1 backups_staged=1")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
check()
|
||||
@@ -0,0 +1,631 @@
|
||||
// Differential model of the Metal wrapper's per-encoder bind tracking.
|
||||
//
|
||||
// The tracking helpers are extracted verbatim from the shipping source by
|
||||
// extract-bind-tracking.py (see bind-tracking.extract.inc); the decision sequences
|
||||
// below are transcriptions of the setter methods in
|
||||
// build/darling-src/src/external/metal/src/Metal/MTLRenderCommandEncoder.mm, with the
|
||||
// source line references, and they call those extracted helpers.
|
||||
//
|
||||
// Two encoder states are driven by the same call sequence:
|
||||
// * `all` applies every call (what a wrapper without tracking would record)
|
||||
// * `forwarded` applies only the calls the tracking forwards
|
||||
// and the two are compared after *every* call. Equal states means every dropped call
|
||||
// was redundant: nothing downstream (Indium's per-stage binding tables, the deferred
|
||||
// op stream replayed from them, the Vulkan descriptor sets built from those) can tell
|
||||
// the two apart.
|
||||
//
|
||||
// The modelled state mirrors Indium's binding tables
|
||||
// (deps/indium/private-include/indium/command-encoder.private.hpp: FunctionResources):
|
||||
// a buffer slot holds an owner plus an offset and inline bytes can replace it, a
|
||||
// texture/sampler slot holds an owner, and a table only covers slots something grew it
|
||||
// to (growToIndex). Calls the model does not need are not part of the changed paths.
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <random>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
typedef uint64_t NSUInteger; // Darwin's typedef; same width here
|
||||
|
||||
#include "bind-tracking.extract.inc"
|
||||
|
||||
static int failures = 0;
|
||||
#define CHECK(cond, ...) do { if (!(cond)) { ++failures; std::printf("FAIL %s:%d: ", __FILE__, __LINE__); std::printf(__VA_ARGS__); std::printf("\n"); } } while (0)
|
||||
|
||||
// ---------------------------------------------------------------- encoder state
|
||||
|
||||
struct SlotState {
|
||||
enum Kind { unbound, buffer, bytes } kind = unbound;
|
||||
const void* object = nullptr; // buffer owner, or the inline bytes' content pointer
|
||||
size_t offset = 0;
|
||||
|
||||
bool operator==(const SlotState& other) const {
|
||||
return kind == other.kind && object == other.object && offset == other.offset;
|
||||
}
|
||||
bool operator!=(const SlotState& other) const { return !(*this == other); }
|
||||
};
|
||||
|
||||
struct EncoderState {
|
||||
std::vector<SlotState> vertexBuffers;
|
||||
std::vector<SlotState> fragmentBuffers;
|
||||
std::vector<const void*> vertexTextures;
|
||||
std::vector<const void*> fragmentTextures;
|
||||
std::vector<const void*> vertexSamplers;
|
||||
std::vector<const void*> fragmentSamplers;
|
||||
const void* pipeline = nullptr;
|
||||
const void* depthStencil = nullptr;
|
||||
|
||||
// Indium grows a table to the index it is asked about; setXBufferOffset only
|
||||
// touches a slot that already exists (it logs and returns otherwise).
|
||||
static void grow(std::vector<SlotState>& slots, NSUInteger index) {
|
||||
if (slots.size() <= index) slots.resize(index + 1);
|
||||
}
|
||||
static void grow(std::vector<const void*>& slots, NSUInteger index) {
|
||||
if (slots.size() <= index) slots.resize(index + 1);
|
||||
}
|
||||
};
|
||||
|
||||
static bool equal(const EncoderState& a, const EncoderState& b) {
|
||||
return a.vertexBuffers == b.vertexBuffers && a.fragmentBuffers == b.fragmentBuffers &&
|
||||
a.vertexTextures == b.vertexTextures && a.fragmentTextures == b.fragmentTextures &&
|
||||
a.vertexSamplers == b.vertexSamplers && a.fragmentSamplers == b.fragmentSamplers &&
|
||||
a.pipeline == b.pipeline && a.depthStencil == b.depthStencil;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- calls
|
||||
|
||||
struct Call {
|
||||
enum Kind {
|
||||
VertexTexture, FragmentTexture, VertexSampler, FragmentSampler,
|
||||
VertexBuffer, FragmentBuffer, VertexBufferOffset, FragmentBufferOffset,
|
||||
VertexBytes, FragmentBytes, VertexBuffers, FragmentBuffers,
|
||||
Pipeline, DepthStencil,
|
||||
} kind = Pipeline;
|
||||
const void* object = nullptr; // the Indium resource's address (what the shipping key compares)
|
||||
size_t offset = 0;
|
||||
NSUInteger index = 0;
|
||||
NSUInteger length = 0;
|
||||
const void* wrapper = nullptr; // the Objective-C wrapper's address; reusable (round-1 key)
|
||||
const void* id = nullptr; // the object's identity (what the encoder state holds); an address
|
||||
// can host several identities over time, that is what ABA is
|
||||
};
|
||||
|
||||
// Applies a call to the model state, in the order the wrapper would forward it.
|
||||
static void apply(const Call& call, EncoderState& state) {
|
||||
// what the encoder state holds is the object's identity, not its address: an address can host
|
||||
// more than one object over time (that is the ABA case), and the state must tell them apart
|
||||
const void* object = call.id ? call.id : call.object;
|
||||
const void* buffers[8];
|
||||
size_t offsets[8];
|
||||
switch (call.kind) {
|
||||
case Call::VertexTexture:
|
||||
EncoderState::grow(state.vertexTextures, call.index);
|
||||
state.vertexTextures[call.index] = object;
|
||||
break;
|
||||
case Call::FragmentTexture:
|
||||
EncoderState::grow(state.fragmentTextures, call.index);
|
||||
state.fragmentTextures[call.index] = object;
|
||||
break;
|
||||
case Call::VertexSampler:
|
||||
EncoderState::grow(state.vertexSamplers, call.index);
|
||||
state.vertexSamplers[call.index] = object;
|
||||
break;
|
||||
case Call::FragmentSampler:
|
||||
EncoderState::grow(state.fragmentSamplers, call.index);
|
||||
state.fragmentSamplers[call.index] = object;
|
||||
break;
|
||||
case Call::VertexBuffer:
|
||||
EncoderState::grow(state.vertexBuffers, call.index);
|
||||
state.vertexBuffers[call.index] = SlotState { SlotState::buffer, object, call.offset };
|
||||
break;
|
||||
case Call::FragmentBuffer:
|
||||
EncoderState::grow(state.fragmentBuffers, call.index);
|
||||
state.fragmentBuffers[call.index] = SlotState { SlotState::buffer, object, call.offset };
|
||||
break;
|
||||
case Call::VertexBufferOffset:
|
||||
if (call.index >= state.vertexBuffers.size()) break;
|
||||
state.vertexBuffers[call.index].offset = call.offset;
|
||||
break;
|
||||
case Call::FragmentBufferOffset:
|
||||
if (call.index >= state.fragmentBuffers.size()) break;
|
||||
state.fragmentBuffers[call.index].offset = call.offset;
|
||||
break;
|
||||
case Call::VertexBytes:
|
||||
EncoderState::grow(state.vertexBuffers, call.index);
|
||||
state.vertexBuffers[call.index] = SlotState { SlotState::bytes, object, 0 };
|
||||
break;
|
||||
case Call::FragmentBytes:
|
||||
EncoderState::grow(state.fragmentBuffers, call.index);
|
||||
state.fragmentBuffers[call.index] = SlotState { SlotState::bytes, object, 0 };
|
||||
break;
|
||||
case Call::VertexBuffers:
|
||||
for (NSUInteger i = 0; i < call.length; ++i) {
|
||||
buffers[i] = object; offsets[i] = call.offset + i;
|
||||
}
|
||||
for (NSUInteger i = 0; i < call.length; ++i) {
|
||||
EncoderState::grow(state.vertexBuffers, call.index + i);
|
||||
state.vertexBuffers[call.index + i] = SlotState { SlotState::buffer, buffers[i], offsets[i] };
|
||||
}
|
||||
break;
|
||||
case Call::FragmentBuffers:
|
||||
for (NSUInteger i = 0; i < call.length; ++i) {
|
||||
buffers[i] = object; offsets[i] = call.offset + i;
|
||||
}
|
||||
for (NSUInteger i = 0; i < call.length; ++i) {
|
||||
EncoderState::grow(state.fragmentBuffers, call.index + i);
|
||||
state.fragmentBuffers[call.index + i] = SlotState { SlotState::buffer, buffers[i], offsets[i] };
|
||||
}
|
||||
break;
|
||||
case Call::Pipeline:
|
||||
state.pipeline = object;
|
||||
break;
|
||||
case Call::DepthStencil:
|
||||
state.depthStencil = object;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// The wrapper's decision: does the call reach Indium? Transcribed from the shipping
|
||||
// methods (line numbers are those of MTLRenderCommandEncoder.mm) and calling the
|
||||
// extracted tracking helpers from bind-tracking.extract.inc.
|
||||
//
|
||||
// `keySource` selects what a record is compared against. The shipping code uses Indium
|
||||
// (MTLBindKey(MTLXGetIndium(...)), the resource's inner pointer); Wrapper is the round-1
|
||||
// key the ABA scenario shows failing (the Objective-C address the caller passed in).
|
||||
enum class BindKeySource {
|
||||
Indium,
|
||||
Wrapper,
|
||||
};
|
||||
|
||||
struct TrackedEncoder {
|
||||
MTLBindObjectSlots vertexTextures;
|
||||
MTLBindObjectSlots fragmentTextures;
|
||||
MTLBindObjectSlots vertexSamplers;
|
||||
MTLBindObjectSlots fragmentSamplers;
|
||||
MTLBindObjectSlots pipelineState;
|
||||
MTLBindObjectSlots depthStencilState;
|
||||
MTLBindBufferSlots vertexBuffers;
|
||||
MTLBindBufferSlots fragmentBuffers;
|
||||
|
||||
BindKeySource keySource = BindKeySource::Indium;
|
||||
// what the shipping setDepthStencilState: stores in _depthStencilOwner (the depth-stencil record
|
||||
// is the one record whose resource Indium itself does not keep alive)
|
||||
const void* depthStencilOwner = nullptr;
|
||||
|
||||
std::map<std::string, unsigned long> drops;
|
||||
void drop(const char* what) { ++drops[what]; }
|
||||
|
||||
const void* key(const Call& call) const {
|
||||
return keySource == BindKeySource::Wrapper ? call.wrapper : call.object;
|
||||
}
|
||||
|
||||
// endEncoding (MTLBindForgetAll + _depthStencilOwner.reset()): records do not survive the pass
|
||||
void clear() {
|
||||
MTLBindForgetAll(vertexTextures);
|
||||
MTLBindForgetAll(fragmentTextures);
|
||||
MTLBindForgetAll(vertexSamplers);
|
||||
MTLBindForgetAll(fragmentSamplers);
|
||||
MTLBindForgetAll(pipelineState);
|
||||
MTLBindForgetAll(depthStencilState);
|
||||
MTLBindForgetAll(vertexBuffers);
|
||||
MTLBindForgetAll(fragmentBuffers);
|
||||
depthStencilOwner = nullptr;
|
||||
}
|
||||
|
||||
bool decide(const Call& call) {
|
||||
switch (call.kind) {
|
||||
case Call::VertexTexture: // :989
|
||||
if (MTLBindObjectSlot(vertexTextures, call.index, key(call))) { drop("VertexTexture"); return false; }
|
||||
return true;
|
||||
case Call::FragmentTexture: // :994
|
||||
if (MTLBindObjectSlot(fragmentTextures, call.index, key(call))) { drop("FragmentTexture"); return false; }
|
||||
return true;
|
||||
case Call::VertexSampler: // :1000
|
||||
if (MTLBindObjectSlot(vertexSamplers, call.index, key(call))) { drop("VertexSamplerState"); return false; }
|
||||
return true;
|
||||
case Call::FragmentSampler: // :1005
|
||||
if (MTLBindObjectSlot(fragmentSamplers, call.index, key(call))) { drop("FragmentSamplerState"); return false; }
|
||||
return true;
|
||||
case Call::DepthStencil: // :1015: record, then retain the state in the record
|
||||
if (MTLBindObjectSlot(depthStencilState, 0, key(call))) { drop("DepthStencilState"); return false; }
|
||||
depthStencilOwner = call.object;
|
||||
return true;
|
||||
case Call::Pipeline: // :1052
|
||||
if (MTLBindObjectSlot(pipelineState, 0, key(call))) { drop("PipelineState"); return false; }
|
||||
return true;
|
||||
case Call::VertexBuffer: // :1116
|
||||
if (MTLBindBufferSlot(vertexBuffers, call.index, key(call), call.offset)) { drop("VertexBuffer"); return false; }
|
||||
return true;
|
||||
case Call::FragmentBuffer: // :1191
|
||||
if (MTLBindBufferSlot(fragmentBuffers, call.index, key(call), call.offset)) { drop("FragmentBuffer"); return false; }
|
||||
return true;
|
||||
case Call::VertexBufferOffset: // :1162
|
||||
if (MTLBindBufferOffset(vertexBuffers, call.index, call.offset)) { drop("VertexBufferOffset"); return false; }
|
||||
return true;
|
||||
case Call::FragmentBufferOffset: // :1219
|
||||
if (MTLBindBufferOffset(fragmentBuffers, call.index, call.offset)) { drop("FragmentBufferOffset"); return false; }
|
||||
return true;
|
||||
case Call::VertexBytes: // :1181
|
||||
MTLBindForgetBufferSlot(vertexBuffers, call.index);
|
||||
return true;
|
||||
case Call::FragmentBytes: // :1230
|
||||
MTLBindForgetBufferSlot(fragmentBuffers, call.index);
|
||||
return true;
|
||||
case Call::VertexBuffers: // :1155: plurals always forward, and record every slot
|
||||
for (NSUInteger i = 0; i < call.length; ++i)
|
||||
MTLBindBufferSlot(vertexBuffers, call.index + i, call.object, call.offset + i);
|
||||
return true;
|
||||
case Call::FragmentBuffers: // :1213
|
||||
for (NSUInteger i = 0; i < call.length; ++i)
|
||||
MTLBindBufferSlot(fragmentBuffers, call.index + i, call.object, call.offset + i);
|
||||
return true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
// ------------------------------------------------------------------ the harness
|
||||
|
||||
struct Harness {
|
||||
EncoderState all; // every call applied
|
||||
EncoderState forwarded; // only the forwarded calls applied
|
||||
TrackedEncoder tracked;
|
||||
unsigned long calls = 0, forwardedCalls = 0, dropped = 0;
|
||||
|
||||
// The ABA scenario drives a sequence in which a divergence is the *result* under test (the round-1
|
||||
// key is expected to produce one), so it can turn the CHECK off and count them instead.
|
||||
bool expectDivergence = false;
|
||||
unsigned long divergences = 0;
|
||||
|
||||
void run(const Call& call) {
|
||||
bool forward = tracked.decide(call);
|
||||
apply(call, all);
|
||||
if (forward) { apply(call, forwarded); ++forwardedCalls; } else ++dropped;
|
||||
++calls;
|
||||
// the game only reads binding state at a draw, so it is checked after every call: stricter
|
||||
if (!equal(all, forwarded)) {
|
||||
++divergences;
|
||||
if (!expectDivergence)
|
||||
CHECK(false, "binding state diverged after call %lu (kind %d, slot %lu)", calls, (int) call.kind, (unsigned long) call.index);
|
||||
all = forwarded; // report once per divergence
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
int main() {
|
||||
static int objects[16];
|
||||
const void* o[16];
|
||||
for (int i = 0; i < 16; ++i) o[i] = &objects[i];
|
||||
const void* bytes = "inline-bytes";
|
||||
|
||||
struct Scenario { const char* name; std::vector<Call> calls; unsigned long minDrops; };
|
||||
std::vector<Scenario> scenarios;
|
||||
|
||||
{ // 1. a run of draws with identical bindings: everything after the first set is redundant
|
||||
std::vector<Call> calls;
|
||||
for (int draw = 0; draw < 8; ++draw) {
|
||||
calls.push_back({ Call::Pipeline, o[0] });
|
||||
calls.push_back({ Call::VertexBuffer, o[1], 0, 3 });
|
||||
calls.push_back({ Call::FragmentBuffer, o[2], 64, 4 });
|
||||
calls.push_back({ Call::FragmentTexture, o[3], 0, 0 });
|
||||
calls.push_back({ Call::FragmentTexture, o[4], 0, 1 });
|
||||
calls.push_back({ Call::FragmentSampler, o[5], 0, 0 });
|
||||
}
|
||||
scenarios.push_back({ "redundant-binds", calls, 8 * 6 - 6 });
|
||||
}
|
||||
{ // 2. rebinding after a pipeline change: the slot state is unchanged, so the bind is still redundant
|
||||
std::vector<Call> calls = {
|
||||
{ Call::FragmentTexture, o[3], 0, 0 },
|
||||
{ Call::Pipeline, o[0] },
|
||||
{ Call::FragmentTexture, o[3], 0, 0 },
|
||||
{ Call::Pipeline, o[0] },
|
||||
{ Call::Pipeline, o[1] },
|
||||
{ Call::Pipeline, o[1] },
|
||||
{ Call::FragmentTexture, o[4], 0, 0 },
|
||||
{ Call::FragmentTexture, o[4], 0, 0 },
|
||||
{ Call::DepthStencil, o[8] },
|
||||
{ Call::DepthStencil, o[8] },
|
||||
};
|
||||
scenarios.push_back({ "rebind-after-pipeline-change", calls, 5 });
|
||||
}
|
||||
{ // 3. inline bytes replace the slot's buffer: the next bind must go through, the one after it must not
|
||||
std::vector<Call> calls = {
|
||||
{ Call::VertexBuffer, o[1], 0, 3 },
|
||||
{ Call::VertexBytes, bytes, 0, 3 },
|
||||
{ Call::VertexBuffer, o[1], 0, 3 },
|
||||
{ Call::VertexBuffer, o[1], 0, 3 },
|
||||
{ Call::VertexBytes, bytes, 0, 3 },
|
||||
{ Call::VertexBuffer, o[1], 0, 3 },
|
||||
};
|
||||
scenarios.push_back({ "bytes-then-buffer", calls, 1 });
|
||||
}
|
||||
{ // 4. the offset belongs to the slot state
|
||||
std::vector<Call> calls = {
|
||||
{ Call::VertexBuffer, o[1], 0, 4 },
|
||||
{ Call::VertexBufferOffset, nullptr, 1024, 4 },
|
||||
{ Call::VertexBufferOffset, nullptr, 1024, 4 },
|
||||
{ Call::VertexBuffer, o[1], 0, 4 },
|
||||
{ Call::VertexBuffer, o[1], 1024, 4 },
|
||||
{ Call::VertexBufferOffset, nullptr, 1024, 4 },
|
||||
{ Call::VertexBufferOffset, nullptr, 4096, 4 },
|
||||
{ Call::VertexBuffer, o[1], 1024, 4 },
|
||||
{ Call::VertexBufferOffset, nullptr, 4096, 4 },
|
||||
{ Call::VertexBufferOffset, nullptr, 4096, 5 }, // never bound: Indium logs and changes nothing
|
||||
{ Call::VertexBuffer, o[1], 4096, 5 },
|
||||
{ Call::VertexBuffer, o[1], 4096, 5 },
|
||||
};
|
||||
scenarios.push_back({ "buffer-offset-state", calls, 3 });
|
||||
}
|
||||
{ // 5. nil is a value: nil over nil is redundant, a real texture over nil is not
|
||||
std::vector<Call> calls = {
|
||||
{ Call::FragmentTexture, nullptr, 0, 2 },
|
||||
{ Call::FragmentTexture, nullptr, 0, 2 },
|
||||
{ Call::FragmentTexture, o[3], 0, 2 },
|
||||
{ Call::FragmentTexture, nullptr, 0, 2 },
|
||||
{ Call::FragmentTexture, nullptr, 0, 2 },
|
||||
{ Call::FragmentBuffer, nullptr, 0, 6 },
|
||||
{ Call::FragmentBuffer, nullptr, 0, 6 },
|
||||
{ Call::FragmentBuffer, o[2], 0, 6 },
|
||||
};
|
||||
scenarios.push_back({ "nil-slots", calls, 3 });
|
||||
}
|
||||
{ // 6. out-of-range slots are never tracked (the tables are 31 entries, like the rest of the file)
|
||||
std::vector<Call> calls;
|
||||
for (int i = 0; i < 4; ++i) calls.push_back({ Call::FragmentTexture, o[3], 0, 31 });
|
||||
for (int i = 0; i < 4; ++i) calls.push_back({ Call::VertexBuffer, o[1], 0, 200 });
|
||||
calls.push_back({ Call::VertexBufferOffset, nullptr, 8, 200 });
|
||||
scenarios.push_back({ "out-of-range", calls, 0 });
|
||||
}
|
||||
{ // 7. plural setters record every slot they cover
|
||||
std::vector<Call> calls = {
|
||||
{ Call::VertexBuffers, o[1], 100, 2, 3 },
|
||||
{ Call::VertexBuffer, o[1], 100, 2 },
|
||||
{ Call::VertexBuffer, o[1], 100, 3 },
|
||||
{ Call::VertexBuffer, o[9], 100, 4 },
|
||||
{ Call::VertexBuffer, o[9], 100, 4 },
|
||||
{ Call::FragmentBuffers, o[2], 0, 0, 4 },
|
||||
{ Call::FragmentBuffer, o[2], 0, 0 },
|
||||
{ Call::FragmentBuffer, o[2], 0, 3 },
|
||||
};
|
||||
scenarios.push_back({ "plural-setters", calls, 3 });
|
||||
}
|
||||
{ // 9. what a material batch looks like: one state setup, then the same draws re-binding it
|
||||
std::vector<Call> calls;
|
||||
for (int draw = 0; draw < 16; ++draw) {
|
||||
calls.push_back({ Call::Pipeline, o[0] });
|
||||
calls.push_back({ Call::VertexBuffer, o[1], 128, 3 });
|
||||
calls.push_back({ Call::VertexBuffer, o[6], 0, 4 });
|
||||
calls.push_back({ Call::FragmentBuffer, o[2], 64, 0 });
|
||||
calls.push_back({ Call::FragmentTexture, o[3], 0, 0 });
|
||||
calls.push_back({ Call::FragmentSampler, o[5], 0, 0 });
|
||||
}
|
||||
scenarios.push_back({ "batches-sharing-a-material", calls, 15 * 6 - 6 });
|
||||
}
|
||||
{ // 10. two materials alternating, differing only in one texture: the shared slots still drop
|
||||
std::vector<Call> calls;
|
||||
for (int draw = 0; draw < 16; ++draw) {
|
||||
calls.push_back({ Call::Pipeline, o[0] });
|
||||
calls.push_back({ Call::VertexBuffer, o[1], 128, 3 });
|
||||
calls.push_back({ Call::FragmentBuffer, o[2], 64, 0 });
|
||||
calls.push_back({ Call::FragmentTexture, (draw % 2) ? o[3] : o[4], 0, 0 });
|
||||
calls.push_back({ Call::FragmentSampler, o[5], 0, 0 });
|
||||
}
|
||||
scenarios.push_back({ "two-materials-alternating", calls, 12 });
|
||||
}
|
||||
|
||||
{ // 8. interleaved stages, slots and indices keep separate records
|
||||
std::vector<Call> calls = {
|
||||
{ Call::VertexTexture, o[7], 0, 0 },
|
||||
{ Call::FragmentTexture, o[7], 0, 0 },
|
||||
{ Call::VertexTexture, o[7], 0, 0 },
|
||||
{ Call::FragmentTexture, o[7], 0, 0 },
|
||||
{ Call::VertexTexture, o[7], 0, 1 },
|
||||
{ Call::FragmentTexture, o[7], 0, 1 },
|
||||
{ Call::VertexTexture, o[7], 0, 1 },
|
||||
{ Call::VertexTexture, o[7], 0, 0 },
|
||||
{ Call::VertexTexture, o[7], 0, 0 },
|
||||
};
|
||||
scenarios.push_back({ "separate-records", calls, 4 });
|
||||
}
|
||||
|
||||
for (auto& scenario: scenarios) {
|
||||
Harness harness;
|
||||
for (const auto& call: scenario.calls) harness.run(call);
|
||||
CHECK(harness.dropped >= scenario.minDrops, "%s: %lu dropped (expected at least %lu)",
|
||||
scenario.name, harness.dropped, scenario.minDrops);
|
||||
CHECK(harness.forwardedCalls + harness.dropped == scenario.calls.size(), "%s: %lu + %lu != %zu",
|
||||
scenario.name, harness.forwardedCalls, harness.dropped, scenario.calls.size());
|
||||
std::printf("scenario %-30s calls=%3zu forwarded=%3lu dropped=%3lu\n",
|
||||
scenario.name, scenario.calls.size(), harness.forwardedCalls, harness.dropped);
|
||||
}
|
||||
|
||||
{ // 11. endEncoding clears the records (MTLBindForgetAll), so a later bind is forwarded again
|
||||
Harness harness;
|
||||
Call bind;
|
||||
bind.kind = Call::FragmentTexture;
|
||||
bind.object = o[3];
|
||||
bind.index = 0;
|
||||
harness.run(bind);
|
||||
CHECK(harness.forwardedCalls == 1 && harness.dropped == 0, "endEncoding-clear: first bind forwarded");
|
||||
harness.tracked.clear();
|
||||
harness.run(bind);
|
||||
CHECK(harness.forwardedCalls == 2, "endEncoding-clear: the bind after the clear must be forwarded");
|
||||
std::printf("scenario %-30s calls=%3lu forwarded=%3lu dropped=%3lu\n",
|
||||
"endEncoding-clear", harness.calls, harness.forwardedCalls, harness.dropped);
|
||||
}
|
||||
|
||||
// ------------------------------------------------- ABA: a released address comes back
|
||||
//
|
||||
// The game binds a resource, releases its wrapper, and the allocator hands the wrapper's address to
|
||||
// a new wrapper around a different Indium resource (transient per-draw buffers do exactly that).
|
||||
// The model makes the two address spaces explicit:
|
||||
//
|
||||
// * the wrapper space is malloc-like: a released wrapper address is handed out again;
|
||||
// * the Indium space only reuses an address once the object there is dead, and the objects the
|
||||
// encoder holds are not dead: `forwarded` (what actually reached Indium) is the holder, the way
|
||||
// FunctionResources keeps bound textures/buffers/samplers and _privatePSO keeps the pipeline
|
||||
// (REPORT.md round 2, "lifetime per kind"). Depth-stencil is the exception -- Indium's setter
|
||||
// keeps no reference -- so the shipping record retains it (_depthStencilOwner), modelled by
|
||||
// `tracked.depthStencilOwner`; `retainDepthStencil` turns that on or off.
|
||||
//
|
||||
// Both keys run the same sequence. The round-1 key (the wrapper address) cannot tell the new
|
||||
// object from the old one at the reused address, so it drops the bind and the encoder-state
|
||||
// diverges from the game's (the bug). The shipping round-2 key (the Indium address) sees that the
|
||||
// old resource's address cannot have come back while the encoder holds it, so it forwards.
|
||||
struct AbaResult {
|
||||
unsigned long dropped = 0;
|
||||
unsigned long divergences = 0;
|
||||
bool addressReused = false; // the wrapper address really came back (malloc behaviour)
|
||||
bool identitiesDistinct = false; // the Indium addresses differ (nothing held the old one)
|
||||
};
|
||||
|
||||
// what the Indium encoder keeps alive after `state` was forwarded: textures, buffers, samplers and
|
||||
// the pipeline, but not the depth-stencil state (see the citation above)
|
||||
static const auto heldByEncoder = [](const EncoderState& state, bool withDepthStencil) {
|
||||
std::vector<const void*> held;
|
||||
for (const void* identity: state.vertexTextures) if (identity) held.push_back(identity);
|
||||
for (const void* identity: state.fragmentTextures) if (identity) held.push_back(identity);
|
||||
for (const void* identity: state.vertexSamplers) if (identity) held.push_back(identity);
|
||||
for (const void* identity: state.fragmentSamplers) if (identity) held.push_back(identity);
|
||||
for (const SlotState& slot: state.vertexBuffers) if (slot.kind == SlotState::buffer && slot.object) held.push_back(slot.object);
|
||||
for (const SlotState& slot: state.fragmentBuffers) if (slot.kind == SlotState::buffer && slot.object) held.push_back(slot.object);
|
||||
if (state.pipeline) held.push_back(state.pipeline);
|
||||
if (withDepthStencil && state.depthStencil) held.push_back(state.depthStencil);
|
||||
return held;
|
||||
};
|
||||
|
||||
// `kind` is FragmentTexture or DepthStencil; `retainDepthStencil` models _depthStencilOwner
|
||||
static const auto runAba = [](BindKeySource keySource, Call::Kind kind, bool retainDepthStencil) {
|
||||
static const char wrapperAddresses[4] = {};
|
||||
static const char indiumAddresses[4] = {};
|
||||
static const char identities[8] = {}; // the objects themselves, independent of any address
|
||||
std::vector<const void*> wrapperHeap, indiumHeap;
|
||||
for (int i = 0; i < 4; ++i) { wrapperHeap.push_back(&wrapperAddresses[i]); indiumHeap.push_back(&indiumAddresses[i]); }
|
||||
|
||||
AbaResult result;
|
||||
Harness harness;
|
||||
harness.tracked.keySource = keySource;
|
||||
harness.expectDivergence = true; // the round-1 key is expected to diverge here
|
||||
|
||||
// X: a new object (wrapper + Indium resource), bound
|
||||
const void* xId = &identities[0];
|
||||
const void* xIndium = indiumHeap.back(); indiumHeap.pop_back();
|
||||
const void* xWrapper = wrapperHeap.back(); wrapperHeap.pop_back();
|
||||
Call bindX;
|
||||
bindX.kind = kind;
|
||||
bindX.object = xIndium;
|
||||
bindX.wrapper = xWrapper;
|
||||
bindX.id = xId;
|
||||
harness.run(bindX);
|
||||
|
||||
// The game releases X's wrapper (malloc gets its address back). The Indium object dies as soon
|
||||
// as nothing holds it: the encoder state holds textures/buffers/samplers/pipeline, the record's
|
||||
// retained reference holds the depth-stencil state; only a dead object's address returns to the
|
||||
// allocator, which is exactly why a recorded Indium address cannot come back around.
|
||||
wrapperHeap.push_back(xWrapper);
|
||||
bool xHeld = kind == Call::DepthStencil && retainDepthStencil && harness.tracked.depthStencilOwner == xIndium;
|
||||
for (const void* id: heldByEncoder(harness.forwarded, kind == Call::DepthStencil && retainDepthStencil))
|
||||
xHeld = xHeld || id == xId;
|
||||
if (!xHeld) indiumHeap.push_back(xIndium);
|
||||
|
||||
// Y is allocated: malloc hands the wrapper address back, and the Indium resource takes the
|
||||
// address the allocator has free (X's, if X was dead)
|
||||
const void* yId = &identities[1];
|
||||
const void* yIndium = indiumHeap.back(); indiumHeap.pop_back();
|
||||
const void* yWrapper = wrapperHeap.back(); wrapperHeap.pop_back();
|
||||
CHECK(yWrapper == xWrapper, "aba: the wrapper allocator must reuse the released address");
|
||||
result.addressReused = (yWrapper == xWrapper);
|
||||
result.identitiesDistinct = (yIndium != xIndium);
|
||||
|
||||
Call bindY;
|
||||
bindY.kind = kind;
|
||||
bindY.object = yIndium;
|
||||
bindY.wrapper = yWrapper;
|
||||
bindY.id = yId;
|
||||
harness.run(bindY);
|
||||
|
||||
result.dropped = harness.dropped;
|
||||
result.divergences = harness.divergences;
|
||||
return result;
|
||||
};
|
||||
|
||||
// the shipping key: the same address cannot come back while the encoder holds the resource, so the
|
||||
// new bind is forwarded and nothing diverges
|
||||
static const auto abaRejects = [](const AbaResult& result) {
|
||||
CHECK(result.dropped == 0, "aba (indium key): %lu binds dropped", result.dropped);
|
||||
CHECK(result.divergences == 0, "aba (indium key): %lu divergences", result.divergences);
|
||||
CHECK(result.identitiesDistinct, "aba (indium key): the new resource must not reuse the held address");
|
||||
};
|
||||
{
|
||||
AbaResult texture = runAba(BindKeySource::Indium, Call::FragmentTexture, false);
|
||||
CHECK(texture.addressReused, "aba: the wrapper address must be reused");
|
||||
abaRejects(texture);
|
||||
AbaResult vertexBuffer = runAba(BindKeySource::Indium, Call::VertexBuffer, false);
|
||||
CHECK(vertexBuffer.addressReused, "aba: the wrapper address must be reused");
|
||||
abaRejects(vertexBuffer);
|
||||
AbaResult depthStencil = runAba(BindKeySource::Indium, Call::DepthStencil, true);
|
||||
CHECK(depthStencil.addressReused, "aba: the wrapper address must be reused");
|
||||
abaRejects(depthStencil);
|
||||
|
||||
// the round-1 key is what this fixes: at the reused wrapper address it drops Y's bind, the
|
||||
// forwarded state keeps X, and the two diverge (one on the Y bind, then the resync)
|
||||
AbaResult round1Texture = runAba(BindKeySource::Wrapper, Call::FragmentTexture, false);
|
||||
CHECK(round1Texture.dropped >= 1 && round1Texture.divergences >= 1,
|
||||
"aba (round-1 key): expected the reused-address bind to be dropped and to diverge (dropped=%lu divergences=%lu)",
|
||||
round1Texture.dropped, round1Texture.divergences);
|
||||
AbaResult round1VertexBuffer = runAba(BindKeySource::Wrapper, Call::VertexBuffer, false);
|
||||
CHECK(round1VertexBuffer.dropped >= 1 && round1VertexBuffer.divergences >= 1,
|
||||
"aba (round-1 key, buffer): expected the same failure (dropped=%lu divergences=%lu)",
|
||||
round1VertexBuffer.dropped, round1VertexBuffer.divergences);
|
||||
// retaining the depth-stencil state does not save the wrapper key: the wrapper address is still
|
||||
// free to be reused, because the record holds the Indium object, not the wrapper
|
||||
AbaResult round1DepthStencil = runAba(BindKeySource::Wrapper, Call::DepthStencil, true);
|
||||
CHECK(round1DepthStencil.dropped >= 1 && round1DepthStencil.divergences >= 1,
|
||||
"aba (round-1 key, depth-stencil): expected the same failure (dropped=%lu divergences=%lu)",
|
||||
round1DepthStencil.dropped, round1DepthStencil.divergences);
|
||||
// and without the retained record the Indium key would fail the same way, which is why the
|
||||
// shipping setDepthStencilState: keeps _depthStencilOwner
|
||||
AbaResult noRetain = runAba(BindKeySource::Indium, Call::DepthStencil, false);
|
||||
CHECK(!noRetain.identitiesDistinct, "aba (indium key, no retention): the freed address must be reusable");
|
||||
CHECK(noRetain.dropped >= 1 && noRetain.divergences >= 1,
|
||||
"aba (indium key, no retention): expected the failure the retained record prevents (dropped=%lu divergences=%lu)",
|
||||
noRetain.dropped, noRetain.divergences);
|
||||
|
||||
std::printf("aba texture indium key: dropped=%lu divergences=%lu | round-1 key: dropped=%lu divergences=%lu\n",
|
||||
texture.dropped, texture.divergences, round1Texture.dropped, round1Texture.divergences);
|
||||
std::printf("aba vertex buffer indium key: dropped=%lu divergences=%lu | round-1 key: dropped=%lu divergences=%lu\n",
|
||||
vertexBuffer.dropped, vertexBuffer.divergences, round1VertexBuffer.dropped, round1VertexBuffer.divergences);
|
||||
std::printf("aba depth-stencil indium key + retained record: dropped=%lu divergences=%lu | without retention: dropped=%lu divergences=%lu\n",
|
||||
depthStencil.dropped, depthStencil.divergences, noRetain.dropped, noRetain.divergences);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------- randomized runs
|
||||
std::mt19937_64 rng(20261014);
|
||||
unsigned long totalDropped = 0, totalCalls = 0;
|
||||
for (int round = 0; round < 200; ++round) {
|
||||
Harness harness;
|
||||
const int callsPerRound = 400 + (int) (rng() % 400);
|
||||
for (int i = 0; i < callsPerRound; ++i) {
|
||||
Call call;
|
||||
call.kind = static_cast<Call::Kind>(rng() % 14);
|
||||
call.object = (rng() % 8 == 0) ? nullptr : o[rng() % 6];
|
||||
call.offset = (rng() % 4) * 256;
|
||||
call.index = (rng() % 40 == 0) ? (NSUInteger) (31 + rng() % 8) : (NSUInteger) (rng() % 5);
|
||||
call.length = 1 + rng() % 3;
|
||||
if (call.kind == Call::VertexBytes || call.kind == Call::FragmentBytes) call.object = bytes;
|
||||
if (call.kind == Call::VertexBuffers || call.kind == Call::FragmentBuffers) {
|
||||
call.index = rng() % 4;
|
||||
call.length = 1 + rng() % 3;
|
||||
}
|
||||
harness.run(call);
|
||||
}
|
||||
totalDropped += harness.dropped;
|
||||
totalCalls += harness.calls;
|
||||
}
|
||||
std::printf("randomized calls=%lu forwarded=%lu dropped=%lu (%.1f%%)\n",
|
||||
totalCalls, totalCalls - totalDropped, totalDropped, 100.0 * totalDropped / totalCalls);
|
||||
std::printf("BIND_DEDUP_MODEL failures=%d\n", failures);
|
||||
return failures != 0;
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
// Model of the two mechanical rewrites in Indium's blit encoder.
|
||||
//
|
||||
// The shipping change (deps/indium/src/indium/blit-command-encoder.cpp) is:
|
||||
// 1. the BlitCommandEncoder entry points take `const std::shared_ptr<Buffer>&`
|
||||
// instead of `std::shared_ptr<Buffer>`, so calling them no longer copies the
|
||||
// shared_ptr (one atomic increment at the call, one atomic decrement at return);
|
||||
// 2. `auto privateSource = std::dynamic_pointer_cast<PrivateBuffer>(source)` became
|
||||
// `auto* privateSource = dynamic_cast<PrivateBuffer*>(source.get())`: the same
|
||||
// dynamic_cast on the same object, without building a shared_ptr for the result.
|
||||
//
|
||||
// The real classes need a Vulkan device, so this models the two rewrites on stand-ins
|
||||
// with the same shape (private classes derived from the public ones) and checks them
|
||||
// against the previous formulation for the cases the wrapper can produce: a real
|
||||
// resource, a null pointer, and a public object that is not the private class.
|
||||
//
|
||||
// It also counts the atomic operations, which is what the change is for.
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
static int failures = 0;
|
||||
#define CHECK(cond, ...) do { if (!(cond)) { ++failures; std::printf("FAIL %s:%d: ", __FILE__, __LINE__); std::printf(__VA_ARGS__); std::printf("\n"); } } while (0)
|
||||
|
||||
struct Buffer {
|
||||
long handle = 1;
|
||||
virtual ~Buffer() = default;
|
||||
};
|
||||
struct PrivateBuffer: Buffer {
|
||||
long privateHandle = 2;
|
||||
};
|
||||
// a public Buffer the private class does not answer for (only the dynamic cast can tell)
|
||||
struct ForeignBuffer: Buffer {
|
||||
virtual ~ForeignBuffer() = default;
|
||||
};
|
||||
|
||||
using BufferPtr = std::shared_ptr<Buffer>;
|
||||
|
||||
// ---------------------------------------------------------------- before the change
|
||||
static long oldPrivateHandle(const BufferPtr& source) {
|
||||
auto privateSource = std::dynamic_pointer_cast<PrivateBuffer>(source);
|
||||
long handle = privateSource ? privateSource->privateHandle : -1;
|
||||
return handle;
|
||||
}
|
||||
static long oldCopy(BufferPtr source, BufferPtr destination) { // by value, as before
|
||||
long result = oldPrivateHandle(source) + oldPrivateHandle(destination);
|
||||
return result;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- after the change
|
||||
static long newPrivateHandle(const BufferPtr& source) {
|
||||
auto* privateSource = dynamic_cast<PrivateBuffer*>(source.get());
|
||||
long handle = privateSource ? privateSource->privateHandle : -1;
|
||||
return handle;
|
||||
}
|
||||
static long newCopy(const BufferPtr& source, const BufferPtr& destination) { // by reference now
|
||||
long result = newPrivateHandle(source) + newPrivateHandle(destination);
|
||||
return result;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ copy counts
|
||||
// The two formulations again, with a stand-in for shared_ptr that counts copies: this
|
||||
// is the difference the signature change removes (each copy is an atomic refcount pair
|
||||
// in a real shared_ptr). Moves are free in both formulations, as they are in libc++.
|
||||
template<class T> struct CountingPtr {
|
||||
std::shared_ptr<T> impl;
|
||||
long* copies;
|
||||
CountingPtr(std::shared_ptr<T> value, long* counter) : impl(std::move(value)), copies(counter) {}
|
||||
CountingPtr(const CountingPtr& other) : impl(other.impl), copies(other.copies) { ++*copies; }
|
||||
CountingPtr(CountingPtr&& other) noexcept : impl(std::move(other.impl)), copies(other.copies) {}
|
||||
CountingPtr& operator=(const CountingPtr&) = delete;
|
||||
T* get() const { return impl.get(); }
|
||||
};
|
||||
|
||||
static long oldCopyCount(const CountingPtr<Buffer> source, const CountingPtr<Buffer> destination) {
|
||||
CountingPtr<Buffer> sourceLocal = source; // the local the cast built
|
||||
long handle = sourceLocal.get() ? 1 : 0;
|
||||
return handle + (destination.get() ? 1 : 0);
|
||||
}
|
||||
static long newCopyCount(const CountingPtr<Buffer>& source, const CountingPtr<Buffer>& destination) {
|
||||
auto* privateSource = dynamic_cast<PrivateBuffer*>(source.get());
|
||||
return (privateSource ? 1 : 0) + (destination.get() ? 1 : 0);
|
||||
}
|
||||
|
||||
int main() {
|
||||
const BufferPtr real = std::make_shared<PrivateBuffer>();
|
||||
const BufferPtr foreign = std::make_shared<ForeignBuffer>();
|
||||
const BufferPtr empty;
|
||||
|
||||
// the dynamic cast answers exactly what the shared_ptr cast answered
|
||||
CHECK(oldPrivateHandle(real) == newPrivateHandle(real), "real private resource");
|
||||
CHECK(oldPrivateHandle(foreign) == newPrivateHandle(foreign), "public resource that is not private");
|
||||
CHECK(oldPrivateHandle(empty) == newPrivateHandle(empty), "null resource");
|
||||
CHECK(newPrivateHandle(foreign) == -1, "a non-private resource must not be treated as private");
|
||||
|
||||
// ... and through a full copy call, with every kind of argument the wrapper produces
|
||||
const long expected = oldCopy(real, real);
|
||||
CHECK(newCopy(real, real) == expected, "copy(real, real)");
|
||||
CHECK(newCopy(real, empty) == oldCopy(real, empty), "copy(real, null)");
|
||||
CHECK(newCopy(foreign, real) == oldCopy(foreign, real), "copy(foreign, real)");
|
||||
// temporaries bind to the const references and live for the whole call
|
||||
CHECK(newCopy(BufferPtr(new PrivateBuffer), BufferPtr(new PrivateBuffer)) == expected, "copy(temporary, temporary)");
|
||||
|
||||
// the point of the change: the by-value call copies the shared_ptr twice (two atomic
|
||||
// refcount pairs per buffer), the by-reference call copies none
|
||||
long oldCopies = 0, newCopies = 0;
|
||||
{
|
||||
CountingPtr<Buffer> a(real, &oldCopies), b(real, &oldCopies);
|
||||
oldCopyCount(a, b);
|
||||
}
|
||||
{
|
||||
CountingPtr<Buffer> a(real, &newCopies), b(real, &newCopies);
|
||||
newCopyCount(a, b);
|
||||
}
|
||||
// the model copies once per by-value parameter plus once for the cast's result
|
||||
CHECK(oldCopies >= 3, "by-value parameters must copy the shared_ptr (%ld copies)", oldCopies);
|
||||
CHECK(newCopies == 0, "by-reference parameters must not copy it (%ld copies)", newCopies);
|
||||
std::printf("blit signature model: copies per buffer->buffer copy: old=%ld new=%ld\n", oldCopies, newCopies);
|
||||
|
||||
std::printf("BLIT_CAST_MODEL failures=%d\n", failures);
|
||||
return failures != 0;
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Extract the bind-tracking helpers from the Metal wrapper verbatim, for the host model.
|
||||
|
||||
The model test (bind-dedup-model.cpp) must exercise the code that ships, not a copy of
|
||||
it: this pulls the region from `struct MTLBindObjectSlots` to the diagnostic counter out
|
||||
of src/Metal/MTLRenderCommandEncoder.mm and writes it as a C++ include fragment.
|
||||
Run it before compiling the model; it fails loudly if the markers moved.
|
||||
"""
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
HERE = Path(__file__).resolve().parent
|
||||
ROOT = HERE.parent.parent
|
||||
SOURCE = ROOT / 'build/darling-src/src/external/metal/src/Metal/MTLRenderCommandEncoder.mm'
|
||||
OUT = ROOT / 'build/riot-metal-bind-model/bind-tracking.extract.inc'
|
||||
OUT.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
text = SOURCE.read_text().splitlines(True)
|
||||
start = next((i for i, line in enumerate(text) if line.startswith('struct MTLBindObjectSlots')), None)
|
||||
end = next((i for i, line in enumerate(text) if line.startswith('// Diagnostic (guest flag /tmp/metal-debug-binds')), None)
|
||||
if start is None or end is None or end <= start:
|
||||
sys.exit('bind-tracking markers not found in %s' % SOURCE)
|
||||
|
||||
region = ''.join(text[start:end]).rstrip() + '\n'
|
||||
OUT.write_text('// Generated by extract-bind-tracking.py from %s (lines %d-%d). Do not edit.\n%s'
|
||||
% (SOURCE.name, start + 1, end, region))
|
||||
print('wrote %s (%d lines)' % (OUT, region.count('\n')))
|
||||
Executable
+11
@@ -0,0 +1,11 @@
|
||||
#!/bin/sh
|
||||
# Host models of the Metal wrapper's bind deduplication (the shipped helpers, extracted verbatim) and of the Indium
|
||||
# blit cast change. Linux only; never runs a Mach-O binary.
|
||||
set -e
|
||||
here=$(cd "$(dirname "$0")" && pwd)
|
||||
out=$(cd "$here/../.." && pwd)/build/riot-metal-bind-model
|
||||
python3 "$here/extract-bind-tracking.py"
|
||||
c++ -std=c++17 -O2 -Wall -I"$out" -o "$out/bind-dedup-model" "$here/bind-dedup-model.cpp"
|
||||
c++ -std=c++17 -O2 -Wall -o "$out/blit-cast-model" "$here/blit-cast-model.cpp"
|
||||
"$out/bind-dedup-model"
|
||||
"$out/blit-cast-model"
|
||||
@@ -0,0 +1,231 @@
|
||||
/*
|
||||
* Host-side benchmark of the libplatform primitives against the host's glibc.
|
||||
*
|
||||
* The `current` copy is the code the installed libsystem_platform.dylib was
|
||||
* built from (memmove.c/bzero.c/memset_pattern.c at -O2 -fno-builtin, the rest
|
||||
* at -O0) and the `new` copy is what tools/build-riot-platform-strings.py
|
||||
* produces (-O2 for every string source, plus the vector paths). Both are the
|
||||
* same sources as the guest library; glibc is measured in the same process for
|
||||
* scale, and is not what the guest runs.
|
||||
*
|
||||
* Sizes 1..256, then 512, 1 KiB, 4 KiB, 16 KiB, 64 KiB and 1 MiB; the source
|
||||
* and destination advance by 32 bytes per iteration so every alignment mod 64
|
||||
* is exercised. Every call is a direct call through a volatile function
|
||||
* pointer and every result feeds a volatile sink, so nothing is folded away.
|
||||
* This is a throughput benchmark, not an FPS claim.
|
||||
*/
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <time.h>
|
||||
#include <unistd.h>
|
||||
|
||||
uint64_t platform_test_capabilities;
|
||||
|
||||
#define DECLARE_VARIANT(prefix) \
|
||||
extern void *prefix##_platform_memmove(void *, const void *, size_t); \
|
||||
extern void *prefix##_platform_memset(void *, int, size_t); \
|
||||
extern int prefix##_platform_memcmp(const void *, const void *, size_t); \
|
||||
extern void *prefix##_platform_memchr(const void *, int, size_t); \
|
||||
extern size_t prefix##_platform_strlen(const char *); \
|
||||
extern int prefix##_platform_strcmp(const char *, const char *);
|
||||
|
||||
DECLARE_VARIANT(current)
|
||||
DECLARE_VARIANT(new)
|
||||
|
||||
static volatile size_t sink;
|
||||
static volatile size_t g_size;
|
||||
static volatile unsigned g_iterations;
|
||||
|
||||
static double now_ns(void)
|
||||
{
|
||||
struct timespec time;
|
||||
|
||||
clock_gettime(CLOCK_MONOTONIC, &time);
|
||||
return (double)time.tv_sec * 1e9 + (double)time.tv_nsec;
|
||||
}
|
||||
|
||||
static unsigned iterations_for(size_t size)
|
||||
{
|
||||
size_t iterations = 4000000 / (size + 16);
|
||||
|
||||
if (iterations < 100)
|
||||
iterations = 100;
|
||||
if (iterations > 200000)
|
||||
iterations = 200000;
|
||||
return (unsigned)iterations;
|
||||
}
|
||||
|
||||
static char *source;
|
||||
static char *destination;
|
||||
static size_t capacity = 4u << 20;
|
||||
|
||||
enum operation { MOVE, FILL, COMPARE, SEARCH, LENGTH, STRINGCOMPARE };
|
||||
static const char *operation_names[] = { "memcpy", "memset", "memcmp", "memchr", "strlen", "strcmp" };
|
||||
|
||||
typedef void (*operation_function)(int which);
|
||||
|
||||
static void run_current(int which)
|
||||
{
|
||||
size_t size = g_size;
|
||||
unsigned iterations = g_iterations;
|
||||
unsigned index;
|
||||
|
||||
for (index = 0; index < iterations; index++) {
|
||||
size_t offset = (index * 32) & 4095;
|
||||
size_t i = (index & 1) ? offset : 0;
|
||||
|
||||
switch (which) {
|
||||
case MOVE: sink = (size_t)current_platform_memmove(destination + i, source + i, size); break;
|
||||
case FILL: sink = (size_t)current_platform_memset(destination + i, 0x5a, size); break;
|
||||
case COMPARE: sink = (size_t)current_platform_memcmp(source + i, destination + i, size); break;
|
||||
case SEARCH: sink = (size_t)current_platform_memchr(source + i, 0xa5, size); break;
|
||||
case LENGTH: sink = current_platform_strlen(source + i); break;
|
||||
default: sink = (size_t)current_platform_strcmp(source + i, destination + i); break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void run_new(int which)
|
||||
{
|
||||
size_t size = g_size;
|
||||
unsigned iterations = g_iterations;
|
||||
unsigned index;
|
||||
|
||||
for (index = 0; index < iterations; index++) {
|
||||
size_t offset = (index * 32) & 4095;
|
||||
size_t i = (index & 1) ? offset : 0;
|
||||
|
||||
switch (which) {
|
||||
case MOVE: sink = (size_t)new_platform_memmove(destination + i, source + i, size); break;
|
||||
case FILL: sink = (size_t)new_platform_memset(destination + i, 0x5a, size); break;
|
||||
case COMPARE: sink = (size_t)new_platform_memcmp(source + i, destination + i, size); break;
|
||||
case SEARCH: sink = (size_t)new_platform_memchr(source + i, 0xa5, size); break;
|
||||
case LENGTH: sink = new_platform_strlen(source + i); break;
|
||||
default: sink = (size_t)new_platform_strcmp(source + i, destination + i); break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void run_glibc(int which)
|
||||
{
|
||||
size_t size = g_size;
|
||||
unsigned iterations = g_iterations;
|
||||
unsigned index;
|
||||
|
||||
for (index = 0; index < iterations; index++) {
|
||||
size_t offset = (index * 32) & 4095;
|
||||
size_t i = (index & 1) ? offset : 0;
|
||||
|
||||
switch (which) {
|
||||
case MOVE: sink = (size_t)memcpy(destination + i, source + i, size); break;
|
||||
case FILL: sink = (size_t)memset(destination + i, 0x5a, size); break;
|
||||
case COMPARE: sink = (size_t)memcmp(source + i, destination + i, size); break;
|
||||
case SEARCH: sink = (size_t)memchr(source + i, 0xa5, size); break;
|
||||
case LENGTH: sink = strlen(source + i); break;
|
||||
default: sink = (size_t)strcmp(source + i, destination + i); break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static double measure(operation_function function, int which)
|
||||
{
|
||||
double best = 1e30;
|
||||
int round;
|
||||
|
||||
for (round = 0; round < 3; round++) {
|
||||
double start, elapsed;
|
||||
|
||||
start = now_ns();
|
||||
function(which);
|
||||
elapsed = now_ns() - start;
|
||||
if (elapsed < best)
|
||||
best = elapsed;
|
||||
}
|
||||
return best / (double)g_iterations;
|
||||
}
|
||||
|
||||
static void prepare_data(int which, size_t size)
|
||||
{
|
||||
size_t boundary;
|
||||
|
||||
switch (which) {
|
||||
case COMPARE:
|
||||
/* a difference at the end, so a compare scans the whole range */
|
||||
memcpy(destination, source, capacity + 8192);
|
||||
if (size != 0)
|
||||
destination[size - 1] = (unsigned char)(source[size - 1] ^ 0x20);
|
||||
break;
|
||||
case SEARCH:
|
||||
/* 0xa5 does not appear in the source */
|
||||
memset(source, 'a', capacity + 8192);
|
||||
break;
|
||||
case LENGTH:
|
||||
case STRINGCOMPARE:
|
||||
/* the strings end every size+1 bytes, so the offset sweep stays valid */
|
||||
memset(source, 'a', capacity + 8192);
|
||||
memset(destination, 'a', capacity + 8192);
|
||||
for (boundary = size + 1; boundary < capacity; boundary += size + 1) {
|
||||
source[boundary] = 0;
|
||||
destination[boundary] = 0;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static void measure_size(size_t size)
|
||||
{
|
||||
int which;
|
||||
|
||||
g_size = size;
|
||||
g_iterations = iterations_for(size);
|
||||
memset(source, 'a', capacity + 8192);
|
||||
memset(destination, 'a', capacity + 8192);
|
||||
for (which = 0; which < 6; which++) {
|
||||
double current, updated, glibc;
|
||||
|
||||
prepare_data(which, size);
|
||||
current = measure(run_current, which);
|
||||
prepare_data(which, size);
|
||||
updated = measure(run_new, which);
|
||||
prepare_data(which, size);
|
||||
glibc = measure(run_glibc, which);
|
||||
printf("BENCH size=%zu op=%s current_ns=%.2f new_ns=%.2f glibc_ns=%.2f current_vs_glibc=%.2f new_vs_glibc=%.2f\n",
|
||||
size, operation_names[which], current, updated, glibc,
|
||||
current / glibc, updated / glibc);
|
||||
}
|
||||
}
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
size_t size;
|
||||
int single = 0;
|
||||
|
||||
if (argc > 1 && strcmp(argv[1], "--no-avx2") == 0) {
|
||||
platform_test_capabilities = 0;
|
||||
single = 1;
|
||||
} else {
|
||||
platform_test_capabilities = 0x20000000 | 0x08000000;
|
||||
}
|
||||
(void)single;
|
||||
|
||||
source = malloc(capacity + 8192);
|
||||
destination = malloc(capacity + 8192);
|
||||
if (source == NULL || destination == NULL)
|
||||
return 1;
|
||||
memset(source, 'a', capacity + 8192);
|
||||
memset(destination, 'a', capacity + 8192);
|
||||
|
||||
for (size = 1; size <= 256; size++)
|
||||
measure_size(size);
|
||||
measure_size(512);
|
||||
measure_size(1024);
|
||||
measure_size(4096);
|
||||
measure_size(16384);
|
||||
measure_size(65536);
|
||||
measure_size(1048576);
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,672 @@
|
||||
/*
|
||||
* Host-side test for the libplatform string primitives the guest uses.
|
||||
*
|
||||
* Two copies of the same sources are linked in:
|
||||
*
|
||||
* current - the flags the installed libsystem_platform.dylib was built with
|
||||
* (memmove.c/bzero.c/memset_pattern.c at -O2 -fno-builtin, the rest
|
||||
* at -O0);
|
||||
* new - the flags tools/build-riot-platform-strings.py uses (-O2
|
||||
* -fno-builtin -fno-strict-aliasing for every string source).
|
||||
*
|
||||
* Both copies read one capability word, so this binary covers the generic
|
||||
* path, the vector path and the difference between them: every case runs
|
||||
* against both copies and against a byte-loop reference, and the guarded
|
||||
* regions turn any access outside [buffer, buffer+size) into a test failure.
|
||||
*
|
||||
* The flags of each copy are the build script's, not this file's; see
|
||||
* tools/build-riot-platform-memory-check.py. Host only: no Darling process,
|
||||
* prefix or guest file is touched.
|
||||
*/
|
||||
#include <stddef.h>
|
||||
#include <signal.h>
|
||||
#include <setjmp.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/mman.h>
|
||||
#include <unistd.h>
|
||||
|
||||
uint64_t platform_test_capabilities; /* the shim's commpage word */
|
||||
|
||||
#define DECLARE_VARIANT(prefix) \
|
||||
extern void *prefix##_platform_memmove(void *, const void *, size_t); \
|
||||
extern void *prefix##_platform_memset(void *, int, size_t); \
|
||||
extern void prefix##_platform_bzero(void *, size_t); \
|
||||
extern void prefix##_platform_memset_pattern16(void *, const void *, size_t); \
|
||||
extern int prefix##_platform_memcmp(const void *, const void *, size_t); \
|
||||
extern void *prefix##_platform_memchr(const void *, int, size_t); \
|
||||
extern size_t prefix##_platform_strlen(const char *); \
|
||||
extern int prefix##_platform_strcmp(const char *, const char *);
|
||||
|
||||
DECLARE_VARIANT(current)
|
||||
DECLARE_VARIANT(new)
|
||||
|
||||
struct primitives {
|
||||
const char *name;
|
||||
void *(*memmove)(void *, const void *, size_t);
|
||||
void *(*memset_)(void *, int, size_t);
|
||||
void (*bzero)(void *, size_t);
|
||||
void (*memset_pattern16)(void *, const void *, size_t);
|
||||
int (*memcmp_)(const void *, const void *, size_t);
|
||||
void *(*memchr)(const void *, int, size_t);
|
||||
size_t (*strlen_)(const char *);
|
||||
int (*strcmp_)(const char *, const char *);
|
||||
};
|
||||
|
||||
#define VARIANT(prefix) \
|
||||
{ #prefix, prefix##_platform_memmove, prefix##_platform_memset, prefix##_platform_bzero, \
|
||||
prefix##_platform_memset_pattern16, prefix##_platform_memcmp, prefix##_platform_memchr, \
|
||||
prefix##_platform_strlen, prefix##_platform_strcmp }
|
||||
|
||||
static const struct primitives variants[] = { VARIANT(current), VARIANT(new) };
|
||||
|
||||
#define VARIANT_COUNT (sizeof(variants) / sizeof(variants[0]))
|
||||
|
||||
#define MARKER 0x5a
|
||||
|
||||
static size_t page_size;
|
||||
static unsigned char *guard_left; /* first mapped byte */
|
||||
static unsigned char *guard_right; /* one past the last mapped byte */
|
||||
static size_t guarded_capacity;
|
||||
|
||||
static sigjmp_buf fault_jump;
|
||||
static volatile int fault_armed;
|
||||
static unsigned long fault_count;
|
||||
|
||||
static unsigned long failures;
|
||||
static unsigned long cases;
|
||||
|
||||
static unsigned char reference_source[1 << 16];
|
||||
static unsigned char reference_result[1 << 16];
|
||||
|
||||
static void fault_handler(int signal)
|
||||
{
|
||||
(void)signal;
|
||||
if (fault_armed) {
|
||||
fault_count++;
|
||||
siglongjmp(fault_jump, 1);
|
||||
}
|
||||
_exit(2);
|
||||
}
|
||||
|
||||
static void fail(const struct primitives *variant, const char *what, const char *detail,
|
||||
size_t a, size_t b)
|
||||
{
|
||||
fprintf(stderr, "FAIL %s %s %s a=%zu b=%zu\n", variant->name, what, detail, a, b);
|
||||
if (++failures > 20)
|
||||
exit(3);
|
||||
}
|
||||
|
||||
static void fill_pattern(unsigned char *buffer, size_t size, unsigned seed)
|
||||
{
|
||||
size_t index;
|
||||
uint32_t state = seed * 2654435761u + 1;
|
||||
|
||||
for (index = 0; index < size; index++) {
|
||||
state ^= state << 13; state ^= state >> 17; state ^= state << 5;
|
||||
buffer[index] = (unsigned char)(state >> 8);
|
||||
}
|
||||
}
|
||||
|
||||
/* Returns the number of differences between the destination and the bytes a
|
||||
* practical memmove leaves there. */
|
||||
static unsigned long move_mismatches(const unsigned char *destination, size_t size)
|
||||
{
|
||||
size_t index;
|
||||
|
||||
for (index = 0; index < size; index++) {
|
||||
if (destination[index] != reference_result[index])
|
||||
return size - index;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* One memmove inside the guarded region. The caller has already placed the
|
||||
* source bytes and filled the rest of the region with MARKER; this snapshots
|
||||
* the whole region, computes what a byte loop would leave there, calls the
|
||||
* implementation and compares the whole region, so a write outside
|
||||
* [destination, destination+size) or into the source shows up as a difference
|
||||
* wherever it lands.
|
||||
*/
|
||||
static void check_move(const struct primitives *variant, unsigned char *destination,
|
||||
const unsigned char *source, size_t size, const char *detail,
|
||||
size_t a, size_t b)
|
||||
{
|
||||
const ptrdiff_t destination_offset = destination - guard_left;
|
||||
const ptrdiff_t source_offset = source - guard_left;
|
||||
size_t index;
|
||||
|
||||
memcpy(reference_source, guard_left, guarded_capacity);
|
||||
memcpy(reference_result, reference_source, guarded_capacity);
|
||||
/* from the snapshot, so the reference has memmove semantics whatever the
|
||||
direction of the overlap */
|
||||
for (index = 0; index < size; index++)
|
||||
reference_result[destination_offset + index] = reference_source[source_offset + index];
|
||||
cases++;
|
||||
if (variant->memmove(destination, source, size) != destination) {
|
||||
fail(variant, "memmove", "return value", size, a);
|
||||
return;
|
||||
}
|
||||
for (index = 0; index < guarded_capacity; index++) {
|
||||
if (guard_left[index] != reference_result[index]) {
|
||||
fail(variant, "memmove", detail, size, a);
|
||||
fprintf(stderr, " size=%zu distance=%zu first difference at %zu (destination %td source %td)\n",
|
||||
size, a, index, destination_offset, source_offset);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* The same comparison for two heap buffers that may overlap. */
|
||||
static void check_move_plain(const struct primitives *variant, unsigned char *destination,
|
||||
const unsigned char *source, size_t size, const char *detail,
|
||||
size_t a)
|
||||
{
|
||||
size_t index;
|
||||
int overlapping = !((const unsigned char *)destination + size <= source ||
|
||||
source + size <= (const unsigned char *)destination);
|
||||
|
||||
memcpy(reference_source, source, size);
|
||||
memcpy(reference_result, reference_source, size);
|
||||
if (!overlapping)
|
||||
memset(destination, MARKER, size);
|
||||
cases++;
|
||||
if (variant->memmove(destination, source, size) != destination) {
|
||||
fail(variant, "memmove", "return value", size, a);
|
||||
return;
|
||||
}
|
||||
if (move_mismatches(destination, size)) {
|
||||
fail(variant, "memmove", detail, size, a);
|
||||
return;
|
||||
}
|
||||
if (!overlapping) {
|
||||
for (index = size; index < size + 64; index++) {
|
||||
if (destination[index] != MARKER) {
|
||||
fail(variant, "memmove", "wrote past destination", size, a);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* memmove/memcpy between buffers with an unmapped page on each side. */
|
||||
static void test_move_guarded(const struct primitives *variant)
|
||||
{
|
||||
static const size_t interesting[] = {
|
||||
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 15, 16, 17, 23, 24, 25, 31, 32, 33,
|
||||
47, 48, 49, 63, 64, 65, 79, 95, 96, 97, 127, 128, 129, 191, 255, 256,
|
||||
257, 383, 511, 512, 513, 1023, 1024, 1025, 2047, 2048, 2049, 4095, 4096, 4097,
|
||||
8191, 8192, 8193,
|
||||
};
|
||||
static const size_t deltas[] = { 0, 1, 2, 3, 7, 8, 15, 16, 17, 31, 32, 33, 63, 64, 65, 129 };
|
||||
size_t index, shift, delta;
|
||||
|
||||
for (index = 0; index < sizeof(interesting) / sizeof(interesting[0]); index++) {
|
||||
size_t size = interesting[index];
|
||||
|
||||
if (size + 64 >= guarded_capacity / 2)
|
||||
continue;
|
||||
for (shift = 1; shift < 32; shift += 7) {
|
||||
/*
|
||||
* Source against the upper page, destination against the
|
||||
* lower one, and the other way round; shift moves the buffer
|
||||
* away from its guard page by up to 31 bytes, so every buffer
|
||||
* keeps a byte of mapped space on each side of it.
|
||||
*/
|
||||
unsigned char *upper = guard_right - shift - size;
|
||||
unsigned char *lower = guard_left + shift;
|
||||
|
||||
memset(guard_left, MARKER, guarded_capacity);
|
||||
fill_pattern(upper, size, (unsigned)(size + shift));
|
||||
check_move(variant, lower, upper, size, "guarded destination low", shift, size);
|
||||
memset(guard_left, MARKER, guarded_capacity);
|
||||
fill_pattern(lower, size, (unsigned)(size + shift));
|
||||
check_move(variant, upper, lower, size, "guarded destination high", shift, size);
|
||||
|
||||
for (delta = 0; delta < sizeof(deltas) / sizeof(deltas[0]); delta++) {
|
||||
size_t distance = deltas[delta];
|
||||
|
||||
if (size + distance + shift >= guarded_capacity / 2)
|
||||
continue;
|
||||
memset(guard_left, MARKER, guarded_capacity);
|
||||
{
|
||||
unsigned char *source = guard_left + shift + distance;
|
||||
unsigned char *destination = guard_left + shift;
|
||||
|
||||
fill_pattern(source, size, (unsigned)(size + shift + distance));
|
||||
check_move(variant, destination, source, size,
|
||||
"overlap destination below", distance, size);
|
||||
}
|
||||
memset(guard_left, MARKER, guarded_capacity);
|
||||
{
|
||||
unsigned char *source = guard_left + shift;
|
||||
unsigned char *destination = guard_left + shift + distance;
|
||||
|
||||
fill_pattern(source, size, (unsigned)(size + shift));
|
||||
check_move(variant, destination, source, size,
|
||||
"overlap destination above", distance, size);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* A destination that only just fits the guarded region, so both sides are
|
||||
* unmapped at once. */
|
||||
static void test_move_both_guards(const struct primitives *variant)
|
||||
{
|
||||
size_t size = guarded_capacity - 128;
|
||||
size_t index;
|
||||
|
||||
memset(guard_left, MARKER, guarded_capacity);
|
||||
fill_pattern(guard_left + 64, size, 99);
|
||||
for (index = 0; index + 64 <= size; index += 509) {
|
||||
check_move(variant, guard_left + 64, guard_left + 64 + index, size - index,
|
||||
"both guards", index, size);
|
||||
}
|
||||
}
|
||||
|
||||
static void test_move_alignments(const struct primitives *variant, size_t limit)
|
||||
{
|
||||
static const unsigned alignments[][2] = {
|
||||
{ 0, 0 }, { 1, 0 }, { 0, 1 }, { 1, 1 }, { 3, 5 }, { 7, 0 }, { 0, 7 },
|
||||
{ 15, 1 }, { 1, 15 }, { 31, 0 }, { 0, 31 }, { 33, 63 }, { 63, 33 },
|
||||
{ 63, 63 }, { 16, 48 }, { 48, 16 },
|
||||
};
|
||||
size_t size, pair;
|
||||
unsigned char *destination = malloc(limit + 256);
|
||||
unsigned char *source = malloc(limit + 256);
|
||||
|
||||
for (size = 0; size <= limit; size++) {
|
||||
for (pair = 0; pair < sizeof(alignments) / sizeof(alignments[0]); pair++) {
|
||||
unsigned char *d = destination + 64 + alignments[pair][0];
|
||||
unsigned char *s = source + 64 + alignments[pair][1];
|
||||
|
||||
fill_pattern(source, limit + 256, (unsigned)(size * 131 + pair));
|
||||
memset(d, MARKER, limit + 128);
|
||||
check_move_plain(variant, d, s, size, "alignment", pair);
|
||||
}
|
||||
}
|
||||
free(destination);
|
||||
free(source);
|
||||
}
|
||||
|
||||
/* Random sizes, alignments and overlap distances. */
|
||||
static void test_move_random(const struct primitives *variant, unsigned long rounds)
|
||||
{
|
||||
const size_t capacity = 1 << 16;
|
||||
unsigned char *destination = malloc(capacity + 128);
|
||||
unsigned char *source = malloc(capacity + 128);
|
||||
uint32_t state = 12345;
|
||||
unsigned long round;
|
||||
|
||||
for (round = 0; round < rounds; round++) {
|
||||
size_t size, destination_offset, source_offset;
|
||||
unsigned char *d, *s;
|
||||
|
||||
state ^= state << 13; state ^= state >> 17; state ^= state << 5;
|
||||
size = state % 4096;
|
||||
if ((state & 0x3f) == 0) {
|
||||
state ^= state << 13; state ^= state >> 17; state ^= state << 5;
|
||||
size = state % 16384;
|
||||
}
|
||||
destination_offset = 1 + state % 63;
|
||||
source_offset = 1 + state % 63;
|
||||
fill_pattern(source, capacity + 128, state);
|
||||
memset(destination, MARKER, capacity + 128);
|
||||
d = destination + destination_offset;
|
||||
s = source + source_offset;
|
||||
check_move_plain(variant, d, s, size, "random", round);
|
||||
}
|
||||
free(destination);
|
||||
free(source);
|
||||
}
|
||||
|
||||
/* The same, with the destination inside the source or the source inside the
|
||||
* destination, which is the case a memmove must get right. */
|
||||
static void test_move_overlap_random(const struct primitives *variant, unsigned long rounds)
|
||||
{
|
||||
const size_t capacity = 1 << 16;
|
||||
unsigned char *buffer = malloc(capacity + 128);
|
||||
uint32_t state = 987654321;
|
||||
unsigned long round;
|
||||
|
||||
for (round = 0; round < rounds; round++) {
|
||||
size_t size, offset, delta;
|
||||
unsigned char *s, *d;
|
||||
|
||||
/* both ranges stay inside the buffer, so 2*size must fit */
|
||||
state ^= state << 13; state ^= state >> 17; state ^= state << 5;
|
||||
size = 1 + state % 4096;
|
||||
offset = 1 + state % (capacity - 2 * size - 2);
|
||||
delta = state % size;
|
||||
fill_pattern(buffer, capacity + 128, state);
|
||||
s = buffer + offset;
|
||||
d = buffer + offset + delta;
|
||||
check_move_plain(variant, d, s, size, "overlap above", delta);
|
||||
|
||||
state ^= state << 13; state ^= state >> 17; state ^= state << 5;
|
||||
size = 1 + state % 4096;
|
||||
offset = 1 + state % (capacity - 2 * size - 2);
|
||||
delta = state % size;
|
||||
fill_pattern(buffer, capacity + 128, state);
|
||||
s = buffer + offset + delta;
|
||||
d = buffer + offset;
|
||||
check_move_plain(variant, d, s, size, "overlap below", delta);
|
||||
}
|
||||
/* A copy onto itself must change nothing. */
|
||||
fill_pattern(buffer, 4096, 5);
|
||||
memcpy(reference_result, buffer, 4096);
|
||||
cases++;
|
||||
variant->memmove(buffer, buffer, 4096);
|
||||
if (move_mismatches(buffer, 4096))
|
||||
fail(variant, "memmove", "self copy", 4096, 0);
|
||||
free(buffer);
|
||||
}
|
||||
|
||||
static void test_fill_guarded(const struct primitives *variant)
|
||||
{
|
||||
static const size_t interesting[] = {
|
||||
0, 1, 2, 3, 4, 7, 8, 15, 16, 17, 31, 32, 33, 63, 64, 65, 127, 128, 129,
|
||||
255, 256, 257, 511, 512, 513, 1023, 1024, 2048, 4095, 4096, 4097, 8192,
|
||||
};
|
||||
static const int values[] = { 0, 0x5a, 0xff, 0x100 + 0x37 };
|
||||
size_t index, shift, value_index;
|
||||
|
||||
for (index = 0; index < sizeof(interesting) / sizeof(interesting[0]); index++) {
|
||||
size_t size = interesting[index];
|
||||
|
||||
if (size + 1 >= guarded_capacity / 2)
|
||||
continue;
|
||||
for (shift = 1; shift < 32; shift += 11) {
|
||||
for (value_index = 0; value_index < sizeof(values) / sizeof(values[0]); value_index++) {
|
||||
int value = values[value_index];
|
||||
unsigned char *d = guard_right - shift - size;
|
||||
size_t position;
|
||||
|
||||
memset(guard_left, MARKER, guarded_capacity);
|
||||
cases++;
|
||||
if (variant->memset_(d, value, size) != d)
|
||||
fail(variant, "memset", "return value", size, (size_t)value);
|
||||
for (position = 0; position < size; position++) {
|
||||
if (d[position] != (unsigned char)value) {
|
||||
fail(variant, "memset", "byte", size, position);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (d[-1] != MARKER || d[size] != MARKER)
|
||||
fail(variant, "memset", "wrote outside", size, (size_t)value);
|
||||
|
||||
d = guard_left + shift;
|
||||
memset(guard_left, MARKER, guarded_capacity);
|
||||
cases++;
|
||||
variant->bzero(d, size);
|
||||
for (position = 0; position < size; position++) {
|
||||
if (d[position] != 0) {
|
||||
fail(variant, "bzero", "byte", size, position);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (d[-1] != MARKER || d[size] != MARKER)
|
||||
fail(variant, "bzero", "wrote outside", size, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (index = 0; index < sizeof(interesting) / sizeof(interesting[0]); index++) {
|
||||
size_t size = interesting[index];
|
||||
unsigned char pattern[16];
|
||||
unsigned char *d;
|
||||
size_t position;
|
||||
|
||||
if (size + 1 >= guarded_capacity)
|
||||
continue;
|
||||
d = guard_right - size - 1;
|
||||
memset(guard_left, MARKER, guarded_capacity);
|
||||
fill_pattern(pattern, sizeof(pattern), 3);
|
||||
cases++;
|
||||
variant->memset_pattern16(d, pattern, size);
|
||||
for (position = 0; position < size; position++) {
|
||||
if (d[position] != pattern[position & 15]) {
|
||||
fail(variant, "memset_pattern16", "byte", size, position);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void test_compare(const struct primitives *variant)
|
||||
{
|
||||
static const size_t lengths[] = { 0, 1, 2, 7, 8, 15, 16, 17, 31, 32, 33, 63, 64, 65, 127, 128, 129, 255, 256, 257, 1024, 4096, 4097 };
|
||||
unsigned char *a = malloc(8192);
|
||||
unsigned char *b = malloc(8192);
|
||||
size_t index, difference, length;
|
||||
|
||||
for (index = 0; index < sizeof(lengths) / sizeof(lengths[0]); index++) {
|
||||
length = lengths[index];
|
||||
for (difference = 0; difference <= length; difference += (length > 64 ? 37 : 1)) {
|
||||
int expected, actual;
|
||||
size_t step;
|
||||
|
||||
fill_pattern(a, 8192, (unsigned)length + 1);
|
||||
memcpy(b, a, 8192);
|
||||
expected = 0;
|
||||
if (difference < length) {
|
||||
b[difference] ^= 0x80;
|
||||
expected = (int)a[difference] - (int)b[difference];
|
||||
}
|
||||
actual = variant->memcmp_(a, b, length);
|
||||
cases++;
|
||||
if ((actual == 0) != (expected == 0) || (actual > 0) != (expected > 0))
|
||||
fail(variant, "memcmp", "sign", length, difference);
|
||||
actual = variant->memcmp_(b, a, length);
|
||||
if (actual != -expected)
|
||||
fail(variant, "memcmp", "swapped value", length, difference);
|
||||
for (step = 0; length != 0 && step < 3; step++) {
|
||||
/* A difference after a longer equal prefix. */
|
||||
size_t at = (difference + step * 7919) % length;
|
||||
|
||||
memcpy(b, a, 8192);
|
||||
b[at] ^= 0x01;
|
||||
actual = variant->memcmp_(a, b, length);
|
||||
expected = (int)a[at] - (int)b[at];
|
||||
if ((actual == 0) != (expected == 0) || (actual > 0) != (expected > 0))
|
||||
fail(variant, "memcmp", "second difference", length, at);
|
||||
}
|
||||
}
|
||||
}
|
||||
free(a);
|
||||
free(b);
|
||||
}
|
||||
|
||||
static void test_search(const struct primitives *variant)
|
||||
{
|
||||
unsigned char *buffer = malloc(4096);
|
||||
size_t length, position;
|
||||
|
||||
for (length = 0; length <= 1024; length++) {
|
||||
unsigned char *found;
|
||||
size_t index;
|
||||
|
||||
for (index = 0; index < 4096; index++)
|
||||
buffer[index] = (unsigned char)(index & 1);
|
||||
for (position = 0; position <= length; position += (length > 32 ? 13 : 1)) {
|
||||
unsigned char needle = 0xa5;
|
||||
|
||||
if (position < length)
|
||||
buffer[position] = needle;
|
||||
found = variant->memchr(buffer, needle, length);
|
||||
cases++;
|
||||
if (position < length) {
|
||||
if (found == NULL || (size_t)(found - buffer) != position)
|
||||
fail(variant, "memchr", "found", length, position);
|
||||
} else if (found != NULL) {
|
||||
fail(variant, "memchr", "false positive", length, position);
|
||||
}
|
||||
if (position < length)
|
||||
buffer[position] = (unsigned char)(position & 1);
|
||||
}
|
||||
}
|
||||
free(buffer);
|
||||
}
|
||||
|
||||
static void test_strings(const struct primitives *variant)
|
||||
{
|
||||
unsigned char *buffer = malloc(2048);
|
||||
size_t length;
|
||||
|
||||
for (length = 0; length <= 1024; length++) {
|
||||
size_t index;
|
||||
|
||||
for (index = 0; index < length; index++)
|
||||
buffer[index] = (unsigned char)('a' + (index % 26));
|
||||
buffer[length] = 0;
|
||||
cases++;
|
||||
if (variant->strlen_((const char *)buffer) != length)
|
||||
fail(variant, "strlen", "length", length, 0);
|
||||
}
|
||||
for (length = 0; length <= 512; length += 7) {
|
||||
size_t difference;
|
||||
|
||||
for (difference = 0; difference <= length; difference += 3) {
|
||||
unsigned char a[1024], b[1024];
|
||||
size_t index;
|
||||
int expected, actual;
|
||||
|
||||
for (index = 0; index < length; index++) {
|
||||
/* never zero, so only the intended byte differs */
|
||||
a[index] = (unsigned char)(1 + (index * 13) % 255);
|
||||
b[index] = a[index];
|
||||
}
|
||||
a[length] = b[length] = 0;
|
||||
expected = 0;
|
||||
if (difference < length) {
|
||||
b[difference] = (unsigned char)(a[difference] + (difference & 1 ? 1 : 251));
|
||||
expected = (int)(unsigned char)a[difference] - (int)(unsigned char)b[difference];
|
||||
}
|
||||
actual = variant->strcmp_((const char *)a, (const char *)b);
|
||||
cases++;
|
||||
if ((actual == 0) != (expected == 0) || (actual > 0) != (expected > 0))
|
||||
fail(variant, "strcmp", "sign", length, difference);
|
||||
}
|
||||
}
|
||||
free(buffer);
|
||||
}
|
||||
|
||||
/* Strings whose terminator is the last byte of a guarded page. */
|
||||
static void test_strings_guarded(const struct primitives *variant)
|
||||
{
|
||||
static const size_t lengths[] = { 0, 1, 2, 7, 8, 9, 15, 16, 17, 31, 32, 33, 63, 64, 65, 127, 128, 255, 256, 511, 512, 1023, 2047 };
|
||||
size_t index;
|
||||
|
||||
for (index = 0; index < sizeof(lengths) / sizeof(lengths[0]); index++) {
|
||||
size_t size = lengths[index];
|
||||
char *string;
|
||||
size_t position;
|
||||
|
||||
if (size + 1 >= guarded_capacity)
|
||||
continue;
|
||||
string = (char *)(guard_right - size - 1);
|
||||
for (position = 0; position < size; position++)
|
||||
string[position] = (char)('a' + (position % 26));
|
||||
string[size] = 0;
|
||||
cases++;
|
||||
if (variant->strlen_(string) != size)
|
||||
fail(variant, "strlen", "guarded", size, 0);
|
||||
if (size > 0) {
|
||||
char *other = (char *)guard_left;
|
||||
|
||||
for (position = 0; position < size; position++)
|
||||
other[position] = string[position];
|
||||
other[size] = 0;
|
||||
if (variant->strcmp_(string, other) != 0)
|
||||
fail(variant, "strcmp", "guarded equal", size, 0);
|
||||
/* the last byte differs, so `string` is the smaller one */
|
||||
other[size - 1] = (char)(string[size - 1] + 1);
|
||||
if (variant->strcmp_(string, other) >= 0)
|
||||
fail(variant, "strcmp", "guarded smaller", size, 0);
|
||||
if (variant->strcmp_(other, string) <= 0)
|
||||
fail(variant, "strcmp", "guarded larger", size, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static int run_all(const struct primitives *variant)
|
||||
{
|
||||
unsigned long before = failures;
|
||||
int faulted = 0;
|
||||
|
||||
if (sigsetjmp(fault_jump, 1) == 0) {
|
||||
fault_armed = 1;
|
||||
test_move_guarded(variant);
|
||||
test_move_both_guards(variant);
|
||||
test_fill_guarded(variant);
|
||||
test_strings_guarded(variant);
|
||||
fault_armed = 0;
|
||||
} else {
|
||||
fault_armed = 0;
|
||||
fprintf(stderr, "FAIL %s page fault\n", variant->name);
|
||||
failures++;
|
||||
faulted = 1;
|
||||
}
|
||||
test_move_alignments(variant, 512);
|
||||
test_move_random(variant, 20000);
|
||||
test_move_overlap_random(variant, 5000);
|
||||
test_compare(variant);
|
||||
test_search(variant);
|
||||
test_strings(variant);
|
||||
return failures == before && !faulted;
|
||||
}
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
struct sigaction action;
|
||||
uint64_t capabilities = 0x20000000 | 0x08000000;
|
||||
void *region;
|
||||
size_t index, pages = 12;
|
||||
int ok = 1;
|
||||
|
||||
for (index = 1; index < (size_t)argc; index++) {
|
||||
if (strcmp(argv[index], "--no-avx2") == 0)
|
||||
capabilities = 0;
|
||||
else if (strcmp(argv[index], "--avx2") == 0)
|
||||
capabilities = 0x20000000;
|
||||
else if (strcmp(argv[index], "--avx2-erms") == 0)
|
||||
capabilities = 0x20000000 | 0x08000000;
|
||||
else {
|
||||
fprintf(stderr, "unknown option %s\n", argv[index]);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
page_size = (size_t)sysconf(_SC_PAGESIZE);
|
||||
region = mmap(NULL, page_size * pages, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
|
||||
if (region == MAP_FAILED || page_size < 4096) {
|
||||
fprintf(stderr, "cannot allocate guarded pages\n");
|
||||
return 1;
|
||||
}
|
||||
if (mprotect(region, page_size, PROT_NONE) != 0 ||
|
||||
mprotect((unsigned char *)region + page_size * (pages - 1), page_size, PROT_NONE) != 0) {
|
||||
fprintf(stderr, "cannot protect guarded pages\n");
|
||||
return 1;
|
||||
}
|
||||
guard_left = (unsigned char *)region + page_size;
|
||||
guard_right = guard_left + page_size * (pages - 2);
|
||||
guarded_capacity = (size_t)(guard_right - guard_left);
|
||||
memset(&action, 0, sizeof(action));
|
||||
action.sa_handler = fault_handler;
|
||||
sigemptyset(&action.sa_mask);
|
||||
sigaction(SIGSEGV, &action, NULL);
|
||||
sigaction(SIGBUS, &action, NULL);
|
||||
|
||||
for (index = 0; index < VARIANT_COUNT; index++) {
|
||||
platform_test_capabilities = capabilities;
|
||||
ok = run_all(&variants[index]) && ok;
|
||||
}
|
||||
printf("PLATFORM_MEMORY_CHECK pass=%d capabilities=%#llx cases=%lu mismatches=%lu page_faults=%lu variants=%zu\n",
|
||||
failures == 0 && ok, (unsigned long long)capabilities, cases, failures, fault_count,
|
||||
(size_t)VARIANT_COUNT);
|
||||
return failures == 0 && ok ? 0 : 1;
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
/* Force-included before every libplatform string source: __FBSDID is used
|
||||
* before the sources include platform/string.h. */
|
||||
#define __FBSDID(s)
|
||||
@@ -0,0 +1,18 @@
|
||||
/* Host-build shim standing in for the kernel's i386/cpu_capabilities.h.
|
||||
*
|
||||
* The guest reads the capability word from the fixed commpage address that
|
||||
* mldr maps. The host test cannot rely on that address being free, so the
|
||||
* address is redirected to a variable the test owns; everything about how the
|
||||
* sources read the word stays the same. The capability bits are the kernel's
|
||||
* values (osfmk/i386/cpu_capabilities.h). */
|
||||
#ifndef I386_CPU_CAPABILITIES_SHIM_H
|
||||
#define I386_CPU_CAPABILITIES_SHIM_H
|
||||
#include <stdint.h>
|
||||
|
||||
extern uint64_t platform_test_capabilities;
|
||||
#define _COMM_PAGE_CPU_CAPABILITIES64 ((uintptr_t)&platform_test_capabilities)
|
||||
|
||||
#define kHasAVX2_0 0x20000000
|
||||
#define kHasENFSTRG 0x08000000
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,46 @@
|
||||
/* Host-build shim standing in for Darling's private/platform/string.h.
|
||||
* The only content the string sources need from it is the
|
||||
* _PLATFORM_OPTIMIZED_* selection macros, all 0 in the installed build. */
|
||||
#ifndef PLATFORM_STRING_SHIM_H
|
||||
#define PLATFORM_STRING_SHIM_H
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include <sys/types.h>
|
||||
|
||||
#define _PLATFORM_OPTIMIZED_BZERO 0
|
||||
#define _PLATFORM_OPTIMIZED_MEMCCPY 0
|
||||
#define _PLATFORM_OPTIMIZED_MEMCHR 0
|
||||
#define _PLATFORM_OPTIMIZED_MEMCMP 0
|
||||
#define _PLATFORM_OPTIMIZED_MEMMOVE 0
|
||||
#define _PLATFORM_OPTIMIZED_MEMSET 0
|
||||
#define _PLATFORM_OPTIMIZED_MEMSET_PATTERN4 0
|
||||
#define _PLATFORM_OPTIMIZED_MEMSET_PATTERN8 0
|
||||
#define _PLATFORM_OPTIMIZED_MEMSET_PATTERN16 0
|
||||
#define _PLATFORM_OPTIMIZED_STRCHR 0
|
||||
#define _PLATFORM_OPTIMIZED_STRCMP 0
|
||||
#define _PLATFORM_OPTIMIZED_STRCPY 0
|
||||
#define _PLATFORM_OPTIMIZED_STRLCAT 0
|
||||
#define _PLATFORM_OPTIMIZED_STRLCPY 0
|
||||
#define _PLATFORM_OPTIMIZED_STRLEN 0
|
||||
#define _PLATFORM_OPTIMIZED_STRNCMP 0
|
||||
#define _PLATFORM_OPTIMIZED_STRNCPY 0
|
||||
#define _PLATFORM_OPTIMIZED_STRNLEN 0
|
||||
#define _PLATFORM_OPTIMIZED_STRSTR 0
|
||||
|
||||
/* The host test builds the non-static variant, as the guest dylib does. */
|
||||
#define VARIANT_STATIC 0
|
||||
|
||||
#define __FBSDID(x)
|
||||
|
||||
extern void *_platform_memmove(void *, const void *, size_t);
|
||||
extern void *_platform_memset(void *, int, size_t);
|
||||
extern void _platform_bzero(void *, size_t);
|
||||
extern void _platform_memset_pattern4(void *, const void *, size_t);
|
||||
extern void _platform_memset_pattern8(void *, const void *, size_t);
|
||||
extern void _platform_memset_pattern16(void *, const void *, size_t);
|
||||
extern size_t _platform_strlen(const char *);
|
||||
extern int _platform_strcmp(const char *, const char *);
|
||||
extern int _platform_memcmp(const void *, const void *, size_t);
|
||||
extern void *_platform_memchr(const void *, int, size_t);
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,46 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Stop background compilation while a League game is running.
|
||||
|
||||
The user measures FPS in real matches, and a compile job on this six-core
|
||||
machine would skew the result. Only /proc/<pid>/cmdline is read: no signal is
|
||||
sent, nothing is attached to, and no process is started or stopped. The match
|
||||
is on the exact argv element of the game binary, so the gate's own command line
|
||||
does not match.
|
||||
"""
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
GAME_EXECUTABLE = "Contents/MacOS/LeagueofLegends"
|
||||
|
||||
|
||||
def game_running():
|
||||
for entry in Path("/proc").iterdir():
|
||||
if not entry.name.isdigit():
|
||||
continue
|
||||
try:
|
||||
command_line = (entry / "cmdline").read_bytes()
|
||||
except OSError:
|
||||
continue
|
||||
for argument in command_line.split(b"\0"):
|
||||
if argument.endswith(b"/" + GAME_EXECUTABLE.encode()):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def wait_until_free(poll_seconds=20):
|
||||
"""Block until no game process is left, reporting each wait."""
|
||||
if not game_running():
|
||||
return
|
||||
print("A League game is running; waiting for it to exit before compiling.", flush=True)
|
||||
while game_running():
|
||||
time.sleep(poll_seconds)
|
||||
print("No game process left; compiling.", flush=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if game_running():
|
||||
print("game running")
|
||||
sys.exit(0)
|
||||
print("no game process")
|
||||
sys.exit(1)
|
||||
@@ -0,0 +1,232 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Audit which runtime library file the guest actually loads for every dylib a
|
||||
binary uses, and estimate how it was compiled.
|
||||
|
||||
The guest resolves a dependent dylib by leaf name through DYLD_LIBRARY_PATH
|
||||
before the install path (dyld3/PathOverrides.cpp:forEachPathVariant and the
|
||||
legacy dyld2.cpp:loadPhase1 both do this), so a file in RuntimeLibraries
|
||||
shadows both the per-user prefix copy and the installed SDK copy in
|
||||
/usr/local/libexec/darling. Frameworks resolve the same way through
|
||||
DYLD_FRAMEWORK_PATH.
|
||||
|
||||
The optimization estimate is the share of instructions that read or write a
|
||||
stack slot, counted only in functions of 20 instructions or more. -O0 spills
|
||||
every local; register-allocated -O2 code mostly does not. Calibrate with
|
||||
--calibrate against the libmalloc -O0/-O2 objects in
|
||||
build/perf-research/memory/o-compare.
|
||||
"""
|
||||
import argparse
|
||||
import collections
|
||||
import json
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
GAME = (Path.home() / ".darling/Applications/League of Legends.app/Contents/LoL/Game"
|
||||
/ "LeagueofLegends.app/Contents/MacOS/LeagueofLegends")
|
||||
# Overlay artifacts the tools/ builders produce and install; used only to name
|
||||
# the builder behind an overlay file.
|
||||
BUILDERS = {
|
||||
"libsystem_kernel.dylib": "build-riot-kqueue-runtime.py",
|
||||
"libsystem_malloc.dylib": "build-riot-malloc-runtime.py",
|
||||
"libsystem_pthread.dylib": "build-riot-pthread-runtime.py",
|
||||
"libsystem_platform.dylib": "build-riot-platform-strings.py",
|
||||
"libobjc.A.dylib": "build-riot-objc-runtime.py",
|
||||
"libc++abi.dylib": "build-riot-cxxabi-runtime.py",
|
||||
"libc++.1.dylib": "build-riot-cxx-optimized.py",
|
||||
"libindium.dylib": "build-riot-indium-runtime.py",
|
||||
"libiridium.dylib": "build-riot-indium-runtime.py",
|
||||
"libz.1.dylib": "-",
|
||||
}
|
||||
MIN_FUNCTION_INSTRUCTIONS = 20
|
||||
|
||||
|
||||
def tool(*arguments):
|
||||
return subprocess.run(arguments, capture_output=True, text=True, errors="replace").stdout
|
||||
|
||||
|
||||
def load_commands(path, requested):
|
||||
"""Direct dependencies of one Mach-O file, in load-command order."""
|
||||
text = tool("llvm-objdump", "--macho", "--dylibs-used", str(path))
|
||||
dependencies = []
|
||||
for line in text.splitlines():
|
||||
match = re.match(r"^\t(\S+) \(compatibility version", line)
|
||||
if match:
|
||||
dependencies.append(match.group(1))
|
||||
return dependencies
|
||||
|
||||
|
||||
def rpaths_of(path):
|
||||
text = tool("llvm-objdump", "--macho", "--private-headers", str(path))
|
||||
return re.findall(r"^\s+path (\S+) \(offset \d+\)$", text, re.M)
|
||||
|
||||
|
||||
def framework_partial_path(path):
|
||||
index = path.find(".framework/")
|
||||
if index < 0:
|
||||
return None
|
||||
return path[path.rfind("/", 0, index) + 1:]
|
||||
|
||||
|
||||
def resolve(install_path, origin, rpaths, config):
|
||||
"""Return the file the guest would load for an install path, or None."""
|
||||
overlay = config["prefix"].rstrip("/") + config["guest_home"] + "/RuntimeLibraries"
|
||||
frameworks = config["prefix"].rstrip("/") + config["guest_home"] + "/RuntimeFrameworks"
|
||||
sdk = config["sdk"].rstrip("/")
|
||||
candidates = []
|
||||
partial = framework_partial_path(install_path)
|
||||
if partial is not None:
|
||||
candidates.append(frameworks + "/" + partial)
|
||||
else:
|
||||
candidates.append(overlay + "/" + install_path.rsplit("/", 1)[-1])
|
||||
if install_path.startswith("@rpath/"):
|
||||
for rpath in list(rpaths) + [None]:
|
||||
base = str(Path(origin).parent) if rpath in ("@loader_path", None) else rpath
|
||||
if rpath == "@executable_path":
|
||||
base = str(Path(config["game"]).parent)
|
||||
if base is not None:
|
||||
candidates.append(base.rstrip("/") + "/" + install_path[len("@rpath/"):])
|
||||
elif install_path.startswith("@loader_path/"):
|
||||
candidates.append(str(Path(origin).parent) + "/" + install_path[len("@loader_path/"):])
|
||||
elif install_path.startswith("@executable_path/"):
|
||||
candidates.append(str(Path(config["game"]).parent) + "/" + install_path[len("@executable_path/"):])
|
||||
else:
|
||||
candidates.append(install_path)
|
||||
for candidate in candidates:
|
||||
for root in ("", config["prefix"].rstrip("/"), sdk):
|
||||
path = Path(root + candidate) if root or candidate.startswith("/") else None
|
||||
if path is not None and path.is_file():
|
||||
return path.resolve()
|
||||
return None
|
||||
|
||||
|
||||
def instructions(path):
|
||||
"""Function-level instruction lists from a Mach-O file."""
|
||||
text = tool("llvm-objdump", "--macho", "--disassemble", "--no-show-raw-insn", str(path))
|
||||
functions = collections.OrderedDict()
|
||||
current = None
|
||||
for line in text.splitlines():
|
||||
name = re.match(r"^([_a-zA-Z][\w.$]*):$", line.strip())
|
||||
if name and not line.startswith(" "):
|
||||
current = name.group(1)
|
||||
functions[current] = []
|
||||
elif current is not None:
|
||||
instruction = re.match(r"^\s+[0-9a-f]+:\t(.*)$", line)
|
||||
if instruction:
|
||||
functions[current].append(instruction.group(1))
|
||||
return functions
|
||||
|
||||
|
||||
def optimization_estimate(path):
|
||||
"""(instructions, stack references, function count) over functions >= 20
|
||||
instructions."""
|
||||
total = stack = functions = 0
|
||||
try:
|
||||
for body in instructions(path).values():
|
||||
if len(body) < MIN_FUNCTION_INSTRUCTIONS:
|
||||
continue
|
||||
functions += 1
|
||||
total += len(body)
|
||||
stack += sum(1 for i in body if "(%rbp)" in i or "(%rsp)" in i)
|
||||
except Exception as error: # a fat or malformed file is reported, not fatal
|
||||
return 0, 0, 0, str(error)
|
||||
return total, stack, functions, None
|
||||
|
||||
|
||||
def imports(path):
|
||||
"""Symbols the file imports, per providing dylib."""
|
||||
counts = collections.Counter()
|
||||
for option in ("--bind", "--lazy-bind", "--weak-bind"):
|
||||
for line in tool("llvm-objdump", "--macho", option, str(path)).splitlines():
|
||||
match = re.match(r"^\S+\s+\S+\s+0x[0-9a-f]+\s+\S+\s+\d+\s+(\S+)\s+(\S+)$", line)
|
||||
if match:
|
||||
counts[match.group(1)] += 1
|
||||
return counts
|
||||
|
||||
|
||||
def walk(config):
|
||||
"""Resolve the transitive closure starting at the game executable."""
|
||||
queue = [Path(config["game"]).resolve()]
|
||||
seen = {}
|
||||
order = []
|
||||
while queue:
|
||||
path = queue.pop(0)
|
||||
if path in seen:
|
||||
continue
|
||||
origin = str(path)
|
||||
seen[path] = rpaths_of(path)
|
||||
order.append(path)
|
||||
for dependency in load_commands(path, None):
|
||||
if dependency.startswith("/usr/lib/libSystem.B.dylib"):
|
||||
continue
|
||||
resolved = resolve(dependency, origin, seen[path], config)
|
||||
if resolved is None:
|
||||
print("warning: cannot resolve " + dependency + " (from " + origin + ")",
|
||||
file=sys.stderr)
|
||||
continue
|
||||
if resolved not in seen:
|
||||
queue.append(resolved)
|
||||
return order
|
||||
|
||||
|
||||
def report(config):
|
||||
order = walk(config)
|
||||
print(f"{'library':42s} {'insns':>8s} {'stack%':>7s} {'funcs':>6s} {'-O':>3s} effective path")
|
||||
names = {}
|
||||
for path in order:
|
||||
total, stack, functions, error = optimization_estimate(path)
|
||||
if error:
|
||||
print(f"{path.name:42s} error: {error}")
|
||||
continue
|
||||
share = 100.0 * stack / max(total, 1)
|
||||
estimate = "-O0" if share > 20 else ("-O1+" if share > 8 else "-O2")
|
||||
print(f"{path.name:42s} {total:8d} {share:6.1f}% {functions:6d} {estimate:>3s} {path}")
|
||||
names[path] = estimate
|
||||
print()
|
||||
for path in order:
|
||||
if path.name not in BUILDERS or BUILDERS[path.name] == "-":
|
||||
continue
|
||||
print(f"builder for {path.name}: tools/{BUILDERS[path.name]}")
|
||||
return order, names
|
||||
|
||||
|
||||
def calibrate():
|
||||
folder = ROOT / "build/perf-research/memory/o-compare"
|
||||
if not folder.is_dir():
|
||||
sys.exit("no calibration objects in " + str(folder))
|
||||
for base in sorted(set(p.name.rsplit(".", 2)[0] for p in folder.glob("*.O?.o"))):
|
||||
row = [base]
|
||||
for level in ("O0", "O2"):
|
||||
path = folder / f"{base}.{level}.o"
|
||||
total, stack, functions, _ = optimization_estimate(path)
|
||||
row.append(f"{level} {total:6d} insns {100.0*stack/max(total,1):5.1f}% stack")
|
||||
print("calibration " + row[0] + ": " + "; ".join(row[1:]))
|
||||
|
||||
|
||||
def parse_arguments(argv=None):
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--binary", type=Path, default=None,
|
||||
help="binary to audit (default: the installed League game)")
|
||||
parser.add_argument("--config", type=Path, default=ROOT / "build/config.json")
|
||||
parser.add_argument("--calibrate", action="store_true",
|
||||
help="print the -O0/-O2 metric on known same-source objects")
|
||||
arguments = parser.parse_args(argv)
|
||||
return parser, arguments
|
||||
|
||||
|
||||
def main():
|
||||
parser, arguments = parse_arguments()
|
||||
if arguments.calibrate:
|
||||
calibrate()
|
||||
return
|
||||
config = json.loads(arguments.config.read_text())
|
||||
config["game"] = str(arguments.binary or GAME)
|
||||
if not Path(config["game"]).is_file():
|
||||
parser.exit(1, "The binary to audit does not exist: " + config["game"] + "\n")
|
||||
report(config)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,152 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Check an overlay dylib against the library it replaces, statically.
|
||||
|
||||
An overlay is only safe to install if the guest cannot tell the two apart:
|
||||
same install name, same versions, same dependent libraries and re-exports, the
|
||||
same exported symbols, no new undefined symbol, and the same segment
|
||||
protections. The optimization estimate of tools/riot_overlay_audit.py has to
|
||||
improve. Nothing here runs a guest process.
|
||||
|
||||
Usage: riot_overlay_verify.py <overlay.dylib> <effective.dylib> [--allow-missing-symbol]
|
||||
"""
|
||||
import argparse
|
||||
import importlib.util
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
|
||||
|
||||
def load_audit():
|
||||
path = ROOT / "tools/riot_overlay_audit.py"
|
||||
spec = importlib.util.spec_from_file_location("riot_overlay_audit", path)
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
def headers(path):
|
||||
return subprocess.run(["llvm-objdump", "--macho", "--private-headers", str(path)],
|
||||
capture_output=True, text=True, errors="replace").stdout
|
||||
|
||||
|
||||
def identity(path):
|
||||
"""Install name and the two versions from LC_ID_DYLIB."""
|
||||
text = headers(path)
|
||||
match = re.search(r"cmd LC_ID_DYLIB\n.*?\n\s+name (\S+).*?\n.*?\n\s+current version ([\d.]+)\n"
|
||||
r"compatibility version ([\d.]+)", text, re.S)
|
||||
if not match:
|
||||
return None
|
||||
return {"name": match.group(1), "current": match.group(2), "compatibility": match.group(3)}
|
||||
|
||||
|
||||
def dependencies(path):
|
||||
text = subprocess.run(["llvm-objdump", "--macho", "--dylibs-used", str(path)],
|
||||
capture_output=True, text=True, errors="replace").stdout
|
||||
return sorted(re.findall(r"^\t(\S+) \(compatibility version", text, re.M))
|
||||
|
||||
|
||||
def reexports(path):
|
||||
text = headers(path)
|
||||
return sorted(re.findall(r"cmd LC_REEXPORT_DYLIB\n.*?\n\s+name (\S+)", text, re.S))
|
||||
|
||||
|
||||
def segments(path):
|
||||
protections = {}
|
||||
name = None
|
||||
for line in headers(path).splitlines():
|
||||
match = re.match(r"\s+segname (\S+)", line)
|
||||
if match:
|
||||
name = match.group(1)
|
||||
match = re.match(r"\s+(initprot|maxprot) (\S+)", line)
|
||||
if match and name:
|
||||
protections.setdefault(name, {})[match.group(1)] = match.group(2)
|
||||
return protections
|
||||
|
||||
|
||||
def weak_definitions(path):
|
||||
"""Symbols the library defines only weakly: the -O0 build emits every
|
||||
inline function out of line, an optimized build may drop the unused ones,
|
||||
and the ABI makes a weak definition optional."""
|
||||
text = subprocess.run(["llvm-nm", "-m", str(path)], capture_output=True, text=True,
|
||||
errors="replace").stdout
|
||||
weak = set()
|
||||
for line in text.splitlines():
|
||||
if "weak external" in line:
|
||||
weak.add(line.split()[-1])
|
||||
return weak
|
||||
|
||||
|
||||
def symbols(path, flag):
|
||||
text = subprocess.run(["llvm-nm", flag, str(path)], capture_output=True, text=True,
|
||||
errors="replace").stdout
|
||||
found = set()
|
||||
for line in text.splitlines():
|
||||
parts = line.split()
|
||||
if flag == "-gU" and len(parts) == 3 and parts[1] in "TDBRS":
|
||||
found.add(parts[2])
|
||||
elif flag == "-u" and len(parts) == 2:
|
||||
found.add(parts[1])
|
||||
return found
|
||||
|
||||
|
||||
def verify(overlay, effective, allow_missing):
|
||||
audit = load_audit()
|
||||
failures, notes = [], []
|
||||
if identity(overlay) != identity(effective):
|
||||
failures.append("install name or versions differ: %s vs %s" % (identity(overlay), identity(effective)))
|
||||
if dependencies(overlay) != dependencies(effective):
|
||||
notes.append("dependent libraries differ:\n overlay: %s\n effective: %s"
|
||||
% (dependencies(overlay), dependencies(effective)))
|
||||
if reexports(overlay) != reexports(effective):
|
||||
notes.append("re-exports differ: %s vs %s" % (reexports(overlay), reexports(effective)))
|
||||
if segments(overlay) != segments(effective):
|
||||
failures.append("segment protections differ: %s vs %s" % (segments(overlay), segments(effective)))
|
||||
overlay_exports, effective_exports = symbols(overlay, "-gU"), symbols(effective, "-gU")
|
||||
missing = effective_exports - overlay_exports
|
||||
added = overlay_exports - effective_exports
|
||||
weak = weak_definitions(effective)
|
||||
missing_strong = missing - weak
|
||||
if missing & weak:
|
||||
notes.append("weak definitions dropped by the optimized build (optional by ABI): %s"
|
||||
% sorted(missing & weak)[:20])
|
||||
if missing_strong and not allow_missing:
|
||||
failures.append("exported symbols missing from the overlay: %s" % sorted(missing_strong)[:20])
|
||||
if added:
|
||||
notes.append("new exported symbols in the overlay: %s" % sorted(added)[:20])
|
||||
overlay_undefined, effective_undefined = symbols(overlay, "-u"), symbols(effective, "-u")
|
||||
new_undefined = overlay_undefined - effective_undefined
|
||||
if new_undefined:
|
||||
failures.append("new undefined symbols: %s" % sorted(new_undefined)[:20])
|
||||
total, stack, functions, error = audit.optimization_estimate(overlay)
|
||||
before, stack_before, functions_before, _ = audit.optimization_estimate(effective)
|
||||
share, share_before = 100.0 * stack / max(total, 1), 100.0 * stack_before / max(before, 1)
|
||||
if error:
|
||||
failures.append("cannot read the overlay: " + error)
|
||||
if share > share_before:
|
||||
failures.append("optimization estimate got worse: %.1f%% -> %.1f%% stack references" % (share_before, share))
|
||||
print("OVERLAY %s vs %s" % (Path(overlay).name, Path(effective).name))
|
||||
print(" exports=%d (was %d) undefined=%d (was %d)" % (len(overlay_exports), len(effective_exports),
|
||||
len(overlay_undefined), len(effective_undefined)))
|
||||
print(" stack references %.1f%% (was %.1f%%), functions %d (was %d)" % (share, share_before, functions, functions_before))
|
||||
for note in notes:
|
||||
print(" note: " + note)
|
||||
for failure in failures:
|
||||
print(" FAIL: " + failure)
|
||||
print("OVERLAY_VERIFY pass=%d %s" % (not failures, Path(overlay).name))
|
||||
return not failures
|
||||
|
||||
|
||||
def parse_arguments(argv=None):
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("overlay", type=Path)
|
||||
parser.add_argument("effective", type=Path)
|
||||
parser.add_argument("--allow-missing-symbol", action="store_true")
|
||||
return parser.parse_args(argv)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
arguments = parse_arguments()
|
||||
sys.exit(0 if verify(arguments.overlay, arguments.effective, arguments.allow_missing_symbol) else 1)
|
||||
+11
-8
@@ -102,7 +102,7 @@
|
||||
"revision": "89751e64bc6c2082f7725061824ee0e33395b0de",
|
||||
"repository": "https://github.com/darlinghq/darlingserver.git",
|
||||
"patch": "patches/darlingserver.patch",
|
||||
"modified_files": 34,
|
||||
"modified_files": 36,
|
||||
"new_files": [
|
||||
"duct-tape/src/fileport.c"
|
||||
]
|
||||
@@ -180,8 +180,10 @@
|
||||
"revision": "5a3e5b529d25c70257dcfa97e94f1826e71e9f40",
|
||||
"repository": "https://github.com/darlinghq/darling-libplatform.git",
|
||||
"patch": "patches/libplatform.patch",
|
||||
"modified_files": 3,
|
||||
"new_files": []
|
||||
"modified_files": 6,
|
||||
"new_files": [
|
||||
"private/platform/string_avx2.h"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "src/external/libpthread",
|
||||
@@ -256,15 +258,16 @@
|
||||
"revision": "fa29287aa2f0115271e091f1031f53c9e024005d",
|
||||
"repository": "https://github.com/darlinghq/darling-xnu.git",
|
||||
"patch": "patches/xnu.patch",
|
||||
"modified_files": 63,
|
||||
"modified_files": 64,
|
||||
"new_files": [
|
||||
"darling/src/libsystem_kernel/emulation/include/conversion/xattr/name.h",
|
||||
"darling/src/libsystem_kernel/emulation/include/xnu_syscall/bsd/impl/misc/mac_syscall.h",
|
||||
"darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/misc/mac_syscall.c",
|
||||
"darling/src/libsystem_kernel/emulation/include/common/cpu_topology.h",
|
||||
"darling/src/libsystem_kernel/emulation/include/common/private_mutex.h",
|
||||
"darling/src/libsystem_kernel/emulation/include/common/slowwait.h",
|
||||
"darling/src/libsystem_kernel/emulation/include/conversion/xattr/name.h",
|
||||
"darling/src/libsystem_kernel/emulation/include/xnu_syscall/bsd/impl/guarded/change_fdguard_np.h",
|
||||
"darling/src/libsystem_kernel/emulation/include/xnu_syscall/bsd/impl/misc/mac_syscall.h",
|
||||
"darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/guarded/change_fdguard_np.c",
|
||||
"darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/misc/mac_syscall.c",
|
||||
"libsyscall/wrappers/__sysctlbyname.S"
|
||||
]
|
||||
},
|
||||
@@ -273,7 +276,7 @@
|
||||
"revision": "8423a7d2f053167030d2bc4a227f96243c740667",
|
||||
"repository": "https://github.com/darlinghq/indium.git",
|
||||
"patch": "patches/metal--deps--indium.patch",
|
||||
"modified_files": 35,
|
||||
"modified_files": 36,
|
||||
"new_files": [
|
||||
"private-include/indium/counters.private.hpp",
|
||||
"private-include/indium/deferred-render-encoder.private.hpp",
|
||||
|
||||
Reference in new issue
Block a user